Subagent Bash tool calls are never executed; run ends with terminal_reason "tool_deferred" while reporting success

Status Open
Reported on v2.1.232
Maintainer reply ✓ Yes — bcherny
Activity 4 comments · opened Aug 14, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

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, Grep and ToolSearch calls 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 Bash calls 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.txt left 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:

  1. 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
  2. The payload comes back with terminal_reason: "tool_deferred" and an empty result.
  3. /tmp/probe.txt does not exist. The same command run directly creates it.

Probe B — tool list, shows the boundary:

  1. Repeat with a subagent restricted to Read/Grep (an agent definition whose frontmatter reads tools: 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 allow entry fails like any other, so allow-list matching is not the discriminator. An ask/deny delivery 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 completed with 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 an allow match bypasses classifier adjudication, and I have not confirmed that it does. The one signal that may distinguish them is the tool_deferred terminal 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.

View original on GitHub ↗

4 Comments

Patch76 · 16 days ago

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 sessionKind marker — "sessionKind":"bg" — and I now have failing subagents on both sides of it:

| origin | subagent Bash calls | 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, attachment and system entries, 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:

⎿  Backgrounded agent (↓ to manage · ctrl+o to expand)
⎿  Allowed by auto mode classifier

The subagent then returned only I'll run the command. Its transcript shows one Bash tool_use for echo TEAMS_PROBE, no tool_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 the terminal_reason: it came from an interactive session rather than --output-format json, so I did not capture the field that reads tool_deferred in the headless runs. ask and deny outcomes 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/deny delivery, 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, Write or WebFetch in a subagent, so I cannot speak to those.

bcherny collaborator · 14 days ago

Tried to reproduce this on 2.1.233 on Linux (fresh install, throwaway directory) and could not.

  • Ran your exact headless command (-p --output-format stream-json --verbose --dangerously-skip-permissions) 3 times: every run shows the subagent's Bash tool_use followed by a tool_result containing SUBAGENT_OK, and the final result is correct (stop_reason: "end_turn", terminal_reason: "completed"). No event carried tool_deferred.
  • Same result with a subagent defined via --agents with tools: ["Bash"], and in an interactive session where the subagent ran as a background agent.

tool_deferred only occurs when a PreToolUse hook returns permissionDecision: "defer" — a feature for programs that drive claude -p and resume the session later (docs). Claude Code never defers a tool call on its own.

Could you share: any PreToolUse hooks 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 --verbose stream-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

github-actions[bot] · 14 days ago

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.

Patch76 · 13 days ago

You were right that it does not reproduce — the cause is on my side, and it is a PreToolUse hook of mine returning permissionDecision: "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:

{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"defer"},"continue":true,"suppressOutput":true}

I had read defer as "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 a tool_result. Replacing that pass-through with "print nothing, exit 0" fixes it: pairing tool_use ids against tool_result across 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. PreToolUse hooks here: one user-level shell hook with matcher Bash (the culprit), three project-level shell hooks also on Bash, and two plugin-provided Python hooks — hookify's, registered with no matcher so it sees every tool, and claude-mem's, matcher Read. I grepped hookify's tree: its permissionDecision is only ever deny, never defer (positive control: the key does occur there). No SDK app or wrapper drives Claude Code here; the closest thing is a plugin whose SessionStart hook starts a background worker, which takes no part in permission decisions. The problem does not survive the hook fix, so the --verbose stream-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:

{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"sh /abs/path/deferhook.sh"}]}]}}

where deferhook.sh reads stdin and prints the JSON above. Then:

claude -p 'Spawn one general-purpose subagent whose entire task is: run `echo HI` with Bash and reply with its output.' --output-format json

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_deferred and not the subagent case; and one of my two attempts had the subagent reach for other tools and never call Bash at all — it then answered with a plausible-looking HI that 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_tool marker gets written changed on the same day. All 44 of those attachment records on this disk carry hookName: "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 Bash calls 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 two Bash calls, 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:

  1. A deferred call in a subagent looks unrecoverable in practice: resumption is driven off the session transcript (-r/--resume, --continue, /resume and 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. Ignoring defer there with a warning, the way the interactive and multi-tool_use cases are already ignored, would make the failure mode impossible rather than silent.
  2. subtype: "success" with is_error: false on a run whose terminal_reason is tool_deferred and whose result is empty is what cost the most time here, together with the task notification reporting completed. You already called this out as something you would want to fix; from the outside it is the highest-value change of the three.
  3. The two ignore paths log a warning naming the hook; the honored path logs nothing. And hookName is 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: defer ignored 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.