|
| 1 | +--- |
| 2 | +title: SessionEnd Hook |
| 3 | +summary: "`almanac hook install` wires the shared capture script into Claude, Codex, and Cursor with agent-specific hook shapes and timing behavior." |
| 4 | +topics: [agents, flows, cli] |
| 5 | +files: |
| 6 | + - src/commands/hook.ts |
| 7 | + - hooks/almanac-capture.sh |
| 8 | + - src/cli/register-wiki-lifecycle-commands.ts |
| 9 | + - test/hook.test.ts |
| 10 | +sources: |
| 11 | + - /Users/kushagrachitkara/.codex/sessions/2026/05/11/rollout-2026-05-11T14-32-08-019e18f4-5e73-7790-ba49-73cc02544a58.jsonl |
| 12 | +verified: 2026-05-11 |
| 13 | +--- |
| 14 | + |
| 15 | +# SessionEnd Hook |
| 16 | + |
| 17 | +`almanac hook install` wires one shared `hooks/almanac-capture.sh` script into each supported agent app, but the event name and config file differ by app. Claude uses `SessionEnd` in `~/.claude/settings.json`, Codex uses `Stop` in `~/.codex/hooks.json`, and Cursor uses `sessionEnd` in `~/.cursor/hooks.json`. |
| 18 | + |
| 19 | +The shared script backgrounds `almanac capture` after the session transcript is available. This makes hook installation part of the capture pipeline, not a Claude-only feature. |
| 20 | + |
| 21 | +A 2026-05-11 capture session verified the main Codex-specific invariant behind this mapping: current Codex builds expose `Stop` as the usable lifecycle hook surface, not a working `SessionEnd` event. For Almanac, "Codex hook support" therefore means "debounced quiet-session capture after `Stop`," not "capture exactly once when a thread is permanently closed." |
| 22 | + |
| 23 | +## Agent-specific install targets |
| 24 | + |
| 25 | +Claude uses a wrapped `SessionEnd` hook entry in `~/.claude/settings.json`. |
| 26 | + |
| 27 | +Codex uses a wrapped `Stop` hook entry in `~/.codex/hooks.json`. `almanac hook install --source codex` also ensures `codex_hooks = true` in `~/.codex/config.toml`, because Codex will not emit hook callbacks unless that feature flag is enabled. |
| 28 | + |
| 29 | +Cursor uses a flat `sessionEnd` hook entry in `~/.cursor/hooks.json`. |
| 30 | + |
| 31 | +## Settings.json shape |
| 32 | + |
| 33 | +Claude Code validates `settings.json` against a strict schema. Each event array entry (e.g. `SessionEnd`) is a `{ matcher, hooks: [...] }` container; actual command objects live inside the nested `hooks` array. `matcher` is always `""` for `SessionEnd`, which matches every session. |
| 34 | + |
| 35 | +```json |
| 36 | +{ |
| 37 | + "hooks": { |
| 38 | + "SessionEnd": [ |
| 39 | + { |
| 40 | + "matcher": "", |
| 41 | + "hooks": [ |
| 42 | + { "type": "command", "command": "/path/to/almanac-capture.sh" } |
| 43 | + ] |
| 44 | + } |
| 45 | + ] |
| 46 | + } |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | +Versions v0.1.0–v0.1.4 of codealmanac wrote command objects directly at the event-array level; that shape is rejected by newer Claude Code. `almanac hook install` migrates any legacy entry it recognizes (by `command` ending in `almanac-capture.sh`) to the wrapped form on install. |
| 51 | + |
| 52 | +## Script path |
| 53 | + |
| 54 | +The hook script is `hooks/almanac-capture.sh`. On install, `almanac hook install` copies it to `~/.claude/hooks/almanac-capture.sh`, and each agent config points at that stable path. This survives npm version bumps, npx cache evictions, and nvm version switches, so the installed hook never depends on an ephemeral package-manager path. |
| 55 | + |
| 56 | +## Install/uninstall |
| 57 | + |
| 58 | +`almanac hook install` — adds the configured agent hook entry or entries; idempotent (re-running is safe). |
| 59 | +`almanac hook uninstall` — removes Almanac's entry; leaves all other entries in `hooks` untouched. |
| 60 | +`almanac hook status` — reports whether the hook is installed, without modifying anything. |
| 61 | + |
| 62 | +`runHookInstall()` already supports `--source <claude|codex|cursor|all>`, but the CLI surface is still asymmetric: `hook uninstall` and `hook status` do not take `--source` today and still default to Claude's `~/.claude/settings.json` path. Future hook UX changes should preserve or intentionally remove that asymmetry rather than assuming all three commands are symmetric now. |
| 63 | + |
| 64 | +## Backgrounding |
| 65 | + |
| 66 | +The hook runs `almanac capture`, which now starts a V1 background job by default. The hook should return quickly after the parent writes the queued run record under [[process-manager-runs]]. Because capture runs headlessly with no terminal attached, provider auth must already be available; Claude can use the saved credential store (`~/.claude/credentials/`) or `ANTHROPIC_API_KEY`. |
| 67 | + |
| 68 | +Codex needs special handling here: `Stop` is turn-scoped, not session-scoped. The shared shell script debounces Codex `Stop` events with a `.almanac/runs/.capture-<session>.debounce` marker and only runs capture after a quiet period. Without that debounce, a long interactive Codex session could trigger capture repeatedly between turns. |
| 69 | + |
| 70 | +## What capture auto-resolves |
| 71 | + |
| 72 | +When triggered by the hook with no explicit transcript path, `capture` uses Claude transcript discovery for the current repo. V1 currently supports Claude latest-session and filtered discovery; Codex/Cursor session discovery is future work. See [[capture-flow]] for the current resolver contract. |
| 73 | + |
| 74 | +## Failure behavior |
| 75 | + |
| 76 | +Capture failure during a hook-triggered run produces no visible output in the ended agent session. The postmortem path is `almanac jobs`, `almanac jobs show <run-id>`, and `almanac jobs logs <run-id>` for the current wiki. `almanac hook status` only confirms the hook is wired. |
| 77 | + |
| 78 | +Hook installs mutate user-level config outside the repo. That means git history answers whether the hook contract was committed, but not whether a local machine is currently wired correctly. Testing a local build can temporarily leave `~/.codex/hooks.json` pointing at the wrong event name even when repo code and tests still expect Codex `Stop`. |
| 79 | + |
| 80 | +## Repo state versus machine state |
| 81 | + |
| 82 | +When debugging "was the hook change committed or pushed?", inspect repo-owned hook files and user-owned config separately. |
| 83 | + |
| 84 | +Repo state lives in `src/commands/hook.ts`, `hooks/almanac-capture.sh`, `test/hook.test.ts`, and this page. `git diff`, `git diff HEAD`, and `git diff origin/<branch>` over those files answer whether the hook contract changed locally, in the current commit, or on the remote branch. |
| 85 | + |
| 86 | +Machine state lives in agent config under the home directory, especially `~/.codex/hooks.json`, `~/.codex/config.toml`, `~/.claude/settings.json`, and `~/.cursor/hooks.json`. Those files are outside the repo, so a bad local `hook_event_name` or stale installed script path can survive even when git shows no hook-related repo changes. |
0 commit comments