Add a hook
Hooks go in aconfig.toml file as [[hooks]] entries. This hook runs a
script before every shell command the model tries to run:
2, o4 blocks the call, and the model gets the error “Tool
execution blocked by PreToolUse hook”:
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:
~~~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
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.
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|:
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 anhttp 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 code2counts only when the hook setson_failure = "block": the model then gets the error “Tool execution blocked by PostToolUse hook” instead of the result. Otherwise exit code2, like any other failure, is ignored.SessionStartandUserPromptSubmitin 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 exceptPreToolUse and PostToolUse, o4 can show hook
results in the session:
- Text a
commandhook prints with exit code0appears as a[hook]message, unless it’s a JSON object. - When a
commandhook exits with2, its output appears as a[hook:error]message, or “hook blocked” if it printed nothing. - When a
command,promptoragenthook times out or can’t run, a[hook:warn]message says why, or[hook:error]withon_failure = "block".ignoreshows nothing.
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
APreToolUse 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.
updatedInputreplaces the tool’s input.permissionDecisionsets whether the call needs approval.allowruns it without asking, like anallowrule.askmakes the call ask every time, like anaskrule, so the permission mode still applies.deny(orblock) refuses it, and the model gets the error “Tool execution denied by PreToolUse hook”. Case doesn’t matter.
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
APostToolUse 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.
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:
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-trustloads 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=1treats every workspace as trusted for that o4 process, which loads all project settings, hooks, and MCP servers.
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 anevent, an
optional matcher and a command; the other fields take their defaults.
skill tool doesn’t turn its hooks on. /hooks lists skill
hooks while they’re active. See Skills.