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

# Hooks

Hooks are an extensibility framework for ZeroCode. They allow
you to inject your own scripts into the agentic loop, enabling features such as:

* Send the chat to a custom logging/analytics engine
* Scan your team's prompts to block accidentally pasting API keys
* Summarize chats to create persistent memories automatically
* Run a custom validation check when a chat turn stops, enforcing standards
* Customize prompting when in a certain directory

Runtime behavior to keep in mind:

* Matching hooks from multiple files all run.
* Multiple matching command hooks for the same event are launched concurrently,
  so one hook can't prevent another matching hook from starting.
* Non-managed command hooks must be reviewed and trusted before they run.

Hooks run at different points in a conversation:

| When                              | Hooks                                                                                                                     |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| During a turn                     | `PreToolUse`, `PermissionRequest`, `PostToolUse`, `PreCompact`, `PostCompact`, `UserPromptSubmit`, `SubagentStop`, `Stop` |
| When a session or subagent starts | `SessionStart`, `SubagentStart`                                                                                           |
| When the main thread ends         | `SessionEnd` (doesn't run for subagents)                                                                                  |

## Where ZeroCode looks for hooks

ZeroCode discovers hooks next to active config layers in either of these forms:

* `hooks.json`
* inline `[hooks]` tables inside `config.toml`

Installed plugins can also bundle lifecycle config through their plugin
manifest or a default `hooks/hooks.json` file. See [Build
plugins](https://developers.zerotwo.ai/plugins/build/plugins#bundled-mcp-servers-and-lifecycle-hooks) for the
plugin packaging rules.

In practice, the four most useful locations are:

* `~/.zerotwo/hooks.json`
* `~/.zerotwo/config.toml`
* `<repo>/.zerotwo/hooks.json`
* `<repo>/.zerotwo/config.toml`

If more than one hook source exists, ZeroCode loads all matching hooks.
Higher-precedence config layers don't replace lower-precedence hooks.
If a single layer contains both `hooks.json` and inline `[hooks]`, ZeroCode
merges them and warns at startup. Prefer one representation per layer.

ZeroCode can also discover hooks bundled with enabled plugins. Plugin-bundled
hooks load alongside other hook sources and use the same trust-review flow as
other non-managed hooks.

Project-local hooks load only when the project `.zerotwo/` layer is trusted. In
untrusted projects, ZeroCode still loads user and system hooks from their own
active config layers.

## Review and trust hooks

ZeroCode lists configured hooks before deciding which ones can run. Before a
non-managed command hook can run, ZeroCode requires you to review and trust the
exact hook definition. ZeroCode records trust against the hook's current hash, so
new or changed hooks are marked for review and skipped until trusted.

Use `/hooks` in the CLI to inspect hook sources, review new or changed hooks,
trust hooks, or disable individual non-managed hooks. If hooks need review at
startup, ZeroCode prints a warning that tells you to open `/hooks`.

Managed hooks from system, MDM, cloud, or `requirements.toml` sources are marked
as managed, trusted by policy, and can't be disabled from the user hook browser.

For one-off automation that already vets hook sources outside ZeroCode, pass
`--dangerously-bypass-hook-trust` to run enabled hooks without requiring
persisted hook trust for that invocation.

## Config shape

Hooks are organized in three levels:

* A hook event such as `PreToolUse`, `PostToolUse`, `PreCompact`,
  `SubagentStart`, or `Stop`
* A matcher group that decides when that event matches
* One or more hook handlers that run when the matcher group matches

```json theme={null}
{
  "description": "Optional lifecycle hooks for this workspace.",
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.zerotwo/hooks/session_start.py",
            "statusMessage": "Loading session notes",
            "additionalContextLimit": 5000
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.zerotwo/hooks/session_end.py",
            "timeout": 3
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.zerotwo/hooks/pre_tool_use_policy.py\"",
            "statusMessage": "Checking Bash command"
          }
        ]
      }
    ],
    "PermissionRequest": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.zerotwo/hooks/permission_request.py\"",
            "statusMessage": "Checking approval request"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.zerotwo/hooks/post_tool_use_review.py\"",
            "statusMessage": "Reviewing Bash output"
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.zerotwo/hooks/user_prompt_submit_data_flywheel.py\""
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.zerotwo/hooks/stop_continue.py\"",
            "timeout": 30
          }
        ]
      }
    ]
  }
}
```

Notes:

* `description` is optional top-level metadata for a `hooks.json` file. It
  doesn't change which hooks run.
* `timeout` is in seconds.
* If `timeout` is omitted, ZeroCode uses `600` seconds for most hooks.
  * `SessionEnd` uses `1` second by default and supports up to `3` seconds.
* `statusMessage` is optional.
* `additionalContextLimit` sets how much `additionalContext` a command hook can
  send to the model before ZeroCode saves the full text to disk and sends a shorter
  preview instead. See [Large hook output](#large-hook-output).
* `commandWindows` is an optional Windows-only command override. In TOML, use
  `command_windows` or `commandWindows`.
* The `async` option is parsed, but asynchronous command hooks aren't supported
  yet.
* Only `type: "command"` handlers run today. `prompt` and `agent` handlers are
  parsed but skipped.
* Commands run with the session `cwd` as their working directory.
* For repo-local hooks, prefer resolving from the git root instead of using a
  relative path such as `.zerotwo/hooks/...`. ZeroCode may be started from a
  subdirectory, and a git-root-based path keeps the hook location stable.

Equivalent inline TOML in `config.toml`:

```toml theme={null}
[[hooks.SessionStart]]
matcher = "^compact$"

[[hooks.SessionStart.hooks]]
type = "command"
command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.zerotwo/hooks/session_start.py"'
additionalContextLimit = 5000

[[hooks.PreToolUse]]
matcher = "^Bash$"

[[hooks.PreToolUse.hooks]]
type = "command"
command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.zerotwo/hooks/pre_tool_use_policy.py"'
timeout = 30
statusMessage = "Checking Bash command"

[[hooks.PostToolUse]]
matcher = "^Bash$"

[[hooks.PostToolUse.hooks]]
type = "command"
command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.zerotwo/hooks/post_tool_use_review.py"'
timeout = 30
statusMessage = "Reviewing Bash output"
```

## Turn hooks off

Hooks are enabled by default. To turn them off in `config.toml`, set:

```toml theme={null}
[features]
hooks = false
```

Use `hooks` as the canonical feature key. `zerocode_hooks` still works as a
deprecated alias. Admins can force hooks off the same way in
`requirements.toml` with `[features].hooks = false`.

## Managed hooks from `requirements.toml`

Enterprise-managed requirements can also define hooks inline under `[hooks]`.
This is useful when admins want to enforce the hook configuration while
delivering the actual scripts through MDM or another device-management system.
To enforce managed hooks even for users who disabled hooks locally, pin
`[features].hooks = true` in `requirements.toml` alongside `[hooks]`. To ignore
user, project, session, and plugin hooks while still allowing administrator
managed hooks, set `allow_managed_hooks_only = true`.

```toml theme={null}
allow_managed_hooks_only = true

[features]
hooks = true

[hooks]
managed_dir = "~/.zerotwo/hooks"
windows_managed_dir = 'C:\enterprise\hooks'

[[hooks.PreToolUse]]
matcher = "^Bash$"

[[hooks.PreToolUse.hooks]]
type = "command"
command = "python3 ~/.zerotwo/hooks/pre_tool_use_policy.py"
command_windows = 'py -3 C:\enterprise\hooks\pre_tool_use_policy.py'
timeout = 30
statusMessage = "Checking managed Bash command"
```

Notes for managed hooks:

* `managed_dir` is used on macOS and Linux.
* `windows_managed_dir` is used on Windows.
* ZeroCode doesn't distribute the scripts in `managed_dir`; your enterprise
  tooling must install and update them separately.
* Managed hook commands should use absolute script paths under the configured
  managed directory.
* `allow_managed_hooks_only = true` skips hooks from user, project, session, and
  plugin sources, but still loads managed hooks from `requirements.toml` and
  other managed config layers.

## Plugin-bundled hooks

When a plugin is enabled, ZeroCode can load lifecycle hooks from that plugin
alongside user, project, and managed hooks.

By default, ZeroCode looks for `hooks/hooks.json` inside the plugin root. A plugin
manifest can override that default with a `hooks` entry in
`.codex-plugin/plugin.json`. The manifest entry can be a `./`-prefixed path, an
array of `./`-prefixed paths, an inline hooks object, or an array of inline
hooks objects.

```json theme={null}
{
  "name": "repo-policy",
  "hooks": "./hooks/hooks.json"
}
```

Manifest hook paths are resolved relative to the plugin root and must stay
inside that root. If a manifest defines `hooks`, ZeroCode uses those manifest
entries instead of the default `hooks/hooks.json`.

Plugin hook commands receive these environment variables:

* `PLUGIN_ROOT` is a ZeroCode-specific extension that points to the installed
  plugin root.
* `PLUGIN_DATA` is a ZeroCode-specific extension that points to the plugin's
  writable data directory.
* ZeroCode also sets `CLAUDE_PLUGIN_ROOT` and `CLAUDE_PLUGIN_DATA` for
  compatibility with existing plugin hooks.

Plugin hooks use the same event schema as other hooks. Installing or enabling a
plugin doesn't automatically trust its hooks; ZeroCode skips plugin-bundled hooks
until you review and trust the current hook definition.

## Matcher patterns

The `matcher` field is a regex string that filters when hooks fire. Use `"*"`,
`""`, or omit `matcher` entirely to match every occurrence of a supported
event.

Only some current ZeroCode events honor `matcher`:

| Event               | What `matcher` filters | Notes                                                        |
| ------------------- | ---------------------- | ------------------------------------------------------------ |
| `PermissionRequest` | tool name              | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
| `PostToolUse`       | tool name              | See [Tool coverage](#tool-coverage)                          |
| `PostCompact`       | compaction trigger     | Values are `manual` or `auto`                                |
| `PreCompact`        | compaction trigger     | Values are `manual` or `auto`                                |
| `PreToolUse`        | tool name              | See [Tool coverage](#tool-coverage)                          |
| `SessionEnd`        | end reason             | Currently only `other`                                       |
| `SessionStart`      | start source           | Values are `startup`, `resume`, `clear`, and `compact`       |
| `SubagentStart`     | subagent type          | Values depend on the subagent that starts                    |
| `SubagentStop`      | subagent type          | Values depend on the subagent that stops                     |
| `UserPromptSubmit`  | not supported          | Any configured `matcher` is ignored for this event           |
| `Stop`              | not supported          | Any configured `matcher` is ignored for this event           |

\*For `apply_patch`, `matcher` values can also use `Edit` or `Write`.

Examples:

* `Bash`
* `^apply_patch$`
* `Edit|Write`
* `mcp__filesystem__read_file`
* `mcp__filesystem__.*`
* `startup|resume|clear|compact`
* `manual|auto`

### Tool coverage

`PreToolUse` and `PostToolUse` can observe more than shell and MCP calls. Most
local function tools use the same hook path, so you can match their tool name,
inspect their JSON arguments, and, for `PreToolUse`, block or rewrite the call.

| Tool path                         | `PreToolUse` | `PostToolUse` | Notes                                                                                                                    |
| --------------------------------- | ------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Shell commands                    | Yes          | Yes           | Match as `Bash`.                                                                                                         |
| Unified exec (`exec_command`)     | Yes          | Yes           | Match as `Bash`. A later `write_stdin` poll can deliver the original command's `PostToolUse` when that command finishes. |
| `apply_patch`                     | Yes          | Yes           | Match as `apply_patch`, `Edit`, or `Write`.                                                                              |
| MCP tools                         | Yes          | Yes           | Match the MCP tool name, such as `mcp__filesystem__read_file`.                                                           |
| Other local function tools        | Yes          | Yes           | Match the function tool name, such as `update_plan`. `spawn_agent` also matches `Agent`.                                 |
| Hosted tools, such as `WebSearch` | No           | No            | These don't use the local function-tool hook path.                                                                       |

`write_stdin` is transport for an existing unified-exec session. It doesn't run
`PreToolUse` again when it sends input or polls a command that already passed
`PreToolUse`.

Some specialized tool paths can opt out of the default hook path. Treat tool
hooks as a useful guardrail, not a complete enforcement boundary.

## Common input fields

Every command hook receives one JSON object on `stdin`.

These are the shared fields you will usually use:

| Field             | Type             | Meaning                                                                |
| ----------------- | ---------------- | ---------------------------------------------------------------------- |
| `session_id`      | `string`         | Current ZeroCode session id. Subagent hooks use the parent session id. |
| `transcript_path` | `string \| null` | Path to the session transcript file, if any                            |
| `cwd`             | `string`         | Working directory for the session                                      |
| `hook_event_name` | `string`         | Current hook event name                                                |
| `model`           | `string`         | ZeroCode-specific extension. Active model slug                         |

Turn-scoped hooks list `turn_id` as a ZeroCode-specific extension in their
event-specific tables.

`SessionStart`, `PreToolUse`, `PermissionRequest`, `PostToolUse`,
`UserPromptSubmit`, `SubagentStart`, `SubagentStop`, and `Stop` also include
`permission_mode`, which describes the current permission mode as `default`,
`acceptEdits`, `plan`, `dontAsk`, or `bypassPermissions`.

`transcript_path` points to a chat transcript for convenience, but the
transcript format isn't a stable interface for hooks and may change over time.

If you need the full wire format, see [Schemas](#schemas).

## Common output fields

`SessionStart`, `PreCompact`, `PostCompact`, `UserPromptSubmit`,
`SubagentStop`, and `Stop` support these shared JSON fields. `SubagentStart`
accepts the same shape for `systemMessage` and hook-specific context, but
`continue: false` doesn't stop the subagent:

```json theme={null}
{
  "continue": true,
  "stopReason": "optional",
  "systemMessage": "optional",
  "suppressOutput": false
}
```

| Field            | Effect                                          |
| ---------------- | ----------------------------------------------- |
| `continue`       | If `false`, marks that hook run as stopped      |
| `stopReason`     | Recorded as the reason for stopping             |
| `systemMessage`  | Surfaced as a warning in the UI or event stream |
| `suppressOutput` | Parsed today but not yet implemented            |

Exit `0` with no output is treated as success and ZeroCode continues.

`PreToolUse` and `PermissionRequest` support `systemMessage`, but `continue`,
`stopReason`, and `suppressOutput` aren't currently supported for those events.
If a `PreToolUse` hook returns one of those unsupported fields, ZeroCode marks
that hook run as failed, reports the error, and continues the tool call.

`PostToolUse` supports `systemMessage`, `continue: false`, and `stopReason`.
`suppressOutput` is parsed but not currently supported for that event.

### Large hook output

By default, ZeroCode limits each model-visible hook-output message to roughly
2,500 tokens. If a hook returns more, ZeroCode saves the full text under
`<temp_dir>/hook_outputs/<session_id>/<uuid>.txt` and gives the model a
head-and-tail preview with the saved-file path. This behavior is called
**spilling**: ZeroCode stores oversized output on disk and replaces it with a
shorter, model-visible preview. If the file can't be written, the model still
receives a truncated preview.

Keep hook and plugin context concise. Context from multiple hooks and plugins
adds up and can degrade model performance. Raising `additionalContextLimit`
increases that risk. Avoid setting the limit to `0` unless the hook enforces a
strict output cap; otherwise, a single hook can consume the entire context
window.

For any command hook that returns `additionalContext`, set
`additionalContextLimit` on the handler to customize the approximate token
threshold:

```json theme={null}
{
  "type": "command",
  "command": "python3 ~/.zerotwo/hooks/session_start.py",
  "additionalContextLimit": 5000
}
```

Omit `additionalContextLimit` to use the default `2500`-token threshold. Use a
positive integer to select a different threshold, or `0` to pass the handler's
complete additional context directly to the model. ZeroCode evaluates each
matching handler independently. For events that can't produce additional
context, ZeroCode ignores `additionalContextLimit` and reports a configuration
warning.

The setting applies only to `additionalContext`. Tool feedback and continuation
prompts keep the default limit.

Because oversized output can be written to disk, avoid returning secrets or
other sensitive data in hook output.

## Hooks

### SessionStart

`matcher` is applied to `source` for this event.

Fields in addition to [Common input fields](#common-input-fields):

| Field    | Type     | Meaning                                                             |
| -------- | -------- | ------------------------------------------------------------------- |
| `source` | `string` | How the session started: `startup`, `resume`, `clear`, or `compact` |

Plain text on `stdout` is added as extra developer context.

JSON on `stdout` supports [Common output fields](#common-output-fields) and this
hook-specific shape:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "Load the workspace conventions before editing."
  }
}
```

That `additionalContext` text is added as extra developer context.

After ZeroCode compacts a root session, `SessionStart` hooks that match
`source: "compact"` run before the next model request. This also applies when
automatic compaction happens in the middle of a turn: ZeroCode delivers the hook's
additional context to the immediate continuation instead of waiting for a
later user turn. If the hook returns `continue: false`, ZeroCode ends the turn
without sending another model request.

### SessionEnd

`SessionEnd` lets you run a command when a session ends, such as saving final
notes or cleaning up files. It runs for the main thread when you archive or
delete a conversation that's still open, when ZeroCode closes normally, or after a
conversation has been idle and isn't open in any connected client for 30
minutes. It won't run for subagents.

Switching away from a conversation or calling `thread/unsubscribe` doesn't end
the session right away, so it won't immediately run `SessionEnd`. Your hook can
still read the session transcript while it runs.

`matcher` filters `reason` for this event. For now, `reason` is always `other`.
You can omit `matcher` or use `other` to run on every `SessionEnd` event.

Fields in addition to [Common input fields](#common-input-fields):

| Field    | Type     | Meaning                        |
| -------- | -------- | ------------------------------ |
| `reason` | `string` | Why the session ended: `other` |

For example, a `SessionEnd` command receives:

```json theme={null}
{
  "session_id": "thr_123",
  "transcript_path": "/workspace/.zerotwo/rollout.jsonl",
  "cwd": "/workspace",
  "hook_event_name": "SessionEnd",
  "reason": "other"
}
```

`SessionEnd` hooks are advisory. Their output won't steer ZeroCode or keep the
thread open. If a command times out or exits with an error, ZeroCode reports it as
a hook failure.

### SubagentStart

`matcher` is applied to `agent_type` for this event.

Fields in addition to [Common input fields](#common-input-fields):

| Field             | Type     | Meaning                                              |
| ----------------- | -------- | ---------------------------------------------------- |
| `turn_id`         | `string` | ZeroCode-specific extension. Active ZeroCode turn id |
| `agent_id`        | `string` | Identifier for the subagent                          |
| `agent_type`      | `string` | Subagent type or profile                             |
| `permission_mode` | `string` | Current permission mode                              |

Plain text on `stdout` is added as extra developer context for the subagent.

JSON on `stdout` supports `systemMessage` and this hook-specific shape:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "SubagentStart",
    "additionalContext": "Review the repository test conventions first."
  }
}
```

That `additionalContext` text is added as extra developer context for the
subagent. `continue: false` is parsed for compatibility, but it doesn't stop the
subagent from starting.

### PreToolUse

`PreToolUse` can intercept Bash, file edits performed through `apply_patch`,
MCP tool calls, and other local function tools. See [Tool
coverage](#tool-coverage) for the supported paths and exceptions.

`matcher` is applied to `tool_name` and matcher aliases. For file edits through
`apply_patch`, `matcher` values can use `apply_patch`, `Edit`, or `Write`; hook input
still reports `tool_name: "apply_patch"`.

Fields in addition to [Common input fields](#common-input-fields):

| Field         | Type         | Meaning                                                                                                                          |
| ------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `turn_id`     | `string`     | ZeroCode-specific extension. Active ZeroCode turn id                                                                             |
| `tool_name`   | `string`     | Canonical hook tool name, such as `Bash`, `apply_patch`, or an MCP name like `mcp__fs__read`                                     |
| `tool_use_id` | `string`     | Tool-call id for this invocation                                                                                                 |
| `tool_input`  | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command`. MCP and other local function tools send their arguments. |

Plain text on `stdout` is ignored.

JSON on `stdout` can use `systemMessage`. To deny a supported tool call, return
this hook-specific shape:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Destructive command blocked by hook."
  }
}
```

ZeroCode also accepts this older block shape:

```json theme={null}
{
  "decision": "block",
  "reason": "Destructive command blocked by hook."
}
```

You can also use exit code `2` and write the blocking reason to `stderr`.

To add model-visible context without blocking, return
`hookSpecificOutput.additionalContext`:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "additionalContext": "The pending command touches generated files."
  }
}
```

To rewrite a supported tool call without blocking, return
`permissionDecision: "allow"` with `updatedInput`:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": {
      "command": "echo rewritten"
    }
  }
}
```

For Bash commands and `apply_patch`, `updatedInput` must include a string
`command` field. For MCP and other local function tools, `updatedInput` is the
replacement arguments object. Return `updatedInput` only with
`permissionDecision: "allow"`; other `updatedInput` shapes are reported as
errors.

`permissionDecision: "ask"`, legacy `decision: "approve"`, `continue: false`,
`stopReason`, and `suppressOutput` are parsed but not supported yet. ZeroCode marks
the hook run as failed, reports the error, and continues the tool call.

### PermissionRequest

`PermissionRequest` runs when ZeroCode is about to ask for approval, such as a
shell escalation or managed-network approval. It can allow the request, deny
the request, or decline to decide and let the normal approval prompt continue.
It doesn't run for commands that don't need approval.

`matcher` is applied to `tool_name` and matcher aliases. Current canonical
values include `Bash`, `apply_patch`, and MCP tool names such as
`mcp__server__tool`; `apply_patch` also matches `Edit` and `Write`.

Fields in addition to [Common input fields](#common-input-fields):

| Field                    | Type             | Meaning                                                                                                        |
| ------------------------ | ---------------- | -------------------------------------------------------------------------------------------------------------- |
| `turn_id`                | `string`         | ZeroCode-specific extension. Active ZeroCode turn id                                                           |
| `tool_name`              | `string`         | Canonical hook tool name, such as `Bash`, `apply_patch`, or an MCP name like `mcp__fs__read`                   |
| `tool_input`             | `JSON value`     | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all the arguments. |
| `tool_input.description` | `string \| null` | Human-readable approval reason, when ZeroCode has one                                                          |

Plain text on `stdout` is ignored.

Some tool inputs may include a human-readable description, but don't rely on a
`tool_input.description` field for every tool.

To approve the request, return:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow"
    }
  }
}
```

To deny the request, return:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "deny",
      "message": "Blocked by repository policy."
    }
  }
}
```

If multiple matching hooks return decisions, any `deny` wins. Otherwise, an
`allow` lets the request proceed without surfacing the approval prompt. If no
matching hook decides, ZeroCode uses the normal approval flow.

Don't return `updatedInput`, `updatedPermissions`, or `interrupt` for
`PermissionRequest`; those fields are reserved for future behavior and fail
closed today.

### PostToolUse

`PostToolUse` runs after supported tools produce output, including Bash,
`apply_patch`, MCP tool calls, and other local function tools. For Bash, it
also runs after commands that exit with a non-zero status. It can't undo side
effects from a tool that already ran. See [Tool coverage](#tool-coverage) for
the supported paths and exceptions.

`matcher` is applied to `tool_name` and matcher aliases. For file edits through
`apply_patch`, `matcher` values can use `apply_patch`, `Edit`, or `Write`; hook input
still reports `tool_name: "apply_patch"`.

Fields in addition to [Common input fields](#common-input-fields):

| Field           | Type         | Meaning                                                                                                                          |
| --------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `turn_id`       | `string`     | ZeroCode-specific extension. Active ZeroCode turn id                                                                             |
| `tool_name`     | `string`     | Canonical hook tool name, such as `Bash`, `apply_patch`, or an MCP name like `mcp__fs__read`                                     |
| `tool_use_id`   | `string`     | Tool-call id for this invocation                                                                                                 |
| `tool_input`    | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command`. MCP and other local function tools send their arguments. |
| `tool_response` | `JSON value` | Tool-specific output. MCP tools send the MCP call result. Other local function tools normally send their model-facing output.    |

Plain text on `stdout` is ignored.

JSON on `stdout` can use `systemMessage` and this hook-specific shape:

```json theme={null}
{
  "decision": "block",
  "reason": "The Bash output needs review before continuing.",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "The command updated generated files."
  }
}
```

That `additionalContext` text is added as extra developer context.

For this event, `decision: "block"` doesn't undo the completed Bash command.
Instead, ZeroCode records the feedback, replaces the tool result with that
feedback, and continues the model from the hook-provided message.

You can also use exit code `2` and write the feedback reason to `stderr`.

To stop normal processing of the original tool result after the command has
already run, return `continue: false`. ZeroCode will replace the tool result with
your feedback or stop text and continue from there.

`updatedMCPToolOutput` and `suppressOutput` are parsed but not supported yet.
ZeroCode marks the hook run as failed, reports the error, and continues normal
processing of the tool result.

#### Tool calls from code mode

When a model uses code mode to call a tool from JavaScript, hook decisions apply
to that nested call. `PreToolUse` can stop the tool before it runs or rewrite
its input. A blocking `PostToolUse` can't undo the tool's side effects, but it
can keep the original result from reaching the running script.

| Hook result                                                      | What code mode sees                                                                                       |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `PreToolUse` blocks                                              | The tool promise rejects before the tool runs.                                                            |
| `PreToolUse` returns `updatedInput`                              | The tool runs with the rewritten input and the promise resolves with that result.                         |
| `PostToolUse` returns `decision: "block"` or exits with code `2` | The tool runs, then the promise rejects with the hook reason.                                             |
| `PostToolUse` returns `continue: false`                          | ZeroCode uses the hook feedback for the model-visible result, but doesn't reject the nested tool promise. |

### PreCompact

`PreCompact` runs before ZeroCode compacts the chat. `matcher` is applied
to `trigger`, whose values are `manual` and `auto`.

Fields in addition to [Common input fields](#common-input-fields):

| Field     | Type     | Meaning                                              |
| --------- | -------- | ---------------------------------------------------- |
| `turn_id` | `string` | ZeroCode-specific extension. Active ZeroCode turn id |
| `trigger` | `string` | What triggered compaction: `manual` or `auto`        |

Plain text on `stdout` is ignored.

JSON on `stdout` supports [Common output fields](#common-output-fields). If a
matching `PreCompact` hook returns `continue: false`, ZeroCode stops before
compacting.

### PostCompact

`PostCompact` runs after ZeroCode compacts the chat. `matcher` is applied
to `trigger`, whose values are `manual` and `auto`.

Fields in addition to [Common input fields](#common-input-fields):

| Field     | Type     | Meaning                                              |
| --------- | -------- | ---------------------------------------------------- |
| `turn_id` | `string` | ZeroCode-specific extension. Active ZeroCode turn id |
| `trigger` | `string` | What triggered compaction: `manual` or `auto`        |

Plain text on `stdout` is ignored.

JSON on `stdout` supports [Common output fields](#common-output-fields). If a
matching `PostCompact` hook returns `continue: false`, ZeroCode stops after
compacting.

### UserPromptSubmit

`matcher` isn't currently used for this event.

Fields in addition to [Common input fields](#common-input-fields):

| Field     | Type     | Meaning                                              |
| --------- | -------- | ---------------------------------------------------- |
| `turn_id` | `string` | ZeroCode-specific extension. Active ZeroCode turn id |
| `prompt`  | `string` | User prompt that's about to be sent                  |

Plain text on `stdout` is added as extra developer context.

JSON on `stdout` supports [Common output fields](#common-output-fields) and
this hook-specific shape:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Ask for a clearer reproduction before editing files."
  }
}
```

That `additionalContext` text is added as extra developer context.

To block the prompt, return:

```json theme={null}
{
  "decision": "block",
  "reason": "Ask for confirmation before doing that."
}
```

You can also use exit code `2` and write the blocking reason to `stderr`.

### SubagentStop

`matcher` is applied to `agent_type` for this event.

Fields in addition to [Common input fields](#common-input-fields):

| Field                    | Type             | Meaning                                              |
| ------------------------ | ---------------- | ---------------------------------------------------- |
| `turn_id`                | `string`         | ZeroCode-specific extension. Active ZeroCode turn id |
| `agent_id`               | `string`         | Identifier for the subagent                          |
| `agent_type`             | `string`         | Subagent type or profile                             |
| `agent_transcript_path`  | `string \| null` | Path to the subagent transcript file, if any         |
| `stop_hook_active`       | `boolean`        | Whether this subagent was already continued          |
| `last_assistant_message` | `string \| null` | Latest subagent assistant message, if available      |

`SubagentStop` expects JSON on `stdout` when it exits `0`. Plain text output is
invalid for this event.

JSON on `stdout` supports [Common output fields](#common-output-fields). To ask
ZeroCode to continue the subagent flow, return:

```json theme={null}
{
  "decision": "block",
  "reason": "Run one more focused pass inside the subagent."
}
```

You can also use exit code `2` and write the continuation reason to `stderr`.

If any matching `SubagentStop` hook returns `continue: false`, that takes
precedence over continuation decisions from other matching `SubagentStop`
hooks.

### Stop

`matcher` isn't currently used for this event.

Fields in addition to [Common input fields](#common-input-fields):

| Field                    | Type             | Meaning                                              |
| ------------------------ | ---------------- | ---------------------------------------------------- |
| `turn_id`                | `string`         | ZeroCode-specific extension. Active ZeroCode turn id |
| `stop_hook_active`       | `boolean`        | Whether this turn was already continued by `Stop`    |
| `last_assistant_message` | `string \| null` | Latest assistant message text, if available          |

`Stop` expects JSON on `stdout` when it exits `0`. Plain text output is invalid
for this event.

JSON on `stdout` supports [Common output fields](#common-output-fields). To keep
ZeroCode going, return:

```json theme={null}
{
  "decision": "block",
  "reason": "Run one more pass over the failing tests."
}
```

You can also use exit code `2` and write the continuation reason to `stderr`.

For this event, `decision: "block"` doesn't reject the turn. Instead, it tells
ZeroCode to continue and automatically creates a new continuation prompt that acts
as a new user prompt, using your `reason` as that prompt text.

If any matching `Stop` hook returns `continue: false`, that takes precedence
over continuation decisions from other matching `Stop` hooks.

## Schemas

The linked `main` branch schemas may include hook fields that are not in the
current release. Use this page as the release behavior reference.

If you need the exact current wire format, see the generated schemas in the
[ZeroCode GitHub repository](https://github.com/zerotwo-ai/tree/main/zerocode-rs/hooks/schema/generated).

### Plain-text aliases

* string | null
