canUseTool callback not invoked for background subagent tool calls in default permission mode
Summary
When using the Claude Agent SDK with a custom canUseTool callback in default permission mode, background subagents spawned via the Task tool (run_in_background: true) do not route tool permission requests through the callback. The SDK denies their tool calls internally without ever invoking canUseTool. Additionally, these internal denials can corrupt the parent session's transport, causing subsequent main-agent tool calls to fail with "Stream closed".
This bug affects default mode specifically. In bypassPermissions mode, everything works correctly. Reproduced with both personal accounts (with bypassPermissions access) and corporate accounts (without), confirming the bug is in default mode handling.
Environment
- Claude Agent SDK: 0.2.42 (bundled with Claude Code 2.1.42)
- Platform: Linux (Debian)
Reproduction
A standalone reproduction case is available at: https://github.com/nicolasnoble/claude-sdk-bug-report
git clone https://github.com/nicolasnoble/claude-sdk-bug-report
cd claude-sdk-bug-report
npm install
npx tsx repro.ts
# For non-default credential locations:
CLAUDE_CONFIG_DIR=~/.claude-work npx tsx repro.ts
The script starts a session in default mode with a canUseTool callback that auto-allows all tools and logs every invocation. It triggers Claude to spawn a background subagent that runs a simple Bash command, then reports whether canUseTool was invoked.
Expected output
[canUseTool] canUseTool called: Task (agentID=main, ...)
[canUseTool] canUseTool called: Bash (agentID=main, ...)
[canUseTool] canUseTool called: Bash (agentID=<subagent-id>, ...)
Subagent calls (1+):
canUseTool called: Bash (agentID=<subagent-id>, ...)
OK: canUseTool was called for subagent Bash calls.
Actual output
[canUseTool] canUseTool called: Task (agentID=main, ...)
[canUseTool] canUseTool called: Bash (agentID=main, ...)
Subagent calls (0):
BUG CONFIRMED: canUseTool was never called for subagent tool uses.
The subagent reports "Permission to use Bash has been denied" without canUseTool ever being invoked.
Additional observations
bypassPermissionsmode works correctly: subagent tool calls are properly handled. The bug is specific todefaultmode.- Silent permission mode downgrade: requesting
bypassPermissionson an account without access silently falls back todefaultwith no error or warning. The only way to discover this is by inspecting thepermissionModefield on thesystem/initmessage. - Transport corruption: after background subagent permission denials, subsequent main-agent Bash calls fail with "Tool permission request failed: Error: Stream closed". The transport self-heals after several minutes.
- Background subagents are isolated from all permission mechanisms: we tested all three available approaches and none reach background subagents:
canUseToolcallback: never invoked for background subagent tool callsallowedToolsin query options: does not propagate to background subagentsBash(*)in user-level settings (the same settings file the session uses, confirmed viaCLAUDE_CONFIG_DIR): does not propagate to background subagents- The
canUseTooltype definition includesagentID?: stringfor subagent identification, suggesting the callback is designed to handle subagent tool calls.
Bugs
canUseToolnot called for background subagents indefaultmode: the SDK denies tool calls internally without invoking the callback. Background subagents appear completely isolated from all three permission mechanisms.- Transport corruption: internal subagent denials break the parent session's transport ("Stream closed"), requiring several minutes to self-heal.
- Silent permission mode downgrade: no error or warning when
bypassPermissionsis unavailable - the SDK silently falls back todefault.
11 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
These are related but not duplicates:
PermissionRequesthooks not firing for subagentsPreToolUsehooks not being enforced on subagentsThis issue is specifically about the SDK
canUseToolcallback not being invoked for background subagents, plus a transport corruption bug where the internal denials cause the parent session's Bash to break with "Stream closed" for several minutes.All three issues likely share the same root cause (subagents don't inherit the parent's permission infrastructure), but the symptoms, affected APIs, and reproduction steps are different. This one includes a standalone reproduction case and documents the transport corruption cascade which the other two don't cover.
Still reproducible on latest Claude Code. The standalone repro at https://github.com/nicolasnoble/claude-sdk-bug-report demonstrates all three symptoms.
Quick update: I've been running SDK 2.1.76 extensively over the past couple weeks with heavy background subagent usage (5+ concurrent background agents doing Bash calls, file edits, grep, etc. in both
defaultandbypassPermissionsmodes) and haven't been able to reproduce this anymore. ThecanUseToolcallback fires correctly for background tasks, and no transport corruption after denials.Going to keep monitoring for another week or so before considering this fully resolved, but it's looking good so far.
Correction on my earlier update: the bug is still present in SDK 2.1.76. It reproduced today during a session that used a background subagent to do research (Read/Grep tools). After the subagent completed, Bash transport was permanently dead for the remainder of the session, and MCP tool transports also went down. Built-in tools (Read, Glob, Grep, Edit) continued working.
The reproduction is intermittent - I ran 10+ concurrent background agents yesterday without triggering it, then a single background agent today caused it. The pattern remains: background subagent completes, subsequent Bash calls in the main context fail silently, and the transport never recovers within the session.
LongBash(a separate MCP server wrappingchild_process.execSync) worked as a workaround since it uses its own transport.New reproduction details that significantly narrow the bug:
Reproduction: Launch 5+ background subagent sessions (
run_in_background: true) concurrently indefaultpermission mode. The subagents only need to use built-in tools (Read, Write, Glob, Grep) - no Bash or MCP tools required. After all subagents complete successfully, the main context's Bash transport is permanently dead ("Tool permission request failed: Error: Stream closed"). All MCP tool transports also die. Built-in tools continue working.Key findings:
/tmpis sufficient.bypassPermissionsmode - onlydefaultmode is affected.run_in_background) work fine with Bash indefaultmode.canUseToolpermission callback when multiple background agents fire tool calls concurrently. InbypassPermissionsmode the callback is skipped entirely, which is why it's unaffected.Workaround: Don't use
run_in_background: truefor agents that need Bash/MCP tools. Background agents are safe for read-only built-in tools, but running many in parallel will corrupt the main context's transport. Use foreground agents for anything that needs Bash.Root cause identified by instrumenting
sdk.mjsand reading throughcli.js(SDK 2.1.76):The bug has two components:
1. Background agents get
shouldAvoidPermissionPrompts: trueIn the agent execution path, background/async agents have their
toolPermissionContextmodified:For SDK-spawned background agents (
isAsync: true,canShowPermissionPrompts: undefined),shouldAvoidPermissionPromptsis alwaystrue.2. Bash is not on the auto-mode tool allowlist
The CLI maintains an allowlist of tools that are safe to auto-approve without prompting (Read, Grep, Glob, Edit, etc.). Bash is deliberately excluded - and rightly so, since Bash can execute arbitrary commands and shouldn't be silently auto-approved in the general CLI case.
The problem is what happens when
shouldAvoidPermissionPromptsis true and a tool isn't on this allowlist: the CLI denies the tool internally withdecisionReason: { type: "asyncAgent" }without ever sending acan_use_toolcontrol request to the SDK. This means the SDK'scanUseToolcallback is never invoked for Bash in background agents - the denial happens inside the CLI before it reaches the SDK layer.3. Transport corruption is a separate downstream effect
I instrumented
ProcessTransport.write()with concurrent-write detection andQuery.handleControlRequest()with concurrency tracking. Findings:can_use_toolcontrol requestsmcp_messagecontrol requests during MCP server initialization4. Root cause in the subagent permission wrapper - hardcodes internal permission check
The subagent permission wrapper function constructs a
canUseToolfor each subagent. It always calls the internal permission checker as its first pass:The internal permission checker is hardcoded - even when the CLI is running in SDK mode with a
canUseToolcallback registered via--permission-prompt-tool stdio. The SDK's callback is the outercanUseToolfunction that replaces the internal checker at the top level, but the subagent wrapper bypasses it by calling the internal checker directly.Suggested fix:
The fix is to pass the parent's
canUseToolfunction into the subagent wrapper instead of hardcoding the internal checker. The infrastructure for this already exists:sdkUrlis set, permission prompt tool is"stdio")canUseToolis already correctly wired to the SDK callback (viacreateCanUseTool()whenpermissionPromptTool === "stdio")canUseToolas an additional parameterThe call site already has access to the parent's
canUseTool(it's a parameter of the enclosing agent execution function), so threading it through is straightforward.Alternatively, the
shouldAvoidPermissionPromptsdeny path (thetype: "asyncAgent"denial) could check forrequireCanUseToolon the toolUseContext and return"ask"instead of"deny"when the SDK callback is registered. This field already exists and is used by the speculation system for exactly this purpose.Still broken as of 25 March 2026. Filed #38859 and #38662 with current version details and new observations (parallel calls bypass, sequential don't; agents completely blocked).
This is a prerequisite for background subagent permission escalation (see #47339). If canUseTool fired for background agents, the runtime could intercept auto-denies and route them to the parent session instead. Currently background agents silently fail on any unapproved tool call.
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.