Plugin manifests define `hooks`, but plugin hooks are not loaded into the Codex hooks runtime
Summary
Plugin manifests appear to support a hooks field, but plugin-defined hooks are not actually loaded into the runtime hook system. As a result, plugins can contribute skills, MCP servers, and apps, but not lifecycle hooks, even though the plugin spec and scaffolding suggest that they can.
This creates a mismatch between:
- plugin manifest/spec expectations
- plugin scaffolding/templates
- actual runtime behavior
Current Behavior
Today, Codex lifecycle hooks are loaded from hooks.json discovered through config layers, via the hooks engine/runtime.
At the same time, plugin manifests and plugin scaffolding suggest support for plugin-scoped hooks:
- plugin sample/spec includes a
hooksfield - plugin scaffolding includes optional hook-related structure
However, in the runtime:
- plugin loading resolves skills, MCP servers, and apps
- plugin manifests do not appear to contribute hook configs to the active
codex-hooksengine - enabled plugins therefore cannot activate lifecycle hooks as part of the plugin bundle
Why This Matters
A plugin already acts as a packaging and composition unit for reusable Codex capabilities. If a plugin can include:
- skills
- MCP servers
- apps
but not:
- lifecycle hooks
then the plugin boundary is incomplete. The plugin can package the tools and instructions for a workflow, but not the runtime lifecycle behavior that should accompany that workflow.
Example use cases:
- a repo/workflow plugin that blocks unsafe shell commands before execution
- a plugin that injects context after specific tool calls
- a plugin that enforces stop-time continuation prompts for a narrow workflow
- a plugin that adds session-start checks when the plugin is enabled
Without plugin-scoped hooks, these behaviors must live in global/workspace hook config, which weakens encapsulation and portability.
Reproduction / Evidence
From local inspection of the repository:
1. Plugin spec/scaffolding suggests hooks are supported
The plugin sample/spec includes a hooks field:
codex-rs/skills/src/assets/samples/plugin-creator/references/plugin-json-spec.md
The plugin creator scaffold also references hooks:
codex-rs/skills/src/assets/samples/plugin-creator/scripts/create_basic_plugin.py
2. Runtime plugin loading does not appear to load hooks
Plugin manifest loading in:
codex-rs/core/src/plugins/manifest.rs
Plugin loading/aggregation in:
codex-rs/core/src/plugins/manager.rscodex-rs/plugin/src/load_outcome.rs
These paths load/aggregate:
- skills
- MCP server config
- apps
but not hook config paths.
3. Hooks runtime is separate and only discovers config-layer hooks
Hook discovery/runtime is implemented under:
codex-rs/hooks/src/engine/discovery.rscodex-rs/hooks/src/registry.rscodex-rs/core/src/hook_runtime.rs
The active handlers are discovered from config layers, not from enabled plugins.
4. Product/UI copy also reflects the current limitation
Plugin render text currently describes plugins as bundles of:
- skills
- MCP servers
- apps
which is consistent with current runtime behavior, but inconsistent with the plugin spec.
Expected Behavior
If a plugin manifest declares:
{
"hooks": "./hooks.json"
}
and the plugin is enabled, then that hook config should be loaded into the active hooks runtime for the session.
A plugin should be able to contribute lifecycle hooks the same way it contributes other capabilities.
Proposed Solution
Minimal approach:
- Treat
hooksas another plugin-contributed runtime capability, alongside:
skillsmcpServersapps
- Extend plugin manifest loading to resolve the
hookspath in the same way it resolves the other plugin component paths.
- Extend plugin load aggregation to expose effective hook config paths for enabled/healthy plugins.
- Pass those effective plugin hook paths into the hooks discovery/runtime layer.
- Keep execution order deterministic:
- config-layer hooks first
- plugin hooks after
This seems like the lowest-risk behavior because it preserves existing global/workspace policy while allowing plugins to layer additional behavior.
Suggested Semantics
- Only enabled plugins contribute hooks.
- Plugins with load errors should not contribute hooks.
- Hook file paths should be resolved relative to plugin root and validated with the same safety rules used for other plugin manifest paths.
- Existing hook event schemas and runtime semantics do not need to change.
- No need to introduce new hook types initially;
type: "command"support is enough.
Compatibility / Risk
This should be backward-compatible:
- existing global/workspace hooks continue to work unchanged
- plugins without
hooksare unaffected - plugin-scoped hooks simply become active when present
Potential questions to decide explicitly:
- ordering between global and plugin hooks
- whether duplicate hook files should be deduplicated
- whether plugin hooks should always be active when the plugin is enabled, or only when the plugin is explicitly invoked/mentioned
My recommendation:
- active whenever the plugin is enabled
- deduplicate by resolved path
- run after config-layer hooks
Requested Outcome
I am not asking for unsolicited code review here.
I am mainly asking:
- Is plugin-scoped hook support intended?
- If yes, would the maintainers accept an invited PR implementing the minimal wiring above?
- If not, should the plugin spec/scaffolding be updated to stop advertising
hookssupport?
If helpful, I can also provide a more concrete implementation outline based on the current Rust crate boundaries.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗