Skip to main content
A hook is a shell command, or an HTTP request, that o4 runs at a set point in a session: before a tool runs, after it finishes, when you send a prompt, when a turn ends, and so on. Use hooks to check or block risky tool calls, log what the model does, change tool input or output, or send yourself a notification.

Add a hook

Hooks go in a config.toml file as [[hooks]] entries. This hook runs a script before every shell command the model tries to run:
The script gets details about the call as JSON on standard input. If it exits with code 2, o4 blocks the call, and the model gets the error “Tool execution blocked by PreToolUse hook”:
Make the script executable with chmod +x scripts/check-command.sh. A script that can’t run exits with another code, which o4 treats as a failure, not a block (see on_failure). Run /hooks in a session to see the hooks o4 has loaded.

Where hooks are configured

o4 reads the project files from the folder you start it in. It runs the hooks from every file; one file’s hooks never replace another’s. When several hooks match an event, they run one at a time in this order: ~/.o4/managed/config.toml, .o4/config.local.toml, .o4/config.toml, ~/.o4/config.toml, then .o4.md. Within a file, they run in the order written. The first hook that blocks stops the rest. An event name o4 doesn’t know is an error in config.toml: o4 stops at startup and lists the valid names. Project hooks only load after you trust the workspace, because a hook can run any command on your machine. See Hook trust.

Hooks in .o4.md

You can keep hooks next to your project instructions in .o4.md. Put them in a fenced code block with the language hooks, using the same fields as config.toml, as [[hook]] or [[hooks]] tables:
A block can also be fenced with ~~~hooks. o4 reads every hooks block in the file. A block that doesn’t parse, such as one with invalid TOML or an unknown event name, is skipped as a whole, and a warning goes to the log. The model sees the whole .o4.md, hooks blocks included, as project instructions. See Project instructions for the rest of .o4.md.

Hook fields

string
required
When the hook runs. See Events.
string
default:"command"
How the hook runs: command, http, prompt or agent. See Handlers.
string
The shell command to run, for command hooks. o4 runs it with /bin/bash -c.
string
For http hooks, the http or https URL that o4 sends the event to.
string
For prompt and agent hooks, the shell command to run. Despite the name, it isn’t sent to a model.
string
For tool events, which tools the hook applies to. See Matchers. Without a matcher, the hook applies to every tool.
string
default:"warn"
What happens when the hook fails: it times out, can’t run, exits with a code other than 0 or 2, or, for an http hook, gets a response other than 2xx. warn continues, ignore continues silently, and block treats the failure like a block. Any other value counts as warn. See What a hook’s result does for what’s shown.
integer
Seconds the hook can run before o4 stops it and counts a failure. The default is 10 for command and http hooks and 30 for prompt and agent hooks. o4 stops the hook’s child processes too.
string
The folder to run the shell command in. By default it runs in o4’s working directory. Not used by http hooks.
table
Extra environment variables for the shell command. Not used by http hooks.
string
Accepted, but it has no effect: no hook calls a model.
Set env as a sub-table of the hook:

Handlers

handler sets how a hook runs. Every handler gets the same JSON input. An http hook must use a public address. o4 refuses localhost, private network and link-local addresses, URLs with a user name or password, and schemes other than http and https. It doesn’t follow redirects, and it ignores proxy settings. A response larger than 1 MiB counts as a failure. With the test switch O4_ALLOW_LOCALHOST=1 set, o4 also accepts loopback and private network addresses; link-local addresses stay blocked. A prompt hook whose prompt contains {{event_type}}, {{tool_name}} or {{tool_args}} doesn’t run: o4 counts it as a failure. Read those values from the JSON on standard input instead. A prompt or agent hook without a prompt field doesn’t run either. This http hook reports each finished turn to a web service:

Events

The compaction events fire only for automatic compaction, not for /compact. See Context and cost. o4 also accepts the names PermissionRequest, TaskCompleted, ConfigChange, WorktreeCreate, WorktreeRemove, and Notification, but in o4 0.2.74 nothing triggers them. /hooks marks hooks for these events as (not yet wired). In print mode, o4 runs SessionStart and UserPromptSubmit before the model starts, AssistantMessage and SessionEnd when it finishes, and Error if the run fails. Stop and the compaction events don’t fire there. Tool and subagent events fire in both modes.

Matchers

A matcher limits a tool hook to certain tools. List several tool names with |:
A matcher names whole tools; it isn’t a pattern. To match every tool, leave matcher out: matcher = "*" matches no tool at all. Matching ignores case, spaces, and punctuation. A few common names from other tools also match o4’s tools: Bash, Shell, ShellCommand, ExecCommand and RunCommand match bash; FileEdit and str_replace_editor match edit; FileRead matches read; and FileWrite matches write. Tool names from MCP servers look like server__tool, for example github__create_issue. See Tools for o4’s tool names. A matcher has no effect on events that aren’t about a tool, such as Stop.

What a hook receives

o4 writes one JSON object to the command’s standard input, or sends it as the body of an http hook’s request:
PostToolUse and PostToolUseFailure hooks don’t get the tool’s input. PostToolUse hooks don’t get the tool’s output either, but they can replace it; see Change a tool’s result. For a CodeMode exec call, tool_args doesn’t include the program: its source is replaced with [REDACTED] and its length. See CodeMode. The command doesn’t inherit your full environment. It gets only PATH, HOME, USER, LOGNAME, SHELL, TERM, TMPDIR, TZ, LANG, PWD, the LC_* locale variables, and anything you set in env. API keys in your shell aren’t passed to hooks.

What a hook’s result does

For an http hook, a 2xx response means continue and any other status is a failure. o4 reads only standard output. Standard error is discarded, and output over 1 MiB counts as a failure. What a block does depends on the event:
  • PreToolUse: the tool doesn’t run, and the model gets the error “Tool execution blocked by PreToolUse hook”.
  • PostToolUse: the tool has already run. Exit code 2 counts only when the hook sets on_failure = "block": the model then gets the error “Tool execution blocked by PostToolUse hook” instead of the result. Otherwise exit code 2, like any other failure, is ignored.
  • SessionStart and UserPromptSubmit in print mode: the run stops with an error before the model starts.
  • Other events: the block only stops the remaining hooks for the event. In the terminal interface, a block doesn’t stop your prompt, the turn or the session.

What o4 shows

For every event except PreToolUse and PostToolUse, o4 can show hook results in the session:
  • Text a command hook prints with exit code 0 appears as a [hook] message, unless it’s a JSON object.
  • When a command hook exits with 2, its output appears as a [hook:error] message, or “hook blocked” if it printed nothing.
  • When a command, prompt or agent hook times out or can’t run, a [hook:warn] message says why, or [hook:error] with on_failure = "block". ignore shows nothing.
Other results show no message, including a failing exit code and anything from an http hook. Every run of these hooks is listed in the activity log (l in Navigation Mode), marked as succeeded or failed. o4 masks common secret patterns, like API keys and tokens, in hook text it shows. Print mode doesn’t show hook output. PreToolUse and PostToolUse hooks show nothing in the session or the activity log. Their effect is on the tool call.

Change a tool call

A PreToolUse hook that exits with 0 can print JSON to change the call. An http hook returns the same JSON as the body of a 2xx response.
  • updatedInput replaces the tool’s input.
  • permissionDecision sets whether the call needs approval. allow runs it without asking, like an allow rule. ask makes the call ask every time, like an ask rule, so the permission mode still applies. deny (or block) refuses it, and the model gets the error “Tool execution denied by PreToolUse hook”. Case doesn’t matter.
Output that isn’t valid JSON of this shape is ignored, and the call goes on unchanged. When several hooks match, each gets the original input. The last updatedInput wins, and the strictest decision wins: deny, then ask, then allow. A permission rule that denies a tool refuses the call before any PreToolUse hook runs. See Permissions. A CodeMode exec call can’t be changed: a hook that returns updatedInput for it refuses the call.

Change a tool’s result

A PostToolUse hook that exits with 0 can replace the text the model sees. An http hook returns the same JSON as the body of a 2xx response.
The new text replaces all of the result’s text. Images in the result are kept. When several hooks match, they run in order and the last updatedOutput wins. Output without updatedOutput leaves the result as it is. For a CodeMode exec call, the hook’s output must be exactly {"updatedOutput": "..."}, with at most 100 KiB of text, or {}. Anything else makes the call fail.

Hook trust

Hooks in ~/.o4/config.toml and ~/.o4/managed/config.toml always load. Hooks from a project (its .o4/config.toml, .o4/config.local.toml, and .o4.md) load only when you trust the workspace:
Run it from the workspace root after you’ve reviewed the project’s hooks. o4 untrust turns project hooks off again. See Workspace trust. For automation that already checks where hooks come from, two options skip the trust check:
  • --dangerously-bypass-hook-trust loads the project’s hooks for that one run without trusting the workspace. It loads only the hooks, not the project’s other settings.
  • O4_TRUST_WORKSPACE=1 treats every workspace as trusted for that o4 process, which loads all project settings, hooks, and MCP servers.
Both options let a repository run commands on your machine as soon as o4 starts. Don’t use them on code you haven’t reviewed.

See your hooks

/hooks lists every loaded hook in the order they run, with its event, handler, matcher (* when it has none), on_failure, the timeout you set, its command, URL or prompt, working folder, and the names of its env variables. Values of env variables aren’t shown, and o4 masks secrets in the rest.
/hooks shows the hooks o4 loaded when the session started. o4 reads hook files only at startup, so start a new session after you change them.

Hooks in skills

A skill can carry its own hooks in its front matter. Each has an event, an optional matcher and a command; the other fields take their defaults.
These hooks apply only while that skill’s turn runs, after you run the skill as a slash command, and they run before all other hooks. They end when the turn does, or when you send your next prompt. A skill loaded by the model through its skill tool doesn’t turn its hooks on. /hooks lists skill hooks while they’re active. See Skills.