> ## Documentation Index
> Fetch the complete documentation index at: https://docs.open4rena.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Hooks

> Run your own commands when o4 reaches certain points in a session.

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:

```toml theme={null}
[[hooks]]
event = "PreToolUse"
matcher = "Bash"
command = "scripts/check-command.sh"
```

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":

```bash theme={null}
#!/bin/sh
# scripts/check-command.sh: block commands that mention "rm -rf"
if grep -q 'rm -rf'; then
  exit 2
fi
exit 0
```

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](#hook-fields)).

Run `/hooks` in a session to see the hooks o4 has loaded.

## Where hooks are configured

| File | Scope |
| - | - |
| `~/.o4/config.toml` | Your hooks, in every workspace |
| `.o4/config.toml` | Project hooks, usually committed |
| `.o4/config.local.toml` | Your own hooks for this project, usually not committed |
| `.o4.md` | Project hooks inside `hooks` code blocks |
| `~/.o4/managed/config.toml` | Hooks set by an administrator |

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](#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:

````markdown theme={null}
## Hooks

```hooks
[[hook]]
event = "PostToolUse"
matcher = "Write|Edit"
command = "scripts/log-edit.sh"
```
````

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](/configuration/project-instructions)
for the rest of `.o4.md`.

## Hook fields

<ParamField path="event" type="string" required>
  When the hook runs. See [Events](#events).
</ParamField>

<ParamField path="handler" type="string" default="command">
  How the hook runs: `command`, `http`, `prompt` or `agent`. See
  [Handlers](#handlers).
</ParamField>

<ParamField path="command" type="string">
  The shell command to run, for `command` hooks. o4 runs it with
  `/bin/bash -c`.
</ParamField>

<ParamField path="url" type="string">
  For `http` hooks, the `http` or `https` URL that o4 sends the event to.
</ParamField>

<ParamField path="prompt" type="string">
  For `prompt` and `agent` hooks, the shell command to run. Despite the name,
  it isn't sent to a model.
</ParamField>

<ParamField path="matcher" type="string">
  For tool events, which tools the hook applies to. See
  [Matchers](#matchers). Without a matcher, the hook applies to every tool.
</ParamField>

<ParamField path="on_failure" type="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](#what-a-hook’s-result-does) for what's shown.
</ParamField>

<ParamField path="timeout_secs" type="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.
</ParamField>

<ParamField path="cwd" type="string">
  The folder to run the shell command in. By default it runs in o4's working
  directory. Not used by `http` hooks.
</ParamField>

<ParamField path="env" type="table">
  Extra environment variables for the shell command. Not used by `http`
  hooks.
</ParamField>

<ParamField path="model" type="string">
  Accepted, but it has no effect: no hook calls a model.
</ParamField>

Set `env` as a sub-table of the hook:

```toml theme={null}
[[hooks]]
event = "Stop"
command = "scripts/notify.sh"
timeout_secs = 5

[hooks.env]
NOTIFY_CHANNEL = "builds"
```

## Handlers

`handler` sets how a hook runs. Every handler gets the same
[JSON input](#what-a-hook-receives).

| Handler | What o4 does |
| - | - |
| `command` | Runs `command` with `/bin/bash -c` and writes the JSON to its standard input. The exit code decides the result. |
| `http` | Sends the JSON in a `POST` request to `url`. A `2xx` response lets the action continue; any other status is a failure. An `http` hook can't block with an exit code. |
| `prompt` | Runs `prompt` as a shell command, like `command`. o4 doesn't call a model. |
| `agent` | Runs `prompt` as a shell command, like `prompt`. o4 doesn't start a subagent. |

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:

```toml theme={null}
[[hooks]]
event = "Stop"
handler = "http"
url = "https://hooks.example.com/o4"
```

## Events

| Event | When it runs |
| - | - |
| `SessionStart` | When o4 starts a session, before the first prompt |
| `SessionEnd` | When you quit o4, or switch to another session, such as with `/resume`. o4 waits at most 10 seconds for these hooks. |
| `UserPromptSubmit` | When you send a prompt, before the model sees it |
| `PreToolUse` | Before a tool runs |
| `PostToolUse` | After a tool succeeds |
| `PostToolUseFailure` | After a tool fails or is refused. o4 doesn't wait for these hooks. |
| `AssistantMessage` | When the model finishes a reply |
| `Stop` | When a turn ends, in the terminal interface |
| `Error` | When a turn fails with an error, such as a failed model request |
| `PreCompact` | After each turn in the terminal interface, before o4 checks whether to compact the conversation |
| `CompactStart` | When automatic compaction starts |
| `CompactEnd` | When automatic compaction finishes, whether or not it made the conversation smaller |
| `SubagentStart` | When a subagent starts |
| `SubagentStop` | When a subagent finishes, fails or is cancelled |
| `TeammateIdle` | When a subagent that belongs to a [team](/guides/subagents#teams) finishes |

The compaction events fire only for automatic compaction, not for
`/compact`. See [Context and cost](/guides/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](/guides/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
`|`:

```toml theme={null}
matcher = "Write|Edit"
```

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](/reference/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:

```json theme={null}
{
  "event_type": "PreToolUse",
  "tool_name": "bash",
  "tool_args": { "command": "cargo test" },
  "session_id": "3f1c2b9e-…",
  "timestamp": 1790298235343,
  "tool_error": null
}
```

| Field | Contents |
| - | - |
| `event_type` | The event name |
| `tool_name` | The tool, for `PreToolUse`, `PostToolUse` and `PostToolUseFailure` |
| `tool_args` | Depends on the event. See the next table. `null` for other events. |
| `session_id` | The session ID. In print mode it's always `print`. |
| `timestamp` | Milliseconds since the Unix epoch |
| `tool_error` | The error text, for `PostToolUseFailure` and `Error`, and for `SubagentStop` and `TeammateIdle` when the subagent failed. Otherwise `null`. |

| Event | `tool_args` |
| - | - |
| `PreToolUse` | The tool's input |
| `UserPromptSubmit` | `{"prompt": ...}` with the text you sent |
| `AssistantMessage` | `{"message": ...}` with the text of the reply |
| `SubagentStart` | `{"agent", "agent_id", "prompt"}`, where `prompt` is the first 120 characters of the task |
| `SubagentStop` | `{"agent", "agent_id", "status"}`, where `status` is `completed`, `failed`, `cancelled` or `max_turns` |
| `TeammateIdle` | `{"teammate_id", "leader_mailbox_path", "summary"}`, where `summary` is the first 120 characters of the subagent's reply |

`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](#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](/extend/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

| Exit code | Effect |
| - | - |
| `0` | Continue |
| `2` | Block |
| Anything else | A failure, handled by `on_failure` |

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](/guides/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.

```json theme={null}
{"updatedInput": {"command": "cargo test --offline"}, "permissionDecision": "ask"}
```

* `updatedInput` replaces the tool's input.
* `permissionDecision` sets whether the call needs approval. `allow` runs it
  without asking, like an `allow` [rule](/safety/permissions#permission-rules).
  `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](/safety/permissions#precedence).

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.

```json theme={null}
{"updatedOutput": "the tool output, with secrets removed"}
```

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:

```bash theme={null}
o4 trust
```

Run it from the workspace root after you've reviewed the project's hooks.
`o4 untrust` turns project hooks off again. See
[Workspace trust](/safety/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.

<Warning>
  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.
</Warning>

## 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.

```text theme={null}
2 hooks configured:

1. PreToolUse [command] matcher=Bash on_failure=warn
   -> scripts/check-command.sh
2. Stop [command] matcher=* on_failure=warn timeout=5s
   -> scripts/notify.sh
   env: NOTIFY_CHANNEL
```

`/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.

```markdown theme={null}
---
description: Run the release checklist.
hooks:
  - event: PreToolUse
    matcher: Bash
    command: scripts/check-release-command.sh
---
```

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](/extend/skills).
