New: watch your coding agents work, step by step, on a live board. Try the demo, no account

Codex hooks generator

Pick what the Codex agent should and shouldn't do, and copy a hooks.json (or the same hooks for config.toml) plus one bash script per hook. Our tests run every generated script under bash with sample hook input, so a blocked command really is blocked and a normal one still runs.

By Jake Marsh. Event names, input fields and output fields checked against OpenAI's Codex hooks docs on 2026-11-02. Runs in your browser and sends nothing anywhere. Every event is listed in the Codex hooks reference; for Cursor, use the Cursor hooks.json generator.

What should the hooks do?
Commands to block
Notify when
Notify with
Where
Config file

Use one form per folder: if both are there, Codex merges them and warns at startup.

1. Install

Run in a terminal. The scripts need jq.

mkdir -p ~/.codex/hooks
# save each script below into that folder, then:
chmod +x ~/.codex/hooks/guard-shell.sh ~/.codex/hooks/protect-env.sh ~/.codex/hooks/notify.sh
command -v jq >/dev/null || echo 'Install jq first: brew install jq (macOS) or apt install jq'
# then start codex and run /hooks to review and trust the new hooks

2. hooks.json

Save as ~/.codex/hooks.json. Then open /hooks in Codex and trust the new hooks.

{
  "description": "Made with taskhold.com/tools/codex-hooks",
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "^Bash$",
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.codex/hooks/guard-shell.sh",
            "timeout": 10,
            "statusMessage": "Checking command"
          }
        ]
      },
      {
        "matcher": "^(Bash|apply_patch)$",
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.codex/hooks/protect-env.sh",
            "timeout": 10,
            "statusMessage": "Checking for secrets files"
          }
        ]
      }
    ],
    "PermissionRequest": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.codex/hooks/notify.sh",
            "timeout": 10
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.codex/hooks/notify.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

3. guard-shell.sh

Save as ~/.codex/hooks/guard-shell.sh

#!/bin/bash
# guard-shell.sh: Codex PreToolUse (Bash) hook. Made with taskhold.com/tools/codex-hooks.
# Reads the hook's JSON from stdin; needs jq.
# Blocks: git push --force / -f, git reset --hard, rm -rf / or ~.
input=$(cat)
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
if printf '%s' "$cmd" | grep -qE '(git push.*(--force|-f( |$)))|(git reset --hard)|(rm -[a-z]*r[a-z]*f?[a-z]* +(/|~)( |$))'; then
  jq -cn '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "This command is blocked by a hook. Use a safer alternative or ask me to run it."}}'
fi
exit 0

4. protect-env.sh

Save as ~/.codex/hooks/protect-env.sh

#!/bin/bash
# protect-env.sh: Codex PreToolUse (Bash, apply_patch) hook. Made with taskhold.com/tools/codex-hooks.
# Reads the hook's JSON from stdin; needs jq.
# Denies commands and patches that name a secrets file. A guardrail, not a sandbox:
# a command that reaches a file without naming it gets through.
input=$(cat)
text=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
hit=$(printf '%s' "$text" | tr -s ' \t\n"=<>;|&()`'"'" '\n' | while IFS= read -r word || [ -n "$word" ]; do
  name=${word##*/}
  case "$name" in
    (.env.example|.env.sample|.env.template) ;;
    (.env|.env.*|*.pem|*.key|id_rsa|id_ed25519) printf '%s' "$word"; break ;;
  esac
done)
if [ -n "$hit" ]; then
  jq -cn --arg p "$hit" '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: ("Kept away from the agent by a hook: " + $p + ". Ask me if you need a value from it.")}}'
fi
exit 0

5. notify.sh

Save as ~/.codex/hooks/notify.sh

#!/bin/bash
# notify.sh: Codex Stop and PermissionRequest hook. Made with taskhold.com/tools/codex-hooks.
# Reads the hook's JSON from stdin; needs jq.
input=$(cat)
event=$(printf '%s' "$input" | jq -r '.hook_event_name // empty')
if [ "$event" = "PermissionRequest" ]; then
  tool=$(printf '%s' "$input" | jq -r '.tool_name // "a tool"')
  msg="Waiting for your approval ($tool)"
else
  last=$(printf '%s' "$input" | jq -r '.last_assistant_message // empty' | tr '\n' ' ' | cut -c1-120)
  msg=${last:-"Codex finished a turn"}
fi
{
  osascript -e 'on run argv' -e 'display notification (item 2 of argv) with title (item 1 of argv)' -e 'end run' "Codex" "$msg" 2>/dev/null
  { afplay /System/Library/Sounds/Glass.aiff || paplay /usr/share/sounds/freedesktop/stereo/complete.oga; } >/dev/null 2>&1 &
} || true
# Stop needs JSON on stdout; PermissionRequest prints nothing so the normal approval prompt shows.
[ "$event" = "Stop" ] && echo '{}'
exit 0

What each recipe uses

  • PreToolUse with matcher ^Bash$ gets the command in tool_input.command. To refuse it, the script returns permissionDecision deny inside hookSpecificOutput, with a permissionDecisionReason the agent reads. No output means carry on.
  • The env guard also matches apply_patch, whose tool_input.command is the patch text, so a patch that edits .env is refused too. .env.example and other templates stay readable.
  • PostToolUse on apply_patch runs after an edit. The script reads the *** Add File: and *** Update File: lines of the patch and formats those files.
  • PermissionRequest runs just before Codex asks for approval. The notify script prints nothing, so you still get the normal prompt; it only tells you one is waiting.
  • Stop runs when a turn ends and must print JSON. Returning {"decision":"block","reason":"..."} makes the reason Codex's next prompt, which is how the tests recipe sends it back with the last lines of the failure.

Exit code 2 with a reason on stderr also blocks a tool call or continues a turn. The scripts print JSON instead, so the reason is explicit and every other command runs as usual.

When a hook doesn't run

  • Open /hooks in Codex: new or edited hooks are skipped until you trust them there.
  • Check jq is installed: jq --version.
  • Run a script by hand with sample input, for example echo '{"tool_input":{"command":"git push -f"}}' | bash ~/.codex/hooks/guard-shell.sh.
  • Project hooks load only when the repo's .codex/ layer is trusted; user hooks in ~/.codex still run.
  • Hooks are on by default; check config.toml doesn't set hooks = false under [features].

Where Taskhold goes further

Hooks act on one Codex session at a time. Taskhold gives Codex, Claude Code, Cursor and other MCP clients one task list: each agent reports its steps and progress there, and asks its questions on a board you can answer from your phone. Try the live board demo without an account, or connect Codex from the Codex setup page.

Questions

Where do Codex hooks go?
~/.codex/hooks.json, or [hooks] tables in ~/.codex/config.toml, apply to every project. .codex/hooks.json or .codex/config.toml in a repo apply to that project once its .codex/ layer is trusted. Every matching hook from every layer runs.
Why doesn't my new hook run?
Codex skips new and changed hooks until you review and trust them with /hooks, and trust is tied to the hook's hash, so editing a hook means approving it again. [features] hooks = false in config.toml turns all hooks off.
Can the tests hook loop forever?
No. Codex sets stop_hook_active when a turn is already running because of a Stop hook, and the generated script lets the turn stop in that case. So a failing test run sends Codex back once per turn.
Does the env guard stop every way of reading a .env file?
No. It denies shell commands and patches that name a .env file, a .pem or .key file or an SSH key. A command that reaches the file without naming it gets through. Codex's docs call tool hooks a guardrail, not a complete enforcement boundary.
Does the page send my settings anywhere?
No. The page builds the text in your browser and makes no requests with what you choose or type.

Sources

Checked 2026-11-02.

See every agent's progress on one board.

Taskhold gives Codex, Claude Code, Cursor and other MCP clients one task list with steps, progress bars and questions you answer from your phone.