Seven modules of files sitting in a folder. This one turns them into something a person on another machine can load, and then tests whether they actually arrived. Those turn out to be two different questions, and the gap between them is most of what this module is about.
The finished plugin is at github.com/01000001-01001110/llm-wiki-plugin, published on 2026-09-02 as commit d1c1799. Nine files. Compare it against yours when you finish.
The manifest is three keys
.claude-plugin/plugin.json Name, description, version. No skills, agents, commands or hooks field.
{
"name": "llm-wiki",
"description": "Git-native compiled knowledge for Claude Code with append-only sources, deterministic provenance, bounded maintenance, and no retrieval service dependency.",
"version": "0.1.0"
} tests/test_wikictl.py Eight tests, standard library unittest, no packages to install.
# Run with:
# python3 -m unittest discover -s tests -v
#
# Observed 2026-09-02:
# Ran 8 tests in 2.532s
# OK
#
# The suite covers root resolution, hashing and registration,
# the append-only violation, frontmatter parsing, supersession,
# and both hook entrypoints. .claude-plugin/plugin.json lives inside a .claude-plugin/ folder while everything it ships sits one level up, beside that folder rather than inside it. skills/, commands/, hooks/ and scripts/ are all siblings of .claude-plugin/, and that root is the plugin root. Getting the two mixed up is the standard first-time error, and it produces a plugin that validates and ships nothing.
The name is doing more work than it looks. It becomes the namespace, a prefix stuck onto every component so two plugins can both ship something called capture without colliding. That prefix is also how you will check the load in a minute.
Validate it, and read the warning
claude plugin validate --strict .
Run against this plugin on 2026-09-02:
Validating plugin manifest: E:\Projects\llm-wiki\.claude-plugin\plugin.json
⚠ Found 1 warning:
❯ author: No author information provided. Consider adding author details for plugin attribution
✘ Validation failed (--strict treats warnings as errors)
It failed. That is not a typo. Without --strict, the same manifest and the same warning end differently:
✔ Validation passed with warnings
Both results are correct and the difference between them is the flag. The manifest has no author field, which is a real omission for something published to a public repository where the whole point is that other people find it. --strict promotes every warning to an error, so it is the mode to run before you publish and the mode that tells you about problems you would otherwise scroll past.
Fixing it is one key, which is the good kind of bug:
"author": { "name": "Your Name" }
That failure is left in this module rather than tidied away, because a passing validator on the first try teaches nothing. The thing worth learning is that the plugin was published in this state, and the check found it afterwards.
Now find out whether anything actually loaded
Here is where most people stop, and it is one step too early.
A validated manifest says the folder is well-formed. It does not say a session received the parts. Those are separate claims, and the second one needs an actual session to answer.
Point a print session at the folder and ask what arrived under the namespace:
claude --plugin-dir /path/to/llm-wiki -p "List ONLY items whose name begins with the literal prefix 'llm-wiki:'. One per line. Nothing else. If none, print NONE."
Three runs on 2026-09-02, one from inside the plugin directory and two from an unrelated directory, all returned the same single line:
llm-wiki:llm-wiki
That is the skill from skills/llm-wiki/SKILL.md, namespaced and present. The three files under commands/ did not appear.
Two things about that result, and the second matters more than the first.
What it shows. The skill loads and the namespace works. Asking for the prefix rather than for “the plugin’s commands” is what makes the answer readable, because this machine has global skills of its own with wiki in their names, and an earlier, looser probe came back with those mixed in.
How much weight it carries. This is a model reporting what it can see inside a print session, not a registry dump. It supports “the commands were not observed loading” and does not support “the commands cannot load”. A print session may enumerate commands differently from an interactive one, and the manifest declares no commands field, so discovery here depends entirely on convention. The plugin’s own README flags the same uncertainty from the other direction: “Current Claude Code documentation still discovers flat commands/ files, though new plugins may also use skills/.”
The honest state, then: the skill is confirmed, the commands are unresolved, and the hooks were never observed loading at all, since nothing in these runs would have triggered SessionStart or PreToolUse with the plugin active. Every hook transcript in this track came from piping fixtures into wikictl.py directly, which proves the script’s behaviour and says nothing about discovery.
The pattern to take away is the ordering. Validate proves shape. A namespaced listing proves arrival. A hook you have watched fire proves wiring. They are three checks, each catching something the previous one cannot, and skipping from the first to a claim of “shipped” is how a plugin gets published with a surface nobody has seen work. Two of the three are green here and the third is unrun, so that is what this module says.
Publishing
Now we need to get it somewhere other people can reach. The plugin is nine files and no build step, so publishing is Git and nothing else:
git init
git add -A
git commit -m "Add llm-wiki plugin"
git branch -M main
git remote add origin https://github.com/<you>/<repo>.git
git push -u origin main
One thing to do before the first commit rather than after. An initial commit is the one push where a mistake becomes permanent history instead of a revertible change, so scan for anything that should not leave the machine while it is still only local:
grep -rInE '(sk-[A-Za-z0-9]{20,}|gh[pousr]_[A-Za-z0-9]{20,}|-----BEGIN [A-Z ]*PRIVATE KEY)' .
Then confirm the push landed by reading the remote rather than trusting the output of the command that just ran:
gh api repos/<you>/<repo>/git/trees/main?recursive=1 -q '.tree[] | select(.type=="blob") | .path'
Nine paths came back for this plugin, matching the nine on disk. That is the difference between “the push command exited 0” and “the files are on the remote”, and it costs one command to know which one you have.
Write .claude-plugin/plugin.json with name, description, version and an author object, so —strict passes rather than warning. Confirm your surfaces sit beside .claude-plugin/ and not inside it. Then run all three checks in order and write down the result of each: claude plugin validate —strict . for shape, a —plugin-dir session listing your namespace prefix for arrival, and a real session that triggers your hook for wiring. Where a check does not pass, or you cannot run it, record that instead of assuming it. A plugin whose three checks you can name the state of is finished; one you have only validated is a folder that parses.
Putting the surfaces inside .claude-plugin/ alongside the manifest, because that is where the manifest is and it looks like the plugin’s folder. Everything about this reads as correct. The JSON parses, claude plugin validate passes, the directory names are spelled right, and the files are exactly where you put them. Then a session loads the plugin and gets nothing, with no error, because the loader looked at the plugin root and found no skills/ there. The only symptom is an empty namespace: you ask what loaded and the answer is nothing, on a plugin that validated a moment ago. This is the case the namespaced listing catches and the validator cannot, which is why they are two checks and not one.
The manifest format, —plugin-dir, and the namespace are Claude Code’s, and none of them exist under those names anywhere else. The wiki itself travels completely, because it is Markdown in Git: point any tool at the folder and the knowledge is readable, with the bookkeeping left behind. And the three-check ordering travels furthest of all. Every packaging system, in every language, lets you validate a manifest that then delivers nothing, and the only defence anywhere is to watch the parts arrive on the far side rather than to trust the check on the near side.
Check yourself
claude plugin validate --strict .fails while the same command without--strictpasses. What is the difference, and which one belongs in the step before you publish?- A validated plugin loads and its namespace listing comes back empty. Name the most likely cause, and say why the validator could not have caught it.
- The skill was observed loading and the hooks were not. Write the sentence you would put in your own README describing that state, without claiming more than was watched.