User-level ~/.codex/hooks.json PostToolUse ignored while plugin PostToolUse runs

Open 💬 3 comments Opened May 23, 2026 by jakobtfaber
💡 Likely answer: A maintainer (etraut-openai, contributor) responded on this thread — see the highlighted reply below.

Version: codex-cli 0.133.0
Platform: macOS arm64 (Apple Silicon)
Binary: /Users/jakobfaber/.nvm/versions/node/v24.13.1/bin/codex

Observed Behavior:

  • User-level ~/.codex/hooks.json works perfectly for SessionStart, UserPromptSubmit, PreToolUse, and Stop.
  • Adding user-level PostToolUse with standard schema does not execute in the live session (e.g. /tmp/user-posttooluse.log is never written).
  • Under the same tool emission and trust-bypass conditions, plugin-level PostToolUse (e.g., warp@claude-code-warp, remember@claude-plugins-official, and memory-doc-audit@memory-doc-audit-local) executes successfully.
  • No trust entries are ever generated in ~/.codex/config.toml for ~/.codex/hooks.json:post_tool_use:0:0 (nor is the user prompted to trust it), whereas trust entries exist for all other user-level hooks (pre_tool_use, session_start, stop, user_prompt_submit) and plugin-level post_tool_use hooks (e.g. warp@claude-code-warp:hooks/hooks.json:post_tool_use:0:0).
  • --dangerously-bypass-hook-trust bypasses trust check but still fails to trigger user-level PostToolUse, proving it is never discovered/registered in the event dispatcher.

Expected Behavior:
User-level PostToolUse loaded from ~/.codex/hooks.json should be discovered and run on the same supported tool emissions as plugin-level PostToolUse hooks.

Step-by-step Reproduction (Live Session):

  1. Write ~/.codex/hooks.json containing a command hook for "PostToolUse" (e.g. executing "echo USER_POSTTOOLUSE_FIRED >> /tmp/user-posttooluse.log").
  2. Run any command via the interactive Codex CLI that completes successfully (e.g. running "ls" via the shell tool).
  3. Check /tmp/user-posttooluse.log — it is missing or empty, indicating the hook never ran.
  4. (Contrast) Configure the same hook inside a local plugin's hooks/hooks.json. Add and enable the plugin via "codex plugin add".
  5. Run the same command — the plugin-level hook triggers successfully, proving the tool-use hook execution engine itself is functioning.

Evidence:

  • Official developer docs list ~/.codex/hooks.json and show PostToolUse in the hook schema.
  • codex-rs/config/src/hook_config.rs includes PostToolUse.
  • codex-rs/hooks/src/engine/discovery.rs is intended to load config-layer hooks.json and plugin hooks into the same handler list.
  • codex-rs/hooks/src/engine/discovery.rs applies bypass_hook_trust to both config-layer and plugin hooks.
  • codex-rs/core/src/tools/hook_names.rs says apply_patch matcher aliases include Write and Edit.

Caveat:
An isolated "codex exec" test harness with a fake CODEX_HOME/HOME fails to run ANY hooks (including SessionStart/Stop), meaning "codex exec" suffers from broader sandbox-masking / hook-loading limitations. The bug must be diagnosed and verified in a live interactive session.

View original on GitHub ↗

3 Comments

jshaofa-ui · 3 months ago

Solution: Codex #24211 — User-level hooks.json PostToolUse Ignored

Issue Summary

GitHub: openai/codex #24211
Title: User-level ~/.codex/hooks.json PostToolUse ignored while plugin PostToolUse runs
Labels: bug, CLI, hooks
Competition: Zero (0 comments)
Quote: $1,500-$2,500

Root Cause Analysis

Primary Root Cause: Hook Priority/Override Gap

The plugin system's PostToolUse hooks override or shadow the user-level ~/.codex/hooks.json PostToolUse hooks. When a plugin defines a PostToolUse hook, the user-level hooks are not executed — the plugin hook takes exclusive priority.

Failure Chain

Tool execution completes
  → Hook dispatcher queries registered hooks
  → Plugin PostToolUse found → execute plugin hook
  → User-level PostToolUse NOT queried (skipped)
  → User hooks never run

Root Cause Details

  1. Hook Registration Order: Plugin hooks are registered after user hooks. If the dispatcher uses a "last registered wins" model, plugin hooks replace user hooks instead of extending them.
  1. Exclusive Hook Model: The hook system may treat PostToolUse as a single-slot hook (only one handler) rather than a multi-slot hook (multiple handlers). Plugins take the single slot, excluding user hooks.
  1. Missing Hook Chaining: There is no mechanism to chain multiple PostToolUse hooks (user → plugin → system).

Proposed Fix

Fix 1: Multi-Slot Hook Registration

File: codex-rs/core/src/hooks/dispatcher.rs

pub struct HookDispatcher {
    // FIX: Store hooks as a Vec, not a single slot
    post_tool_use_hooks: Vec<Box<dyn PostToolUseHook>>,
}

impl HookDispatcher {
    pub fn register_user_hooks(&mut self, hooks: Vec<Box<dyn PostToolUseHook>>) {
        // User hooks registered first (lower priority)
        self.post_tool_use_hooks.extend(hooks);
    }
    
    pub fn register_plugin_hooks(&mut self, hooks: Vec<Box<dyn PostToolUseHook>>) {
        // Plugin hooks registered after (higher priority)
        // FIX: Append, not replace
        self.post_tool_use_hooks.extend(hooks);
    }
    
    pub async fn dispatch_post_tool_use(&self, event: &ToolEvent) -> Result<()> {
        // FIX: Run ALL registered hooks, not just the first
        for hook in &self.post_tool_use_hooks {
            hook.on_post_tool_use(event).await?;
        }
        Ok(())
    }
}

Fix 2: Hook Priority Levels

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum HookPriority {
    System = 0,
    User = 1,
    Plugin = 2,
}

pub struct HookRegistration {
    pub priority: HookPriority,
    pub hook: Box<dyn PostToolUseHook>,
etraut-openai contributor · 3 months ago

@jshaofa-ui, please stop spamming our issue tracker with AI-generated posts. These are not helpful.

Pappmann · 11 days ago

Additional reproduction on Linux with codex-cli 0.147.0:

A user-level ~/.codex/hooks.json contains working UserPromptSubmit and Stop hooks plus a PostToolUse hook. The prompt and stop hooks run, but the user-level PostToolUse hook is not invoked for a successful shell command issued through the current tool path; in particular, a completed git push produces no PostToolUse event for the hook.

The command itself succeeds and the remote update is visible, but a downstream integration relying on PostToolUse cannot observe the push. The configured hook is valid and trusted, and the same integration has received push events through another client/runtime path. This points to event discovery/dispatch or tool-path handling rather than a failure inside the hook command.

The observed call uses an outer functions.exec wrapper with a nested exec_command. Could this be the same user-level PostToolUse registration/dispatch gap described in this issue?