Skip to main content

Kanon

Every rule governing this session, named. Including the ones you thought loaded and didn't.

A CLAUDE.md can reach your context from half a dozen places, and Claude Code reports none of them. What you cannot see load, you cannot weigh.

What it does

Kanon is a Claude Code plugin rather than an MCP server: hooks that write things down the moment an instruction file loads. By the end of a session you know which file reached your context, in what order, and the reason Claude Code gave for loading it.

A CLAUDE.md can arrive from a lot of directions. The project you are in, your own ~/.claude setup, a directory above you, a subdirectory Claude wandered into, an @path import four hops deep, or a dependency that quietly ships one. Claude Code loads them all the same way and mentions none of them.

A Kanon report: six loaded instruction files listed with their origins, a vendored CLAUDE.md flagged FOREIGN and untracked, a DRIFT section naming one file that appeared, one whose bytes changed and one that vanished, and a NOT LOADED section separating a missing file from ones that were simply not triggered

FOREIGN is the line that matters most. Such a file got a voice in your session without you choosing to give it one. missing is a fault: that file was meant to load at launch and did not. quiet is no fault at all, only a fact about this session: the rule was there, nothing triggered it.

What changed since last time

Kanon remembers which instruction files have governed this repository before, with a sha256 of each. A file it has never watched load is reported as appeared, one whose bytes moved as changed, and one that has left the disk as vanished, once, on the way out.

The case that section exists for is a dependency that updates and quietly rewrites its CLAUDE.md. The list of files looks entirely normal and only the contents moved. A digest catches that without Kanon having to read what the file says, which is also why it can tell you a file changed and never what changed in it. That diff is yours to read.

What Claude is told

Claude holds every instruction file merged into one context with no idea where any of it came from. A rule you wrote and a rule a dependency shipped look identical from the inside. At the start of every session Kanon puts that right:

The session-start brief: six instruction files named against their origins, the foreign one quoting the directive it carries, a rule that was expected and never loaded, and below that what has changed since the previous session

Every file against its origin, the first directive each foreign one carries quoted, what has moved since the previous session, and an instruction to raise anything that stands out with you. It goes out on both channels SessionStart has: additionalContext reaches the model, systemMessage sits in your transcript. Both carry the same text, because a brief that told you one thing and Claude another is the exact failure this plugin exists to catch.

An instruction file can also load hours later, long after that brief has gone out. When a foreign one does, Kanon names it at your next prompt. That has to happen there, because Claude Code discards the output of InstructionsLoaded: the hook that sees the load cannot say anything about it. On an ordinary turn the check costs one wc -l, and if the session log has not grown, Bun never starts.

Quick start

Needs Bun 1.1 or newer, and nothing else. Kanon makes no network request of any kind, has no API key, and never blocks a session.

bash
claude plugin marketplace add https://aranea-development.nl/plugins/marketplace.json
claude plugin install kanon@aranea

Hooks bind when a session starts, so start a new session before Kanon sees anything. What happened before it was installed cannot be reconstructed.

If the install stops at ssh: connect to host github.com port 22, git is cloning the plugin from its GitHub repository over SSH and cannot reach GitHub that way on your machine. The message points at access rights, but the repository is public and the transport is what failed. One line switches git to HTTPS, then install again:

bash
git config --global --add url."https://github.com/".insteadOf "git@github.com:"

The README has the longer version, including how to undo it.

Three questions, three commands

/kanon prints the report for the session you are in. With no arguments it takes the most recent session recorded from this repository, and it never falls back to one from a different directory: reporting one repository's loads against another's expectations invents alarms that are not real.

/kanon:whose <phrase> answers where a rule came from. Claude holds every instruction file merged into one context with no attribution, so when it does something you did not expect, there is no way to ask which file told it to. This is that question.

text
WHOSE  "geen em dashes"                                   observed
  user       ~/.claude/rules/schrijfstijl.md         line 24
             "- **Geen em dashes (—).** Gebruik een komma of twee korte zinnen."

Matching is case-insensitive and reads the file with runs of whitespace collapsed, so a phrase that straddles a line break still matches. Instruction files are hard-wrapped, and the phrase you remember is usually the one a line break cut in half.

The empty answer is often the useful one. If no governing file contains the phrase, Kanon says so and names how many it searched. That directive then reached the session from a surface Kanon does not see: a skill, an MCP server or another plugin's hook. Or it was never in an instruction file at all.

/kanon:audit lists every instruction file that could govern a session in this checkout. It needs no session, so it works in a repository nothing has ever run in. That is the point: you have just cloned something, or an install added packages, and you want to know what got a voice before it uses one.

A Kanon audit of a repository: a vendored CLAUDE.md flagged FOREIGN with its first directive quoted, then seven of the project's own instruction files with their origins and how each would load, closing on a tally of one foreign file out of eight

The second column says how a file would load, never that it did. An on-demand file inside a dependency fires only when Claude reads something in that directory. The audit deliberately enters dependency and dot directories, which the rest of Kanon refuses to do. That refusal holds while there is a session to observe a load, and an audit runs exactly where nothing has run. .git is never entered.

Two layers, and why that matters

The plugin is two halves, deliberately kept apart.

The first half watches. A hook appends every event raw to ~/.kanon/sessions/<id>.jsonl and parses almost nothing. This half needs no model of Claude Code, so it is never in doubt.

The second half predicts. It reimplements how Claude Code resolves instruction files in order to say what should have loaded. That can be wrong, and the code admits it: when a file loads that the model never predicted, Kanon says so and marks its own NOT LOADED section unreliable for that session. What actually loaded still stands, because that half leans on no model at all.

Every report carries a ruleset stamp. Kanon has to model Claude Code to have expectations at all, and that behaviour belongs to Anthropic and can change. The stamp makes a stale model visible rather than silent.

A report showing all three of Kanon's self-doubt sections: a reachability NOTE, an origin disagreement, and two files it could not read

What it does not do

Kanon reports which files reached your context and where they came from. It does not read them for meaning, score them, rank them, or scan them for prompt injection. Whether a dependency's instructions belong in your session is your call. Kanon makes sure you know they are there.

One exception to not reading: the brief quotes the first real directive line of a foreign file, so Claude can match it against the instructions it is already carrying. It quotes, and stops there.

/kanon:whose searches those same files for a literal string and quotes the line it sits on. That is provenance, the same category as the digest and the quoted first directive: it locates text without forming a view of it. Matches come back in origin order, never scored or ranked, and Kanon never tells you whether a rule is a good one.

A directive can also arrive from a skill, an MCP server, an output style, or another plugin's hook. Kanon models none of those, and the reason is structural: Claude Code fires a hook when an instruction file loads and fires nothing when a skill's text or a server's instructions reach your context. Everything Kanon reports about instruction files is either observed or labelled as a prediction you can catch it getting wrong. On those other surfaces it has no such footing, and /kanon:whose says so at the moment it counts.

It never blocks. ConfigChange can hold up a configuration change, and Kanon declines to do so.

Everything lives under ~/.kanon/, and Kanon writes nowhere else. It reads ~/.claude/ and never writes to it. Anything older than 90 days is pruned on the next run.