Skip to content

codeburn guard: opt-in session-time hook pack for Claude Code #605

Description

@iamtoruk

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions