Checkwright plugin — design record
The plugin package lists Checkwright in a harness’s plugin catalog. It registers Checkwright’s lifecycle stage skills and wires guard-kit’s guards, and a marketplace at the repository root lists it. It is a discovery surface: installing the plugin installs no kit. Its install skill runs the installer’s init, which remains the only thing that vendors (installer/SPEC.md §init), and the gates stay in the binary, outside every harness. plugin/README.md is the usage tier in front of this file.
The component
plugin/ is repo-root-governed and not a kit. It carries no checks/ and no smoke/, so no kit-root resolver admits it and no install payload carries it. The harness names live here, in the root marketplace file and on the docs pages, and never in a kit SPEC or a kit literal (gate-sdk/SPEC.md §The provenance seam).
The package serves two formats from one tree:
- Agent Plugins 1.0 (agent-plugins.org). The manifest is
plugin.jsonat the package root, requiring$schemaandname. Skills are the immediate subdirectories ofskills/holding aSKILL.mdin the Agent Skills format (agentskills.io). Hooks, commands and agents are outside the standard’s portable core, and it defines no marketplace and no install route, leaving distribution to each client. - Claude Code’s plugin format. The manifest is
.claude-plugin/plugin.json, requiring onlyname. It reads the sameskills/<name>/SKILL.mdlayout and a standard-layouthooks/hooks.json. A marketplace is.claude-plugin/marketplace.jsonat a repository’s root, and/plugin marketplace add <owner>/<repo>reads it.
The two formats read the same skills/ layout, so one package carries both manifests over one set of skills, and a client implementing either reads its own manifest.
The manifests
plugin/plugin.json, the portable manifest, carries$schema(the published Agent Plugins schema URL),name,description,author,homepage,repositoryandlicense.plugin/.claude-plugin/plugin.jsoncarries the same six fields after$schema, and no component paths, since the standard layout already locatesskills/andhooks/hooks.json.
Every field has a reader. name keys the install. description, author, homepage, repository and license are what a listing shows a user deciding to install. $schema is read by the portable client and by the validation leg’s schema check. keywords and extensions are left out because no reader of them was found.
Neither manifest carries version. The marketplace entry carries it (§The marketplace pin). In Claude Code a plugin.json version wins over the entry’s, so a tracked one would be a second version line to bump by hand, and a stale one would stop updates. claude plugin validate plugin warns on the missing version and passes.
The skills
The plugin registers exactly the templates this repository binds in its own skills: one skill per binding shim in LIFECYCLE_KIT_SKILLS_DIR (a skill whose file carries lifecycle-kit’s binding directive, lifecycle-kit/SPEC.md §check-skill-binding), named for its shim, plus one skill of its own, install. What the plugin ships is what this repository’s own iterations run. The roster is derived from the shims and held by check-plugin-parity, never listed.
- Front matter.
nameis the directory’s name.descriptionsays what the skill does, in one or two sentences, and that it runs only when the user asks for it. No other key, so the file stays inside the Agent Skills base set. -
The body is one fixed rendering. Its only variable is
<path>, the template the shim’s directive names:Execute the template at `<path>` in this repository. - If this repository has a skill of its own that binds `<path>`, run that skill instead. - Otherwise, take each `*<slot: …>*` placeholder's own text as its binding. - If `<path>` does not exist, Checkwright is not installed here, or not with the kit that ships this template. Say so, and stop: the `install` skill installs it.
A skill runs a vendored template and binds nothing. A stage template’s slots are the consumer’s to bind in its own skill. That is rule content a package serving every adopter cannot carry: a default chosen here would be this repository’s calibration shipped as everyone’s. So the skill defers to a skill of the repository’s own that binds the template, and otherwise takes each slot’s own text as its binding.
install tells the agent to:
- choose a profile with the user, from the installer README’s Choosing a profile section;
- confirm the worktree is clean;
- run the one-line install for the host with
init --profile <profile>as its arguments, the PowerShell twin on Windows in the form docs/install.md gives; - run each command in the
next:blockinitprints; - hand to the
adoptskill.
The skill carries no template directive. It advertises init on a route, so it is a front-door page (installer/SPEC.md §The front door’s verbs).
adopt is install’s sibling rather than its extension. Fitting the knobs and triaging the first reds run over files init wrote, so they are a vendored template’s job, gate-sdk/SPEC.md §templates/adopt.md, while install runs before anything is vendored. adopt is an ordinary shim-derived skill, and install’s last step hands to it.
Vendoring stays with init. A plugin that vendored kits itself would be a second install model with no manifest, update or uninstall story; the install skill hands to the installer, whose checkwright.lock the plugin never writes.
The guards
plugin/hooks/hooks.json is a rendering of guard-kit/templates/settings-hooks.json, for the one harness that reads a plugin’s hooks; the portable manifest carries skills alone, since the standard excludes hooks from its core. The rendering:
- takes each block under
hooks, its event, its matcher and its hook entries, in order; - reads each hook’s member through the registration parser (gate-sdk/SPEC.md §The harness-integration arm) and composes its
commandas the fail-open prefix followed by the shell-form front-end call of that member:r=$(git -C "${CLAUDE_PROJECT_DIR:-.}" rev-parse --show-toplevel 2>/dev/null) && test -f "$r/gate-sdk/bin/run-gates.sh" || exit 0; CLAUDE_PROJECT_DIR=$r; bash "${CLAUDE_PROJECT_DIR}/gate-sdk/bin/run-gates.sh" --hook <member>, dropping the template’sargs; - fails at exit 2 on a template hook that is no member registration, since the plugin has no command to give it;
- drops the template’s
//note, which is merge advice for a settings file.
A plugin’s hooks fire in every repository where the plugin is enabled, so each command first tests for the vendored front end and exits 0 without it. That test takes a shell, so the plugin’s hooks keep the shell form where the template’s registrations take the exec form. The plugin therefore owes bash on every host: the harness runs a shell-form hook command under a POSIX shell, Git for Windows’ bash on native Windows. It runs it in the session’s current directory, which a subdirectory launch or the session’s own cd moves off the root, so the prefix resolves the repository’s toplevel from the harness’s project directory. The shell-guard fires anywhere inside a vendored repository and stays silent outside one, and declines, through the front end, where git refuses or skips the repository the session stands in.
The prefix differs from the settings rendering’s anchor because the plugin loads where project settings do not. Launched from a subdirectory, a session loads no project settings, and a hook supplied any other way runs with its working directory and CLAUDE_PROJECT_DIR both naming that subdirectory. So the variable cannot name the root here, and the prefix asks git for the toplevel, one spawn per call measured at under a millisecond, git being the adopter floor’s one unconditional member. It falls back to the working directory where the variable is unset, exits 0 where there is no repository or no vendored front end, and rebinds CLAUDE_PROJECT_DIR to the toplevel for the front-end call after it. The rebinding is local to the hook’s shell and its children, so one rendered command serves a root launch, a subdirectory launch and a cd alike.
context-kit’s session-start wiring is not rendered: it is a context brief rather than a guard.
The marketplace pin
.claude-plugin/marketplace.json carries name checkwright, owner, a one-sentence description, and one plugin entry: name checkwright, a source of {"source": "git-subdir", "url": "checkwright/checkwright", "path": "plugin", "ref": "v<pin>"}, version <pin>, and the manifests’ description and author.
<pin> is the hosted install pin (installer/SPEC.md §The hosted install pin), so the plugin and the one-line install always serve one release. The pin moves in the commit after the tag that moves the install pin (RELEASING.md), and the entry’s ref and version move with it.
Publication is a release. The package ships from the pinned tag, so a package the pinned release does not carry is published by the next release’s pin move, and until then the marketplace names a tag the harness cannot install from. check-plugin-parity assertion E holds that to the close’s release disposition.
check-plugin-parity
A repo-root gate (this repo’s scripts/), born native in native/src/gates/plugin_parity.rs. It checks only what no external tool knows, that the package matches this repository; the manifests’ own validity is the validation leg’s, whose oracles are the harness’s CLI and the published schema, since a reader of our own would re-implement schemas that drift.
- (A) The manifests agree. Both parse.
name,description,author,homepage,repositoryandlicenseare present and equal across the two. Neither carriesversion. The portable manifest’s$schemaishttps://agent-plugins.org/schemas/<X.Y.Z>/plugin.schema.json. - (B) The skills match the repository. The skill directories equal, as a set, the binding shims in
LIFECYCLE_KIT_SKILLS_DIRplusinstall. EachSKILL.mdcarries front matter whosenameis its directory and whosedescriptionis 1 to 1024 characters. Each non-install body, after the front matter and its following blank lines, equals §The skills’ rendering for the template its shim names, byte for byte, and that template exists. - (C) The hooks match the template.
plugin/hooks/hooks.jsonequals §The guards’ rendering ofguard-kit/templates/settings-hooks.json, compared as JSON, so a block the template gains reds until the plugin carries it. - (D) The marketplace is pinned.
.claude-plugin/marketplace.jsoncarries exactly one entry, named as the manifests. Its source isgit-subdir, withpathpluginandurlthe manifests’ repository slug. Itsrefisv<pin>and itsversionis<pin>,<pin>read bycheck-install-pin’s own pin reader. - (E) The pinned release carries the package.
v<pin>:plugin/.claude-plugin/plugin.jsonresolves. A tag lacking it is admitted on the pending terms of installer/SPEC.md §The front door’s verbs, through that section’s reader: while.workflow/release-disposition.txtcarries no line for the queue header’s iteration, or a line whose field is a release. Anoneordeferred:field reds it, which leaves the close two remedies, releasing or withdrawing the marketplace file. Wherev<pin>does not resolve, as in a shallow checkout, E is dormant and the clean line says so. - (F) The plugin carries the license text.
plugin/LICENSEequals the repository’s rootLICENSEbyte for byte, since the marketplace installsplugin/alone. A red prints the remedy,cp LICENSE plugin/LICENSE.
Each red names the file, the field and the expected value; a (B) body red and a (C) red print the expected rendering. A missing or unreadable file, unparsable JSON, a missing skills dir, and the pin’s own refusals exit 2. The marketplace file is the one exception: its absence is the withdrawn state E’s remedy leaves, so D and E are dormant there and the clean line says so. It reads LIFECYCLE_KIT_SKILLS_DIR, declared on its registry row, and runs at precommit, coupled to plugin/, .claude-plugin/, the skills dir’s shims, the hooks template, docs/install.sh, the disposition file, the root LICENSE and its module. Its positional form, check-plugin-parity [plugin-dir marketplace skills-dir hooks-template install-sh disposition queue pinned license], points it at a fixture tree, license naming the root file F compares against. pinned is a file whose first word, carries or lacks, stands for the tag’s answer, or - for a tag that does not resolve, so the fixture holds still as this repository’s tags move.
The validation leg
The plugin-validate job in .github/workflows/gates.yml runs each external oracle at a pinned version:
- The harness’s CLI. It installs
@anthropic-ai/claude-codefrom npm at a pinned version and runsclaude plugin validate pluginandclaude plugin validate .. Each must pass. The missing-version warning is expected (§The manifests), and--strictis not used, since it turns that warning into a failure. The marketplace check reads the entry’s shape and fetches nothing. - The portable manifest. It fetches the published schema and validates
plugin/plugin.jsonagainst it withajv-cliunder JSON Schema draft 2020-12, the draft the schema declares. - The skills. It validates each skill directory with the Agent Skills reference validator,
skills-ref validate <dir>, installed at a pinned commit. That library says it is for demonstration and not production, so a finding it raises is read against the Agent Skills specification before a skill is changed for it.
A build that changes the package runs the same commands locally. It installs the plugin from a scratch marketplace with a relative source into a scratch harness config, to see the skills registered; CLAUDE_CONFIG_DIR pointed at a scratch directory leaves the user’s own config untouched. A scratch config carries no login, so the guards are seen in a session that loads the package with --plugin-dir, which installs nothing: firing in a vendored scratch repository and silent in an empty one. Two more sessions follow, and the guard fires in both: one launched from a subdirectory of the vendored repository, and one whose Bash tool changes into a subdirectory before its next call.
Honest limits
- Only Claude Code’s install is run. Every other Agent Plugins client is read off the standard.
- A skill takes each slot’s own text as its binding, which is weaker than a repository’s own bound skill.
- An adopter who also merged guard-kit’s settings wiring runs each guard twice, and the friction log counts each fall-through twice, so the README says to keep one of the two.
- The fail-open prefix means a tree whose front end is missing runs unguarded and says nothing about it.
- On native Windows the harness hands the hook
CLAUDE_PROJECT_DIRin a Windows spelling. Thatgit -Caccepts it andtest -faccepts the forward-slash toplevel git answers is inferred rather than run: no Windows host has driven the prefix.