Ignore subagent hook events fired after a turn's Stop (#199)#208
Merged
dhilgaertner merged 1 commit intomainfrom Apr 24, 2026
Merged
Ignore subagent hook events fired after a turn's Stop (#199)#208dhilgaertner merged 1 commit intomainfrom
dhilgaertner merged 1 commit intomainfrom
Conversation
Claude Code 2.1.108's awaySummaryEnabled "session recap" generates a SubagentStop hook event ~minutes after the user's turn has ended. The old TaskCreated/TaskCompleted/SubagentStop arm unconditionally drove claudeState back to .working, trapping the sidebar dot until session end. Track lastTopLevelStopAt on SessionHookState; when set, suppress state elevation in the SubagentStart and Subagent/TaskCreated/TaskCompleted arms. Cleared on UserPromptSubmit (next real turn), SessionStart, and SessionEnd. Also adds a CROW_HOOK_DEBUG=1 env-gated [hook-event] NSLog stream documenting event arrival + ClaudeState transitions, used to confirm the fix and kept in for future hook-lifecycle bugs (Claude Code's hook schema changes often). Documented in docs/troubleshooting.md alongside the user-side workaround for older builds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dgershman
approved these changes
Apr 24, 2026
Collaborator
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None.
Security Review
Strengths:
CROW_HOOK_DEBUGreads from the process environment once at startup and uses a simpleBool— no injection surface, no runtime overhead when disabled.- Debug logging truncates session IDs to 8 chars (
shortID) — safe, but even full UUIDs aren't sensitive here since they're local-only. Date()stored onlastTopLevelStopAtis never serialized or sent externally — pure in-memory state.
Concerns:
- None identified. No new external inputs, no new persistence paths, no new network calls.
Code Quality
Well done:
- The
lastTopLevelStopAtguard is the right abstraction level — it's event-schema-agnostic, so it naturally handles future background-event scenarios without needing pattern updates per event type. - Clearing the guard on
UserPromptSubmit,SessionStart, andSessionEndcovers all legitimate "turn is live" transitions. No gap I can find where a real mid-turn subagent would be incorrectly suppressed. - Capturing
stateBeforebefore the switch block and logging the transition only when the state actually changed is clean — no noise in debug output. - The troubleshooting docs include the three workaround paths (settings.json,
/config, env var) for users on older builds — good completeness.
One observation (not blocking):
lastTopLevelStopAtstores aDatebut the value is only ever checked fornilvs non-nil — a plainBoollikehasStoppedwould be slightly simpler. That said, keeping the timestamp costs nothing extra and could be useful for future diagnostics (e.g. "how long ago did the turn end?"), so this is fine as-is.
Summary Table
| Priority | Issue |
|---|---|
| 🟢 | lastTopLevelStopAt could be a Bool instead of Date? — but Date? is arguably better for future debug use |
Recommendation: Approve. The fix is well-scoped, correctly guarded, and all 112 CrowCore tests pass. The approach is resilient to future Claude Code hook schema changes since it keys off the turn lifecycle (Stop → UserPromptSubmit) rather than matching specific event names or payloads.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #199. Claude Code 2.1.108's
awaySummaryEnabled"session recap" generates aSubagentStophook event ~minutes after the user's turn has ended. Crow's oldTaskCreated, TaskCompleted, SubagentStoparm unconditionally droveclaudeStateback to.working, trapping the sidebar dot until session end.The fix tracks
lastTopLevelStopAt: Date?onSessionHookState. When set, the dispatcher suppresses state elevation in theSubagentStartandTaskCreated/TaskCompleted/SubagentStoparms — those events are treated as background. The flag is cleared onUserPromptSubmit(next real turn),SessionStart, andSessionEnd, so legitimate mid-turn subagents (/explore, sub-tasks, etc.) are unaffected.This generalizes beyond recap: any future "Claude wakes up after a turn ends" scenario (background telemetry, async cleanup hooks) will be correctly ignored.
Evidence the fix works
Captured with the new
CROW_HOOK_DEBUG=1flag during a real session that produced a recap:Pre-fix: that
SubagentStopwould drivedone → workingand never recover. Post-fix:lastTopLevelStopAt != nil, soclaudeStatestays at.done. A separate session that was actively mid-turn during the same capture window correctly kept its state at.workingwhile runningTaskCreated/TaskCompletedevents, confirming the guard is scoped to post-Stop.Why hook events, not terminal text
Per the ticket: text-pattern matching
※ recap:would rot the next time Claude Code changes the recap glyph or copy. The fix lives in the hook dispatcher where the actual lifecycle bug is.Also included
CROW_HOOK_DEBUG=1-gated[hook-event]NSLog stream — logs every event arrival + everyClaudeStatetransition. Off by default. Kept in for the next time Claude Code's hook schema shifts (it changes often). Documented indocs/troubleshooting.md.awaySummaryEnabled = false//config/CLAUDE_CODE_ENABLE_AWAY_SUMMARY=0).Test plan
make test)SubagentStoparrives 3 min after user-turnStop)/exploreor another subagent-heavy slash command — sidebar should still show working during the run🤖 Generated with Claude Code