Skip to main content
Start here: silence is the normal state. base suppresses output that has not changed, so a session that says nothing at the start is usually a session where nothing needed saying. Before debugging, decide whether something is actually wrong.

Preview instead of guessing

Why it works. base context runs the same matching engine the hooks run, against text you supply, and prints what would have been injected. It is read-only and does not touch session state. This answers most questions in one step. If base context returns nothing for the text you actually sent, the problem is upstream of the hooks entirely.

See what a prompt actually got

base log matches reads the match log: for every prompt and file touch, which domains matched and by which keyword or folder, which rules and decisions were served or held back, and their scores. --session and --rule narrow it; --json prints the rows. base hooks show lists the blocks of this session’s last prompt-hook output, and base hooks show <block> prints one exactly as the hook built it, including a block the budget held back. Each session’s full, untrimmed output is also on disk under .base/hook-output/<session id>/ (session-start.md, prompt-submit.md), and every pointer line names that session’s own file.

The four things that control injection

The first three are purely about triggers and content. Only the bracket layer looks at how full your context window is.

When it does not fit

Claude Code delivers only so much of a hook’s text to the model and drops the rest. Since 0.16.0 base fits its output to a budget first, counted in bytes because that is how Claude Code counts: 10,000 bytes for session start and for the prompt hook by default, the size measured to arrive whole on Claude Code 2.1.287. What matters most goes first. At the prompt: rules matched to the prompt or to a file the session touched, then that domain’s context, relay messages, your global rules and decisions, then the bracket rules. When it does not fit, whole blocks are held back from the end of that order, never cut mid-sentence, and each leaves one line naming it and base hooks show <block>. Held-back rules still reach Claude as a title each, with an instruction to read the rule before acting on anything it covers.
base doctor shows the last and the largest recent output of each hook against its budget, and which blocks were held back. base doctor --measure asks your Claude Code, with a few headless calls on a cheap model, how much of each hook’s text reaches the model, writes the budgets and [budget] measured_on, and prints before and after. Doctor tells you when the Claude Code you run no longer matches measured_on. Nothing runs the measurement on its own, because it spends model calls.

Brackets, and why the same rules keep appearing

Brackets stage injection by how deep you are into a session: FRESH, MODERATE, DEPLETED, CRITICAL. In percent mode, base reads real depletion from the live transcript and compares it against bracket.context_window (default 200000), with thresholds at 20, 45 and 70 percent. Why it works. Bracket rules re-inject as the bracket moves, so the standing rules you care most about do not quietly erode as a conversation fills up. Since 0.16.0 each one comes once per session rather than on every prompt, and comes back when the bracket moves to a new tier. A bracket rule can carry covered_by, a line of text: when a CLAUDE.md that Claude Code loads already contains that line, the rule is not sent at all. The gotcha. bracket.mode absent means turns, not percent. That is deliberate: defaulting an absent key to percent would measure every older install against a 200k window, and anyone on a larger-context model would compute several times their real depletion and pin to CRITICAL permanently. New installs write the key explicitly. Verified from source at 0ace1ba, the commit the shipped 0.15.2 binary was built from.

The one that catches everyone

A matched *star command short-circuits domain matching for that prompt. Your rules do not arrive that turn, and nothing tells you.Rule out this cause first. It is the most common one by a distance.

Turn on the diagnostics

Hook output then carries a block naming which domains matched, why, and what was deduplicated or suppressed, and Claude ends every reply with a DEVMODE block. Devmode is off by default since 0.16.0, and base does not update itself while it is on. base log matches answers the same questions without either, so reach for it first. Turn devmode off when you are done tuning: base config set devmode.enabled false.

Run the hook by hand

Why it works. This bypasses your agent entirely and puts the hook’s own stdout and stderr in front of you. Claude Code sends six events: session-start, user-prompt-submit, pre-tool-use, post-tool-use, stop and session-end. A seventh, upgrade, is one base starts itself after an update. The gotcha. Read stderr. It is the only place errors go.

Why errors are invisible

Hooks fail open. Every error is caught, written to stderr, and the process exits 0 with empty stdout. A broken hook is indistinguishable from a hook that had nothing to say.
This is deliberate and it is the right trade: a corrupt graph or a failed extraction should never stop you working. The cost is that silent breakage can persist unnoticed, because your agent will usually not show you hook stderr. So do not infer failure from missing output. Run the hook by hand, or read the log.

The audit trail

Every hook invocation appends a line with the event, whether it succeeded, and counts including domains matched and rules injected. This is the record that tells you whether hooks are firing at all. The gotcha. The file grows. It is truncated at 10MB, keeping the last 5000 lines, but that truncation runs from the dashboard rather than from the hook writer, so a machine that never opens base dashboard has nothing trimming it. Check its size occasionally, or open the dashboard now and then.

The ladder, in order

1

Did you type a star command?

It short-circuits everything else for that prompt.
2

Does the domain have rules?

base rule list --domain <name>. An empty list means the trigger fires into nothing.
3

Does the trigger match?

base context "<the exact text>". Keywords match as whole words, literally.
4

Was it held back, or already shown?

base log matches --last 5 and base hooks show. A held-back rule is listed by title at the end of the prompt’s output; a rule shown earlier in the session is not repeated.
5

Are you in a registered workspace?

base project list. Outside one there is no workspace graph to match against.
6

Is the hook running?

Pipe JSON into base hook <event> and read stderr.
7

Is the graph healthy?

base doctor. It exits nonzero when it is not.

Next

Graph health

What base doctor checks, and how to repair or roll back.

Rules and domains

Getting the trigger right in the first place.