Hooks

Hooks are Node.js scripts attached to Claude Code lifecycle events. They inject context and manage session state. Brewcode registers 4 hooks in hooks.json, plus a shared lib/ that holds the reminder text and I/O helpers they import.

What's new in 6.2.0

forced-eval.mjs no longer fires on every prompt — it now fires on the 1st real prompt, then every 10th, via a session-keyed counter (same pattern as think-short-prompt-counter.mjs). The injected REMINDER_TEXT also shrank 636 → 472 chars. See the detailed description below.

Summary table

#HookEventMatcherChannelTimeoutPurpose
1forced-evalUserPromptSubmitadditionalContext2sManager-role + split-discipline reminder
2session-startSessionStartadditionalContext3sSession init, permission_mode tag
3role-recallSessionStartcompactadditionalContext2sRe-inject [ROLE]/[SPLIT]/[BRANCH] after a compaction
4compact-recallSessionStartcompactadditionalContext2sRe-anchor plan, intent and task graph after a compaction

Execution flow

UserPromptSubmit
  └── forced-eval.mjs          [ROLE] delegate-to-expert + [SPLIT] bounded units + [BRANCH] default-to-main

SessionStart
  └── session-start.mjs        Version check, plan-symlink, permission_mode tag

SessionStart (matcher: compact)
  ├── role-recall.mjs          Same [ROLE]/[SPLIT]/[BRANCH] text, unconditional
  └── compact-recall.mjs       [PLAN] or [INTENT], plus [TASKS] when a task graph exists

Hook files

brewcode/hooks/
  forced-eval.mjs              UserPromptSubmit handler
  session-start.mjs            SessionStart handler
  role-recall.mjs              SessionStart handler, matcher "compact"
  compact-recall.mjs           SessionStart handler, matcher "compact"
  hooks.json                   Hook registry (event bindings)
  lib/
    utils.mjs                  Shared I/O, configuration, logging
    reminder.mjs                REMINDER_TEXT: [ROLE] + [SPLIT] + [BRANCH]
  tests/
    run.sh                      Runs every suite, reports pass/fail counts
    suite-session-start.mjs     Covers session-start.mjs and compact-recall.mjs -- 68 checks, all passing

I/O protocol

All hooks follow a unified protocol:

  1. Read JSON from stdin (via readStdin())
  2. Receive fields: session_id, cwd, source (SessionStart), permission_mode (all events), transcript_path (compact-recall only)
  3. Output JSON to stdout (via output())
  4. Write logs to stderr

Shared utilities:

  • hooks/lib/utils.mjs — I/O, configuration, logging, and projectRoot(): CLAUDE_PROJECT_DIR env var when it points at a real path, else walk upward from the hook’s cwd for the nearest .git or .claude directory, else the starting cwd itself — never throws, never guesses beyond that
  • hooks/lib/reminder.mjs — the one normative copy of [ROLE]/[SPLIT]/[BRANCH], imported by forced-eval.mjs and role-recall.mjs so the two hooks cannot drift apart

Detailed description

1. forced-eval.mjs

UserPromptSubmit additionalContext

Keeps delegation discipline in front of the model without taxing every single turn. Fires on the 1st real prompt of a session, then every 10th prompt after that (10, 20, 30, …), and appends three short lines: the Manager role rule, the split rule, and the branch-default rule. There is no skill-activation nudge — modern models pick skills on their own.

ParameterValue
EventUserPromptSubmit
ChanneladditionalContext
Timeout2000 ms
Cadence1st real prompt, then every 10th (INTERVAL = 10)

Exact injected text (REMINDER_TEXT, from hooks/lib/reminder.mjs — the same text role-recall.mjs re-injects after a compaction; 472 chars):

[ROLE] Manager: check .claude/agents/ (project first); domain expert -> delegate regardless of size, else self.
[SPLIT] One agent for an hour = drift you cannot observe: bounded units (1 deliverable, ~5 files, ~20 min), fan out in ONE message; real data handoff = dependency, else parallel; spawn prompt: goal + scope + done-so-far + consumer + acceptance.
[BRANCH] No branch/PR instruction -> stay current, else main; take over ALL workspace changes incl. other sessions.

What it does:

  • Receives the user prompt
  • Skips meta-replies first (see below) — they never touch the counter
  • Bumps a session-keyed counter (private 0700 tmp dir, atomic write, lstat-only — same pattern as think-short-prompt-counter.mjs) and injects only when the new count is 1 or a multiple of 10
  • Injects REMINDER_TEXT via additionalContext — UserPromptSubmit cannot rewrite the prompt (updatedInput is silently dropped in CC 2.1.x)
  • Output is capped at 9000 chars (10K disk-spill safety, CC 2.1.174+)

When it fires: On the 1st real user prompt of a session, then every 10th one after that (prompts 10, 20, 30, …) — not every prompt. Meta-replies that carry no task (yes/no/ok/thanks/continue/confirm-style answers, a bare number, or a single letter) are skipped before the counter is touched, so they never consume or land on an inject slot. Coverage after a compaction is unaffected — role-recall.mjs re-injects the same text unconditionally on every compaction, independent of this cadence.


2. session-start.mjs

SessionStart additionalContext

Initializes the session, checks for brewcode/Claude updates, manages Plan Mode symlinks, and reports the active permission_mode in the system message for audit.

ParameterValue
EventSessionStart
ChanneladditionalContext
Timeout3000 ms

The hook reads permission_mode from the hook payload and appends it to systemMessage so every session records its trust level (default / plan / bypassPermissions). Output is capped at ~9000 chars (10K disk-spill safety, CC 2.1.174+).

Logic by session source:

SourceBehavior
initLog session_id, append permission_mode tag
resumeLog session_id, append permission_mode tag
clearCreate symlink LATEST.md -> newest plan

LATEST.md symlink:

  1. Checks ~/.claude/plans/ for .md files
  2. Picks the newest one (by mtime)
  3. If the file is less than 60 seconds old — creates .claude/plans/LATEST.md -> ~/.claude/plans/<newest>.md

If <root>/.claude/plans is itself a symlink out of the project, the hook detects it, logs a warning, and returns without creating anything — it no longer follows that symlink to write LATEST.md somewhere outside the project root.


3. role-recall.mjs

SessionStart / compact additionalContext

Re-injects the same [ROLE]/[SPLIT]/[BRANCH] frame as forced-eval.mjs, unconditionally, the moment a session starts back up after a compaction — the one point where the summary has just collapsed every earlier copy of it.

ParameterValue
EventSessionStart
Matchercompact
ChanneladditionalContext
Timeout2000 ms

Why it exists: an auto-compaction has no prompt, so forced-eval.mjs never fires for it. Without this hook the session quietly stops delegating after a few compactions.

What it does:

  • Fires only when input.source === 'compact'; any other source (startup, resume, clear, fork) returns {} — those already carry the frame
  • Injects REMINDER_TEXT from hooks/lib/reminder.mjs, capped at 9000 chars — the same import forced-eval.mjs uses, so the two cannot drift
  • Stateless: compactions can chain, and it fires again on every one

4. compact-recall.mjs

SessionStart / compact additionalContext

Re-anchors plan, original intent and task graph after a compaction, so the session does not start a brand-new task graph on top of the one it already had. additionalContext on SessionStart is the only channel that reaches the model here — PostCompact stdout is UI-only, and UserPromptSubmit never fires on an auto-compaction because there is no prompt.

ParameterValue
EventSessionStart
Matchercompact
ChanneladditionalContext
Timeout2000 ms

It reads only this session’s transcript_path — one readFileSync, guarded by a statSync check (must be a regular file, at most 64 MB) — then scans the raw buffer for a handful of markers, no JSONL parsing. Measured on an 8.13 MB transcript: the scan itself (one buffer read plus five substring scans) is ~6 ms; the full process wall clock is ~30 ms standalone and ~55 ms spawned from a node parent, where node startup dominates.

Decision ladder, first match wins:

BranchConditionInjects
plan-filelast recorded plan path still exists on disk[PLAN] — read that file before any other action
plan-latestno usable planFilePath in the transcript, but <project-root>/.claude/plans/LATEST.md exists[PLAN] — read that project-local link before any other action
plan-missingplan path recorded, but the file is gone (~/.claude/plans gets pruned)[PLAN] — rebuild from the compact summary plus TaskList
plan-in-summaryplan mode ran, no plan file was ever recorded[PLAN] — follow a plan if the summary holds one, else fall back to intent
intentnone of the above[INTENT] — re-read the user’s original task from the compact summary

plan-latest exists because the hook’s only signal, "planFilePath":", is written INTO the transcript — a plan that predates the transcript (after --resume or /clear) leaves no such marker to scan for. The project-local LATEST.md symlink is the same one session-start.mjs maintains on source === 'clear', so this rung can only ever point at this project’s own most recent plan, never a foreign one.

[TASKS] is appended whenever the transcript contains a TaskCreate call, and is ordered before the plan is acted on — the built-in task reminder can lag several turns and show empty, so TaskList is the authority.

Guarantee: on source === 'compact' it always injects something — every failure path degrades to [INTENT], never to silence, and it never names a plan from outside this session’s own transcript.

Trade-off: two plans in one session — the LAST recorded plan path wins.


🚀

Latest Release

Download, changelog, and installation instructions.

🔗

View on GitHub

Source code, README, and configuration files.

🚀

Brewcode Overview

Plugin overview, skills, and agents.

Updating plugins

Use /brewtools:plugin-update to check and update the brewcode plugin suite in one command. See the FAQ for details.