[BUG] SubagentStart/SubagentStop unreliable for settings.json hooks — cascade failures break agent lifecycle management
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
SubagentStart and SubagentStop hooks configured in settings.json do not fire reliably for Task tool dispatches in the main orchestrator process. This makes it impossible to build deterministic agent lifecycle management (trace initialization, trace finalization, proof-of-work gates, auto-verification pipelines) on top of these events.
The hooks sometimes fire, sometimes don't, and when SubagentStop does fire, it often lacks the last_assistant_message field or arrives with an empty agent_type — making the payload unusable even when the event is delivered.
This is not a documentation gap. The events are registered, matched, and expected to fire — they silently don't. The result is a class of cascade failures that are invisible to users and impossible to work around cleanly.
Cascade Impact (with data)
We operate a hook-driven governance system across 370+ agent traces. Because SubagentStart/SubagentStop don't fire reliably, every subsystem that depends on agent lifecycle events has degraded:
| Subsystem | Depends On | Failure Mode | Impact |
|-----------|-----------|--------------|--------|
| Trace initialization | SubagentStart | init_trace() never called → no trace directory, no manifest | 42% of traces end in "partial" outcome (157/370) because the trace was never properly opened |
| Trace finalization | SubagentStop | finalize_trace() never called → manifest stays "status": "active" forever, .active-* sentinel files never cleaned | Stale sentinels block subsequent agent dispatches (tester blocked by orphaned implementer marker) |
| Auto-verification | SubagentStop:tester | Hook doesn't fire → AUTOVERIFY: CLEAN signal never evaluated → proof-status stuck at needs-verification | Guardian dispatch permanently blocked; user must manually intervene every time |
| Proof-status lifecycle | SubagentStop (all types) | Finalization race: when SubagentStop does fire, it often times out before reaching cleanup code | Orphaned .proof-status-{hash} files accumulate, create cross-session state confusion |
The net result: an orchestrator that was designed to be hands-off (implementer → tester → auto-verify → guardian → merge) requires manual intervention at nearly every stage because the lifecycle hooks don't fire.
Workarounds Attempted (and why they're insufficient)
We've implemented 6 architectural workarounds over 4 weeks, each addressing one edge case of this bug:
| Workaround | What It Does | Why It's Insufficient |
|-----------|-------------|----------------------|
| PreToolUse:Task as SubagentStart replacement | Fires before Task dispatch; runs init_trace() | Works for initialization but PreToolUse fires BEFORE the agent runs — can't access response or outcome |
| PostToolUse:Task as SubagentStop replacement | Fires after Task completes; reads tool_input.subagent_type | Missing last_assistant_message — the agent's response text is not in the PostToolUse payload. Must reconstruct from trace artifacts on disk (fragile) |
| Summary.md fallback for auto-verify | When last_assistant_message is empty, reads tester's summary.md from disk | Depends on agent writing file before returning (race condition); file may not exist if agent crashed |
| 5-minute staleness self-heal in dispatch gates | Detects orphaned .active-* markers older than 5 min; force-completes them | Adds 5-minute delay to every blocked dispatch; doesn't prevent the orphan in the first place |
| Dual-write proof-status to 3 paths | Writes verified to worktree, scoped, and legacy proof files | Different hooks read different paths — Gate A hardcodes scoped path, resolve_proof_file() may return worktree path. Path disagreement causes silent gate failures |
| Pre-create guardian marker at dispatch | Writes .active-guardian-* sentinel in PreToolUse:Task to close race window | Band-aid for the gap between PreToolUse and the (non-firing) SubagentStart |
Each workaround adds ~50-100 lines of bash, a @decision annotation, and a new failure mode. The total workaround surface is ~600 lines of code compensating for two events that should just fire.
What PostToolUse:Task Cannot Replace
PostToolUse:Task is the closest available substitute, but it's structurally inadequate as a SubagentStop replacement:
| Field | SubagentStop | PostToolUse:Task |
|-------|-------------|-----------------|
| last_assistant_message | Yes (agent's response text) | No — only tool_input and tool_name |
| agent_type | Yes | Must parse from tool_input.subagent_type |
| agent_id | Yes | No |
| Lifecycle semantics | "Agent has stopped" | "Tool call completed" (ambiguous — could be any Task) |
| Response text access | Direct in payload | Must read from disk artifact (race-prone) |
The missing last_assistant_message is the critical gap. Auto-verification requires parsing the tester's response for confidence signals (AUTOVERIFY: CLEAN, **High** confidence). Without it in the payload, we reconstruct from summary.md on disk — which the agent may not have written yet, may have written incompletely, or may have skipped entirely.
Steps to Reproduce
- Configure
settings.jsonwithSubagentStartandSubagentStophooks that log to a file:
{
"hooks": {
"SubagentStart": [{
"matcher": ".*",
"hooks": [{"type": "command", "command": "echo \"$(date) START agent_type=$AGENT_TYPE\" >> /tmp/hook-log.txt"}]
}],
"SubagentStop": [{
"matcher": ".*",
"hooks": [{"type": "command", "command": "echo \"$(date) STOP agent_type=$AGENT_TYPE\" >> /tmp/hook-log.txt"}]
}]
}
}
- Use the
Tasktool to dispatch a subagent (e.g., typeExploreorgeneral-purpose) - Wait for the agent to complete
- Check
/tmp/hook-log.txt
Expected: Both START and STOP entries present for every dispatch
Actual: START is frequently missing. STOP fires intermittently — sometimes with empty agent_type, sometimes not at all.
Expected Behavior
SubagentStart and SubagentStop should fire reliably and symmetrically for every Task tool dispatch, with:
agent_typepopulated fromtool_input.subagent_typeagent_idpopulatedSubagentStopincludinglast_assistant_messagewith the agent's response text- Guaranteed ordering:
SubagentStartbefore agent execution,SubagentStopafter agent returns
Environment
- Claude Code version: latest (2026-02-22)
- OS: macOS (Darwin 25.3.0)
- Hook configuration:
settings.json(global,~/.claude/settings.json) - Not using agent frontmatter hooks
- Not using teammate/pane-based mode
Related Issues
- #27423 —
SubagentStopfires without correspondingSubagentStartfor internal agents (orphaned events with emptyagent_type) - #27153 — Only 6 of 16 hooks actually fire in agent frontmatter (documents which hooks work)
- #24175 — Consistent hook execution across spawn modes (teammate-focused, but same root cause)
- #19170 — Missing definition and input schema for
SubagentStarthook event - #6885 — Agent context detection in hook events
Severity
This is effectively a P1 for anyone building hook-driven workflows. The workaround surface grows with every feature that depends on agent lifecycle, and each workaround introduces its own failure modes. A reliable SubagentStart/SubagentStop pair would eliminate ~600 lines of workaround code and 6 architectural decisions from our system.
This issue has 9 comments on GitHub. Read the full discussion on GitHub ↗