Problem
--allowedTools is documented and intuitive as a whitelist, but at runtime it
behaves additively — it appends to the Claude CLI's default safe-list of
built-in tools rather than replacing it. The only way to truly restrict a
subprocess to a specific tool set today is to enumerate everything else in
--disallowedTools, which means tracking every new built-in tool Claude Code
ships.
Reproduction
Spawn a Claude Code subprocess with:
claude --print --output-format stream-json \
--allowedTools Write \
--disallowedTools Bash,PowerShell,Edit,MultiEdit,NotebookEdit,Glob,Grep,Task,ToolSearch,WebFetch,WebSearch \
--strict-mcp-config --mcp-config <per-run-config>.json \
--model claude-opus-4-7 \
...
The intent is "this subprocess should only call Write." In the
agent_session_init event, the subprocess actually has 20 built-in tools
available:
AskUserQuestion, CronCreate, CronDelete, CronList,
EnterPlanMode, EnterWorktree, ExitPlanMode, ExitWorktree,
ListMcpResourcesTool, Monitor, PushNotification, Read,
ReadMcpResourceTool, RemoteTrigger, ScheduleWakeup,
Skill, TaskOutput, TaskStop, TodoWrite, Write
In our case the agent auto-invoked Skill('init') (the built-in /init
skill that generates CLAUDE.md from codebase analysis), then re-read its
own output file twice, ballooning a 130 s / $0.85 step into 255 s / $1.94
with 9 turns instead of the expected 3.
Use case context
We run Claude Code as a non-interactive subprocess from a Python orchestrator
("agent step in a multi-step pipeline"). Each step has a narrow contract —
e.g., "read inline-injected inputs, emit two files, exit." We've already done:
- Fake
HOME with only .credentials.json copied (no ~/.claude.json, no
user skills, no CLAUDE.md memory)
- Scrubbed
CLAUDE_* and CLAUDECODE env vars
--strict-mcp-config with a per-run config listing only the MCP servers
the step needs
--allowedTools listing only what the step's prompt requires
User-installed state (skills, MCP servers, settings) is correctly isolated.
But built-in tools ship inside the claude binary itself — filesystem
isolation can't strip them, so they leak into every subprocess.
Proposed solution
A flag like:
--strict-tools # symmetric to --strict-mcp-config
Semantics: --allowedTools becomes an exclusive whitelist. Built-in tools
not in --allowedTools are unavailable (same effect as listing them in
--disallowedTools), without callers having to enumerate them.
Alternatively, a complementary flag:
--default-tools none # disable all built-in tools by default
So callers can opt out of the default safe-list once instead of subtracting
each tool by name.
Why this matters
Without it, every Claude Code release that adds a built-in tool potentially
breaks downstream agentic harnesses. We've already hit this twice in the past
month — Skill and ScheduleWakeup showed up in subprocesses that hadn't
asked for them. The maintenance burden falls on each caller to keep their
--disallowedTools list in sync with the CLI's tool catalogue across versions.
A strict-mode flag would let callers express the constraint they actually
mean ("only these tools, please") and stay forward-compatible.
Workaround (today)
Enumerate every current built-in in --disallowedTools:
Bash,PowerShell,Edit,MultiEdit,NotebookEdit,Glob,Grep,Task,ToolSearch,
WebFetch,WebSearch,Read,Skill,TodoWrite,AskUserQuestion,EnterPlanMode,
ExitPlanMode,EnterWorktree,ExitWorktree,ScheduleWakeup,CronCreate,
CronDelete,CronList,Monitor,PushNotification,RemoteTrigger,TaskOutput,
TaskStop,ListMcpResourcesTool,ReadMcpResourceTool
This works but is fragile — every new built-in is silently opt-in until it
breaks something.
Related
--strict-mcp-config already exists and solves the exact same problem for
MCP servers; the same pattern for built-in tools would be a natural
extension.
Thanks for considering!
Problem
--allowedToolsis documented and intuitive as a whitelist, but at runtime itbehaves additively — it appends to the Claude CLI's default safe-list of
built-in tools rather than replacing it. The only way to truly restrict a
subprocess to a specific tool set today is to enumerate everything else in
--disallowedTools, which means tracking every new built-in tool Claude Codeships.
Reproduction
Spawn a Claude Code subprocess with:
The intent is "this subprocess should only call
Write." In theagent_session_initevent, the subprocess actually has 20 built-in toolsavailable:
In our case the agent auto-invoked
Skill('init')(the built-in/initskill that generates
CLAUDE.mdfrom codebase analysis), then re-read itsown output file twice, ballooning a 130 s / $0.85 step into 255 s / $1.94
with 9 turns instead of the expected 3.
Use case context
We run Claude Code as a non-interactive subprocess from a Python orchestrator
("agent step in a multi-step pipeline"). Each step has a narrow contract —
e.g., "read inline-injected inputs, emit two files, exit." We've already done:
HOMEwith only.credentials.jsoncopied (no~/.claude.json, nouser skills, no
CLAUDE.mdmemory)CLAUDE_*andCLAUDECODEenv vars--strict-mcp-configwith a per-run config listing only the MCP serversthe step needs
--allowedToolslisting only what the step's prompt requiresUser-installed state (skills, MCP servers, settings) is correctly isolated.
But built-in tools ship inside the
claudebinary itself — filesystemisolation can't strip them, so they leak into every subprocess.
Proposed solution
A flag like:
Semantics:
--allowedToolsbecomes an exclusive whitelist. Built-in toolsnot in
--allowedToolsare unavailable (same effect as listing them in--disallowedTools), without callers having to enumerate them.Alternatively, a complementary flag:
So callers can opt out of the default safe-list once instead of subtracting
each tool by name.
Why this matters
Without it, every Claude Code release that adds a built-in tool potentially
breaks downstream agentic harnesses. We've already hit this twice in the past
month —
SkillandScheduleWakeupshowed up in subprocesses that hadn'tasked for them. The maintenance burden falls on each caller to keep their
--disallowedToolslist in sync with the CLI's tool catalogue across versions.A strict-mode flag would let callers express the constraint they actually
mean ("only these tools, please") and stay forward-compatible.
Workaround (today)
Enumerate every current built-in in
--disallowedTools:This works but is fragile — every new built-in is silently opt-in until it
breaks something.
Related
--strict-mcp-configalready exists and solves the exact same problem forMCP servers; the same pattern for built-in tools would be a natural
extension.
Thanks for considering!