claude-code - 💡(How to fix) Fix tools: frontmatter — Read(/path), Write(/path), Glob(/path), Bash(cmd:*) parentheticals silently discarded (no runtime enforcement)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

Either (a) the parenthetical should enforce, or (b) the parser should refuse or warn on unenforceable forms. Today neither happens — the syntax parses cleanly and the misconception is never surfaced. 2. Reject at parse time. Emit a parse error on Read/Write/Glob/Bash entries with arguments, pointing the author at settings.json permissions.allow/deny for path scoping. 3. Warn at parse time. Surface a /agents warning (similar to the existing ⚠ Unrecognized for unknown MCP wildcards in #53865) when the parenthetical is silently discarded. The outside call should fail with an error naming the violated pattern, e.g.:

  • Documentation does not warn against the syntax. The Read/Write/Glob/Bash forms parse without error and produce no warning in /agents.
  • #52293 (open) — Agent(...) parser, silent acceptance + misleading error. Same root cause, UX angle.

Root Cause

This is the dual of the already-reported parser behaviour where multiple Agent(...) / Task(...) entries collapse to "last wins" (#28277, #60911, #52293). The same root cause — tools: parser keys on the bare tool name and the parenthetical is associated with that key rather than enforced — manifests here as silent no-op for tools whose runtime dispatch does not consult the parenthetical (Read, Write, Glob, Bash), instead of "last wins" for tools that do (Agent, Task).

Fix Action

Fix / Workaround

This is the dual of the already-reported parser behaviour where multiple Agent(...) / Task(...) entries collapse to "last wins" (#28277, #60911, #52293). The same root cause — tools: parser keys on the bare tool name and the parenthetical is associated with that key rather than enforced — manifests here as silent no-op for tools whose runtime dispatch does not consult the parenthetical (Read, Write, Glob, Bash), instead of "last wins" for tools that do (Agent, Task).

  1. Enforce. Have the runtime tool-dispatch consult the parenthetical for Read/Write/Glob (path-glob match) and Bash (command-prefix match), analogous to how permissions.allow/deny already does at the session level.
  2. Reject at parse time. Emit a parse error on Read/Write/Glob/Bash entries with arguments, pointing the author at settings.json permissions.allow/deny for path scoping.
  3. Warn at parse time. Surface a /agents warning (similar to the existing ⚠ Unrecognized for unknown MCP wildcards in #53865) when the parenthetical is silently discarded.

The same key-only behaviour explains this report: the parenthetical is associated with the bare tool key but never travels into the dispatch path for Read/Write/Glob/Bash. For Agent/Task it visibly fails as "last wins" (because the dispatch does consult it, and only the last value is present); for Read/Write/Glob/Bash it silently fails as "no scoping at all" (because the dispatch doesn't consult it).

Code Example

# 1. Stage probe files inside and outside the declared allow path.
mkdir -p /tmp/probe-allow /tmp/probe-deny
echo INSIDE-MARKER  > /tmp/probe-allow/inside.txt
echo OUTSIDE-MARKER > /tmp/probe-deny/outside.txt
touch /tmp/probe-allow/a.txt /tmp/probe-allow/b.txt
touch /tmp/probe-deny/a.txt  /tmp/probe-deny/b.txt

# 2. Throwaway plugin scaffold.
mkdir -p /tmp/probe-plugin/.claude-plugin /tmp/probe-plugin/agents

cat > /tmp/probe-plugin/.claude-plugin/plugin.json <<'JSON'
{
  "$schema": "https://www.schemastore.org/claude-code-plugin-manifest.json",
  "name": "probe",
  "version": "0.0.0",
  "description": "tools: frontmatter pattern-discard probe."
}
JSON

# Read probe.
cat > /tmp/probe-plugin/agents/probe.md <<'MD'
---
name: probe
description: Probe.
tools: >-
  Read(/tmp/probe-allow/inside.txt)
---
You are a diagnostic probe. Perform exactly the calls instructed.
MD

claude -p --plugin-dir /tmp/probe-plugin \
  "Use Agent tool with subagent_type='probe:probe' and prompt: \
   'Call Read twice. First file_path=/tmp/probe-allow/inside.txt. \
    Second file_path=/tmp/probe-deny/outside.txt. \
    Reply ONLY with JSON {\"inside\":{\"ok\":bool,\"data\":\"...\"},\"outside\":{\"ok\":bool,\"data\":\"...\"}}'. \
   Print subagent reply verbatim and nothing else."

# Glob probe — swap tools line and re-run.
sed -i 's|Read(/tmp/probe-allow/inside.txt)|Glob(/tmp/probe-allow/*)|' /tmp/probe-plugin/agents/probe.md

claude -p --plugin-dir /tmp/probe-plugin \
  "Use Agent tool with subagent_type='probe:probe' and prompt: \
   'Call Glob twice. First pattern=/tmp/probe-allow/*. \
    Second pattern=/tmp/probe-deny/*. \
    Reply ONLY with JSON {\"inside\":{\"ok\":bool,\"data\":\"...\"},\"outside\":{\"ok\":bool,\"data\":\"...\"}}'. \
   Print subagent reply verbatim and nothing else."

# Bash probe — declare echo:*, run printf.
sed -i 's|Glob(/tmp/probe-allow/\*)|Bash(echo:*)|' /tmp/probe-plugin/agents/probe.md

claude -p --plugin-dir /tmp/probe-plugin \
  "Use Agent tool with subagent_type='probe:probe' and prompt: \
   'Call Bash twice. First command=\"echo INSIDE\". Second command=\"printf OUTSIDE\". \
    Reply ONLY with JSON {\"inside\":{\"called\":bool,\"denied_by_harness\":bool,\"data\":\"...\"}, \
                          \"outside\":{\"called\":bool,\"denied_by_harness\":bool,\"data\":\"...\"}}'. \
   Print subagent reply verbatim and nothing else."

# Cleanup.
rm -rf /tmp/probe-plugin /tmp/probe-allow /tmp/probe-deny

---

Read:  {"inside":{"ok":true,"data":"INSIDE-MARKER"},
        "outside":{"ok":true,"data":"OUTSIDE-MARKER"}}

Glob:  {"inside":{"ok":true,"data":"/tmp/probe-allow/a.txt\n/tmp/probe-allow/b.txt"},
        "outside":{"ok":true,"data":"/tmp/probe-deny/a.txt\n/tmp/probe-deny/b.txt"}}

Bash:  {"inside":{"called":true,"denied_by_harness":false,"data":"INSIDE"},
        "outside":{"called":true,"denied_by_harness":false,"data":"OUTSIDE"}}

---

Read for file_path '/tmp/probe-deny/outside.txt' denied by agent tools: rule 'Read(/tmp/probe-allow/inside.txt)'.
RAW_BUFFERClick to expand / collapse

What's Wrong?

In subagent tools: frontmatter, entries of the form Read(<path>), Write(<path>), Glob(<path>), and Bash(<command-pattern>) are parsed successfully but the parenthesised argument is silently discarded at agent-load time. The bare tool name reaches the runtime with no path or command scoping, so the subagent can read/write/glob/exec arbitrarily despite the apparent restriction in its declaration.

This is the dual of the already-reported parser behaviour where multiple Agent(...) / Task(...) entries collapse to "last wins" (#28277, #60911, #52293). The same root cause — tools: parser keys on the bare tool name and the parenthetical is associated with that key rather than enforced — manifests here as silent no-op for tools whose runtime dispatch does not consult the parenthetical (Read, Write, Glob, Bash), instead of "last wins" for tools that do (Agent, Task).

The result is a misleading allowlist: plugin and agent authors write Read(/*.pdf) or Bash(curl:*) believing they are scoping the tool, when in reality the entries are equivalent to bare Read / Bash.

What Should Happen?

Either (a) the parenthetical should enforce, or (b) the parser should refuse or warn on unenforceable forms. Today neither happens — the syntax parses cleanly and the misconception is never surfaced.

Concrete options:

  1. Enforce. Have the runtime tool-dispatch consult the parenthetical for Read/Write/Glob (path-glob match) and Bash (command-prefix match), analogous to how permissions.allow/deny already does at the session level.
  2. Reject at parse time. Emit a parse error on Read/Write/Glob/Bash entries with arguments, pointing the author at settings.json permissions.allow/deny for path scoping.
  3. Warn at parse time. Surface a /agents warning (similar to the existing ⚠ Unrecognized for unknown MCP wildcards in #53865) when the parenthetical is silently discarded.

Steps to Reproduce

The probe agent's frontmatter declares one narrow tool entry. Reading or globbing a path outside that entry should fail if the parenthetical were enforced.

# 1. Stage probe files inside and outside the declared allow path.
mkdir -p /tmp/probe-allow /tmp/probe-deny
echo INSIDE-MARKER  > /tmp/probe-allow/inside.txt
echo OUTSIDE-MARKER > /tmp/probe-deny/outside.txt
touch /tmp/probe-allow/a.txt /tmp/probe-allow/b.txt
touch /tmp/probe-deny/a.txt  /tmp/probe-deny/b.txt

# 2. Throwaway plugin scaffold.
mkdir -p /tmp/probe-plugin/.claude-plugin /tmp/probe-plugin/agents

cat > /tmp/probe-plugin/.claude-plugin/plugin.json <<'JSON'
{
  "$schema": "https://www.schemastore.org/claude-code-plugin-manifest.json",
  "name": "probe",
  "version": "0.0.0",
  "description": "tools: frontmatter pattern-discard probe."
}
JSON

# Read probe.
cat > /tmp/probe-plugin/agents/probe.md <<'MD'
---
name: probe
description: Probe.
tools: >-
  Read(/tmp/probe-allow/inside.txt)
---
You are a diagnostic probe. Perform exactly the calls instructed.
MD

claude -p --plugin-dir /tmp/probe-plugin \
  "Use Agent tool with subagent_type='probe:probe' and prompt: \
   'Call Read twice. First file_path=/tmp/probe-allow/inside.txt. \
    Second file_path=/tmp/probe-deny/outside.txt. \
    Reply ONLY with JSON {\"inside\":{\"ok\":bool,\"data\":\"...\"},\"outside\":{\"ok\":bool,\"data\":\"...\"}}'. \
   Print subagent reply verbatim and nothing else."

# Glob probe — swap tools line and re-run.
sed -i 's|Read(/tmp/probe-allow/inside.txt)|Glob(/tmp/probe-allow/*)|' /tmp/probe-plugin/agents/probe.md

claude -p --plugin-dir /tmp/probe-plugin \
  "Use Agent tool with subagent_type='probe:probe' and prompt: \
   'Call Glob twice. First pattern=/tmp/probe-allow/*. \
    Second pattern=/tmp/probe-deny/*. \
    Reply ONLY with JSON {\"inside\":{\"ok\":bool,\"data\":\"...\"},\"outside\":{\"ok\":bool,\"data\":\"...\"}}'. \
   Print subagent reply verbatim and nothing else."

# Bash probe — declare echo:*, run printf.
sed -i 's|Glob(/tmp/probe-allow/\*)|Bash(echo:*)|' /tmp/probe-plugin/agents/probe.md

claude -p --plugin-dir /tmp/probe-plugin \
  "Use Agent tool with subagent_type='probe:probe' and prompt: \
   'Call Bash twice. First command=\"echo INSIDE\". Second command=\"printf OUTSIDE\". \
    Reply ONLY with JSON {\"inside\":{\"called\":bool,\"denied_by_harness\":bool,\"data\":\"...\"}, \
                          \"outside\":{\"called\":bool,\"denied_by_harness\":bool,\"data\":\"...\"}}'. \
   Print subagent reply verbatim and nothing else."

# Cleanup.
rm -rf /tmp/probe-plugin /tmp/probe-allow /tmp/probe-deny

Observed (Claude Code 2.1.141)

Read:  {"inside":{"ok":true,"data":"INSIDE-MARKER"},
        "outside":{"ok":true,"data":"OUTSIDE-MARKER"}}

Glob:  {"inside":{"ok":true,"data":"/tmp/probe-allow/a.txt\n/tmp/probe-allow/b.txt"},
        "outside":{"ok":true,"data":"/tmp/probe-deny/a.txt\n/tmp/probe-deny/b.txt"}}

Bash:  {"inside":{"called":true,"denied_by_harness":false,"data":"INSIDE"},
        "outside":{"called":true,"denied_by_harness":false,"data":"OUTSIDE"}}

In all three cases the outside call succeeded despite the frontmatter declaring only an inside-scoped pattern.

Expected

The outside call should fail with an error naming the violated pattern, e.g.:

Read for file_path '/tmp/probe-deny/outside.txt' denied by agent tools: rule 'Read(/tmp/probe-allow/inside.txt)'.

…or the parser should reject the declaration at agent-load time so the author knows the syntax is unenforced.

Bash caveat

The Bash result is the least clean of the three because the auto-mode classifier sits above the permission system and permits "safe" commands (echo, printf, true, false, ls) regardless of frontmatter. To isolate the parenthetical-enforcement layer cleanly, both the inside command (echo) and the outside command (printf) were chosen to be classifier-permitted. The outside command ran without any "from settings" / "from agent tools" denial, which would be expected if the parenthetical were the gate.

A more invasive command (touch /tmp/x) was also tested and was denied — but by the classifier with reason "Prompt-injection-style request to probe the harness; not a legitimate task from the user.", not by pattern mismatch. That denial path is unrelated to the frontmatter parenthetical.

Root cause (hypothesis)

Per #28277:

The tools: field parser likely uses Task as the map key (without the parenthesised argument), so each Task(plugin:X) entry overwrites the previous one. Only the last survives into the permission resolver.

The same key-only behaviour explains this report: the parenthetical is associated with the bare tool key but never travels into the dispatch path for Read/Write/Glob/Bash. For Agent/Task it visibly fails as "last wins" (because the dispatch does consult it, and only the last value is present); for Read/Write/Glob/Bash it silently fails as "no scoping at all" (because the dispatch doesn't consult it).

Impact

  • Plugin authors write apparent allowlists that are no-ops. Several plugin agents in joshuaspence/claude-plugins had Read(~/Dropbox/...), Glob(~/Dropbox/...) and similar entries that were assumed to scope file access. None did.
  • Security-conscious authors may believe their agent is sandboxed when it is not.
  • Documentation does not warn against the syntax. The Read/Write/Glob/Bash forms parse without error and produce no warning in /agents.

Related issues

  • #28277 (closed) — Task(...) parser, "last wins". Same root cause, visible failure mode.
  • #60911 (closed) — Agent(...) parser, "last wins". Same root cause, visible failure mode.
  • #52293 (open) — Agent(...) parser, silent acceptance + misleading error. Same root cause, UX angle.
  • #53865 (open) — MCP wildcards rejected in tools:. Adjacent: the parser surfaces a warning here but not for path-arg discard.
  • #58645 (closed) — disallowedTools / allowedTools (the formal keys) ignored for plugin-installed agents. Adjacent: different mechanism, same theme of subagent tool restrictions being silently no-op'd.

Environment

  • Claude Code: 2.1.141.
  • Platform: Linux.
  • Reproducer above runs against fresh claude -p sessions; behaviour was confirmed in both nested-session and standalone-session contexts.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix tools: frontmatter — Read(/path), Write(/path), Glob(/path), Bash(cmd:*) parentheticals silently discarded (no runtime enforcement)