Plugin manifests define `hooks`, but plugin hooks are not loaded into the Codex hooks runtime

Resolved 💬 4 comments Opened Apr 10, 2026 by andre-menutole Closed Apr 10, 2026

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 hooks field
  • 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-hooks engine
  • 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.rs
  • codex-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.rs
  • codex-rs/hooks/src/registry.rs
  • codex-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:

  1. Treat hooks as another plugin-contributed runtime capability, alongside:
  • skills
  • mcpServers
  • apps
  1. Extend plugin manifest loading to resolve the hooks path in the same way it resolves the other plugin component paths.
  1. Extend plugin load aggregation to expose effective hook config paths for enabled/healthy plugins.
  1. Pass those effective plugin hook paths into the hooks discovery/runtime layer.
  1. 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 hooks are 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:

  1. Is plugin-scoped hook support intended?
  2. If yes, would the maintainers accept an invited PR implementing the minimal wiring above?
  3. If not, should the plugin spec/scaffolding be updated to stop advertising hooks support?

If helpful, I can also provide a more concrete implementation outline based on the current Rust crate boundaries.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗