tools: frontmatter — misleading error message + silent acceptance of non-documented Agent(a), Agent(b) syntax
Upstream issue draft — Claude Code 2.1.118: tools: frontmatter — misleading error message + silent acceptance of non-documented Agent(a), Agent(b) syntax
Draft for submission at https://github.com/anthropics/claude-code/issues*
---
Summary
Two related usability issues in the tools: frontmatter parsing of subagent definition files (.claude/agents/*.md). Neither is a parser bug per se — the parser behaves logically — but they compound into a confusing 4-hour debugging session when a user writes a non-documented variant of the Agent(...) syntax:
- Error message is misleading. When a subagent invocation is blocked because the invoking agent's
tools:whitelist does not include it, the error reads:Agent type 'X' has been denied by permission rule 'Agent(X)' from settings.This points the user towardsettings.jsonpermission rules, when the actual restriction lives in the calling agent'stools:frontmatter (a separate, internal validator).
- Parser silently degrades non-documented syntax. The official documentation (https://code.claude.com/docs/en/sub-agents section on
tools:) shows canonical whitelist syntax as a singleAgent(a, b, c)entry. However, multiple-entry notationAgent(a), Agent(b), Agent(c)is silently accepted and interpreted as "only the lastAgent(X)wins" (each declaration overrides the previous whitelist set). There is no warning, no hint, no error — the user sees behavior consistent with canonical syntax for one subagent and denial for the others.
Together, these two issues created a diagnostic dead-end that consumed ~4 hours of a single developer's time before an official-doc re-read disambiguated.
Version affected: Claude Code 2.1.118 (likely the entire 2.1.x line — not bisected).
---
Reproducer
Environment: Ubuntu 24.04 WSL 2, Claude Code 2.1.118 via native installer, defaultMode: bypassPermissions.
mkdir -p /tmp/cc-syntax-repro/.claude/agents && cd /tmp/cc-syntax-repro
cat > .claude/settings.json <<'JSON'
{"agent":"orchestrator","permissions":{"defaultMode":"bypassPermissions","allow":[]}}
JSON
# Variant A: non-documented multi-declaration syntax
cat > .claude/agents/orchestrator.md <<'MD'
---
name: orchestrator
description: Delegate probes to alice and bob.
tools: Read, Agent(alice), Agent(bob)
model: sonnet
---
Invoke Agent(subagent_type="alice") with prompt "ACK probe", then invoke Agent(subagent_type="bob") with prompt "ACK probe". Report each return verbatim.
MD
cat > .claude/agents/alice.md <<'MD'
---
name: alice
description: probe
tools: Read
model: sonnet
---
Respond with the literal line "ACK alice" and stop.
MD
cat > .claude/agents/bob.md <<'MD'
---
name: bob
description: probe
tools: Read
model: sonnet
---
Respond with the literal line "ACK bob" and stop.
MD
claude "invoke alice and bob and report each result verbatim"
Observed:
bob(lastAgent(X)in tools) returnsACK bob.aliceinvocation returns:Agent type 'alice' has been denied by permission rule 'Agent(alice)' from settings.
Expected from the user's perspective (having written both Agent(alice) and Agent(bob)): both subagents are invokable. The user does not read this as two overlapping whitelist declarations — it reads as an additive list.
Correct syntax (per official doc): tools: Read, Agent(alice, bob) — single declaration, both subagents invokable.
---
Debug log excerpts (CC 2.1.118)
2026-04-23T02:22:30.912Z [DEBUG] Applying permission update: Adding 98 allow rule(s) to destination 'projectSettings': ["Agent(alice)", …]
2026-04-23T02:22:37.855Z [INFO] [Stall] tool_dispatch_start tool=Agent toolUseId=toolu_01LXjs… permissionDecisionMs=0
2026-04-23T02:22:37.857Z [WARN] [Stall] tool_dispatch_end tool=Agent toolUseId=toolu_01LXjs… outcome=error durationMs=2
2026-04-23T02:22:37.858Z [DEBUG] Agent tool error (2ms): Agent type 'alice' has been denied by permission rule 'Agent(alice)' from settings.
2026-04-23T02:22:37.858Z [ERROR] Error: Error: Agent type 'alice' has been denied by permission rule 'Agent(alice)' from settings.
Key observation: permissionDecisionMs=0 — the permission engine itself allowed the call in 0 ms. The denial originates 2 ms later in the Agent-tool dispatch handler (Agent tool error (2ms)), which is a different layer. The error message's "from settings" suffix misleads the user toward permissions.allow/deny inspection.
---
Why this matters
Users who write multi-subagent orchestrators typically reach for the notation tools: Agent(a), Agent(b), Agent(c) by analogy with how other comma-separated values work in the same frontmatter line (e.g., Bash(rg:*), Bash(git:*), Bash(wc:*) — each is a separate allowed pattern). The analogy breaks down silently for Agent(...), where the intended semantics is a single allowlist set, not a composable list of patterns.
The misleading error message compounds the problem: it points to the wrong layer entirely. Users begin debugging settings.json permission configuration when the issue is in the calling agent's frontmatter.
In our case (SENTINELLE multi-specialist pipeline with 7 named subagents), we spent approximately 4 hours on a 10-iteration bisect before an official-doc consultation (triggered by a "verify the doc before filing" discipline check) surfaced the canonical syntax. Full timeline: LEARNING 0003 in our internal knowledge base.
---
Proposed fixes (non-exhaustive, either/both welcome)
Fix 1 — disambiguate the error message
Current:
Error: Agent type 'X' has been denied by permission rule 'Agent(X)' from settings.
Proposed:
Error: Agent type 'X' is not in the invoking agent's `tools:` whitelist. Current whitelist: [<the set>]. Reminder: for multiple subagents, use a single `Agent(x, y, z)` entry in `tools:`, not multiple `Agent(x), Agent(y)` entries.
Or minimally:
Error: Agent type 'X' is not allowed by the invoking agent's `tools:` frontmatter whitelist (not a `settings.json` permission rule).
The critical UX win is removing the "from settings" phrase when the source is not settings.
Fix 2 — parse-time warning on non-documented multi-declaration syntax
When parsing tools:, if more than one Agent(...) declaration is encountered (with at least one having a specifier), emit a warning to stderr or to CC's debug log at startup:
warning: .claude/agents/<name>.md: `tools:` contains multiple `Agent(...)` declarations. Only the last specifier set will apply (e.g., `Agent(a), Agent(b)` ⇒ only `b` is invokable). To whitelist multiple subagents, use a single `Agent(a, b, …)` entry. See https://code.claude.com/docs/en/sub-agents#tools.
Ideally this warning surfaces on first invocation of the Agent tool from that file, not only at parse time, so the developer sees it in context.
Fix 3 (optional, nice-to-have) — reject the syntax entirely
If backward compatibility allows, treat multiple Agent(...) declarations as a parse error rather than silently accepting them. This forces the user to the documented syntax. Breaking change, so lower priority.
---
Related but distinct issues
- #17853 —
TypeError: f.description.split is not a function— distinct (slash-command parser, not subagenttools:frontmatter). - #18812 — menu parsing error — distinct.
- #20264 —
bypassPermissionssubagent inheritance — distinct layer. - #38806 —
bypassPermissions2.1.77+ regressions — distinct. - #40580, #34692 —
PreToolUsedeny not consistently honored on Agent — distinct layer.
None cover the specific UX issues reported here.
---
Impact
Low-severity in terms of system correctness (the parser behaves logically given its input), but measurable developer-time cost when it bites. For a single developer, 4 hours. For a multi-specialist orchestrator project in early iteration, the impact is proportional to how many agents are being defined. The disambiguation fix is localized and should be cheap to ship.
---
Acknowledgments
Reported from the SENTINELLE project (private). Full internal investigation trace preserved in .claude/memory/learnings/0001-cc-agent-tools-frontmatter-bug.md (original, partially superseded) and .claude/memory/learnings/0003-tools-frontmatter-syntax-misread.md (corrected after doc audit).
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗