Subagent Bash tool calls are never executed; run ends with terminal_reason "tool_deferred" while reporting success
Preflight
- [x] Searched existing issues — the two closest are #86471 and #86673; both are discussed under Relation to existing reports below.
- [x] Single bug report.
- [x] On the latest published version (2.1.232).
What's Wrong?
Since 2026-08-14, a subagent's Bash tool call is not executed. The agent emits the tool_use, nothing runs, no tool_result arrives, and the run ends there. The task notification reports completed, and the result field carries whatever text preceded the call — usually a one-line preamble, which makes a dead run look like the model answered with a fragment.
The headless payload names the state: a failing run terminates with stop_reason and terminal_reason both tool_deferred, an empty result, and a deferred_tool_use block holding the exact call that never ran — while subtype stays "success" and is_error stays false.
Scope of the failure:
Read,GrepandToolSearchcalls in the same subagents return normally. A subagent whose tool list excludes the shell completed a 21-call audit and returned a full report.- The parent session's own
Bashcalls work throughout, in the same minutes, in the same session. - In a side-effect probe the command did not run: a subagent asked to execute
date -u > /tmp/probe.txtleft no file behind, while the identical command from the parent created it immediately. So it is not a result lost in delivery.
What Should Happen?
The deferred call is either executed and its result returned, or surfaced as an error. In particular subtype / is_error should not read as success when terminal_reason is tool_deferred, and a task notification should not report completed for a run that produced nothing.
Error Messages/Logs
No error is raised, but the terminal state is machine-readable. Failing run (--output-format json, local path redacted):
{"subtype":"success","is_error":false,"num_turns":2,
"stop_reason":"tool_deferred","terminal_reason":"tool_deferred","result":"",
"deferred_tool_use":{"name":"Bash","input":{"command":"python3 -c ..."}}}
Healthy run for contrast:
{"subtype":"success","is_error":false,"num_turns":1,
"stop_reason":"end_turn","terminal_reason":"completed","result":"PROBE_ALIVE"}
The subagent transcript (~/.claude/projects/<proj>/<session>/subagents/agent-<id>.jsonl) simply stops after the call, with no tool_result and no final assistant message. Several runs emit two to four consecutive Bash calls, none answered, before stopping.
Steps to Reproduce
Two separate probes; each is one run.
Probe A — side effect, shows the command never executes:
claude -p 'Use the Agent tool to spawn one general-purpose subagent. The subagent task is exactly: run the shell command date -u > /tmp/probe.txt with the Bash tool, then reply DONE. Then reply with exactly what the subagent replied.' --output-format json- The payload comes back with
terminal_reason: "tool_deferred"and an emptyresult. /tmp/probe.txtdoes not exist. The same command run directly creates it.
Probe B — tool list, shows the boundary:
- Repeat with a subagent restricted to
Read/Grep(an agent definition whose frontmatter readstools: Read, Grep, Glob). That run completes and answers normally.
Reproduction rate on 2026-08-14: 26 of 26 subagent Bash calls unanswered, across 15 runs. Both probes were run from a non-interactive background session; I have not tested whether an interactive session behaves differently, and given that spawn defaults differ between the two, that may matter.
Claude Model
Parent session Opus 5 (1M context). Reproduced with subagents on both the inherited model and Haiku 4.5.
Is this a regression?
Yes. Pairing every Bash tool_use id against its tool_result across all subagent transcripts in one project directory:
| date | answered | unanswered | of those, mid-run |
|---|---|---|---|
| 2026-08-04 | 589 | 0 | 0 |
| 2026-08-05 | 423 | 0 | 0 |
| 2026-08-06 | 207 | 2 | 1 |
| 2026-08-07 | 495 | 4 | 1 |
| 2026-08-08 | 246 | 7 | 4 |
| 2026-08-09 | 660 | 11 | 5 |
| 2026-08-10 | 491 | 16 | 13 |
| 2026-08-11 | 149 | 4 | 3 |
| 2026-08-12 | 306 | 5 | 3 |
| 2026-08-13 | 202 | 6 | 5 |
| 2026-08-14 | 0 | 26 | 11 |
3,768 answered against 55 unanswered over the ten preceding days. Of those 55, 20 are the last call of an interrupted transcript, which has no result by construction; the remaining 35 are mid-run drops — the same symptom at a low rate, first appearing on 08-06 and peaking at 13 on 08-10. So this did not switch on cleanly on 08-14; it went from a background rate to total.
Last Working Version
None that I can name. 2.1.229 was the last version under which subagent shell calls worked (2026-08-13), but running the same probe against the 2.1.229 binary still on disk today reproduces the same failure mode. Since an older client fails identically in the same environment, the change is unlikely to be in the client release — which also means the same-day update to 2.1.232 here (installed 06:12 UTC, first truncated run 06:46 UTC) is a coincidence in timing, not the trigger.
Version
2.1.232
---
What I checked locally, and how far it goes
- Client version — the same probe run directly against the 2.1.229 and 2.1.232 binaries, same environment, same prompt: same failure mode.
- Permission configuration — a command matched by an exact, wildcard-free
allowentry fails like any other, so allow-list matching is not the discriminator. Anask/denydelivery path is untested. - Local
PreToolUse(Bash)hooks — the three configured here return exit 0 with no measurable delay when fed the real payload shape. That measures them outside a subagent and does not cover their behaviour inside one. My own hook edits that day landed five hours after the day's first truncated run. - Context size — subagent token totals from the notifications: completed runs at 27,815 / 34,051 / 59,148; truncated runs at 28,585 / 28,644 / 40,260 / 40,290 / 40,482 / 41,290 / 70,590. The largest run of the day is truncated and exceeds every completed run; the smallest run of the day is a completed one, and the ranges interleave from 28,585 upward.
- Environment restart — container uptime spans the whole onset window.
Relation to existing reports
- #86471 (background subagents report
completedwith empty or partial results) proposes a context/size cliff around 100–150k subagent tokens. Every run in my data sits below that threshold, so my numbers cannot speak to the cliff itself; what they show is that failures here do not sort by size. That reporter's runs were an order of magnitude larger and image-heavy, so a size effect in their setup is not ruled out by anything I measured. - #86673 (safety-classifier unavailability blocking
Bash/Skill/Agent/MCP) matches the shape closely — read-only file operations keep working while shell calls do not. I cannot separate the two cleanly: an exactly allow-listed command still fails here, which would argue against a permission path, but only if anallowmatch bypasses classifier adjudication, and I have not confirmed that it does. The one signal that may distinguish them is thetool_deferredterminal reason above; a classifier denial has its own path and, in my parent session, surfaces as a visible denial rather than silence.
I may be wrong about these being separate. If a maintainer reads them as one bug, merging is fine by me.
Workaround
Restrict the agent's tool list rather than instructing it. An agent defined with tools: Read, Grep, Glob has not hit the failure in any of my runs — though I have not exercised Glob, Edit, Write or WebFetch in a subagent, so I cannot speak to those. A prompt-level prohibition is not enough: one agent emitted a no-op echo despite an explicit instruction not to use the shell, and ended there.
Environment: Alpine Linux v3.21 on x86_64, non-interactive background session.
4 Comments
Two updates. The first closes the open question at the end of Steps to Reproduce; the second sharpens the permission framing, which was too strong.
Session kind is not the discriminator. The report left this untested and flagged that it might matter. It does not. Transcripts here carry a
sessionKindmarker —"sessionKind":"bg"— and I now have failing subagents on both sides of it:| origin | subagent
Bashcalls | answered ||---|---|---|
| a session whose entries carry the marker | 14 | 0 |
| three sessions started with
claude -p, no marker on any entry | 2 each | 0 each |One caveat on that marker so nobody reproduces my classification wrongly: it is not on every line. It sits on
user,assistant,attachmentandsystementries, and not on the head-of-file metadata records (mode,permission-mode,last-prompt). Per session it is all-or-nothing across the entries that carry it — I found no session mixing both — but I have not confirmed what the marker actually tracks, so treat "background" here as my label, not as its documented meaning.Permission is not the discriminator either — though this half is UI-only evidence. The two lines below are terminal rendering; the run's transcript records no trace of them, so take them as a report of what I saw rather than as a log excerpt. The failure itself is persisted, and that part is not in question.
Running a probe of the same shape from a session with a human at the terminal, the CLI printed, in order:
The subagent then returned only
I'll run the command.Its transcript shows oneBashtool_useforecho TEAMS_PROBE, notool_result,stop_reason: "tool_use", and no final message. So a call the classifier visibly allowed still did not run. What I cannot add for this particular run is theterminal_reason: it came from an interactive session rather than--output-format json, so I did not capture the field that readstool_deferredin the headless runs.askanddenyoutcomes remain untested.Worth noting separately: that spawn was backgrounded even though a human was driving the session.
Where that leaves the axes. Measured and excluded: client version (2.1.229 and 2.1.232 alike), session kind, allow-list coverage, and an explicit classifier allow. Not excluded, and I do not want to overstate them:
ask/denydelivery, and context size — my runs span roughly 28k to 71k subagent tokens and do not sort by size within that range, but they say nothing about the 100–150k cliff proposed in #86471.The only thing I have found that avoids the failure is a subagent whose tool list contains no shell, and I have not exercised
Glob,Edit,WriteorWebFetchin a subagent, so I cannot speak to those.Tried to reproduce this on 2.1.233 on Linux (fresh install, throwaway directory) and could not.
-p --output-format stream-json --verbose --dangerously-skip-permissions) 3 times: every run shows the subagent's Bashtool_usefollowed by atool_resultcontainingSUBAGENT_OK, and the final result is correct (stop_reason: "end_turn",terminal_reason: "completed"). No event carriedtool_deferred.--agentswithtools: ["Bash"], and in an interactive session where the subagent ran as a background agent.tool_deferredonly occurs when aPreToolUsehook returnspermissionDecision: "defer"— a feature for programs that driveclaude -pand resume the session later (docs). Claude Code never defers a tool call on its own.Could you share: any
PreToolUsehooks in your user/project settings or from plugins, whether you run Claude Code under an SDK app or wrapper that adds hooks, whether the problem persists with hooks removed, and the full--verbosestream-json of a failing run (redacted)? If a defer-returning hook is firing on a subagent's tool call, the dead-run-reported-as-completed behavior you describe is exactly what we'd want to fix, and those details would let us pin it down.🤖 Generated with Claude Code
We weren't able to reproduce this. Could you provide steps to trigger the issue — what you ran, what happened, and what you expected? This issue will be closed automatically if there's no activity within 7 days.
You were right that it does not reproduce — the cause is on my side, and it is a
PreToolUsehook of mine returningpermissionDecision: "defer".What the hook did. A user-level hook (matcher
Bash) gates five command classes of mine and passes everything else through. Its pass-through emitted:I had read
deferas "no opinion, carry on". It is the feature you linked. Nothing here resumes a session, and of the 44 deferred calls recorded on this disk, none ever received atool_result. Replacing that pass-through with "print nothing, exit 0" fixes it: pairingtool_useids againsttool_resultacross all my subagent transcripts gives 0 answered / 39 unanswered on 08-14, 0/13 on 08-15, 0/1 on 08-16, and, after the change on 08-17, dozens answered again — the only unanswered calls left that day are the ones from the defer testbed described below. (These daily counts cover every project directory; my original table counted one.)Answering your questions.
PreToolUsehooks here: one user-level shell hook with matcherBash(the culprit), three project-level shell hooks also onBash, and two plugin-provided Python hooks — hookify's, registered with no matcher so it sees every tool, and claude-mem's, matcherRead. I grepped hookify's tree: itspermissionDecisionis only everdeny, neverdefer(positive control: the key does occur there). No SDK app or wrapper drives Claude Code here; the closest thing is a plugin whoseSessionStarthook starts a background worker, which takes no part in permission decisions. The problem does not survive the hook fix, so the--verbosestream-json of a failing run is no longer the useful artefact — the repro below is smaller.Minimal repro, in an empty directory, with this
.claude/settings.json:where
deferhook.shreads stdin and prints the JSON above. Then:Two caveats so you do not chase a phantom: the model sometimes runs the command itself instead of delegating, in which case you get the parent-level
tool_deferredand not the subagent case; and one of my two attempts had the subagent reach for other tools and never callBashat all — it then answered with a plausible-lookingHIthat nothing had produced. Pin the subagent to a shell call if you want it deterministic.The part I think is worth your attention: when the
hook_deferred_toolmarker gets written changed on the same day. All 44 of those attachment records on this disk carryhookName: "settings", and they split like this:| when | records | where |
|---|---|---|
| 07-18 … 08-13 | 34 | every one in a subagent sidechain |
| 08-14 | 7 + 1 | 7 in parent transcripts, 1 in a subagent |
| 08-17 (my testbed) | 2 | both in the parent |
So until 08-13 the marker was written on the subagent path and nowhere else. From 08-14 it essentially stops there: of 53 unanswered subagent
Bashcalls across 08-14 to 08-16, exactly 1 carries a marker, where on 08-10 it was 8 of 16 and on 08-13 4 of 6. My testbed reproduces the same shape — the parent's deferred call persisted its attachment, its subagent emitted twoBashcalls, received nothing, and left no attachment behind. I am reporting the correlation, not a cause; it coincides with 2.1.232, whose changelog line I quote below. If it is real, the one durable trace of a deferred subagent call disappeared exactly when background spawns became the default, which is a large part of why this took three days to find.Three smaller observations:
-r/--resume,--continue,/resumeand friends), and a subagent sidechain is not a session anyone resumes — inference on my part, from the marker's absence rather than from reading the resume path end to end. Ignoringdeferthere with a warning, the way the interactive and multi-tool_usecases are already ignored, would make the failure mode impossible rather than silent.subtype: "success"withis_error: falseon a run whoseterminal_reasonistool_deferredand whoseresultis empty is what cost the most time here, together with the task notification reportingcompleted. You already called this out as something you would want to fix; from the outside it is the highest-value change of the three.hookNameis the literal string"settings"in all 44 records, which cannot tell four same-source hooks apart. Either would have pointed at the right file immediately.One correction to my own report while I am here. The issue body says the local
PreToolUse(Bash)hooks "return exit 0 with no measurable delay when fed the real payload shape". That is true of the culprit — it exits 0. It also prints the defer JSON on stdout, which my check never looked at, and it was a fourth hook the sentence did not count. So I measured the wrong property, not merely the wrong scope. The reason the parent session kept working while every subagent that used the shell died is the condition your client already implements:deferignored in interactive sessions, honored when a single tool call is in flight non-interactively. On my side the step change on 08-14 lines up with 2.1.232's "non-teammate agent spawns in interactive sessions now run in the background by default"; that those background spawns then land on the honored side of the interactive check is my inference, not something I verified.I no longer think this shares a cause with #86673, so that one should not be merged with it. Close this if you like, or keep it scoped to the marker change and points 1–3 — your call, and thanks for the pointer that made it findable.