Skip to content

refactor(plugin): adopt Claude's canonical manifest layout, convention-only discovery - #3431

Merged
max-sixty merged 1 commit into
mainfrom
plugins
Jul 12, 2026
Merged

refactor(plugin): adopt Claude's canonical manifest layout, convention-only discovery#3431
max-sixty merged 1 commit into
mainfrom
plugins

Conversation

@max-sixty

Copy link
Copy Markdown
Owner

What this does

Moves the Claude plugin manifest to the canonical plugins/worktrunk/.claude-plugin/plugin.json and strips it to metadata only, dropping the hooks and skills keys so that both component types load purely by Claude Code's convention discovery. Rewrites the layout docs and inverts test_plugin_layout_is_consolidated to pin the new invariants.

Background

The plugin layout has been in flux: #3382 Claude-scoped the hooks filename, which silently broke hook loading (#3417), and #3418 reverted it. That revert restored working hooks but left two claims baked into the layout, the docs, and the test that this PR found to be stale.

Research method

Everything below was verified empirically against claude-cli 2.1.207 (the current release), not from docs alone:

  • Scratch marketplace probe: a throwaway marketplace mirroring worktrunk's exact shape (repo-root .claude-plugin/marketplace.json, plugin in a ./plugins/<name> subdir source, skills as a symlink pointing outside the plugin dir, hooks at the conventional path) with two plugin variants — manifest in .claude-plugin/plugin.json vs. bare plugin.json at the plugin root — installed through the real claude plugin marketplace add + claude plugin install flow.
  • Component inventory: claude plugin details and claude plugin validate on both variants.
  • Live sessions: headless claude -p runs confirming skills appear in-session and hook commands actually execute (marker files, and reading the 🤖 activity marker from inside a live session).
  • Loader inspection: the manifest resolution order read directly out of the claude-code binary's bundled loader code.

Findings

  1. The "no .claude-plugin/ wrapper" claim no longer holds. The repo documented — and the test pinned — that Claude wants plugin.json bare at the plugin root, and that a .claude-plugin/ wrapper in a subdir-source plugin fails with "Plugin not found". On 2.1.207 the wrapper form installs, loads skills, and fires hooks end-to-end. The loader tries .claude-plugin/plugin.json first with root plugin.json only as a runtime fallback, several marketplace-resolution paths read only the wrapper, and claude plugin validate rejects the root-level form outright ("No manifest found in directory. Expected .claude-plugin/marketplace.json or .claude-plugin/plugin.json").
  2. "Claude has no skill auto-discovery" is wrong. Skills are auto-discovered by scanning skills/*/SKILL.md; a skills manifest array only adds directories to that scan (entries equal to the default dir are filtered). An unlisted skill behind the out-of-plugin symlink loaded fine — the installer dereferences the symlink into a real directory in the install cache. Dropping the array also makes the old test guard ("every skill must be listed or it's silently invisible") structurally unnecessary: a new repo-root skill now ships to Claude, Codex, and Gemini with no manifest change anywhere.
  3. The hooks manifest key was dead config. Claude plugin hooks silently stop loading after hooks.json → claude-hooks.json rename (#2677f05) #3417 established the string-path override isn't honored for plugin loads; convention discovery of hooks/hooks.json is the mechanism that works. A key pointing at the conventional path adds nothing and could mask a future mislocated file, so it's gone (one mechanism per guarantee).

Changes

  • plugins/worktrunk/plugin.jsonplugins/worktrunk/.claude-plugin/plugin.json; content reduced to name/description/author.
  • plugins/worktrunk/CLAUDE.md: layout diagram and Claude path-resolution bullet rewritten to the convention-only model; the refuted wrapper claim and the stale no-auto-discovery claim removed.
  • Root CLAUDE.md: the Plugin Layout pointer updated (dropped the stale "Codex no-hooks re-enablement" phrase — Codex hooks shipped inline in feat(plugin): ship Codex-native activity hooks #3364).
  • test_plugin_layout_is_consolidated: now asserts the wrapper manifest exists, a root-level plugin.json does not, the manifest carries no hooks/skills keys, the plugin skills symlink resolves to the repo-root skills/ (unix only — Windows checkouts without core.symlinks materialize it as a file), and every repo-root skill dir carries a SKILL.md (the scan silently ignores a dir without one).

Codex and Gemini are untouched: Codex reads only its .codex-plugin/ wrapper, and its inline hooks object still overrides Codex's own convention discovery, so the #3362 collision stays closed independent of any of this.

Verification

  • claude plugin validate plugins/worktrunk passes (it failed on the old layout). Remaining warnings are deliberate: no version field (installs pin the git SHA as the version; a hand-maintained semver would go stale) and the plugin-root CLAUDE.md (developer docs, not plugin context).
  • A live claude --plugin-dir session on this exact tree loads both skills and the UserPromptSubmit hook sets the 🤖 marker, observed from inside the session.
  • The scratch-marketplace probe covered the same shape through the full marketplace install flow, including the symlinked skills dir.
  • Full pre-merge gate: 4389 tests passed plus lints, doctests, and docs-sync.

Risk

The one scenario this could regress is a Claude Code version old enough to read only the root-level manifest. That couldn't be tested directly; the mitigations are that the wrapper is the documented convention, the loader prefers it today, and Claude Code self-updates aggressively. The old layout's "verified end-to-end" claim most likely dates from an earlier 2.1.x behavior or a misdiagnosed failure.

This was written by Claude Code on behalf of max

…n-only discovery

Move the Claude manifest to plugins/worktrunk/.claude-plugin/plugin.json
(the only location `claude plugin validate` accepts) and drop its
`hooks`/`skills` keys: the string-path hooks override is not honored for
plugin loads (#3417) and skills are auto-discovered from skills/, so both
keys were dead or redundant config. Update the layout docs and invert
test_plugin_layout_is_consolidated to pin the new invariants.

Verified against claude-cli 2.1.207: a scratch marketplace mirroring this
exact shape installs, loads both skills, and fires the hooks; the prior
root-level manifest loads only via an undocumented fallback and fails
validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@max-sixty
max-sixty merged commit 6c122a5 into main Jul 12, 2026
39 checks passed
@max-sixty
max-sixty deleted the plugins branch July 12, 2026 19:35
max-sixty added a commit that referenced this pull request Jul 12, 2026
…eld (#3434)

Follow-up to #3431: records in `plugins/worktrunk/CLAUDE.md` why the
Claude manifest deliberately omits a `version` field — installs pin the
marketplace git SHA, so a semver would be a second version to maintain
with nothing consuming it — so a future cleanup pass doesn't "fix" the
`claude plugin validate` warning by adding one.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
max-sixty added a commit that referenced this pull request Jul 13, 2026
…mirror (#3440)

Codex installs of the worktrunk plugin carried no skills. `codex plugin
add` copies the plugin into `$CODEX_HOME/plugins/cache/` via
`copy_dir_recursive` (codex-rs core-plugins), which handles only regular
files and directories — so the `skills -> ../../skills` symlink (and the
nested `reference/README.md` link inside the tree) were silently
dropped, and sessions load from that cache copy. Verified against
codex-cli 0.144.1 both by reading the tagged sources and by installing
this repo's plugin into a scratch `CODEX_HOME`: the installed root had
no `skills/` at all and `codex debug prompt-input` showed an empty
plugin skill inventory.

This PR cuts the plugin's `skills` symlink over to a **generated
real-file mirror** of the authored repo-root `skills/`, kept current by
a new `sync_plugin_skills_mirror` stage in `test_docs_are_in_sync`
(dereferences symlinks, deletes stale files, self-heals and fails on
drift — same pattern as the other generated mirrors). Repo-root
`skills/` stays the authored home: Gemini hard-probes it at the
extension root, the docs sync writes into it, and Windows checkouts read
it. Reversing the symlink direction instead would put a symlink at the
repo root, breaking Gemini's install copy and every Windows checkout —
which also surfaces a latent bug this fixes: symlinks materialize as
plain text files on Windows clones, so installs from a Windows checkout
shipped no skills to Claude or Codex either.

It also drops the Codex manifest's `skills: "./skills/"` key: with no
key, Codex scans `<plugin-root>/skills/` by convention
(`default_skill_roots` in `codex-rs/core-plugins/src/loader.rs`), and
the explicit path resolves to the same directory, so the key was
redundant — symmetric with the Claude manifest cutover in #3431.

**For the reviewer:**

- The 20 files under `plugins/worktrunk/skills/` are the generated
mirror (byte-identical to repo-root `skills/`; git stores shared blobs
once). `plugins/worktrunk/CLAUDE.md` → "Plugin skills are a generated
mirror" documents the rationale with codex-rs citations.
- `sync_plugin_skills_mirror` in
`tests/integration_tests/readme_sync.rs` is the sync stage (Step 3b of
the pipeline); it already proved itself once in this branch — merging
main regenerated `reference/{config,list}.md` in the mirror.
- `test_plugin_layout_is_consolidated` now pins the mirror shape
cross-platform (real directory, no symlinks anywhere under it),
replacing the unix-only symlink assertion.

**Verification:** fresh scratch-`CODEX_HOME` install now carries both
skills into the cache and `codex debug prompt-input` lists `worktrunk`
and `wt-switch-create`; `claude plugin validate` passes and `claude
--plugin-dir … plugin details` discovers Skills (2) plus all 8 hooks;
Gemini's repo-root path is untouched. A 2×2 probe matrix (manifest key
present/absent × real dir/symlink) confirmed key-absence changes nothing
and symlinks ship nothing.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants