Hooks
“Hooks” here means Claude Code’s hook mechanism: scripts the assistant invokes on events like SessionStart and Stop. kenkeep consumes them; it does not expose a hook API of its own for third-party extension.
init registers three hook scripts in .claude/settings.json:
| Script | Event(s) | Mode |
|---|---|---|
kk-capture.mjs | Stop, SessionEnd, PreCompact | sync, ≤1s |
kk-proposal-drain.mjs | SessionStart | async |
kk-session-start.mjs | SessionStart | sync, ≤1s |
The two SessionStart entries are independent: a failure in one does not block the other.
Recursion guard
All three hooks exit immediately if KENKEEP_BUILDER_INTERNAL=1 is set. Two surfaces set this var on the harness child they exec:
- The
proposal-drainhook, when it spawns its headless extractor. - The CLI launchers (
bootstrap,curate,node add), which exec<harness> -p "/kk-<name>".
Without the guard, the spawned session would fire its own SessionStart hooks and recurse.
CAUTION
If you wrap a harness CLI, propagate KENKEEP_BUILDER_INTERNAL=1 only into intentionally-internal subprocesses. Leaking it elsewhere silently disables capture and injection.
kk-capture.mjs (capture)
- Read hook input from stdin.
- Validate
session_idviaassertValidSessionId(strict UUID v4 shape). On bad input, throw with a named error; the catch handler writes it to stderr. - Parse the transcript (
user/assistanttext, role-tagged). - Write
_sessions/<YYYYMMDD-HHmm-<sessionId>>.mdwith frontmatter and the transcript slice. A re-fire for the samesession_id(multi-turn sessions,PreCompactafterStop) reuses the existing file viafindSessionLogBySessionId, so the count stays at one log per session.
The only difference between the three triggers is the captured_by field (stop, session_end, pre_compact).
Never invokes the LLM. 1s deadline: a miss exits silently and the next trigger retries.
CAUTION
kenkeep does not scan or redact the transcript. Secrets present in the session are written verbatim to _sessions/. Secret hygiene is the consumer’s responsibility (see Installation → commit-time hardening).
Capture failure modes
| Condition | Outcome |
|---|---|
KENKEEP_BUILDER_INTERNAL=1 | Exit. No capture. |
| Empty / malformed stdin | Exit silently. |
session_id not a UUID v4 | Write the error to stderr; no session log. |
transcript_path missing | Exit silently. |
| Transcript empty | Exit silently. |
| 1s deadline exceeded | Exit silently; next trigger retries. |
kk-proposal-drain.mjs (extraction)
Per SessionStart:
- Recursion guard.
- Acquire the
proposal-drainlock (PID + 30-min TTL). Stale locks are reclaimed. - Load the prompt (local override first, bundled fallback).
- Sweep
_sessions/*.mdfor frontmatter withproposal_status: pending. - Per pending log: spawn the active harness’s headless driver in streaming-JSON mode, stream to
_logs/proposal/<session-id>__<ts>.jsonl, parse the finalresult, validate againstProposalOutputSchema. - On success: set
proposal_status: done, populatedproposals.{practice,map}, dedupedtopics. - On failure: set
proposal_status: failedwithproposal_error.
NOTE
Drain failures (timeout, schema mismatch, bad JSON) do not heal on retry, so the drain does not rotate them.
kk-session-start.mjs (consume)
- Recursion guard.
- Load
INDEX.md. If missing, emit_The knowledge base is empty._. - Compare frontmatter
nodes_hashagainst the live hash ofnodes/. On drift, append> kk index is stale, run \npx kenkeep index rebuild``. - Count pending logs. If the count is at or above
curationThreshold(default 5) and the last nudge was over an hour ago, append a one-line nudge and writelast_nudged_at. The nudge escalates to a loud🚨 kenkeep curation queue is overdueheading when the queue is large or stale:pending >= 10, orpending >= curationThresholdwith the oldest log at leaststaleDays(default 7) old. -
Emit:
{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"..."}}
1s hard deadline. Overrun exits 0 so session startup is not blocked.
Registration shape
After init, .claude/settings.json carries one block per event:
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "node .claude/hooks/kk-capture.mjs" }] }
],
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "node .claude/hooks/kk-proposal-drain.mjs", "async": true }] },
{ "hooks": [{ "type": "command", "command": "node .claude/hooks/kk-session-start.mjs" }] }
]
}
}
NOTE
User-defined hooks in the same file are preserved on re-init.