codex_hooks do not fire in interactive sessions when configured via repo-local .codex/config.toml

Open 💬 8 comments Opened Apr 12, 2026 by hahnpierre
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Bug Report Draft

Title

codex_hooks do not fire in interactive sessions when configured via repo-local .codex/config.toml

Summary

Repo-local .codex/config.toml appears to be loaded for normal project configuration, but hook configuration inside that file does not result in interactive SessionStart or Stop hooks firing.

Environment

  • Codex CLI version: 0.120.0
  • Installation: npm global package
  • Platform: macOS Apple Silicon
  • Repo uses project-local config: ./.codex/config.toml

Reproduction

  1. Create a repo-local .codex/config.toml with:
hooks = "/absolute/path/to/repo/.codex/hooks.json"

[features]
codex_hooks = true
  1. Create hooks.json with SessionStart and Stop command hooks.
  2. Point the hook commands at scripts that:
  • read hook payload JSON from stdin
  • emit valid hook JSON on stdout
  • write debug artifacts to disk so execution can be confirmed
  1. Start an interactive session with codex from the repo root.
  2. Try to confirm:
  • startup context injection
  • stop-time block/reminder behavior

Expected

  • SessionStart should fire when the interactive session starts
  • Stop should fire when the interactive session exits
  • hook output should affect the live session just like manual hook execution output does

Actual

  • interactive sessions start and stop normally
  • no evidence that SessionStart fired
  • no evidence that Stop fired
  • no startup additionalContext appears in the session rollout log
  • no stop block/reminder occurs

Important Control Result

The same hook scripts work when run manually:

  • SessionStart script emits valid JSON with hookSpecificOutput.additionalContext
  • Stop script emits valid JSON with decision = "block" and a reason
  • debug artifacts are written correctly

This suggests the problem is not the script payload format itself, but that repo-local hook config is not actually being honored by interactive hook execution.

Notes

  • Moving hooks = ".../hooks.json" under [features] is invalid and causes a TOML type error. The correct placement is top-level.
  • The repo-local config format itself parses cleanly once hooks is top-level.
  • The issue affects at least both SessionStart and Stop, which suggests a config-loading or hook-registration problem rather than an event-specific matcher issue.

View original on GitHub ↗

8 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #17518

Powered by Codex Action

driptaroop · 3 months ago

can confirm. faced the same issue.

sly-codechum · 3 months ago

Same

abhinav-oai contributor · 3 months ago

Just setup a Stop hook locally and I'm not able to repro

Here's my local setup:

─────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
     │ File: .codex/hooks.json
─────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ {
   2 │   "hooks": {
   3 │     "Stop": [
   4 │       {
   5 │         "hooks": [
   6 │           {
   7 │             "type": "command",
   8 │             "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/stop_keep_going.py\"",
   9 │             "timeout": 30,
  10 │             "statusMessage": "blocking end of turn"
  11 │           }
  12 │         ]
  13 │       }
  14 │     ]
  15 │   }
  16 │ }
─────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
─────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
     │ File: .codex/config.toml
─────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ [features]
   2 │ codex_hooks = true
─────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
─────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
     │ File: .codex/hooks/stop_keep_going.py
─────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ #!/usr/bin/env python3
   2 │
   3 │ import json
   4 │ import sys
   5 │ import time
   6 │
   7 │
   8 │ def main() -> int:
   9 │     json.load(sys.stdin)
  10 │     json.dump({"decision": "block", "reason": "keep going"}, sys.stdout)
  11 │     sys.stdout.write("\n")
  12 │     return 0
  13 │
  14 │
  15 │ if __name__ == "__main__":
  16 │     raise SystemExit(main())
─────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

As well as how my conversation looks

› hi


• Hi.

• Stop hook (blocked)
  feedback: keep going

Would be super helpful if you could attach more details, thank you!

---
Also FWIW we don't support a custom path to a hooks.json via top-level hooks field in the config.toml. Codex will resolve the hooks.json by looking in the same directories as any enabled config layers.

leorrose · 3 months ago

same

coladapo · 2 months ago

+1 — confirmed reproducible on Codex CLI 0.128.0 (macOS, ChatGPT auth). Adding empirical detail in case it helps narrow the cause.

Test setup

Minimal probe hook at /tmp/codex-probe.sh:

#!/bin/bash
date -u +"%Y-%m-%dT%H:%M:%SZ probe fired" >> /tmp/codex-probe.log
echo '{"systemMessage":"PROBE WORKED"}'

Registered at ~/.codex/hooks.json:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume",
        "hooks": [{ "type": "command", "command": "/tmp/codex-probe.sh", "statusMessage": "PROBE…" }]
      }
    ]
  }
}

~/.codex/config.toml has [features] codex_hooks = true (also confirmed default-true in v0.128.0 source).

Results

| Invocation | Probe log written? | systemMessage shown? | codex_hooks debug log |
|---|---|---|---|
| codex exec 'echo test' | ✅ yes | n/a (non-interactive) | hook: SessionStart + hook: SessionStart Completed |
| codex (interactive TUI) | ❌ no | ❌ no | (no hook log lines) |

So the discovery + parsing path works (since exec finds and runs the same hooks.json), but the interactive TUI startup path skips hook execution entirely. Same ~/.codex/hooks.json, same ~/.codex/config.toml, same probe script — only the launch mode differs.

Reproduction

  1. Install probe + config above
  2. codex exec --skip-git-repo-check 'echo'/tmp/codex-probe.log gets a line
  3. codex (interactive) → no log entry, no system message
  4. Both layers tested (user ~/.codex/ and project ./.codex/) — same result

This affects cross-platform tooling that targets all three of Claude Code / Gemini CLI / Codex via SessionStart hooks (e.g. memory injection at session start). Header + context render fine on Claude and Gemini interactive sessions, silently fail on Codex interactive.

Happy to PR a fix if you can point at where the interactive TUI startup path should call into the hook engine.

oxysoft · 2 months ago

Indexed this hook ticket in the umbrella tracker: #21753

Goal: collect the scattered Codex hook requests and bugs into one parity matrix for Full Claude Code Hook Parity (29+), while preserving this issue as the detailed thread for its specific behavior.

swchen44 · 2 months ago

Confirming this is still reproducible on codex CLI 0.132.0 (macOS arm64, ChatGPT auth, 2026-05-21), and adding empirical data that may help narrow the cause.

Updates vs previous comments

The thread previously stopped at 0.128.0 with the issue still open. The behavior is unchanged on 0.132 across both modes:

| Mode | Result |
|---|---|
| codex exec (headless CLI) with --dangerously-bypass-hook-trust + [projects."<ws>"] trust_level = "trusted" pre-seeded in $CODEX_HOME/config.toml | hook command never executes |
| codex (interactive TUI via tmux) — accept directory trust dialog → accept "Trust all and continue" hook trust dialog → trusted_hash IS written to $CODEX_HOME/config.toml → restart codex (trust persists) → submit a turn | hook command never executes |

So the per-hook trust storage layer works correctly, and the directory-trust UX path works correctly — but the hook executor itself never runs the command for either mode.

Codebase observation (may help triage)

In codex-rs/hooks/, the codex_hooks crate contains zero tracing:: calls across engine/mod.rs, engine/discovery.rs, registry.rs, and hook_runtime.rs. With RUST_LOG=info,codex_hooks=trace the codex-tui.log for an interactive session shows session_init / shell_snapshot / mcp_manager_init / etc., but nothing about hook discovery, loading, or firing — even with hooks visibly registered in the TUI Hooks dialog (Installed: 1, Active: 0, Review: 1 before trust → Installed: 1, Active: 1, Review: 0 after). This makes any kind of behavioral debugging from user-side opaque.

The Rust unit tests in codex-rs/core/tests/suite/hooks.rs all drive codex via the in-process test_codex().submit_turn() library API, never through the exec / tui CLI binaries. So the existing test matrix wouldn't catch a regression in the CLI → core wiring of run_pending_session_start_hooks.

Reproduction script

End-to-end matrix test that exercises both modes and reports per-mode PASS/FAIL:

https://github.com/swchen44/c_code_scanner_for_memory/tree/main/tools/codex_hook_firing_repro

The script:

  • Builds an isolated $CODEX_HOME with auth.json + hooks.json containing a SessionStart hook that does /usr/bin/touch <marker>
  • Tests codex exec mode with all trust gates explicitly bypassed
  • Tests codex TUI mode via tmux, programmatically accepting both trust dialogs
  • Returns PASS/FAIL per mode based on whether the marker file exists

On my machine both modes return FAIL. Equivalent SessionStart hooks for Claude Code (.claude/settings.json) and OpenCode (TS plugin via event bus) on the same machine fire correctly — so the workspace, environment, and hook script itself are all fine. It's specifically the codex client-side dispatch that doesn't deliver the event.

Impact (for context)

For an internal usage-tracking integration we built, the SessionStart hook silently no-ops on codex sessions, so dashboards undercount codex adoption relative to CC + OpenCode. We've shipped all the client-side mitigations we can (per-test CODEX_HOME isolation, project trust pre-seed, --dangerously-bypass-hook-trust flag) — none of it helps because the executor itself never runs.

Happy to provide additional traces, test on a nightly build, or refine the repro script if any of that would help.