codeburn guard: opt-in session-time hook pack for Claude Code
Part of the acting-layer epic. Depends on the action journal issue (settings edits go through it). Parallel-safe with the --apply issue.
Why
On real data the large majority of flagged waste is behavioral: runaway low-yield sessions, context-heavy sessions, cost outliers, and a large share of weekly spend in sessions that never ship. Config cleanup cannot catch these; something has to be present at session time. Claude Code natively supports lifecycle hooks, which gives us a fully local, off-by-default, cleanly removable acting mechanism with no request interception.
Important first step for the implementer
Verify the current hook protocol against the official docs (hook events, stdin JSON shape, stdout/exit-code contract, settings.json schema) at https://docs.anthropic.com/en/docs/claude-code/hooks before writing code, and encode what you verify in tests. Do not trust this spec's field names over the live docs; the protocol has evolved.
What to build
codeburn guard install [--global | --project <path>] # default: --project cwd
codeburn guard uninstall [--global | --project <path>]
codeburn guard status
codeburn guard hook <event> # internal: invoked by Claude Code, reads stdin JSON
install writes hook entries into the target settings file (~/.claude/settings.json or <project>/.claude/settings.json) via the action journal (kind: guard-install), merging with existing user hooks non-destructively: append our entries, never replace the array, tag ours with a recognizable command prefix (codeburn guard hook ...) so uninstall removes exactly ours. uninstall also works when the user has moved settings around: remove by command-prefix match, journal the edit.
All hook handlers are subcommands of the already-installed codeburn binary; nothing else gets written to disk. Handlers must be fast (target < 300ms) and fail open: any internal error exits 0 with no output so a broken guard can never block the user's session.
Hook 1: budget cap (PreToolUse)
- Read the hook input JSON from stdin (it includes the session transcript path).
- Compute session cost so far by parsing that transcript with the existing session parsing utilities (reuse, do not reimplement pricing). Cache incrementally in
~/.codeburn/guard/<session-id>.json (bytes offset + running totals) so each invocation parses only the tail; a cold parse of a 100MB transcript on every tool call is not acceptable.
- Thresholds from
~/.codeburn/guard.json (created by install with defaults): softUSD: 5, hardUSD: 15, both nullable to disable.
- Soft threshold crossed: emit a non-blocking warning (per protocol: systemMessage / stderr with exit 0) once per threshold per session (record in the session cache).
- Hard threshold crossed: return the blocking decision with a reason like
Session cost passed $15 (guard). Run 'codeburn guard allow' to lift the cap for this session, or raise hardUSD in ~/.codeburn/guard.json. codeburn guard allow writes a session flag file the hook checks first.
Hook 2: flagged-project session opener (SessionStart)
install and codeburn guard refresh compute a per-project flag list from existing optimize signals (low-worth and context-heavy candidate detectors are already exported from src/optimize.ts) and store it in ~/.codeburn/guard/flags.json with a generated-at timestamp.
- On SessionStart in a flagged project, output the matching one-time session opener text (the same opener strings optimize prints today, single source of truth: export them from optimize rather than duplicating) as additional context per protocol. Stale flags (> 7 days) emit nothing.
Hook 3: yield checkpoint (Stop)
- On Stop, using the same incremental session cache: if the session has cost over
checkpointUSD (default 3) and contains zero file-edit tool calls and no git commit command, emit a short non-blocking note: This session is $X with no edits or commits yet. If exploring is the goal, fine; otherwise consider a fresh session with a named deliverable.
- Hard rule: never block on Stop, never emit more than once per session.
Statusline (optional flag)
codeburn guard install --statusline additionally configures the Claude Code statusline command to codeburn guard statusline, which prints a single line with session cost and cache-freshness (time since last turn vs the 5-minute cache TTL). Skip entirely if the user already has a statusline configured; say so.
Config
~/.codeburn/guard.json: { softUSD, hardUSD, checkpointUSD, openerEnabled, updatedAt }. guard status prints resolved config, install locations found, flag list size and age.
Tests (tests/guard-install.test.ts, tests/guard-hooks.test.ts)
- install into empty settings, alongside pre-existing user hooks (theirs untouched), and idempotent re-install (no duplicates)
- uninstall removes exactly our entries; settings restored byte-identical when ours were the only change
- budget hook: crafted transcript fixtures below/above soft and hard thresholds produce correct stdout/exit per protocol; second soft warning suppressed; allow flag lifts the hard block
- incremental cache: appending turns to a fixture transcript and re-invoking parses only the tail (assert via bytes-read counter) and totals match a cold parse
- stop hook fires only for the no-edit no-commit expensive case; never for cheap or shipped sessions
- every handler exits 0 with empty output on malformed stdin
Acceptance
- Install, trigger a soft warning in a real session, uninstall, and diff shows settings back to pre-install state.
- All handlers under 300ms on a warm cache against a 50MB transcript.
- Zero new dependencies; core commands unaffected for non-opted-in users.
Part of epic #602.
codeburn guard: opt-in session-time hook pack for Claude CodePart of the acting-layer epic. Depends on the action journal issue (settings edits go through it). Parallel-safe with the --apply issue.
Why
On real data the large majority of flagged waste is behavioral: runaway low-yield sessions, context-heavy sessions, cost outliers, and a large share of weekly spend in sessions that never ship. Config cleanup cannot catch these; something has to be present at session time. Claude Code natively supports lifecycle hooks, which gives us a fully local, off-by-default, cleanly removable acting mechanism with no request interception.
Important first step for the implementer
Verify the current hook protocol against the official docs (hook events, stdin JSON shape, stdout/exit-code contract, settings.json schema) at https://docs.anthropic.com/en/docs/claude-code/hooks before writing code, and encode what you verify in tests. Do not trust this spec's field names over the live docs; the protocol has evolved.
What to build
installwrites hook entries into the target settings file (~/.claude/settings.jsonor<project>/.claude/settings.json) via the action journal (kind: guard-install), merging with existing user hooks non-destructively: append our entries, never replace the array, tag ours with a recognizable command prefix (codeburn guard hook ...) souninstallremoves exactly ours.uninstallalso works when the user has moved settings around: remove by command-prefix match, journal the edit.All hook handlers are subcommands of the already-installed
codeburnbinary; nothing else gets written to disk. Handlers must be fast (target < 300ms) and fail open: any internal error exits 0 with no output so a broken guard can never block the user's session.Hook 1: budget cap (PreToolUse)
~/.codeburn/guard/<session-id>.json(bytes offset + running totals) so each invocation parses only the tail; a cold parse of a 100MB transcript on every tool call is not acceptable.~/.codeburn/guard.json(created by install with defaults):softUSD: 5,hardUSD: 15, both nullable to disable.Session cost passed $15 (guard). Run 'codeburn guard allow' to lift the cap for this session, or raise hardUSD in ~/.codeburn/guard.json.codeburn guard allowwrites a session flag file the hook checks first.Hook 2: flagged-project session opener (SessionStart)
installandcodeburn guard refreshcompute a per-project flag list from existing optimize signals (low-worth and context-heavy candidate detectors are already exported from src/optimize.ts) and store it in~/.codeburn/guard/flags.jsonwith a generated-at timestamp.Hook 3: yield checkpoint (Stop)
checkpointUSD(default 3) and contains zero file-edit tool calls and no git commit command, emit a short non-blocking note:This session is $X with no edits or commits yet. If exploring is the goal, fine; otherwise consider a fresh session with a named deliverable.Statusline (optional flag)
codeburn guard install --statuslineadditionally configures the Claude Code statusline command tocodeburn guard statusline, which prints a single line with session cost and cache-freshness (time since last turn vs the 5-minute cache TTL). Skip entirely if the user already has a statusline configured; say so.Config
~/.codeburn/guard.json:{ softUSD, hardUSD, checkpointUSD, openerEnabled, updatedAt }.guard statusprints resolved config, install locations found, flag list size and age.Tests (
tests/guard-install.test.ts,tests/guard-hooks.test.ts)Acceptance
Part of epic #602.