[Bug] Subagent delegation lacks timeout, monitoring, and abort controls—caused 12+ hour session hang
Bug Description
Title: Subagent delegation has no timeout, no monitoring, and no sanity checks — caused multi-hour session failure
During a session involving GitHub Actions workflow review, plan mode was activated. Claude Code was asked to verify a claim it had made about a specific GitHub Action.
This required a single web search that would have completed in under 10 seconds via a direct WebFetch call.
Critical Issue - Zero sanity checks on subagent execution:
When Claude Code spawns a subagent, there is no mechanism available to the user or to Claude Code itself to bound, monitor, or abort that subagent's execution.
Specifically:
- There is no user-configurable timeout. Users cannot set a parameter such as "this subagent task should not exceed 60 seconds."
- The main Claude Code process has no check-in interval. It simply waits for the subagent to return with no periodic status reporting.
- There is no abort trigger. Once a subagent is spawned, the user cannot cancel it.
- There is no progress visibility. The user has no way to see what the subagent is currently doing or how far along it is.
- The subagent starts with zero knowledge of Claude Code's prior conversation context, including the specific claim or suggestion that prompted the search. This means
the subagent cannot self-correct or stay anchored to the original intent — it only knows what Claude Code wrote in the prompt it was handed.
A subagent will run until it completes its task or exhausts its own context limit. In this session, a subagent tasked with a single web lookup instead made 10+
sequential web fetches because Claude Code wrote an over-scoped prompt and there was nothing — no timeout, no check-in, no guardrail — to catch or stop it. The user had
no ability to detect the problem or intervene. The session ran over 12 hours.
This is a fundamental reliability gap. Users have no way to "ground" a subagent in terms of runtime expectations, and Claude Code has no way to self-detect that a
delegated task has gone off the rails.
Secondary Issue - Plan mode forces subagent delegation with no user transparency:
Plan mode in Claude Code requires ALL research tasks to be routed through Explore subagents. Claude Code cannot make direct tool calls such as WebFetch while plan mode
is active. When this constraint forces a task to be slower or more indirect than it would otherwise be, Claude Code does not inform the user. The user has no visibility
into why something is taking long and no opportunity to exit plan mode before the delegation occurs.
This constraint exists for legitimate reasons — large codebase exploration benefits from subagent isolation. But applying it to a clearly bounded single-URL lookup is a
structural mismatch, and the user has no way to know it is happening.
What Anthropic should review:
- Implement user-configurable subagent timeouts — the single most important missing control
- Implement a check-in mechanism — if a subagent exceeds a time threshold, surface a warning to the user with an option to abort
- Require Claude Code to disclose upfront when a mode constraint is forcing subagent delegation instead of a direct tool call, and what the user's options are
- Evaluate whether plan mode's subagent requirement should apply to clearly bounded single-tool operations
- Consider whether subagents should receive a summary of the conversation context that prompted them, so they can stay anchored to the original intent
Impact: User lost 12+ hours of a session to what should have been a 30-minute task. The absence of subagent sanity checks is a significant operational gap that will affect any sufficiently long or complex delegated task. In time-sensitive or higher-stakes contexts this is a serious reliability and safety concern.
12 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Proposed Solution: Subagent Delegation Lacks Timeout/Monitoring/Abort
Root Cause
When the main agent delegates to a subagent:
This caused a 12+ hour hang during a simple web search task.
Proposed Fix
File:
src/agents/delegation.ts— Add timeout configuration:File:
src/agents/monitor.ts— Add heartbeat monitoring:Key Changes
src/agents/delegation.ts: Add timeout + idle timeout + retry configsrc/agents/monitor.ts: Heartbeat monitoring + idle detectionsrc/agents/control.ts: User-facing/abort <subagent-id>commandsrc/agents/delegation.ts: Progress reporting interfaceFull solution:
solutions/claude-code-61405-subagent-delegation-timeout-monitoring-fix.mdPosting this as one signal among many — this issue is sub-pattern 3 in a cluster of six sub-agent issues filed over the past 5 days that share an observability axis. Wrote up an independent analysis at https://gist.github.com/yurukusa/9857a9ed407696ba8483b354917ff161 decomposing them into four sub-patterns: dispatch fabrication (#61107, #61167), silent stall (#61315, #60987), absence of observation and control (this issue), and scope expansion (#61102).
The structural point relevant here: this is the sub-pattern that does not admit a complete operator-side defense, because the failure mode is the absence of a primitive (timeout/abort/progress), not the misuse of one. The other three sub-patterns can be partially closed with hooks (cc-safe-setup PRs #264, #275, #281, #286); this one's only operator-side workaround is a wall-clock alarm that surfaces a visible alert + manual kill instructions after a configurable threshold. The real fix lands at the harness layer.
Filing as a structural cluster (rather than four separate dups) matters because Anthropic's duplicate-detection bot collapses on lexical overlap and would not derive the observability axis from the issue texts themselves.
Disclosure at the bottom of the Gist; relevant short version: yurukusa author of cc-safe-setup (MIT, free) and two paid books (Claim-Verify Handbook, Postmortems) where the Appendix D corpus this analysis sits next to lives.
Shipped an operator-side proxy for the absent timeout primitive: yurukusa/cc-safe-setup#298 (
examples/dispatch-liveness-watchdog.sh, 28/28 tests passing).Scope honesty. This does not fix the underlying issue (the Agent tool has no timeout / monitoring / abort primitive). It makes the symptom observable at a point the operator already looks (the next UserPromptSubmit) so a hung dispatch surfaces before the operator has to notice that nothing is happening. The harness fix has to land on the Agent-tool layer.
How it works. One hook file, three event registrations:
PreToolUse+Agentmatcher: writes~/.claude/state/dispatch-watchdog/<session>/<unix-ts>-<rand>with a short dispatch summary.PostToolUse+Agentmatcher: removes the oldest state file in the session directory (FIFO).UserPromptSubmit+""matcher: lists state files older thanCC_DISPATCH_WATCHDOG_THRESHOLD_SEC(default 1800 = 30m), emits an advisory naming each one (wall-clock elapsed + summary).Advisory text quotes the three operator-side options at the moment a hang is surfaced: wait if plausibly still progressing, identify the OS-level process and SIGKILL (with the loss-of-parent-state caveat noted explicitly), or raise the threshold if legitimate dispatches normally run longer.
Limits I'd flag: out-of-order completion retires the wrong state file (the FIFO assumption); this keeps the count of in-flight dispatches correct but can mislabel which specific dispatch is the stale one. Per-dispatch identity tracking would require harness cooperation we don't have. Also: in strict mode (opt-in via
CC_DISPATCH_WATCHDOG_MODE=strict) the watchdog blocks the next prompt withexit 2, which is disruptive — default is advisory only.This sits at sub-pattern 3 in the four-sub-pattern decomposition I wrote up at https://gist.github.com/yurukusa/9857a9ed407696ba8483b354917ff161 — the one sub-pattern in that cluster where the operator side can only surface the symptom; the real fix is the timeout primitive you asked for in this issue.
Follow-up after reviewing the docs with Claude 4.7 — wanted to refine the asks now that I've checked what's already in place for subagents.
What's already there (and isn't being applied):
maxTurnsalready provides a tool-call budget per subagent ("Maximum number of agentic turns before the subagent stops"). The problem is that:settings.jsonkey to set a sane default for the built-ins.My 12-hour session ran through the Plan subagent — which has no user-visible turn cap at all.
/agents→ Running tab provides live observability and a stop control. Useful, but it's pull-based. If you don't know to open it, you don't see the runaway. No push notification when a subagent exceeds a threshold.SubagentStart/SubagentStophooks exist, plus per-subagentPreToolUse/PostToolUsehooks. The lifecycle plumbing is already there — just not surfaced as first-class user controls.BASH_DEFAULT_TIMEOUT_MSandBASH_MAX_TIMEOUT_MSalready give users timeout control over individual Bash tool calls viasettings.json. The pattern for what I'm asking for already exists at the Bash layer — it just doesn't extend up to the subagent lifecycle.Related: #25569 reports that
CLAUDE_CODE_MAX_OUTPUT_TOKENSisn't respected by subagents — they ignore the setting and use a hardcoded 32K. That's evidence of a recurring architectural pattern: subagents are a layer where existing user-facing controls quietly stop applying. My session is a stronger version of the same shape.Why user-settable controls are required:
A subagent can't reliably know its own progress without a sharply defined success criterion baked into the delegation. Self-introspection at "% complete" is not something LLMs do well without additional tooling/metering, and the original issue shouldn't be read as asking for that. Because the agent can't reliably self-bound, the user has to be able to set the bounds of the runtime. Tool-call count is the unit the agent can reason about; a global, local, and project based user controlled timeout is the missing control here. Both belong in
settings.json, and neither is sufficient alone —maxTurnsdoesn't fire if a single tool call hangs on a slow MCP or rate-limited fetch.Concrete asks, each individually filable:
settings.json— e.g.,subagentMaxRuntimeMs, with optional override in subagent frontmatter. Bash already has this pattern; subagents should match. Single highest-leverage change.maxTurnsfor built-in subagents (Explore, Plan, general-purpose) and allow defaults insettings.json. The control exists but doesn't reach where it's needed.maxTurnsoverride so the parent agent can scale the budget to the task. A single-URL fetch should get 2–3 turns; a codebase exploration can have 20. The current static-per-definition model can't make that distinction./agentsRunning tab already has the data; surface it actively.WebFetchto a specific URL, singleReadof a specific file, singleWebSearchwith a specific query) without forcing them through the Plan subagent. The mode-level constraint is a blunt instrument; that's the structural mismatch that produced this issue.Happy to provide any additional detail that will resolve this lack of controls for Claude Code sub-agents. A simple timeout setting that I could set for all sub-agents would have prevented this.
@meefs — the "subagents are a layer where existing user-facing controls quietly stop applying" framing is the cleanest articulation of the structural pattern I've seen. Three independent corroborations of that shape, then the operator-side substrate I shipped yesterday and where I think Ask #1 lands hardest.
The pattern at the budget axis is a cluster, not a single point. Three independent controls already exist at the parent layer and do not propagate across the subagent boundary:
BASH_DEFAULT_TIMEOUT_MS/BASH_MAX_TIMEOUT_MS— reaches the Bash tool, does not extend to the subagent runtime that contains Bash calls (the gap you named).CLAUDE_CODE_MAX_OUTPUT_TOKENS— reaches the harness, hardcoded to 32K at the subagent (#25569, closed on 2026-02 but the architectural pattern stands).maxTurns— exists in subagent frontmatter, not exposed for the built-in Explore/Plan/general-purpose subagents that get used when no custom subagent is in play.Three controls, three different axes (wall-clock, token budget, tool-call budget), same shape: the user-articulated budget lives at the parent layer and stops at the subagent boundary. The fix surface is the same in all three cases —
settings.jsonhas to carry through.This is the budget-axis variant of the broader recognition-without-arrest cluster. The cluster organizing on #60226 catalogs claim-verify gaps at the verification axis — where the verification step would have to read from a different source than the claim derives from, and the verification step does not exist. Your case is the control-propagation axis: the user-articulated control surface exists, but does not extend across the boundary where it would need to apply. Same architectural inversion at a different cross-section. Worth surfacing because the eventual platform fix should consider both axes together — the boundary is the unit, not the individual control.
Operator-side substrate available today (scope-honest). I shipped
examples/dispatch-liveness-watchdog.shin yurukusa/cc-safe-setup#298 (28/28 tests passing) on 2026-05-22 — aPreToolUse(Agent)+PostToolUse(Agent)+UserPromptSubmittriplet that writes per-dispatch state files and surfaces hung dispatches at the next operator turn with wall-clock-elapsed advisories. What it does: makes the symptom observable at a point the operator already looks (your next prompt submission), so a runaway surfaces before the operator has to remember to open/agents → Running. What it does not do: it cannot abort the dispatch. The harness has to expose that primitive — a hook can observe, but it cannot send a SIGTERM-equivalent into a running subagent because the harness doesn't expose that affordance. This is the half-substrate; the harness fix you're asking for is the other half.The 2026-06-15 billing reclassification adds urgency to Ask #1. Anthropic's split routes
--printinvocations to a separate "programmatic" credit bucket. A subagent that runs for 12 hours unbounded today is a clock-time problem; after 2026-06-15 it's also a direct $/turn cost problem — a runaway can burn through the programmatic bucket while the user is asleep with no abort surface.subagentMaxRuntimeMsbecomes a financial control in 23 days, not just a reliability control. That changes the priority ordering on your asks: #1 (settings-level runtime cap) clears the failure mode in your report and the post-2026-06-15 cost-exposure mode at the same time, with the minimum platform change.Specific endorsement on the five asks: #1 + #3 + #4 is the operationally complete set. #1 sets the global default (settings.json carries the boundary), #3 lets the parent agent scale per-task (WebFetch 2–3 turns vs. codebase exploration 20+), #4 surfaces the threshold breach push-based so the operator doesn't have to remember to look. #2 (expose
maxTurnsfor built-ins) is a strict subset of #3 and lands as a side effect. #5 (plan-mode bounded-operation exception) is the right separate ask — the mode-level constraint is structurally distinct from the subagent-budget cluster and earns its own thread.Disclosure: I'm yurukusa; I write commercial material adjacent to this territory (Migration Playbook, Claim-Verify Handbook, Safety Lab) and ship the MIT-licensed cc-safe-setup hook collection that PR #298 belongs to. The structural argument and the cross-references to #60226 / #25569 stand independently of either.
@meefs @yurukusa — one measurement-axis addendum to the control-propagation framing.
The verification-axis surface (#60226) and the control-propagation axis you've named here are structurally parallel but produce inverse evidence forms, and that asymmetry affects the measurement substrate design.
Verification-axis failures produce positive evidence — fabricated receipts, false tool-call reports — that a measurement hook can detect by checking whether the asserted action occurred at the event boundary. The detection grammar has something to match against.
Control-propagation failures produce negative evidence — a constraint that should have applied didn't apply — and are detectable only when the unconstrained subagent either (a) times out visibly, (b) emits more output than the parent-layer budget would have permitted, or (c) takes an action blocked at the parent layer but not propagated. PR #298's dispatch-liveness-watchdog surfaces case (a). Cases (b) and (c) have no operator-side observable today because the harness doesn't expose the post-subagent budget-consumed field in a hook-readable form.
The 2026-06-15 billing reclassification changes case (b)'s measurability: the gap between parent-layer budgeted programmatic spend and actual credit consumed becomes a billing artifact. That's the first operator-accessible proxy for unmeasured subagent overruns — not a real-time control, but an after-the-fact measurement substrate for auditing whether deployed subagents are running within parent-layer budget intent. An operator who wants to quantify the control-propagation failure rate before 2026-06-15 has no clean signal; an operator who runs an audit cohort after 2026-06-15 gets the credit-differential proxy for free, as a billing side effect.
Worth surfacing to Anthropic as a minimal ask alongside asks #1–#5: expose programmatic credit consumed per-subagent in the API response or a hook-readable artifact. That turns the billing side effect into a first-class measurement primitive without requiring harness changes to the abort surface.
@waitdeadai — accepting the inverse-evidence-form asymmetry as a structural property of the cluster catalog and the Ask #6 proposal as a measurement primitive worth surfacing alongside @meefs's Asks #1–#5. Three responses then the propagation surface.
On the positive-vs-negative evidence asymmetry as a catalog axis. The framing is sharper than the recognition-without-arrest cluster's existing organizing principle and earns its own annotation column in the cluster catalog. The cluster has been organizing on the verification-axis (gate-absent at the lifecycle event), which the receipts substrate measures cleanly because the failure produces a detectable artifact — the fabricated narrative is in the transcript, the missing tool call is in the session JSONL, and the cross-claim mismatch is computable at the next lifecycle event. The control-propagation axis you've named here has no detectable artifact at the failure moment by construction — the constraint that should have applied left no trace of its absence, only the downstream consequences (timeout, over-budget output, or unconstrained tool action) make the failure observable. Two axes, two evidence forms, one measurement-substrate-design implication: a catalog row needs to be tagged with its evidence form so the operator picking up a defense knows whether they're looking for a detection grammar (positive) or a constraint-propagation primitive plus a downstream measurement proxy (negative).
I'll add an
evidence_formcolumn to the Matrix Gist in the 72h propagation window — valuespositive(verification-axis, gate-absent surface; PRs #282 / #283 / #285 / #286 / #289 / #296 / #297 family) andnegative(control-propagation axis; PR #298 dispatch-liveness-watchdog as the case (a) operator-side observable). The asymmetry then surfaces at the column level for any future reader and the §6.2 sub-table placement criterion stays orthogonal — a row earns §6.2 because its decomposition's denominator is partially out-of-process, independent of which evidence form the row falls under.On PR #298 as the case (a) operator-side observable, and cases (b) and (c) as the unsolved control-propagation surface today. The mapping is exact and clarifies the scope of what PR #298 covers vs. what remains unaddressable from the operator side. Dispatch-liveness-watchdog surfaces case (a) — visible timeout becomes a UserPromptSubmit advisory at the next event boundary — because the parent-layer can observe the absence of the expected dispatch-end receipt within the timeout window. Cases (b) (subagent emits more output than parent-layer budget would permit) and (c) (subagent takes action blocked at parent layer but not propagated) have no parent-layer observable because the harness doesn't expose the post-subagent budget-consumed or post-subagent tool-call-list fields in a hook-readable form. Articulating that boundary honestly in PR #298's description matters — the hook is the case (a) surface, not the full control-propagation defense, and operators relying on it shouldn't infer that cases (b) and (c) are covered.
I'll update PR #298's description on the next revision to mark the scope boundary explicitly and cross-reference the case (b)/(c) gap as the unaddressable surface that depends on harness exposure of the missing fields. The carve-out keeps the hook honest about what it measures and what it doesn't, matching the §6.2 sub-table's honest-measurement-boundary discipline on the verification-axis side.
On Ask #6 — exposing programmatic credit consumed per-subagent. Accepting the proposal as a first-class measurement primitive worth filing alongside @meefs's Asks #1–#5. The reframing — turning the 2026-06-15 billing reclassification's side effect into an after-the-fact audit substrate for case (b) — is the cleanest path to case (b) measurability that doesn't require harness changes to the abort surface. Two operational properties make Ask #6 land cleanly:
claude -pbilling routing (Gist f936ba84) baselines parent-layer billing surfaces before automation; Ask #6's per-subagent credit field baselines child-layer attribution after the fact. The two together would give operators a closed loop on programmatic spend attribution: parent-layer baseline (pre-flight) + per-subagent attribution (post-hoc) = full causal chain on where the credit went. Without Ask #6, the per-subagent attribution gap means operators auditing post-2026-06-15 see only aggregate burn, not which subagent class drove it.Worth filing as a standalone issue tagged for the 2026-06-15 reclassification window, framed as a billing-side-effect-as-measurement-primitive ask rather than as a control ask — that framing preempts the "we'd need to redesign the abort path" objection that would land on a control-primitive ask.
Forward. Three propagation items in the 72h window: (1)
evidence_formcolumn added to the Matrix Gist with positive/negative tagging across all current rows; (2) PR #298 description updated with the case (a) vs. (b)/(c) scope boundary; (3) Ask #6 filed as a standalone issue (I'll draft and post; flag if you want to co-file). The §6.2 sub-table propagation from the #61102 thread stays on the same 72h schedule. Next coordination point is when an N=9 row gets surfaced from a new lifecycle event that doesn't fit the existing rows, or when an operator-side empirical pass on cases (b)/(c) produces the first proxy measurement (the post-2026-06-15 credit-differential data from Ask #6, if filed and shipped in time).— yurukusa
Propagation status update (3/3 commitments fulfilled, within the 72h window).
evidence_formcolumn added (commit history). Rows 1–8 taggedpositive(verification-axis). Row 9 added withnegative(control-propagation, case (a) only) pointing at PR #298 as the dispatch-liveness-watchdog surface. The inverse-evidence-form asymmetry articulated in the surrounding prose explicitly carries: positive rows need a detection grammar against recorded receipts; negative rows need a constraint-propagation primitive plus a downstream measurement proxy. Audit-step §4 in the operator-guidance section now tells operators not to compose positive and negative rows into a single F1 number because they measure structurally different things.Next coordination point per the previous lock is either an N=10 row from a new lifecycle event that doesn't fit the existing matrix, or an operator-side empirical pass on cases (b)/(c) once Ask #6 ships and the post-2026-06-15 credit-differential data becomes available.
— yurukusa
@yurukusa — confirmed, all three landed cleanly. #61934 captures the per-subagent credit ask as a measurement primitive exactly as scoped, and the positive/negative
evidence_formsplit with the no-compose-into-one-F1 caveat is the right guardrail against collapsing distinct failure shapes into a single misleading number. No further coordination needed from my side until an N=10 row materializes or the post-2026-06-15 credit-differential pass surfaces a divergence worth tracking.The timeout gap is the most dangerous one. An unbounded subagent delegation turns a bug in the delegated task into an hours-long runaway — and since there's no monitoring surface, the user can't tell whether the agent is making progress or spinning. The 12+ hour hang you describe is exactly what happens when the task decomposes to something that requires human input but the subagent has no ceiling.
From running Claude Code dispatches in an out-of-process coordinator (cron-driven Rust service with explicit per-step timeouts), a few concrete things that would materially reduce this failure class:
Subagent timeout (most impactful): A
timeout_secondsparam onAgent()/Dispatch()— if the child doesn't return within N seconds, the parent gets aSubagentTimeoutresult with whatever partial output accumulated. The parent can then decide: retry with a tighter prompt, escalate to human, or continue without that result. Right now the only option is to kill the entire session.Progress heartbeat: every M seconds, the subagent writes a brief status line to a shared location — not a full state dump, just a "still alive, currently on step X" marker. The parent can check this to distinguish "slow" from "stuck." This is easy to implement as a hook convention even before there's native support.
Graceful delegation abort:
/abort-task <subagent-id>in the TUI, or an API call that signals the child to wrap up and return whatever it has. SIGTERM semantics — not a kill.For anyone hitting this now: the practical mitigation is to cap subagent task scope aggressively. Single-function, bounded-input tasks (not "review the whole workflow" but "check line 42 of deploy.yml for this specific condition") are far less likely to hang. Small tasks with a single exit condition rather than open-ended analysis.
This relates directly to #48965 (session registry so the parent can find and signal children) and #62631 (child turn-end not visible to parent). All three are symptoms of the same missing piece: observable, bounded subagent execution.
Closing for now — inactive for too long. Please open a new issue if this is still relevant.