Skip to content

fix(hooks): capture Stop turn ends#773

Open
pg-adm1n wants to merge 1 commit into
mksglu:nextfrom
pg-adm1n:fix/claude-stop-hook
Open

fix(hooks): capture Stop turn ends#773
pg-adm1n wants to merge 1 commit into
mksglu:nextfrom
pg-adm1n:fix/claude-stop-hook

Conversation

@pg-adm1n

@pg-adm1n pg-adm1n commented Jun 3, 2026

Copy link
Copy Markdown

Summary

Adds Claude Code Stop hook support and aligns Stop-hook persistence semantics across Claude Code and Codex.

  • register Claude Code Stop in generated hook config and the shipped hooks/hooks.json
  • add hooks/stop.mjs to capture assistant turn-end state as turn_end
  • keep Stop hook output as {} so Claude Code is never asked to continue
  • change Codex hooks/codex/stop.mjs from session_end to turn_end
  • update docs/tests and rebuild tracked runtime bundles

Root cause

Claude Code now exposes a Stop hook, but context-mode did not ship or register a Claude Code Stop runtime. That meant assistant turn-end state was not captured for Claude Code sessions.

Codex already registered a Stop hook, but the runtime wrote session_end. Stop fires at the end of an assistant turn, not at actual process/session shutdown, so session_end was too strong and could pollute lifecycle timelines.

Fix

  • Add a Claude Code Stop runtime that records turn_end with stop_hook_active and a bounded last_assistant_message payload.
  • Wire context-mode hook claude-code stop through the CLI dispatcher.
  • Add Stop to Claude Code adapter hook constants, generated config, static hook plugin config, docs, and health-check expectations.
  • Update Codex Stop to record turn_end while preserving its existing {} response behavior.
  • Add regression coverage for Claude Code and Codex Stop hook DB writes.
  • Rebuild tracked bundles (cli.bundle.mjs, server.bundle.mjs, hooks/session-db.bundle.mjs, hooks/session-extract.bundle.mjs) after npm run build.

Affected platforms

  • Claude Code hook/plugin installs
  • Codex hook installs that already emit Stop
  • Session timeline consumers that distinguish turn_end from session_end

Test plan

  • git diff --check
  • npm run build
  • npx vitest run tests/adapters/claude-code.test.ts tests/hooks/claude-stop.test.ts tests/adapters/codex.test.ts tests/hooks/kimi-hooks.test.ts --testNamePattern "Stop|stop|generateHookConfig|hooks/hooks.json" — 20 passed / 136 skipped
  • TMP_HOME=$(mktemp -d) && env -u CONTEXT_MODE_PLATFORM HOME="$TMP_HOME" npm test — 184 files passed, 4207 passed / 28 skipped

Note: the isolated HOME/unset CONTEXT_MODE_PLATFORM keeps local Codex/Claude session state from influencing project-dir detection during the full test run.

Checklist

  • Tests added/updated
  • npm test passes
  • npm run build passes
  • Docs updated
  • Rebuilt tracked runtime artifacts
  • Targets next branch

@pg-adm1n pg-adm1n marked this pull request as ready for review June 3, 2026 15:53
@mksglu

mksglu commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Hi @pg-adm1n Thanks! Did you test on manually? Are we sure that feature?

@pg-adm1n

pg-adm1n commented Jun 4, 2026

Copy link
Copy Markdown
Author

Hi @mksglu — yes, tested manually on Claude Code (beyond the unit tests). Here's the runtime evidence that the Stop hook fires and persists turn_end.

Setup — point the Claude Code runtime at this branch

npm run build && npm link          # `context-mode` now resolves to this checkout

Claude Code settings.json registers the Stop hook this PR adds:

"Stop": [{ "hooks": [{ "type": "command", "command": "context-mode hook claude-code stop" }] }]

context-mode doctor — the Stop runtime ships and is valid

[OK] SessionStart hook: configured
[OK] Hook script: PASS — hooks/stop.mjs
[OK] Hook script: PASS — hooks/sessionstart.mjs   (+ pretooluse / posttooluse / precompact / userpromptsubmit)
[OK] FTS5 / SQLite: PASS
[OK] Version: v1.0.162

Live capture — real session, then read the session DB

Ran a Claude Code session, completed two assistant turns, then queried the session store:

sqlite3 ~/.claude/context-mode/sessions/<session>.db \
  "SELECT id, type, source_hook, created_at FROM session_events WHERE type='turn_end' ORDER BY id;"
96 |turn_end|Stop|2026-06-04 03:49:55
146|turn_end|Stop|2026-06-04 04:02:10

Each completed assistant turn produced exactly one turn_end row, written by source_hook=Stop. The payload carries the turn-end state the PR intends:

{ "stop_hook_active": false, "last_assistant_message": "<final assistant message of the turn>" }

stop_hook_active is captured, and the hook returns {} so Claude Code is never prompted to continue — matching the PR description. (Before this PR there was no Claude Code Stop runtime, so no turn_end rows were written at all.)

On the Codex side, the same path now records turn_end instead of session_end, so a Stop no longer reads as a process/session shutdown in the timeline.

Automated coverage (already in the PR)

tests/hooks/claude-stop.test.ts + the Stop assertions in tests/adapters/claude-code.test.ts and tests/adapters/codex.test.ts — green in the test plan above.

Happy to capture anything else you'd like to see.

@ken-jo

ken-jo commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

@mksglu Validation completed according to the maintainer review requirements.

Verdict

The behavior is valuable and functionally validated, but this PR needs an update before merge because GitHub reports mergeStateStatus: DIRTY.

After the conflict is resolved, I would re-run the same 3OS validation before final merge.

What this PR meaningfully fixes

This PR treats Claude Code Stop as an assistant-turn-end event, not as a true session shutdown event.

That distinction matters for context-mode because session_end has stronger lifecycle meaning. It should represent an actual terminal/session lifecycle close, not every assistant turn. If Stop writes session_end, analytics, continuity, and resume logic can interpret normal turn completion as a closed session.

The intended invariant is:

  • Claude Code Stop records turn_end.
  • Claude Code Stop does not record session_end.
  • session_end remains reserved for true session lifecycle boundaries.

Git archaeology checked

I checked the existing hook history and related adapter behavior before accepting the change.

The important precedent is already present in the repo:

  • Kimi has explicit comments/tests that Stop is per-turn and SessionEnd is the true session-close event.
  • Codex Stop also records turn_end, not session_end.
  • Existing SessionDB consumers still distinguish turn_end and session_end.

So this PR aligns Claude Code with the existing Kimi/Codex invariant instead of assigning session-close meaning to Stop.

Validation performed

GitHub status:

  • Merge state: DIRTY
  • test (ubuntu-latest): pass
  • test (macos-latest): pass
  • test (windows-latest): pass
  • openclaw-e2e (ubuntu-latest): pass
  • openclaw-e2e (macos-latest): pass

Targeted tests passed:

  • tests/hooks/claude-stop.test.ts
  • tests/adapters/claude-code.test.ts
  • tests/adapters/codex.test.ts

Live Claude Code validation

Per maintainer requirements, live validation used Claude Haiku.

Linux:

  • Ran a real Claude Code session with the PR plugin loaded.
  • After assistant completion, SessionDB contained a turn_end row from source_hook = Stop.
  • No session_end row was created for that session.

macOS:

  • Ran a real Claude Code session with the PR plugin loaded.
  • SessionDB contained type = turn_end, source_hook = Stop.
  • No session_end row was created for that session.

Windows:

  • Ran a real Claude Code session with the PR plugin loaded.
  • SessionDB contained type = turn_end, source_hook = Stop.
  • No session_end row was created for that session.

This proves the actual Claude Code hook path works on Linux, macOS, and Windows, not only the unit-test path.

Remaining update needed

The PR is worth merging after the merge conflict is resolved and the same 3OS live validation is re-run.

@ken-jo

ken-jo commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Hi @pg-adm1n, thank you for contributing this PR.

I validated the change according to the maintainer review requirements. The behavior looks valuable and worth merging: the Claude Code Stop hook now records turn_end instead of session_end, and I verified the actual Claude Haiku hook path on Linux, macOS, and Windows.

The only blocker I see right now is that the PR has merge conflicts (mergeStateStatus: DIRTY).

Could you please resolve the conflicts? After that, we can re-run the same validation and move this toward merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants