A root plugin.json silently disables all of a plugin's hooks
A root plugin.json silently disables all of a plugin's hooks
What happens
When a plugin directory contains a root plugin.json (the
Agent Plugins 1.0.0 manifest), Codex loads it through
the Agent Plugins loader — and that loader has no hook support. The hooks field
in .codex-plugin/plugin.json is never read, so every hook the plugin declares
stops running, with no warning, error, or log line.
Shipping both manifests is a reasonable thing to want: .codex-plugin/plugin.json
for Codex, the root manifest so the same directory loads in Cursor and other spec
clients. Today those two goals are mutually exclusive, and choosing one silently
costs you the other.
Why it is hard to notice
The failure is invisible from every surface a user would check:
- The TUI still prints
hook: SessionStart/hook: SessionStart Completed
lines — for other plugins' hooks. The count looks plausible.
~/.codex/config.tomlkeeps its[hooks.state]entries for the affected
plugin with valid trusted_hash values, so the hooks appear registered.
codex doctorhas no hooks section.- Nothing is logged.
Two of our plugins were dead for a week before anyone noticed, and locating the
cause took a long bisect against an installed cache.
Reproduction
Codex 0.149.0, Linux (WSL2), plugin installed from a git marketplace.
Plugin layout:
condux/
.codex-plugin/plugin.json # { ..., "hooks": "./hooks/codex-hooks.json" }
.claude-plugin/plugin.json
plugin.json # Agent Plugins 1.0.0 manifest
hooks/codex-hooks.json # SessionStart + Stop
hooks/session-start.mjs
skills/...
hooks/codex-hooks.json:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node \"${PLUGIN_ROOT}/hooks/session-start.mjs\" --codex",
"additionalContextLimit": 4000
}
]
}
]
}
}
With a tracer added to the body of session-start.mjs (editing the command
string instead would invalidate the trusted hash and prompt), runcodex exec "hi" twice — once with the root plugin.json present, once with it
moved aside. Nothing else changes.
| root plugin.json | SessionStart hooks fired | Tracer |
| --- | --- | --- |
| present | 2 (both from an unrelated plugin) | never runs |
| removed | 3 | runs |
A second plugin in the same install reproduces it identically, including aUserPromptSubmit hook that goes from absent to firing. And a third plugin that
ships no root manifest has its hooks working throughout — which is what
isolates the manifest as the cause.
What we tried
| Attempt | Result |
| --- | --- |
| hooks field on the root manifest | ignoring unknown Agent Plugins manifest field |
| extensions | only namespace com.openai is read, keys default_tools_approval_mode / enabled_tools / disabled_tools / tools. No hooks slot |
| conventional hooks/hooks.json alongside | also suppressed |
| move the manifest to .cursor-plugin/plugin.json | hooks return — but that is a Cursor Plugin manifest, a different format |
So there is no supported way to be a spec-conformant Agent Plugin and keep
Codex hooks. We shipped the removal, which costs those two plugins their spec
conformance.
Suggested fix
Either of these would resolve it:
- Precedence — when
.codex-plugin/plugin.jsonis present, let it keep
authority over host-specific fields (hooks, and presumably interface)
even if a root manifest also exists. This matches the documented resolution
order .codex-plugin/ → .claude-plugin/ → .cursor-plugin/, which the
Agent Plugins loader currently bypasses entirely.
- A hooks slot under
extensions— e.g.
extensions["com.openai"].hooks, keeping the root manifest inside its
closed schema.
Failing either, a warning would have saved the week: something likeplugin <name> declares hooks in .codex-plugin/plugin.json but ships a root at load time. Right now the mode is entirely
plugin.json; hooks are disabled
silent.
Environment
- Codex CLI 0.149.0 (npm, linux-x86_64), Ubuntu 24.04 on WSL2
[features] hooks = true- Plugins installed from a git marketplace source
1 Comment
+1