You add allowed-tools: Bash(git add *) Bash(git commit *)
to a skill. You type /commit
, and Claude stages and commits without asking. You send one more message, ask for another commit — and the permission prompt is back.
Nothing broke. allowed-tools
did exactly what it is specified to do. It is just narrower than the name suggests.
allowed-tools
pre-approves the listed tools for the turn that invokes the skill. That is the entire scope. The docs are explicit about the expiry: the grant "clears when you send your next message."
Invoking the skill again re-applies it for that turn. So /commit
, then a follow-up message asking Claude to commit again, are two different worlds: the first is covered, the second is not.
Two things happen when a skill is invoked, and they have different lifetimes:
| What | Lifetime |
|---|---|
The rendered SKILL.md content |
|
| Enters the conversation as a single message and stays for the rest of the session | |
The allowed-tools grant |
|
| Clears when you send your next message |
Instructions persist. Permissions do not.
That mismatch is why two contradictory-sounding statements are both true: the skill is still loaded and the skill stopped working. Claude still has the instructions. It just lost the pre-approval, so it asks.
If you hit this, you have two honest options:
This is the part that trips people who read the field as a whitelist:
It does not restrict which tools are available: every tool remains callable, and your permission settings still govern tools that are not listed.
So a skill with allowed-tools: Read Grep
has not been sandboxed to reading and grepping. Claude can still call Bash. The field only says "these specific ones don't need a prompt right now." Everything else falls back to your normal permission rules.
If you actually want a tool taken away, that is a different field.
disallowed-tools
is the restricting half
disallowed-tools
removes tools from Claude's available pool while the skill is active. It is the right field for an autonomous skill that should never call something — a background loop that must not stop to ask you a question, for example.
Two limits worth knowing:
EndConversation
while any other tool remains.To block a tool across all skills and prompts rather than one skill, use deny rules in your permission settings.
| What you want | Where it goes | How long it lasts |
|---|---|---|
| Skip prompts for a few commands when this skill runs | ||
allowed-tools in SKILL.md |
||
| the invoking turn | ||
| Keep a tool away from one autonomous skill | ||
disallowed-tools in SKILL.md |
||
| while the skill is active | ||
| Skip prompts for the whole session | allow rules in permission settings | your settings files |
| Block a tool everywhere | deny rules in permission settings | your settings files |
The rule of thumb: frontmatter is for this invocation, settings are for this project or user. If you find yourself wanting allowed-tools
to stick around, you have discovered that the rule belongs in settings.
Skills can ship their own scripts. The problem is that a hardcoded path breaks the moment the skill is installed somewhere else — and for a plugin skill, ${CLAUDE_SKILL_DIR}
points at the skill's subdirectory inside the plugin, not the plugin root, so guessing is worse than it looks.
The fix is that Claude Code substitutes ${CLAUDE_SKILL_DIR}
in two places: the skill's markdown content, and Bash rules in allowed-tools
. Use the same variable in both and the allow rule matches the exact command the body tells Claude to run:
---
name: render-chart
description: Render a chart from a CSV file
allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/render.sh *)
---
Run `${CLAUDE_SKILL_DIR}/scripts/render.sh <csv>` to render the chart.
Installed at ~/.claude/skills/render-chart/
, both occurrences expand to that directory, the rule matches, and the script runs without a prompt. Move the skill and it still matches, because neither side was hardcoded.
There is a sibling variable, ${CLAUDE_PROJECT_DIR}
, for project-local scripts — that one needs Claude Code v2.1.196 or later.
For skills checked into a project's .claude/skills/
directory, allowed-tools
takes effect after you accept the workspace trust dialog for that folder — the same as permission rules in .claude/settings.json
.
Read that in reverse: a skill in a repo can grant itself broad tool access, and clicking "trust" is what turns it on. Reviewing the skills is part of reviewing the repository, not a separate chore.
A 10-second pass before trusting an unfamiliar project:
grep -rn "allowed-tools\|disallowed-tools" .claude/skills/ .claude/commands/ 2>/dev/null
If a skill you did not write pre-approves Bash(* )
or anything close to it, that is worth a second look before you accept the dialog.
Bash(git commit *)
covers git commit -m "x"
, not a command routed through something else.disallowed-tools
.allowed-tools
is a receipt you hand over at the door, not a badge you wear all day. It gets you past the prompt for the turn you invoked the skill, and then it is spent. Everything durable — what Claude may do session-wide, what it may never do — lives in your permission settings.
Once you stop expecting the frontmatter to be a sandbox, the field becomes genuinely useful: it removes the friction from the two or three commands a skill actually needs, without quietly widening what Claude can do for the rest of the day.
Written while building Rulestack — packs of Cursor rules, Claude Code skills, and AGENTS.md setups for people who would rather not rediscover this by trial and error.
More notes like this as I find them: @ai-shop.bsky.social on Bluesky.