claudeplugins.
claudeplugins7 min read

Claude Code Skill Allowed-Tools: Restrict Automation Surface

Lock down which tools a Claude Code skill can invoke using the allowed-tools YAML field. Patterns, gotchas, and a safer-automation checklist.

Claude Code Skill Allowed-Tools: Restrict Automation Surface

A Claude Code skill that can call any tool is a skill that can do anything. If your seo-audit skill has access to Bash, it can rm -rf. If your format-json skill has access to WebFetch, it can exfiltrate the file it just read. The allowed-tools field in skill YAML is the cheapest mitigation you'll add today, and most skills in the wild ship without it.

This piece walks through how the allowlist actually behaves at runtime, the patterns that hold up across real workflows, and where the field stops protecting you.

What allowed-tools does (and doesn't)

allowed-tools is a YAML key in the skill's frontmatter that constrains which tools Claude is permitted to invoke for the duration of that skill's execution. It's an allowlist, not a denylist. Anything not on the list is unavailable, which is the right default for a security control.

---
name: format-mdx
description: Format MDX frontmatter and body
allowed-tools: [Read, Edit, Glob]
---

You are a formatter. Read the target file, normalize frontmatter keys, and edit in place.

In that skill, Claude cannot call Bash, cannot call WebFetch, cannot call any MCP tool. The runtime returns a tool-not-available error if the model tries. The model adapts and routes around the missing capability, usually by asking the user.

What it does NOT do:

  • It does NOT validate tool arguments. Bash on the allowlist means any shell command. Edit means any file the harness can write to.
  • It does NOT prevent the model from suggesting unsafe actions in plain text. The user can copy-paste them into a terminal.
  • It does NOT cascade into sub-agent calls. If your skill spawns an Agent (and Agent is on the allowlist), the spawned agent runs with its own tool inheritance rules.

The mental model: allowed-tools is process isolation, not sandboxing. Treat it like Unix file permissions \u2014 necessary but not sufficient.

The four patterns that survive contact with reality

After running this on roughly 30 skills in production rotations, four allowlist shapes keep recurring. Pick the smallest one that lets the skill do its job.

Pattern 1: Read-only auditor

For skills that produce a report and never mutate state. SEO audits, dependency scans, code review summaries.

allowed-tools: [Read, Glob, Grep, WebFetch]

WebFetch is included because audits often verify that an external URL resolves or check a published API contract. Drop it if your audit is purely local. The absence of Edit, Write, Bash, and Agent means the worst-case outcome is a wrong report, not a corrupted repo.

Pattern 2: Single-file editor

Formatters, frontmatter normalizers, lint-fixers. The skill reads one file, edits one file, and stops.

allowed-tools: [Read, Edit]

Notice no Write. Edit requires a prior Read, which means the model can't fabricate a new file out of thin air. This single substitution catches a surprising number of model errors where it tries to "rewrite" the whole file from a hallucinated baseline.

Pattern 3: Scoped shell runner

Skills that need a build tool or test runner. The trick is to NOT add Bash and instead add only the exact MCP tool that wraps your runner.

allowed-tools: [Read, Glob, mcp__pytest__run, mcp__pytest__list_tests]

If you're stuck with Bash, at least pair it with the harness-level permissions config in .claude/settings.json so only specific commands are pre-approved. The skill's allowed-tools and the project's permissions block stack \u2014 the skill says what's possible, the permissions block says what's automatic.

Pattern 4: Multi-tool orchestrator

Skills that legitimately need to call sub-agents. Keep Agent on the allowlist, but be deliberate about everything else.

allowed-tools: [Read, Glob, Grep, Agent, TodoWrite]

This pattern shows up in planning and review skills. The orchestrator reads context, dispatches sub-agents for narrow tasks, and tracks progress. It deliberately does NOT have Edit, Write, or Bash \u2014 the sub-agents do, and they have their own (tighter) allowlists.

The allowlist vs. the harness permissions block

These two mechanisms confuse people because they look like they overlap. They don't.

MechanismScopePurpose
Skill allowed-toolsPer-skill, in YAMLControls what tools exist at all during skill execution
Project permissions (.claude/settings.json)Per-project, applies to ALL skills + main loopControls which tool calls auto-approve vs. prompt the user

A tool can be on the skill allowlist AND require a user permission prompt. A tool can be auto-approved in permissions but unavailable because the active skill omits it from allowed-tools. The skill list wins for availability; the permissions block wins for friction.

For a real safer-automation setup you want both:

  1. Tight allowed-tools per skill, sized to the job
  2. A project permissions allowlist for the read-only operations that show up everywhere (Bash(git status:*), Bash(ls:*), etc.)

The Anthropic docs spell out the permissions schema in detail at https://docs.claude.com/en/docs/claude-code/iam and the skill frontmatter spec lives at https://docs.claude.com/en/docs/claude-code/skills.

Naming MCP tools in the allowlist

MCP tools use a fully-qualified name like mcp__<server>__<tool>. The allowlist accepts these verbatim:

allowed-tools:
  - Read
  - Edit
  - mcp__playwright__browser_navigate
  - mcp__playwright__browser_snapshot

You cannot wildcard MCP tools (mcp__playwright__* is not honored \u2014 last verified against Claude Code 2.x). List each one explicitly. This is annoying for MCP servers that ship 40+ tools, but the verbosity is the point: a 40-line allowlist is a 40-line audit log of what this skill can do.

If you find yourself wanting wildcards, that's usually a signal the skill is doing too much. Split it.

Common failure modes

Forgetting Glob for any file-discovery skill. Without Glob, the model resorts to running Bash(find:*) or Bash(ls:*), which means you have to grant Bash, which defeats the point. Add Glob even when it feels redundant.

Granting Bash "just for git status". Use the project permissions block to auto-approve specific git read commands and leave Bash off the skill allowlist entirely. The model will fall back to telling the user to run git status themselves, which is acceptable for a read-only skill. For skills that genuinely need git mutations, scope to Bash(git add:*) etc. in permissions and accept that the skill needs Bash available.

Allowing Agent without thinking. A skill that can spawn agents can spawn agents with broader tool access than the parent skill. The sub-agent's allowlist comes from the agent definition, not from the parent skill. If you allow Agent, audit the agent definitions you might dispatch.

Allowing WebFetch in a skill that touches secrets. A skill with Read access to .env and WebFetch available has a one-step exfiltration path. Either remove WebFetch or arrange for the skill to never see the secret-bearing files. The harness can't stop the model from base64-ing a file body into a query string.

A 90-second audit checklist

For every skill in .claude/skills/:

  1. Does the skill mutate state? If no \u2192 no Edit, no Write, no Bash.
  2. Does it need network? If no \u2192 no WebFetch, no WebSearch, no MCP tool that hits the internet.
  3. Does it spawn sub-agents? If no \u2192 no Agent.
  4. Does it really need Bash, or can an MCP tool wrap the operation? Bash is the most expensive grant \u2014 anything else is cheaper.
  5. Are the MCP tools listed individually, with the mcp__server__tool format? Wildcards don't work and silently degrade to "no MCP tools allowed".

A typical first pass shrinks each skill's allowlist by 30-50%. The model adapts. The skill keeps working. The blast radius drops by an order of magnitude.

When the allowlist is not enough

If a skill processes untrusted input (web pages, user-pasted text, third-party PRs), the allowlist won't save you from prompt injection. An attacker who can convince the model to call Edit on ~/.ssh/authorized_keys doesn't care that Bash is off the list. For untrusted-input skills, run them in a sandboxed working directory, audit the output before applying it, and consider running the skill against a git worktree (Claude Code's isolation: worktree agent option does this automatically) so any unintended mutation is contained to a throwaway branch.

The allowlist is a 5-line change that prevents 80% of accidental damage. It does not prevent deliberate compromise. Pair it with worktree isolation, file-level permissions, and the project permissions block for the rest.

References: