Plugin docs/examples imply plugin-local hooks, but runtime only executes global hooks.json
Summary
Codex plugin docs/examples imply that plugin-local hooks.json is a supported plugin surface, but the current runtime appears to only load hooks from config-layer hooks.json files (for example ~/.codex/hooks.json).
In practice, an installed plugin can contribute skills / .mcp.json / .app.json, but its plugin-local hooks.json does not seem to run.
Environment
- Codex CLI:
v0.118.0 - OS: Linux
Repro
- Create or install a local plugin with this shape:
.codex-plugin/plugin.jsonhooks.jsonscripts/runtime-hook.cjs
- Put simple lifecycle hooks in
hooks.json, for exampleSessionStart,UserPromptSubmit,Stop, each running a command that writes a file under/tmpor$HOME. - Install/enable the plugin normally so it is present under
~/.codex/plugins/cache/<marketplace>/<plugin>/localand enabled in~/.codex/config.toml. - Start a new Codex session.
Expected
If plugin-level hooks are supported, the installed plugin's hooks.json should be discovered and executed.
Actual
The plugin installs and can be enabled, but its lifecycle hooks do not run.
If I place the exact same hooks into ~/.codex/hooks.json, they immediately start working.
Why this looks inconsistent
There are several signals that suggest plugin-local hooks are intended to work:
- The plugins docs/examples mention
hooks.jsonas a plugin companion surface. - The public plugin examples include plugin folders with
hooks.json. - Example manifests include a
hooksfield pointing at./hooks.json.
But the current runtime code seems to say otherwise:
codex-rs/core/src/plugins/manifest.rsparses plugin manifests and recognizesskills,mcpServers, andapps, but nothooks.codex-rs/hooks/src/engine/discovery.rsdiscovers hooks by scanning config-layer folders forhooks.json, not installed plugin roots.
So right now the observed behavior is:
- plugin install/load path: plugin capabilities work for skills/MCP/apps
- hook runtime path: only config-layer
hooks.jsonis actually executed
Request
Could you clarify which of these is intended?
- Plugin-local
hooks.jsonis supposed to work, and this is a runtime bug. - Plugin-local
hooks.jsonis not supported yet, and the docs/examples should be updated to avoid implying support.
If plugin-local hooks are intended to be supported, that would make plugin packaging much cleaner because lifecycle hooks could live inside the plugin bundle instead of requiring a second global ~/.codex/hooks.json installation path.
4 Comments
A bit of additional technical framing that may help here:
Plugins already act as a packaging/composition boundary for reusable Codex capabilities. Today, an enabled plugin can contribute:
From that perspective,
hookslook like a missing runtime capability in the same composition model. Right now, a plugin can package the tools and instructions for a workflow, but not the lifecycle behavior that should accompany that workflow.That is why this feels more like an incomplete plugin boundary than just a docs mismatch.
A minimal implementation path seems to be:
hooksin plugin manifest loading, alongsideskills/mcpServers/appsThat would preserve current global/workspace behavior while allowing plugins to package lifecycle behavior together with the rest of the plugin bundle.
Adding a current-version confirmation: I can still reproduce this on Codex
0.128.0-alpha.1on macOS, specifically incodex exec.I tested while packaging a small local plugin with both:
UserPromptSubmithookResults:
UserPromptSubmithook worksUserPromptSubmithook does not executeI used a marker-file test for the hook command, so this was not just “the model didn’t see the additionalContext”; the plugin hook command itself did not run.
I also tried:
hooks/hooks.jsonwith top-level{ "hooks": { ... } }{ "UserPromptSubmit": [...] }hooksfield present / absent[features] codex_hooks = trueAll plugin-bundled hook variants failed in
codex exec.One related install-path note:
codex plugin marketplace add <local path>registers the marketplace in config but does not appear to install/activate local plugin components by itself;codex plugin marketplace upgrade <name>rejects local marketplaces because it expects a Git marketplace.Confirming reproduction on
0.128.0(release) on macOS — same setup as @zorkary's report (.codex-plugin/plugin.jsonwith explicithooks: "./hooks/hooks.json"+hooks/hooks.json+ marketplace registered viacodex plugin marketplace add .+ plugin enabled +[features] codex_hooks = true).Plugin hooks silent. Same exact command moved to top-level
[[hooks.SessionStart]]/[[hooks.UserPromptSubmit]]in~/.codex/config.tomlfires immediately. Marker-file test confirms the hook command never runs from the plugin path — not a "model didn't see additionalContext" issue, the process itself isn't spawned.@andre-menutole's 4-step plan upthread looks correct to me.
Use case context: filed #21675 for the broader rule-cache proposal that this bug currently blocks distributing as a plugin. Once
hooksdiscovery from plugin manifests works, the entire workaround collapses to a single--uninstallcommand.Confirming this is still a blocker on Codex CLI
0.130.0on macOS.Use case: packaging SLOPE lifecycle guards as a Codex plugin. SLOPE can generate a
.codex-plugin/plugin.jsonand package the guard dispatcher/scripts cleanly, but the lifecycle hooks still need to live in a config-layer.codex/hooks.jsonor~/.codex/hooks.jsonto be discovered and reviewed by/hooks.Observed locally:
codex features listreportshooksas stable/trueplugin_hooksis present but under development/false/hooksdiscovers config-layer project hooks from~/Development/fathoms/.codex/hooks.jsonThis means a clean
slope hook add --level=full --harness=codeximplementation currently has to install a shim into global/project hook config rather than shipping lifecycle behavior entirely inside the plugin bundle. The desired fix from @andre-menutole above would resolve that: plugin manifests should expose effective hook config paths to the existing hook discovery/runtime.