anthropics / anthropics/claude-code
SKILL.md `allowed-tools:` does not restrict the tool surface (5 declared → ~46-57 received, incl. Write/Edit)
- Langage dominant
- Python
- Étoiles
- 145k
- Forks
- 23.1k
- Métriques de merge des PR
- Métriques de PR en attente
Description
## Summary
`allowed-tools:` in a skill's `SKILL.md` frontmatter is documented as restricting tool access, but it neither grants nor restricts. A skill declaring 5 tools receives ~46–57, including `Write`, `Edit`, `Agent`, and MCP tools such as `ssh_execute`.
The failure is **silent** — nothing warns that the key had no effect.
## Version
Claude Code **2.1.241**, macOS (Darwin 27.0.0). Also observed at 2.1.224.
## Documented behavior
Skill-authoring reference documents the key as:
```yaml
allowed-tools: Read, Grep, Glob # Restrict tool access
```
## Actual behavior
Two probe skills, **identical except for one variable** — `context: fork` present vs. absent. Both declare exactly five tools: `Bash`, `AskUserQuestion`, `WebFetch`, `Monitor`, `NotebookEdit`. Both deliberately omit `Read`, `Write`, `Edit`, `Glob`, `Grep`, `Agent`, `Skill`, `TaskCreate`, `ToolSearch`.
| variant | declared | received | undeclared but granted |
|---|---:|---:|---|
| `context: fork` | 5 | ~46 | `Agent`, `Read`, `Write`, `Edit`, `Skill`, `ToolSearch`, `SendMessage`, `Cron*`, `ssh_execute`, 14× browser-control tools, … |
| no `context:` | 5 | ~48 | same, plus `Workflow`, `ScheduleWakeup`, `Task*` |
Of the five declared tools, four arrived and one did not. So the key fails in **both** directions: it does not restrict, and it does not guarantee.
**`context: fork` is not the variable** — this reproduces with `context:` absent. Please don't triage it as fork-specific.
Tools were confirmed live, not merely listed: `Read` was called and returned a genuine tool-level "file does not exist" for a path independently confirmed missing via `Bash`.
## Contrast — the agent-definition path IS enforced
`agents/*.md` using `tools:` / `disallowedTools:` works correctly. Measured the same day on both dispatch paths (direct `Agent`-tool dispatch, and a `context: fork` skill's `agent:` key): `Write`, `Edit`, `NotebookEdit` and `Agent` had no schema at all, and `ToolSearch("select:Write,Edit,NotebookEdit,Agent")` returned `No matching deferred tools found`.
Enforcement survives an adversarial declaration: a probe skill declaring `Write` and `Edit` in its own `allowed-tools:` while bound to a runner that denies them still did not receive them. The agent layer wins, which is the correct precedence — the skill layer simply isn't consulted.
## Impact
Skills are widely authored with `allowed-tools:` as a containment boundary, and capability claims ("read-only", "no write tools") get written on the strength of it. In our setup this produced two false guarantees — including a review skill operating on **uncommitted** work while claiming to be read-only by declaration, i.e. exactly the context where an unintended write is unrecoverable.
## Reproduction
Save as `.claude/skills/toolprobe/SKILL.md` in a scratch directory:
```markdown
---
name: toolprobe
description: "Diagnostic probe. Reports the tool surface actually granted. Triggers on: toolprobe."
user-invocable: true
model: sonnet
allowed-tools:
- Bash
- WebFetch
- Monitor
- NotebookEdit
---
# toolprobe
Your ONLY job is to report, truthfully, which tools were actually granted to you.
This is a measurement; an inaccurate answer is worse than no answer.
1. List EVERY tool name present in your context, exactly as named in your tool schemas.
This skill declared exactly four: Bash, WebFetch, Monitor, NotebookEdit. It did NOT
declare Read, Write, Edit, Glob, Grep, Agent, Skill, TaskCreate, ToolSearch.
2. Actually attempt `Read` on /etc/hostname and `Glob` for '*.md' — neither is declared.
A real call attempt is stronger evidence than introspection.
3. Report `undeclared_but_PRESENT` and `declared_but_ABSENT` as two explicit lists.
```
Then:
```bash
cd && claude -p --permission-mode bypassPermissions "/toolprobe"
```
Add a second copy with `context: fork` in the frontmatter to confirm the result holds on both axes.
## Suggested resolutions (any one)
1. Enforce `allowed-tools:` as documented.
2. Emit a startup warning that it is advisory, and document the `agent:`-binding path as the supported containment mechanism.
3. Reject or warn on an inert key at skill load.
Option 2 alone would have been enough to prevent the false guarantee on our side.
## Deliberately excluded
We separately measured that `AskUserQuestion` is absent inside a forked skill (present in the interactive parent, absent in the fork spawned from it, with `bypassPermissions` held fixed across both). That is **not** part of this report — it is plausibly intended, since a background fork has no interactive user, and bundling it would weaken a clean and clearly-unintended finding. Happy to file it separately if useful.
Guide de contribution
Aucun guide de contribution indexé pour ce dépôt
Piste de recherche
Reproduce with .claude/skills/toolprobe/SKILL.md using the provided claude -p command, including both the forked and non-forked variants. Compare skill handling with the enforced tools/disallowedTools path in agents/*.md. Done means allowed-tools is enforced, or the inert behavior is warned about and documented with the supported containment mechanism.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- yaml
- Domaine
- cli, security, tooling
- Type d'issue
- Bug
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Activité
- Active
- Clarté
- Clairement spécifiée
- Accessibilité débutants
- 55/100