Summary
Claude Code added a session recap feature (shipped in v2.1.108 on 2026-04-14, controlled by the awaySummaryEnabled setting) that prints a one-line summary at the end of a turn, for example:
✻ Brewed for 35s
※ recap: Adding a GitHub Actions workflow to push Corveil images to commercial ECR on
merge to main (ticket #324); PR #326 is open and ready for review. Next: merge it,
then run the post-merge ECR verification. (disable recaps in /config)
After this prints, Crow's session indicator stays on working and never transitions back to done, even though the agent is idle and ready for input.
Why this matters
The whole value of the status indicator is knowing when a session is done so I can pick up the next one. With the recap feature on (which is the default for recent Claude Code versions), every session I finish looks like it's still running. I end up force-checking each one manually.
Root cause — not terminal parsing, but hook events
Crow does not grep terminal output for ✻ or ※ recap: — it transitions state purely from Claude Code hook events via the hook-event RPC handler:
Sources/Crow/App/AppDelegate.swift:802-968 — hook-event dispatcher
Packages/CrowCore/Sources/CrowCore/Models/Enums.swift:33-38 — ClaudeState { idle, working, waiting, done }
AppDelegate.swift:872 — PreToolUse → .working
AppDelegate.swift:930-931 — UserPromptSubmit → .working
AppDelegate.swift:933-935 — Stop → .done
So the sequence for a normal turn is: UserPromptSubmit → .working → (tool calls) → Stop → .done.
The hypothesis: when the recap generates, Claude Code re-enters a generation cycle after the turn has already emitted Stop. That secondary cycle likely fires one of UserPromptSubmit / PreToolUse / SubagentStart, which flips us back to .working — but the closing event for that cycle is either missing, uses a different event name (e.g. SubagentStop not mapped to .done), or is swallowed, so we never transition back.
This needs to be confirmed by capturing the actual hook-event stream around a recap. Possible outcomes:
- Recap runs as a subagent: fires
SubagentStart → .working, then SubagentStop which is not currently mapped back to .done at line 933-935.
- Recap fires a
UserPromptSubmit (synthetic, internal prompt): flips to .working, then completes with a Stop that arrives but is missed because of ordering (e.g. two Stops collapse, or state is clobbered by a later event).
- Recap doesn't emit any hook events at all and the
.working state we're seeing is left over from something else the session did right before the recap rendered.
Option 1 is the most likely fit — the recap is essentially a short summarization task, and Claude Code's recap docs describe it as running in the background.
Reproduction
- Use Claude Code ≥ v2.1.108 (recap defaults to on).
- Start a Crow session on any ticket that takes more than a couple of turns to complete.
- Let the agent finish naturally (it will print a
※ recap: ... (disable recaps in /config) line at the end).
- Observe that the Crow sidebar keeps showing the session as
working indefinitely.
Suggested fix
Investigate first, then implement:
- Instrument
AppDelegate.swift hook-event handler to log every inbound event name + source/session ID for a single reproduction. Capture what actually fires around a recap.
- Based on what we see, either:
- Add a mapping for
SubagentStop (or whatever closing event the recap uses) → .done in the Stop/StopFailure block at AppDelegate.swift:933-938.
- Or: tag events originating from the recap generator (if Claude Code identifies them via a
source, agentType, or similar field on the hook payload) and ignore them for state-transition purposes so the state stays .done across the recap.
Do NOT try to detect the recap by pattern-matching terminal text for ※ recap: — it sidesteps the actual hook-lifecycle bug and will rot the next time the recap format changes.
Workaround for users in the meantime
Users can disable the feature in one of three ways:
settings.json: \"awaySummaryEnabled\": false
/config inside Claude Code → toggle "Session recap" off
- Env:
CLAUDE_CODE_ENABLE_AWAY_SUMMARY=0
Worth calling out in the README or the "Known issues" note once we know the exact behavior — but the real fix belongs in the hook-event handler.
References
- Claude Code changelog entry for v2.1.108 (2026-04-14): session recap feature.
- Claude Code interactive-mode docs: describe recap as background-generated and invokable via
/recap.
- Claude Code settings docs:
awaySummaryEnabled.
Summary
Claude Code added a session recap feature (shipped in v2.1.108 on 2026-04-14, controlled by the
awaySummaryEnabledsetting) that prints a one-line summary at the end of a turn, for example:After this prints, Crow's session indicator stays on
workingand never transitions back todone, even though the agent is idle and ready for input.Why this matters
The whole value of the status indicator is knowing when a session is done so I can pick up the next one. With the recap feature on (which is the default for recent Claude Code versions), every session I finish looks like it's still running. I end up force-checking each one manually.
Root cause — not terminal parsing, but hook events
Crow does not grep terminal output for
✻or※ recap:— it transitions state purely from Claude Code hook events via thehook-eventRPC handler:Sources/Crow/App/AppDelegate.swift:802-968— hook-event dispatcherPackages/CrowCore/Sources/CrowCore/Models/Enums.swift:33-38—ClaudeState { idle, working, waiting, done }AppDelegate.swift:872—PreToolUse→.workingAppDelegate.swift:930-931—UserPromptSubmit→.workingAppDelegate.swift:933-935—Stop→.doneSo the sequence for a normal turn is:
UserPromptSubmit→.working→ (tool calls) →Stop→.done.The hypothesis: when the recap generates, Claude Code re-enters a generation cycle after the turn has already emitted
Stop. That secondary cycle likely fires one ofUserPromptSubmit/PreToolUse/SubagentStart, which flips us back to.working— but the closing event for that cycle is either missing, uses a different event name (e.g.SubagentStopnot mapped to.done), or is swallowed, so we never transition back.This needs to be confirmed by capturing the actual hook-event stream around a recap. Possible outcomes:
SubagentStart→.working, thenSubagentStopwhich is not currently mapped back to.doneat line 933-935.UserPromptSubmit(synthetic, internal prompt): flips to.working, then completes with aStopthat arrives but is missed because of ordering (e.g. twoStops collapse, or state is clobbered by a later event)..workingstate we're seeing is left over from something else the session did right before the recap rendered.Option 1 is the most likely fit — the recap is essentially a short summarization task, and Claude Code's recap docs describe it as running in the background.
Reproduction
※ recap: ... (disable recaps in /config)line at the end).workingindefinitely.Suggested fix
Investigate first, then implement:
AppDelegate.swifthook-event handler to log every inbound event name + source/session ID for a single reproduction. Capture what actually fires around a recap.SubagentStop(or whatever closing event the recap uses) →.donein theStop/StopFailureblock atAppDelegate.swift:933-938.source,agentType, or similar field on the hook payload) and ignore them for state-transition purposes so the state stays.doneacross the recap.Do NOT try to detect the recap by pattern-matching terminal text for
※ recap:— it sidesteps the actual hook-lifecycle bug and will rot the next time the recap format changes.Workaround for users in the meantime
Users can disable the feature in one of three ways:
settings.json:\"awaySummaryEnabled\": false/configinside Claude Code → toggle "Session recap" offCLAUDE_CODE_ENABLE_AWAY_SUMMARY=0Worth calling out in the README or the "Known issues" note once we know the exact behavior — but the real fix belongs in the hook-event handler.
References
/recap.awaySummaryEnabled.