The hook
The writer: what Claude Code runs on every registered session event, logging the session to CouchDB and S3 as it happens. It never blocks or fails a session: every external call is wrapped, errors go to stderr, and it always exits 0.
Setup: hook-setup.md. Events: hook-events.md (generated). What each action does: actions.md.
Where it lives
In the CLI, at packages/cli/src/hook/:
index.ts dispatch: payload → the actions bound to that event in the blueprint
handlers.ts one handler per action
runtime.ts runtime config, CouchDB + S3 clients, per-session state in /tmp
claude-transcripts hook run reads one payload on stdin; hook install registers that command in ~/.claude/settings.json. The plugin in hooks/ holds no logging code: scripts/dispatch.ts pipes the payload to claude-transcripts hook run and exits 0. hooks/hooks/hooks.json is generated by bun run gen:hooks.
There is one implementation. The plugin once carried its own copy of the writer because a plugin directory can't import the workspace; that ended when the installed CLI became the hook (ADR 0004).
Dispatch
Event → action bindings come from the blueprint (BINDINGS), the same source the registration is generated from, so dispatch and registration can't drift. An event's actions run concurrently and settled: one failing doesn't stop the others.
SessionStart and SessionEnd get a 180 s timeout because they do real work (seeding state; writing the summary and uploading the transcript); every other event gets 5 s.
Eight of the eleven events are registered async: true, so Claude Code doesn't wait for the writer (a synchronous PostToolUse costs roughly 130–160 ms per tool call). Three stay synchronous:
| Event | Why |
|---|---|
SessionStart | Its actions print the banner and the recall primer, and Claude Code discards an async hook's output. |
SessionEnd | A fire-and-forget upload at teardown could be killed half way. |
UserPromptSubmit | Claude Code ignores async on it. |
The split lives in hookAsync() (packages/cli/src/hook/index.ts); sync-hooks.ts applies it to the plugin, and hook install rewrites an older registration in place. On async events Claude Code also discards exit codes and stderr, so a broken writer shows up in the statusline (◐ ct stalled), /claude-transcripts:status and the session-start banner instead.
Runtime config
From CT_HOOK_CONFIG, else ~/.config/claude-transcripts/config.json (hook-setup.md). No config means the hook does nothing; the session-start banner then says the session is not being recorded.
Checking it
claude-transcripts hook status— what is registered, where, and any mirrors.claude-transcripts doctor— the whole write → read path.claude-transcripts statusline— the live indicator (● ct rec,◐ ct stalled,○ ct off), read from the hook's local state with no network calls.