domains.toml, rule text in the graph.
Creating a domain
prompt_keywords match against what you type, as whole words. file_keywords match against the content of a file being worked on. paths match against the files this session has touched and the file a tool is about to touch. Any one of them firing is enough.
A project’s own domain (the one named after the project) needs no path trigger at all since 0.16.0: touching a file inside the project’s folder loads it. See the file you touch decides.
The gotcha. Creating a domain gives you a bucket with no rules in it. A match with nothing attached injects nothing, silently, which is the single most common reason people conclude base is not working. Add a rule next, not later.
Adding a rule
--rationale. It is injected with the rule as “rule, because rationale”, and a rule with a reason attached survives being argued with in a way that a bare imperative does not.
Next rung. base rule remove --domain billing --index 2 removes one by the index shown in rule list. Indexes shift after a removal, so list again between removals rather than working down a list you printed earlier.
Saying when a rule matters
Since 0.16.0 a rule can carry its own matchers instead of riding its domain’s triggers:
A rule with matchers of its own is served on them, not through its domain’s keywords or folder. A rule with none is served through its domain, as before.
base rule list --domain billing shows each rule’s id, kinds and matchers. --path is stored as its full path; for a project’s domain it must lie inside that project, not in a child project or around another one.
Each rule is served once per session, and again after you edit it or when the context bracket moves to a new tier, so a domain matching again does not repeat what Claude already has.
Test prompts
--fires-on is a prompt that must serve the rule (up to 3); --quiet-on is one that must not (up to 2). base rule test runs them through the prompt hook’s own matching, lists every miss and false fire, counts the rules with no tests, and exits 1 on a failure. Any command that changes a domain’s matching re-runs that domain’s tests and prints one line.
Inside Claude Code, where CLAUDECODE=1 is set, base rule add refuses a rule without --keywords (or --words) and --fires-on. Every rule an AI adds arrives with the words that should bring it back and a prompt to prove it.
Writing a rule that reaches Claude
Each message carries at most[budget] prompt_bytes (10,000 bytes) of base’s text, and rules that do not fit are held back. Five habits keep yours in:
- One or two sentences, the instruction first.
- The reason in its own field,
--rationale "<why>", not in the text. - Specific keywords: words that appear in the prompts the rule is for, so it comes only when it matters.
- For a rule about one file or folder,
--path, so it loads only when that file is touched. GLOBAL, or any always-on domain, only for a rule every session needs whatever it is about. Those rules go into every session and take room from the rules a prompt matched.
base hooks show <block> command with its rules’ titles (each rule’s first sentence, at most 100 characters), most relevant first. base hooks show <block> prints the block in full. base doctor lists the rules held back most often in the last 30 days and the rules over 500 bytes, each with what to do: move it out of an always-on domain, give it a path, or shorten it. To shorten a rule added with base rule add, add the shorter text and remove the old one; a rule written in domains.toml is edited there.
The global tier
-g goes on rule itself, before the verb. After the verb is invalid.
The gotcha. Six commands carry -g: rule, decision, handoff, fork, learn and changes. Everything else decides its tier from where you are standing, and outside a registered workspace base refuses the write rather than picking one for you. Check base help <command> instead of assuming.
Checking a domain fires
base context runs the real matcher against text you supply, without sending anything anywhere. This is the fastest possible answer to “would this have worked”.
When nothing gets injected
Work down this ladder in order. It is ordered by how often each cause is the real one.1
Did you type a star command in that prompt?
A matched
*command short-circuits domain matching for that turn. Your rules do not arrive, and nothing says so. This is the most common cause by a distance, especially if you have settled into a mode you type by habit.2
Does the domain have any rules?
domains.toml holds triggers only, so a domain can look perfectly configured and be empty.3
Does the trigger actually match?
invoice does not fire on billing or on invoices, and nothing is doing anything clever on your behalf.4
Was it served, held back, or already shown?
base log matches shows what each prompt matched and which rules were served or held back. A rule held back by the budget is listed by title at the end of that prompt’s output, and base hooks show <block> prints it. A rule already shown in this session is not repeated until you edit it.5
Are you inside a registered workspace?
base scaffold fixes it.6
Is the hook itself running?
Watching it work while you tune
base log matches is the quiet way: it lists what every prompt and file touch matched, by which keyword or folder, and what was served, without adding anything to Claude’s context. With devmode on, hook output also carries a diagnostic block showing which domains matched, why, and what was deduplicated or suppressed.
The gotcha. Devmode is off by default since 0.16.0. Turn it back off when you are done: it adds output to every hook event, Claude ends every reply with a DEVMODE block, and base does not update itself while it is on.
When add-trigger refuses
domains.toml is byte for byte what it was before you ran it. A folder that is the domain’s own project folder and also holds child projects is accepted once each child is linked with base project update <child> --parent <project>.
Why it refuses rather than warns. A trigger on a folder that holds ten projects cannot say which project you are in. Refusing at write time turns that into an error you read immediately. A broad trigger you already had still loads, outside the projects it holds; base domain paths --suggest proposes an exact path for each one.
The related case: base project add --name <name> --path <path> under such a path still registers the project, but creates no same-named domain, and tells you so with no domain was created. The domain it would have made could never have fired.
Keyword triggers are never refused this way. Only a path can be a broadcast.
Removing triggers and domains
base domain remove is a triggers-level delete: the rules you added still exist in the graph, they simply have nothing left to fire them. If you want a domain quiet but recoverable, remove its triggers instead.
All four domain write commands (create, add-trigger, remove-trigger, remove) act on the domains.toml of the workspace you are in, or on the global one with --global. base domain list merges both tiers, so pass --global to remove something that lives in the global file.
Pushing config into the graph
domains.toml into the graph so domains and their bootstrap rules exist as entities. It also runs on its own at session start, gated by a timestamp so it is a no-op when nothing has changed.
Next
Debugging injection
The rest of the story: brackets, suppression, and where the audit trail is.
Context reference
Every domain and rule command, with its flags.