allowed-tools in a SKILL.md’s frontmatter reads like a sandbox boundary. It is not one. A skill that declares five tools can receive 46 to 57 of them at runtime — Write and Edit included — with no warning that the field had no effect. That’s a confirmed, open Claude Code bug, not a misconfiguration. Separately, a still-open bug in the same subsystem means that simply documenting the ! inline-shell syntax inside your own SKILL.md — in a fenced code block, exactly as a responsible author would — can execute that example for real in whatever directory the skill loads in. Both bugs sit inside the same mechanism that Claude Code changed in v2.1.271: how auto mode reviews the shell commands a skill injects into its own context. Here’s what’s actually true today, sourced from the current docs and the open issues themselves, not from what the frontmatter looks like it should do.
What ! Inline Commands Are For
Skills can inject live data into their own body before Claude ever sees it, using a preprocessor syntax:
## Current changes
!`git diff HEAD --stat`
The backtick form runs once, and the command’s output replaces the placeholder as plain text — Claude receives the actual diff, not the command. Multi-line versions use a fenced block:
```!
git status --short
node --version
```
This is what lets a skill say “load the current git state” or “check which package manager is in use” without asking Claude to run a Bash tool call and wait for a round trip. It’s a legitimate, useful feature — most of the guides on skill authoring (including our own) recommend it for exactly this kind of context injection.
The part that’s easy to get wrong is what happens when Claude Code decides whether to actually run that injected command.
allowed-tools Is a Pre-Approval, Not a Restriction
The frontmatter field that’s supposed to scope a skill’s capabilities looks like this:
---
name: code-reviewer
description: "Reviews staged changes for style issues. Read-only."
allowed-tools: Read, Grep, Glob
---
Read that and you’d reasonably conclude the skill can only use Read, Grep, and Glob — the comment pattern shown in skill-authoring references even says “Restrict tool access.” Current official documentation for the field says something narrower: allowed-tools lists “tools Claude can use without asking permission during the turn that invokes this skill,” and the grant “does not restrict which tools are available: every tool remains callable, and your permission settings still govern tools that are not listed.”
In other words: allowed-tools pre-approves. It doesn’t fence in.
GitHub issue #89195, open and labeled bug with a reproduction, measured exactly this gap. A probe skill declaring five tools — Bash, AskUserQuestion, WebFetch, Monitor, NotebookEdit — and deliberately omitting Read, Write, Edit, Glob, and Grep instead received 46 to 57 tools at runtime, including Write, Edit, Agent, and MCP tools like ssh_execute. The Read tool wasn’t just listed — it was called and returned a genuine “file does not exist” result for a path independently confirmed missing via Bash. Four of the five declared tools arrived; the fifth didn’t. The field failed in both directions, silently, with no warning that it had no effect.
The issue’s author flags the actual risk plainly: skills get written with allowed-tools treated as a containment boundary, and “read-only” claims get made on the strength of it. A code-review skill that’s supposed to only inspect a diff, running against uncommitted work, with an unintended Write or Edit call available — that’s the exact scenario where the assumption becomes expensive.
disallowed-tools Is the Field That Actually Restricts
If you need a real boundary rather than a pre-approval, the field to reach for is the other one:
---
name: background-monitor
description: "Watches a log file and reports anomalies. Never needs user input."
disallowed-tools: AskUserQuestion, Write, Edit
---
Current documentation describes disallowed-tools as tools “removed from Claude’s available pool while this skill is active” — a materially different guarantee than a grant that “doesn’t restrict which tools are available.” If your goal with allowed-tools was actually “keep this skill from writing files,” write that boundary as a disallowed-tools entry instead, or as a permissions.deny rule in settings.json that isn’t scoped to the skill’s activation window at all. Neither allowed-tools nor disallowed-tools survives your next message — both grants and restrictions clear the moment you send a new prompt.
What v2.1.271 Changed About Auto Mode and Injected Commands
Separately from the tool-surface question, Claude Code’s v2.1.271 release (September 14, 2026) changed how the injected ! commands themselves get reviewed in auto mode. The changelog entry: auto mode now makes a skill’s or slash command’s inline ! shell commands “follow default-mode permission rules instead of the classifier,” and “a command no rule decides runs as a reviewed tool call.”
The current documented flow, per the official skills reference:
| Situation | What happens |
|---|---|
A permissions.deny rule matches the injected command | Invocation aborts immediately with Shell command permission check failed for pattern "..." — in every mode |
Outside auto mode, no rule matches (and no allowed-tools covers it) | Invocation aborts the same way, as if a deny rule had matched |
Outside auto mode, allowed-tools pre-approves the command | Runs silently, no prompt |
| In auto mode, no rule matches | Doesn’t abort. The skill loads with an instruction telling Claude to run the command itself, and that call goes through auto mode’s usual classifier review as a normal Bash tool call |
A forked skill that sets agent in its frontmatter | Aborts regardless of mode — the exception above doesn’t apply |
The practical effect for anyone authoring skills under auto mode (the default permission mode on Pro, Max, and Team plans since mid-August): an injected command with no explicit rule no longer runs invisibly as part of skill rendering. It now shows up as a visible, classifier-reviewed Bash call in the transcript, with the round-trip latency that implies. If your skill relies on !git diff HEAD or similar for fast, silent context loading, and you haven’t pre-approved it with allowed-tools, you’ll now see that call surface explicitly rather than resolve instantly. Adding the command’s pattern to allowed-tools restores the silent, immediate behavior — which is also the one place in this whole system where allowed-tools does exactly what its name suggests, because a pre-approval genuinely is the right tool for “let this specific command run without a round trip.”
The Bug That Makes Documenting the Syntax Dangerous
The part that turns this from a permissions nuance into an active hazard: the preprocessor that expands !`command` doesn’t check whether that text sits inside a fenced code block or an inline code span before running it.
GitHub issue #87730, open since August 18, 2026, reproduced this in Anthropic’s own first-party plugin-dev plugin. The plugin’s SKILL.md documents the ! syntax for other skill authors, using a fenced example:
```markdown
---
description: Run tests for specific file
argument-hint: [test-file]
allowed-tools: Bash(npm:*)
---
Run tests: !`npm test $1`
Analyze results and suggest fixes for failures.
```
That’s a correctly-fenced documentation example — exactly what a responsible author should write. Invoking /plugin-dev:command-development in a project with no package.json runs npm test for real anyway, and surfaces the resulting shell error, because the preprocessor scans the entire skill body, fence markers included, and executes anything matching the pattern. An Anthropic-affiliated commenter confirmed the same behavior with a minimal repro: an echo command wrapped in a fenced ```markdown block, and a second one inside an inline code span, both executed and reached the model as literal output, on Claude Code 2.1.234.
The npm test case is loud and harmless. The issue’s author points out the actual scope: sixteen !`...` patterns exist in that one SKILL.md alone, and any documentation example showing !`git clean -fdx`, !`rm -rf ./build`, or a deploy command would not fail as harmlessly as a missing package.json. There is currently no supported way to write a literal, non-executing example of the ! syntax inside a SKILL.md or slash command file. As of this writing, the fix hasn’t shipped.
A Containment Checklist for Skill Authors
- Don’t rely on
allowed-toolsfor containment. Use it for what it actually does — pre-approving specific, trusted commands so they run without a prompt or a round trip. For an actual boundary, usedisallowed-tools, or apermissions.denyrule insettings.jsonscoped outside the skill’s activation window. - Review third-party
SKILL.mdfiles before running them, especially theallowed-toolsline. Documentation explicitly warns that workspace trust doesn’t gate this field: a project skill’sallowed-toolsapplies whenever it’s invoked, including in an untrusted directory during a-prun. - Don’t write a live example of the
!syntax in your own skill documentation until issue #87730 is resolved. Describe the syntax in prose, or use a placeholder character that isn’t!at the start of a line, rather than a fenced block containing the real pattern. - Set
disableSkillShellExecution: truein managed settings for shared team skill libraries if you can’t audit every contributedSKILL.mdfor this class of issue. It replaces every injected command with[shell command execution disabled by policy]instead of running it — bundled and managed skills are exempt, and users can’t override it when it’s set at the managed-settings level. We cover the rest of the managed-settings surface in our settings.json field reference. - Expect a visible Bash call for unmatched injected commands under auto mode, post-v2.1.271. If you want the old silent, instant behavior back for a specific trusted command, pre-approve its exact pattern with
allowed-tools. - Test new or third-party skills in a scratch directory first, ideally outside auto mode or with an explicit
askrule in place, so an unexpected injected command prompts you instead of running.
Related Articles
- How to Write Claude Code Skills: Complete Developer Guide (2026)
- Claude Code Skills vs Slash Commands: Complete 2026 Guide
- Claude Code Permissions and Trust Levels: A Complete Guide
- CVE-2026-54316: The Pre-Approved WebFetch Domain That Turned Claude Code Into a Data Exfiltration Channel
Browse real-world SKILL.md and settings.json permission configurations in our gallery.