One command creates the whole shape. Then every other command in the plugin has to find that shape again from wherever you happen to be standing when you run it. Those two problems are the same problem seen from both ends, which is why they share a module.
Let’s start with the command. Python 3 is the only requirement, and the script uses nothing outside the standard library, so there is no install step to get wrong:
python3 scripts/wikictl.py init --root .
Run against an empty folder on 2026-09-02, it printed this and exited 0:
{
"created": [
".llm-wiki/config.json",
".llm-wiki/manifest.jsonl",
".llm-wiki/CLAUDE.md",
"raw/",
"wiki/index.md",
"wiki/log.md",
"wiki/archive/"
],
"git_initialized": true,
"initialized": true,
"root": "...\\wikidemo",
"schema_version": 1
}
Every command in this plugin answers in JSON, and that is a choice with a reason behind it. The caller is usually a language model, and a model reading "changed": [] is on firmer ground than a model reading an English sentence that says nothing changed. Prose can be hedged. An empty list cannot.
git_initialized: true means the folder was not a repository, so init made one. Point it at an existing repository and that field comes back false with nothing else different.
What just got created, and the two that carry state
.llm-wiki/config.json Five keys. Two of them are paths you may move, one is a byte budget you will meet again in module 49.
{
"version": 1,
"raw": "raw",
"wiki": "wiki",
"instructions": ".llm-wiki/CLAUDE.md",
"session_context_bytes": 8192
} .llm-wiki/manifest.jsonl Created empty. One JSON object per line as sources are registered. Module 46 fills it.
(empty at init)
After registering one source it holds one line:
{"path":"raw/hsm-notes.md","registered_at":"2026-09-02T13:01:09Z","schema_version":1,"sha256":"e21c...5b28","size":50,"source_id":"src_660bd5986ecaf88dbf95","status":"active","superseded_by":null} .llm-wiki/CLAUDE.md The instance rules. Not the project root CLAUDE.md, and the distinction is the point.
# LLM Wiki instance
This directory is an LLM Wiki instance. Customize the domain and page taxonomy below.
## Non-negotiable invariants
1. `raw/` is append-only and Git-tracked. Never edit or delete an existing raw source. A revision is a new file.
2. `wiki/` is derived knowledge. Claude may create, edit, consolidate, link, and archive wiki pages.
3. Provenance is page-level in v1. Every knowledge page lists the raw source IDs and exact source hashes it depends on.
4. A source becomes stale only through explicit supersession. Similarity alone never implies supersession.
5. `wikictl` observes/verifies state and performs deterministic bookkeeping. Claude interprets sources and changes knowledge.
6. `dream` performs maintenance only. It never ingests raw sources. Read more
A project may already have its own CLAUDE.md holding build commands and conventions. Writing wiki rules into it would either overwrite that file or bury the build commands under a page of wiki policy. Keeping the instance rules at .llm-wiki/CLAUDE.md means the plugin never touches a file it did not create, and the SessionStart hook in module 49 is what gets these rules in front of the model instead.
wiki/archive/.gitkeep An empty directory cannot exist in Git, so init writes a placeholder file to hold the slot.
(empty file)
Git tracks files, never directories. Without this, wiki/archive/ would vanish on clone. wiki/index.md Written with real frontmatter, so the linter you build in module 47 has something valid to compare against from day one.
---
title: Wiki Index
type: index
last_linted: 2026-09-02
sources: []
---
# Wiki Index
This is the navigation map for compiled knowledge. Add concise links as pages are created. wiki/log.md Append-only history. Ingests and maintenance runs write one entry each.
---
title: Wiki Log
type: log
last_linted: 2026-09-02
sources: []
---
# Wiki Log raw/ Created empty and left that way. You put the first source here in module 46.
Directory. Append-only once its contents are registered. Seven paths, and five of them are content or placeholders. Two hold state that matters: .llm-wiki/config.json says where things are, and .llm-wiki/manifest.jsonl says what has been seen. If you ever need to know whether a wiki is intact, those are the two files to read.
Now notice what init did not do. It did not write a CLAUDE.md at the project root. The plugin’s own wiki-init command spells the reason out in step 4, verbatim: “Do not create or overwrite a project-root CLAUDE.md. Instance instructions intentionally live at .llm-wiki/CLAUDE.md.” A project usually already has that file, holding build commands somebody else wrote. A tool that appends a page of wiki policy to it has damaged the file it was installed to help.
Finding the root again, four steps deep
Next up is the other half of the problem. init gets an explicit --root handed to it. Every other command has to work out where the wiki is from nothing but the directory you are standing in, and this is where tools normally start guessing.
The resolution order, from resolve_root at scripts/wikictl.py:91:
| Step | Source | What happens if it is set but wrong |
|---|---|---|
| 1 | --root on the command line | Error naming the path: no .llm-wiki/config.json under it |
| 2 | LLM_WIKI_ROOT in the environment | Error naming the variable and the path it pointed at |
| 3 | Walk upward from the working directory until .llm-wiki/config.json is found | Falls through to step 4 |
| 4 | Stop. Exit 2. | The command does no work at all |
Step 3 is the one that makes the tool usable day to day. walk_for_root (line 81) iterates over (current, *current.parents), which is the current directory followed by every ancestor up to the drive root, and returns the first one holding the config file. So you can be six directories deep inside wiki/archive/2026/ and every command still finds the root.
Step 4 is the one worth arguing about, so let’s argue about it. When no root is found the script raises and exits 2, with this message:
No wiki root found walking upward from <dir>; expected .llm-wiki/config.json. Use --root or set LLM_WIKI_ROOT.
The obvious alternative is to fall back to the Git repository root, and the README rules it out in one line: “There is no ‘guess the Git root’ fallback.” Here is what that costs and what it buys. It costs you a convenience: a wiki whose root is not where you are gets an error instead of a helpful guess. It buys you the guarantee that a command never operates on a wiki you did not mean. Those two are not close in value once you remember that some of these commands write files and one of them makes Git commits.
The exit code is part of the interface
One more thing before we build. Three exit codes, and the middle one surprises people:
| Code | Meaning |
|---|---|
0 | Success, nothing to report, or the bookkeeping you asked for succeeded |
1 | Success, with findings |
2 | Configuration or tool failure |
Exit 1 does not mean the command broke. It means the command worked and found something you should look at: an unregistered file, a broken link, a stale page. The plugin’s SKILL.md states it as an instruction to the model reading the output: “Treat exit 1 as structured findings, not as a crashed command.”
This matters more than it looks. A model that reads exit 1 as a crash retries the command, gets exit 1 again, and either loops or gives up. Splitting “it broke” from “it has news” into two different codes is what lets the caller tell those apart without parsing prose.
That is it. One command to create the shape, four steps to find it again, three codes to report what happened. Everything in the next six modules runs on top of those three things.
Create scripts/wikictl.py in your plugin folder with two functions and an argparse parser: resolve_root implementing the four steps in the table above, and cmd_init writing the seven paths in the FileTree. Have every command print JSON to stdout and return one of the three exit codes. Then confirm it from a subdirectory: cd wiki/archive && python3 ../../scripts/wikictl.py inventory should find the root by walking up and print JSON rather than an error. Run it once more from your home directory, where there is no wiki above you, and confirm you get exit 2 and the message naming .llm-wiki/config.json. A resolution order you have not watched fail on step 4 is one you are guessing about.
Resolving the root from the Git repository root, because git rev-parse —show-toplevel is right there and it usually gives the same answer. Here is where it stops giving the same answer. You keep your wiki in a folder inside a monorepo. A colleague adds a second wiki elsewhere in the same repository. Now both resolve to the repository root, which contains neither .llm-wiki/config.json, so the tool either errors confusingly or, worse, finds the wrong one of the two and writes to it. What you see is a commit touching a wiki you were not working in, and you see it later, in a diff, rather than at the moment it happened. The fix is the rule this plugin follows: the marker file is the root, and nothing else is.
All of it, and further than the rest of the track. A marker file plus an upward walk is how git finds a repository, how npm finds a package root, and how pytest finds its configuration. Writing your own is fifteen lines and it works in any language on any harness, because it depends on nothing but the filesystem. The JSON-on-stdout and three-exit-code contract travels the same way: it is the shape every well-behaved command-line tool already has, and it is what makes a script safe to hand to a model.
Check yourself
- You are in
~/projects/notes/wiki/archive/andLLM_WIKI_ROOTis set to a folder that does not exist. What doesinventorydo, and which of the four steps decided it? lintexits 1. Name one reason that is not a failure, and say what a caller should do differently from the way it treats exit 2.initwrites.llm-wiki/CLAUDE.mdrather than a project-rootCLAUDE.md. What breaks in a real project if you change that?