Skip to content

Claude Code scoped disallowedTools rules remove Bash from --agent sessions #23

Description

@durandom

Summary

When a named Claude Code agent contains a scoped Bash rule in its frontmatter, such as:

disallowedTools: Bash(sed *)

Claude Code removes the entire Bash tool from the top-level --agent session. The problem is not the --agent flag by itself.

The resulting error is:

Error: No such tool available: Bash. Bash exists but is not enabled in this context.

Confirmed with Claude Code v2.1.156 in the fullsend sandbox and v2.1.207 locally.

Validated root cause

Controlled tests on v2.1.207:

Agent configuration Tool count Bash available?
No --agent 24 Yes
Named agent, no tool restrictions 24 Yes
disallowedTools: Write, Edit 22 Yes
disallowedTools: Bash(sed) 23 No
disallowedTools: Bash(sed *) 23 No
Full code-agent Bash deny patterns 23 No

The same result occurs with --bare, which disables hooks, plugins, custom settings, and other integrations.

Claude's documentation says a scoped rule such as Bash(rm *) should keep Bash visible and deny only matching commands. Named sessions launched with --agent are documented to use the agent's tool restrictions. The observed behavior therefore appears to be a Claude Code bug in scoped disallowedTools handling.

Documentation:

Minimal reproduction

Run a named agent with a scoped Bash deny rule:

claude \
  --agents '{"bash-test":{"description":"Minimal test agent","prompt":"Reply OK.","disallowedTools":["Bash(sed *)"]}}' \
  --agent bash-test \
  --print --verbose --output-format stream-json \
  'Reply exactly OK without tools.' |
jq 'select(.type == "system" and .subtype == "init") |
    {tool_count: (.tools | length), has_bash: (.tools | index("Bash") != null)}'

Actual result:

{"tool_count":23,"has_bash":false}

Control case: remove disallowedTools from the inline agent definition. Bash remains present and the tool count is 24.

Fullsend impact

Fullsend launches named sessions with:

claude --agent '<name>' --dangerously-skip-permissions

The upstream code and fix agents currently contain multiple scoped Bash rules, including Bash(sed *), Bash(awk *), and Bash(git push *). As a result, those top-level agents start without Bash and delegate shell work to subagents.

The first observed production example was:

This confirms significant overhead, although the exact incremental cost and slowdown require a controlled comparison with an equivalent fixed run.

How we discovered it: the agent self-recovers through subagents

The failure did not initially look like a hard runtime error because the top-level agent worked around it autonomously. The transcript from the cited run shows the complete recovery path:

Time (UTC) Event
09:35:09 The top-level code agent attempts its required Bash setup command and receives No such tool available: Bash.
09:35:28 It explicitly recognizes the limitation: “I don't have direct Bash access in this context” and decides to spawn a subagent.
09:35:36 It spawns another subagent_type: code to perform the implementation. That agent inherits the same broken scoped disallowedTools rules, attempts Bash twice, and also fails.
09:39:36 The top-level agent probes a generic subagent_type: claude with echo, pwd, and git status. Bash works immediately.
09:40:11 Having identified a working agent type, it delegates the complete implementation to a generic claude subagent.
10:08:53 The generic subagent completes the implementation after 75 tool calls, including 49 Bash calls.
10:08:59 The top-level agent spawns a second generic claude subagent to verify the branch, commit, diff, and output file using six more Bash calls.
10:09:46 The top-level agent reports success.

The resulting session tree was:

Session Role Tool calls Bash calls Bash availability errors
Top-level code agent Orchestration 12 1 1
Spawned code agent First implementation attempt 47 2 2
Generic claude agent Bash availability probe 1 1 0
Generic claude agent Actual implementation 75 49 0
Generic claude agent Final verification 6 6 0

This behavior is important for diagnosis:

  • The run succeeds instead of failing fast, so the underlying tool-inventory bug is easy to miss.
  • Bash works in the same sandbox when a generic claude agent is used, ruling out the sandbox image and OpenShell policy as the cause.
  • A spawned code agent fails in the same way as the top-level code agent, tying the failure to that agent definition.
  • The workaround multiplies contexts and tool calls: the top-level session performed only 12 tool calls, while the complete agent tree performed 141.
  • The successful result therefore masks substantial latency and token cost caused by automatic delegation.

The evidence is in the fullsend-code artifact attached to:

Findings ruled out

  • --agent alone: a named agent without tool restrictions retains Bash.
  • Security hooks: reproduced with --bare; hooks cannot explain the missing init tool.
  • Managed agent definitions: the minimal inline-agent reproduction does not depend on them.
  • Built-in code agent: Claude Code has no built-in agent with that name.
  • agent: "" harness override: fullsend composition treats an empty scalar as unset and inherits the base agent, so this does not clear the agent field.

Workarounds and proper fix

Interim workaround

Remove scoped disallowedTools: Bash(...) entries from the code and fix agent frontmatter. This restores Bash access.

Implemented for rhdh-agentic in:

Do not rely on a frontmatter Bash allowlist with fullsend

A definition such as tools: Bash(gh,echo) keeps Bash in the inventory. However, fullsend uses --dangerously-skip-permissions; under that mode, testing showed that an unlisted sed command still executed. This is not suitable for command-level enforcement in fullsend.

Recommended fullsend fix

Use permissions.deny in the generated project .claude/settings.json.

Validated on v2.1.207 with --dangerously-skip-permissions:

  • echo executed successfully.
  • sed remained a Bash tool call but was denied specifically.
  • Bash stayed present in the init tool inventory.

Fullsend already generates /sandbox/workspace/.claude/settings.json for hooks, but the generated structure currently has no permissions block. Extend the harness schema and settings generator to support scoped deny rules, then remove the broken frontmatter rules.

Related upstream reports

These are related tool-inventory bugs, but they are not the same minimal reproduction:

Related fullsend-ai work

A search of open and closed issues and pull requests in fullsend-ai/fullsend found the following related work:

Item Status Relationship
fullsend#303 Open Direct implementation issue for adding permissions.deny to generated .claude/settings.json. This is the primary fullsend-side fix.
fullsend#4437 Open Direct workaround issue for removing broken disallowedTools entries from code/fix agents. The actual agent definitions now live in fullsend-ai/agents.
fullsend#558 Open Related linting proposal for agent frontmatter. Its premise that tools and disallowedTools are inert under --agent is outdated and should be corrected.
fullsend#3874 Closed Earlier production evidence of the same recovery pattern: a code agent without Bash falls back to a generic claude subagent, adding time, tool calls, and cost.
fullsend PR #293 Merged Added ADR-0027 and selected permissions.deny as the intended steering mechanism. The ADR's claim that agent frontmatter is inert under --agent is now stale.
fullsend PR #2098 Merged Fixed adjacent subagent-dispatch and agent-definition problems, but does not fix the top-level scoped-disallowedTools bug.
fullsend#2826 Open Adjacent work on separating permission profiles between inference and deterministic post-run automation.

No open pull request currently implements permissions.deny, a permissions_deny harness field, or an extension to GenerateClaudeSettings().

Work-item split

  • This issue: Claude Code bug, minimal reproduction, and production evidence.
  • fullsend#303: generate and install scoped permissions.deny rules for top-level agent sessions.
  • fullsend#4437 / fullsend-ai/agents: remove the broken code/fix frontmatter rules and eliminate per-repository agent forks.

Action items

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions