feat(plugin): ship Codex-native activity hooks - #3364
Conversation
Codex auto-discovers the shared Claude hooks/hooks.json when its manifest omits a `hooks` key, surfacing Claude-branded events in Codex sessions (#3362). This ships Codex-native activity hooks instead of suppressing them (the counterpart to #3363): an inline `hooks` object in the Codex manifest overrides discovery, and its commands use Codex's native $PLUGIN_ROOT rather than the Claude-branded $CLAUDE_PLUGIN_ROOT alias. Events (Codex's HookEventsToml vocabulary): UserPromptSubmit -> 🤖, PermissionRequest/Stop -> 💬. Codex added a Stop turn-end event after codex-cli 0.130.0, so 🤖 returns to 💬 at turn end. Codex has no session-exit event, so the marker persists after a session ends (documented tradeoff, same class as Claude's "persists after interrupt"). Closes #3362 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The new "Activity markers appear in wt list" hint used a plain string, so the <underline> tag rendered literally. Wrap it in cformat! and regenerate the codex-install snapshot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Is it possible for Claude to have a dedicated plugin subdirectory like Codex or does it lack interoperability/support for conventions like the |
|
Good question — the two tools aren't symmetric here, and it turns out a dedicated subdir wouldn't buy what you might expect. Can Claude take a dedicated manifest subdir like But — and this is the key point — a manifest subdir wouldn't isolate the payload anyway. Codex's On the AGENTS.md interoperability angle: yes, it's the same class of situation. There's no shared cross-tool convention — each tool hardcodes its own loader/manifest path with no fallback, and Claude reads If any of those tools grows real cross-tool interop (a shared manifest convention, or Codex payload isolation), the layout could collapse to fewer pointers — but today the three-pointer + shared-payload split is the minimal thing that works against all three real CLIs. |
|
Could the structure of this repo be improved at all to make the Claude plugin config less "prominent" and isolate what is possible? |
|
Good instinct, and it splits cleanly into "forced by the CLIs" vs. "actually improvable." What's forced (can't isolate):
What this PR actually unlocks (a real improvement): Before this PR, Claude's
That's the one place isolation is genuinely available today, and it's a byproduct of the override this PR introduces. I'd keep it out of this PR (it's cleanly separable and this one's already scoped to the Codex hooks), but it's a good follow-up for a maintainer to take — the layout rationale lives in The honest summary: the manifest prominence is fixed by the CLIs, but the hooks payload can now be Claude-scoped, and that's the meaningful isolation lever. |
|
@worktrunk-bot Now that this has been merged, what follow-up items do you have in mind to implement? |
|
Three, in rough priority order — one actionable now, two gated on things outside this repo. 1. Claude-scope the shared hooks payload (actionable now, cleanly separable). 2. Confirm the Codex hooks against a live session, then close the loop (gated on a maintainer with Codex). 3. Wire The only one worth opening now is (1). Happy to take it if a maintainer wants it — otherwise the layout rationale in |
|
Go ahead and implement that now while I test this change myself tomorrow. |
|
Done — opened #3382. It renames Enjoy testing tomorrow — if the markers don't behave as documented (🤖 on prompt, back to 💬 at turn end, persisting after exit), that's a bug to file rather than a follow-up. |
…indow (#3386) Five commits landed on `main` during #3385's CI wait, so they ship inside v0.66.0's tree but weren't in its changelog (the step-12 drift check in the release skill caught this). This adds the user-facing three to the 0.66.0 section: - **Codex activity markers** ([#3364](#3364), closes [#3362](#3362), thanks @ofek for reporting) - **SCP-style SSH remotes with custom usernames** ([#3371](#3371), thanks @fcoury-oai) - **Windows app-alias Settings path** ([#3372](#3372), thanks @ofek) Excluded as internal: #3360 (trait docstrings), #3379 (argv[0] display refactor). Every entry verified against the actual diffs. The v0.66.0 tag will point at this PR's squash commit so the tagged tree carries the complete changelog. 🤖 Generated with [Claude Code](https://claude.com/claude-code) > _This was written by Claude Code on behalf of Maximilian Roos_ Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…n-only discovery (#3431) ## 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](#3382) Claude-scoped the hooks filename, which silently broke hook loading ([#3417](#3417)), and [#3418](#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.** #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.json` → `plugins/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 #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_ Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
What this does
This is the "support" counterpart to #3363. Where #3363 suppresses Codex's auto-discovery of the shared Claude
hooks/hooks.json(settinghooks: {}so nothing loads), this PR ships Codex-native activity hooks sowt listshows 🤖/💬 for Codex sessions the same way it does for Claude Code, OpenCode, and Gemini. Meeting users where they are: #3362 (comment).Both PRs fix the original bug (Claude-branded events surfacing in Codex sessions). They differ only in direction — a maintainer picks one.
How
The Codex manifest (
.codex-plugin/plugin.json) now carries an inlinehooksobject — a Codex-tailored hooks file embedded directly in the manifest, rather than a path or an absent key:Two deliberate choices, both verified against Codex's source (
98d28aab):hookskey makes Codex auto-discoverhooks/hooks.json— the Claude file — and surface Claude events (the Codex plugin hooks should not rely on compatibility with Claude #3362 bug). An inline object takes Codex'sSome(Inline)branch inresolve_manifest_hooks, which overrides discovery — so only these Codex-native events fire, and there's no second file to collide with the Claude one. (This is the same override mechanism fix(plugin): suppress Codex auto-discovery of Claude hooks #3363 uses with an empty inline object.)$PLUGIN_ROOT, not$CLAUDE_PLUGIN_ROOT. Codex exports both to hook commands (discovery.rs#L230-L232—PLUGIN_ROOTnative,CLAUDE_PLUGIN_ROOTan OOTB-compat alias). Using the native var keeps everything Claude-branded out of a Codex session, directly addressing the "poor UX to see anything about Claude while using Codex" concern.The event set is Codex's
HookEventsTomlvocabulary:UserPromptSubmit→ 🤖 (working),PermissionRequest/Stop→ 💬 (waiting). Codex gained aStopturn-end event after codex-cli 0.130.0 (the version whose lack of a turn-end event motivated the original removal in #2786), so 🤖 correctly returns to 💬 when a turn completes — the transition the old "no turn-end event" limitation lacked.Known limitation (documented, not a regression)
Codex's
HookEventsTomlhas noSessionEnd/session-exit event, so there's no hook to clear the marker when a Codex session exits. The resting state after a normal exit is 💬 (set by the lastStop), which lingers until the next session or a manualwt config state marker clear. This is the same class of limitation already documented for Claude ("Status persists after user interrupt"). If Codex later adds a session-exit event, wiring amarker clearhandler is a one-line addition.Files
plugins/worktrunk/.codex-plugin/plugin.json— inline Codex-nativehooksobjectplugins/worktrunk/CLAUDE.md— rewrote the "Codex ships no hooks" limitation into "Codex activity hooks", documenting the inline/$PLUGIN_ROOTrationale and the no-SessionEndtradeoffsrc/commands/config/codex.rs— install now hints that markers appear inwt listsrc/cli/config.rs— Codex plugin--helpno longer claims markers are Claude-onlydocs/content/claude-code.md(+ synced skill mirror) — capability table and prose updatedtests/integration_tests/config_show.rs— asserts the inline hooks object exists, defines exactly the Codex-native events, and uses$PLUGIN_ROOT(never the Claude alias)Testing
test_codex_plugin_metadata_is_valid_json, thetest_helpsnapshots, andtest_docs_are_in_syncpass locally;cargo fmt/clippyclean.The hooks' end-to-end behavior depends on Codex runtime and isn't verifiable from CI — a maintainer should confirm against a live Codex session before merge (marker appears on prompt, returns to 💬 at turn end, persists after exit as documented).
Closes #3362 (as an alternative to #3363).