Reference

allowed-tools: what it controls, and how to scope it

One frontmatter field that says which tools a skill may use without stopping to ask. Getting it wrong fails in both directions: too broad pre-approves more than you meant, too narrow and the agent interrupts for work the skill does every time.

What it controls

allowed-tools declares the tools a skill needs. While the skill is active, those tools are pre-approved — the agent reaches for them without stopping to ask.

It is a pre-approval field, not a ceiling. Claude Code's documentation is explicit that it does not restrict which tools are available: listing a tool removes the permission prompt for it rather than granting a capability the agent lacks, and omitting the field entirely means the skill prompts exactly as it would otherwise — which is the right default for most skills.

Not a security boundary

Treat allowed-tools as a way to keep a skill focused, not as a sandbox. It expresses intent about what a skill needs; it is not an isolation mechanism, and it does not protect against instructions in the body that ask for something else.

Splitting on whitespace tears a token

A token is a tool name plus an optional parenthesised scope, and that scope may contain spaces — Bash(pnpm test:*) is one token, not two. Split the field on whitespace and it arrives as Bash(pnpm and test:*): two fragments, neither of which matches anything, so the pre-approval silently stops applying.

This is not hypothetical. Of 18 third-party implementations that tokenize allowed-tools, surveyed in August 2026, 12 split it on whitespace and tear a scoped token in half. That is a count of the parsers we could find and read, not a census of the ecosystem: the search that surfaced them looked for splitting idioms, and a tool that keeps the field as an opaque string and hands it to its host cannot get this wrong.

What the field saysTokens intendedAfter a whitespace split
Bash(pnpm test:*)One token.Two fragments.
Bash(git add *) Bash(git commit *) Bash(git status *)Three tokens.Nine fragments.
Bash(git:*) Bash(jq:*) ReadThree tokens.Three tokens. Nothing looks wrong.

Read the last row, because it is the reason this survives review. That value is the example the Agent Skills specification prints for this field, and no scope in it contains a space, so a parser tested against the spec goes green; the failure only shows against the syntax Claude Code's own documentation uses, which is the row above it — three tokens, each carrying a space inside its scope. Nor is that shape something an author has to go looking for. It is what Claude Code's permission dialog writes for you when you choose "Yes, don't ask again" for a command prefix.

It fails closed, which is why it goes unnoticed

A fragment like Bash(pnpm matches no command, so the grant never applies and the agent asks for permission exactly as it would have without the field — or a validator reports one correct token as two errors. Nothing crashes and nothing is over-permitted. The cost is a skill that quietly stops being pre-approved, and an author with no way to see why.

So tokenize the field rather than splitting it: match a tool name followed by a balanced parenthesised scope, and fall back to a run of non-whitespace so malformed input stays visible to validation instead of being silently swallowed.

value.match(/[^\s()]+\([^)]*\)|\S+/g)

The whole tokenizer for the string form — it returns 1, 3 and 3 tokens for the three rows above.

Splitting on commas instead is not the answer: it keeps the first row whole and collapses each of the other two into a single unrecognised token. Nor is a string the only shape the field takes — Claude Code accepts a YAML list as well, so a reader that tests only for a string reports no tools at all for allowed-tools: [Read, Bash(pnpm test:*)]. That is a silent drop rather than a tear: the same class of failure, one layer up.

And this is not someone else's mistake. We shipped the whitespace split in five places of our own before the last copy was fixed — three of them behind a passing unit test whose fixture was Bash(git:*), which has no space in it, and the other two with nothing testing the field at all. The sequence form went worse: every reader we had gated on the string, so all of them reported nothing, and the validator that should have caught it gated the same way.

The token grammar

A space-separated list of tokens. Each token is a tool name, optionally followed by a parenthesised scope:

allowed-tools: Read Grep Bash(pnpm test:*)

The specification does not say what a tool name may contain, so what follows is the shape hosts accept rather than a rule it states. A built-in tool starts with an uppercase letter and contains letters and digits. An MCP tool is written mcp__server__tool, with a double underscore between the server and the tool it exposes; only the mcp__ prefix is always lowercase, because the segments after it are whatever the operator named their server — mcp__Ahrefs__search carries capitals and mcp__my-server__do_thing a hyphen. Either form may carry a parenthesised scope, which narrows it further:

TokenWhat it permits
ReadThe Read tool, unrestricted.
Bash(git:*)Shell access, limited to git commands.
Bash(pnpm test:*)Shell access, limited to pnpm test and its subcommands.
mcp__github__create_issueOne tool from a connected MCP server — here, opening an issue on GitHub.
mcp__githubThe named MCP server as a whole — all of the tools it exposes.

The space-separated string is the form this page documents, and it is not the only one the field takes. The Agent Skills specification defines that form and nothing else; Claude Code's own frontmatter reference says it accepts a space- or comma-separated string, or a YAML list. So a comma is not universally wrong — it is undefined by the specification, and Contexory's validator warns about it:

FormWhere it is read
allowed-tools: Read Grep Bash(pnpm test:*)The specification's form, and the one Claude Code's own worked examples use. Read and validated by Contexory.
allowed-tools: [Read, Grep, Bash(pnpm test:*)]A YAML sequence. Claude Code accepts it, and Contexory reads and validates it identically — the tokens go through the same check. The specification does not define it, though, so a third-party reader that tests only for a string reports no tools at all.
allowed-tools: Read, GrepClaude Code accepts it. The specification does not define it and Contexory's validator warns about it: the comma is read as part of the token, so Read, is flagged rather than read as Read.

Write the space-separated string. It is the only form the specification defines and the one Claude Code's own worked examples use, so it is the form with the best odds of being read as you meant it. That is a statement about what to write, not a guarantee about how it is parsed: of the 18 readers surveyed, 12 tear a scoped token in exactly this form, which is what the rest of this page is about.

Tokens no known host accepts

The four that come up most often. These are what this validator reports — Claude Code is more permissive about the separator, as the token grammar sets out:

TokenWhy it fails
readBuilt-in tool names are capitalised — Read, not read. A lowercase name is one hosts recognise only as an MCP tool, which begins mcp__; the segments after that prefix may carry capitals.
Read, GrepComma-separated. The comma is read as part of the token, so this is Read, and Grep. Use spaces.
Bash(git:*Unclosed parenthesis. The scope must be balanced.
*A bare wildcard is not a tool name, and there is no token that pre-approves everything. Omitting the field is the opposite: nothing is pre-approved, and the agent asks as it normally would.

Scoping it well

The useful question is not "what might this skill need?" but "what does it actually run?":

  • Scope shell access. Bash pre-approves every command; Bash(git add *) pre-approves the family the skill uses.
  • List reads explicitly. A skill that consults its own references/ needs Read.
  • Omit the field when the skill is pure prose. A checklist that only tells the agent how to think needs no tools, and an empty-but-present list is easy to misread as a mistake.
# Too broad — grants every shell command
allowed-tools: Bash

# Scoped — grants the two command families the skill actually runs
allowed-tools: Read Grep Bash(git:*) Bash(pnpm test:*)

Broad versus scoped, on the same skill:

A published skill doing this: changeset-writer in our gallery declares allowed-tools: Read Grep Glob Bash(git diff:*) Bash(git log:*) Bash(python3:*) — six tokens, three of them scoped to a command family, and two of those scopes containing a space. Split that field on whitespace and it arrives as eight fragments, four of which match nothing. The gallery page shows the whole SKILL.md the field belongs to, and its own allowed-tools block links back here.

How hosts treat it

Hosts differ in how much of the list they honour, and in which tool names exist at all. A tool name from one host may not exist in another. Prefer scoping by capability family over naming a host's specific integration, for the reasons the host comparison sets out.

Say the capability, not the tool id

In the body of the skill, describe what the agent should do ("search existing issues for anything matching the title") rather than naming a specific tool to call. Tool names differ per host and per connection; capabilities do not.
NextClaude Code vs OpenClaw