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

# Hooks

Hooks let you intercept the Agent's main execution flow at key points in Qoder CLI while remaining decoupled from the CLI itself. Common use cases include: blocking dangerous operations before tool execution, sending desktop notifications when a task completes, automatically running lint after writing files, and more.

Hooks are defined via JSON configuration files — no code changes required. Edit the config file and they take effect immediately.

## Quick Start

The following example demonstrates how to use a Hook to block dangerous commands — automatically preventing execution when the Agent attempts to run `rm -rf`.

**Step 1: Create the script**

```bash theme={null}
mkdir -p ~/.qoder/hooks
cat > ~/.qoder/hooks/block-rm.sh << 'EOF'
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command')

if echo "$command" | grep -q 'rm -rf'; then
  echo "Dangerous command blocked: $command" >&2
  exit 2
fi

exit 0
EOF
chmod +x ~/.qoder/hooks/block-rm.sh
```

**Step 2: Edit the configuration file**

Add the following to `~/.qoder/settings.json`:

```json theme={null}
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}
```

**Step 3: Verify**

Start Qoder CLI and ask the Agent to run a command containing `rm -rf`. The Hook will block execution and report back to the Agent.

## Configuration

### Configuration File Locations

Hook configuration is loaded from the following three files. All three sources are **loaded and merged together** (hooks for the same event do not override each other):

```
~/.qoder/settings.json                    # User level, applies to all projects
${project}/.qoder/settings.json           # Project level, applies to the current project; can be committed to git for team sharing
${project}/.qoder/settings.local.json     # Project level (local), recommended to add to .gitignore
```

### Configuration Format

```json theme={null}
{
  "hooks": {
    "EventName": [
      {
        "matcher": "match condition",
        "hooks": [
          {
            "type": "command",
            "command": "command to execute",
            "timeout": 600
          }
        ]
      }
    ]
  }
}
```

A single event can contain multiple matcher groups, and each group can contain multiple hook entries.

**Group (HookDefinition) fields:**

| Field     | Required | Description                                                                                                                                                         |
| --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `matcher` | No       | Match condition; matches all if omitted                                                                                                                             |
| `hooks`   | Yes      | Array of hook entries in this group                                                                                                                                 |
| `async`   | No       | When `true`, all hooks in the group run in the background without blocking the current operation; results are injected as additional context in the next model turn |

### Hook Entry Types

Each hook entry declares its type via `type`. Different types support different fields.

#### command (run a shell command)

```json theme={null}
{
  "type": "command",
  "command": "~/.qoder/hooks/check.sh",
  "timeout": 600,
  "shell": "bash",
  "env": { "FOO": "bar" }
}
```

| Field           | Required | Description                                                                                                                                                                  |
| --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `command`       | Yes      | The shell command to execute                                                                                                                                                 |
| `timeout`       | No       | Timeout in seconds, default 600                                                                                                                                              |
| `shell`         | No       | `"bash"` or `"powershell"`; system default if omitted                                                                                                                        |
| `env`           | No       | Extra environment variables, merged with the system environment                                                                                                              |
| `if`            | No       | Conditional filter, e.g. `"ToolName"` or `"ToolName(arg_pattern)"`, fires only when the tool name / argument matches                                                         |
| `async`         | No       | When `true`, this single hook runs in the background; overrides the group-level `async`                                                                                      |
| `asyncRewake`   | No       | When `true`, runs in the background; if it exits with code 2, the CLI builds a system reminder from stderr/stdout/error and wakes the model — useful for long-running checks |
| `rewakeMessage` | No       | With `asyncRewake`, overrides the prefix of the injected system message                                                                                                      |
| `rewakeSummary` | No       | With `asyncRewake`, overrides the one-line summary (max 300 chars)                                                                                                           |
| `once`          | No       | When `true`, the hook is removed from the registry after its first successful execution; only effective for session-scoped hooks                                             |
| `statusMessage` | No       | Custom description shown in the spinner / status line                                                                                                                        |
| `args`          | No       | Optional argv array. When set, the hook runs in exec form (no shell). See [Exec form vs Shell form](#exec-form-vs-shell-form) below.                                         |

##### Referencing placeholders in `command`

Placeholders like `${QODER_PROJECT_DIR}` and `${QODER_PLUGIN_ROOT}` are exported as environment variables in the hook's subprocess (see [Environment Variables](#environment-variables)). Under `bash`, the shell expands them at runtime — the command template is not pre-substituted by the CLI. Recommended writing styles:

* **Double-quote the placeholder** (recommended): `"${QODER_PLUGIN_ROOT}"/scripts/hook.sh`. Paths containing spaces or shell metacharacters (`'`, `$`, backticks, etc.) parse correctly as a single token.
* **Plain shell variable syntax**: `"$QODER_PLUGIN_ROOT/scripts/hook.sh"`. Equivalent to the form above when wrapped in double quotes.
* **Unquoted** (not recommended): `${QODER_PLUGIN_ROOT}/scripts/hook.sh`. POSIX shells still apply field splitting and pathname expansion to unquoted parameter expansions, so paths containing spaces or `*` will be split or globbed.

> The preflight check that warns when `${QODER_PLUGIN_ROOT}` or `${QODER_PLUGIN_DATA}` is used outside a plugin only matches the `${...}` form (to avoid false positives on literal `$VAR` text). Plugin authors who want preflight coverage should prefer the `${...}` form.

Under `powershell`, `${QODER_PROJECT_DIR}`, `${QODER_PLUGIN_ROOT}`, and `${QODER_PLUGIN_DATA}` are substituted into the command template by the CLI before invocation (because PowerShell uses `$env:NAME` rather than `${NAME}` for environment access).

##### Exec form vs Shell form

Command hooks support two execution forms:

* **Shell form** (default): `command` is a shell snippet. The CLI runs `bash -c "<command>"` (or PowerShell). Pipes, redirection, glob expansion, and `${VAR}` env-var expansion all work.
* **Exec form** (when `args` is set): `command` is the path/name of a single executable, and each element of `args` is one literal argv entry. The CLI runs the binary directly without a shell — no quoting, splitting, or globbing is performed. The `shell` field is ignored when `args` is set.

```json theme={null}
{
  "type": "command",
  "command": "/usr/bin/python3",
  "args": ["${QODER_PLUGIN_ROOT}/scripts/check.py", "--strict"]
}
```

Choose based on what the hook needs:

* **Default to shell form** — env-var expansion handles paths containing spaces, single quotes, `$`, backticks, etc. when you wrap placeholders in double quotes (`"${QODER_PLUGIN_ROOT}"/scripts/check.sh`).
* **Use shell form when** the hook needs pipes (`grep | tee`), redirection (`>`), globs (`*.json`), or other shell features.
* **Use exec form when** the path or arguments contain shell metacharacters that would require complex quoting, or when you want to be certain no shell parsing happens.

Caveats for exec form on Windows:

* `.bat`/`.cmd` scripts cannot be exec'd directly. Use `{"command": "cmd.exe", "args": ["/c", "script.bat"]}` instead.
* MSYS / Cygwin programs receive argv in their own conventions; consult the target program's documentation for any argument quoting it expects internally.

#### http (send an HTTP request)

The hook input is POSTed as JSON to the URL; the response is expected to be a JSON HookOutput.

```json theme={null}
{
  "type": "http",
  "url": "https://example.com/hook",
  "headers": { "Authorization": "Bearer ${MY_TOKEN}" },
  "timeout": 600
}
```

| Field                           | Required | Description                                                                                            |
| ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------ |
| `url`                           | Yes      | URL that receives the POST                                                                             |
| `headers`                       | No       | Custom request headers; values support `${ENV_VAR}` interpolation                                      |
| `allowedEnvVars`                | No       | Whitelist of environment variables allowed to be interpolated in `headers`; all are allowed if omitted |
| `timeout`                       | No       | Timeout in seconds, default 600                                                                        |
| `if` / `once` / `statusMessage` | No       | Same as command                                                                                        |

#### prompt (single LLM call)

Evaluate the hook event via an isolated single-turn LLM call. The model returns `{ ok, reason }`: `ok=true` means allow, `ok=false` means block, and `reason` is shown to the Agent on block.

```json theme={null}
{
  "type": "prompt",
  "prompt": "Decide whether the command is safe; when not safe, return ok=false and provide a reason.",
  "model": "haiku",
  "timeout": 30
}
```

| Field                           | Required | Description                                                                            |
| ------------------------------- | -------- | -------------------------------------------------------------------------------------- |
| `prompt`                        | Yes      | The prompt template sent to the evaluator. The serialized event JSON is appended to it |
| `model`                         | No       | Model override; uses the session default if omitted                                    |
| `timeout`                       | No       | Timeout in seconds, default 30                                                         |
| `if` / `once` / `statusMessage` | No       | Same as command                                                                        |

**Isolated evaluation.** The evaluator runs in its own session, sees only your `prompt` and the current event, and has no view into the main conversation's prior tool calls, model output, or anything that happened earlier. Write conditions that can be decided from the event itself; rules that depend on conversation history cannot be evaluated here — use a `command` hook that maintains its own state, or an `agent` hook that can inspect the filesystem.

#### agent (sub-agent verification)

Spawn a sub-agent to verify a condition. The sub-agent must call the `StructuredOutput` tool with `{ ok: boolean, reason?: string }`: `ok=true` allows, `ok=false` blocks.

```json theme={null}
{
  "type": "agent",
  "prompt": "Review the following changes: $ARGUMENTS",
  "tools": ["Read", "Grep"],
  "maxTurns": 50,
  "timeout": 60
}
```

| Field                           | Required | Description                                                                                                                                                                                                           |
| ------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prompt`                        | Yes      | Verification prompt; supports the `$ARGUMENTS` placeholder (replaced with the hook input JSON)                                                                                                                        |
| `tools`                         | No       | Whitelist of tools the sub-agent may use. Inherits all available tools when omitted, but tools unsafe inside hooks (recursive Agent calls, plan-mode tools, interactive prompts, etc.) are filtered out automatically |
| `maxTurns`                      | No       | Max agentic turns, default 50                                                                                                                                                                                         |
| `model`                         | No       | Model override                                                                                                                                                                                                        |
| `timeout`                       | No       | Timeout in seconds, default 60                                                                                                                                                                                        |
| `if` / `once` / `statusMessage` | No       | Same as command                                                                                                                                                                                                       |

**Isolated evaluation.** Like `prompt`, the sub-agent runs in its own session and cannot see the main conversation history. The difference is tool access: it can read files, grep the codebase, and run checks, making it suitable when verification must inspect real state.

### Matcher and `if` Rules

`matcher` (group level) filters when a hook fires. Different events match against different fields (see each event description) — typically tool names, event triggers, or sources:

| Syntax             | Meaning               | Example                               |
| ------------------ | --------------------- | ------------------------------------- |
| Omitted or `"*"`   | Match all             | All tools fire                        |
| Exact value        | Exact match           | `"Bash"` matches only the Bash tool   |
| `\|` separated     | Match multiple values | `"Write\|Edit"` matches Write or Edit |
| Regular expression | Regex match           | `"mcp__.*"` matches all MCP tools     |

`if` (entry level) is a finer per-hook filter, of the form `"ToolName"` or `"ToolName(arg_pattern)"`:

* The tool-name part reuses the same matching logic as `matcher` (so regex and `|` are supported).
* The `arg_pattern` inside parentheses uses **glob matching** (not regex), and is checked against the tool's primary argument (e.g., Bash's `command`, file tools' `file_path`).

Examples:

| `if` value      | Meaning                                                        |
| --------------- | -------------------------------------------------------------- |
| `"Bash"`        | Fires when the tool is Bash                                    |
| `"Bash(git *)"` | Fires when the tool is Bash and the command starts with `git ` |
| `"Edit(*.ts)"`  | Fires when the tool is Edit and `file_path` matches `*.ts`     |

## Writing Hook Scripts

Hook scripts receive JSON input via stdin and control behavior through exit codes and stdout. This section describes the input/output format common to all events. Event-specific fields are listed in [Event Reference](#event-reference).

### Input

Hook scripts receive JSON data via **stdin**. All events include the following common fields:

| Field             | Description                                           |
| ----------------- | ----------------------------------------------------- |
| `session_id`      | Current session ID                                    |
| `transcript_path` | Path to the current transcript file                   |
| `cwd`             | Current working directory                             |
| `hook_event_name` | Name of the triggered event                           |
| `permission_mode` | Current permission mode (when the event provides one) |
| `agent_id`        | Current agent ID (when the event provides one)        |
| `agent_type`      | Current agent type (when the event provides one)      |

Different events append additional fields on top of these (see each event description).

Parse input with `jq`:

```bash theme={null}
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
```

### Output

Hooks control behavior through exit codes and stdout.

#### Exit codes

* `0`: success; stdout is parsed according to the rules below.
* `2`: blocking; stderr content is fed back to the Agent (only effective for events that support blocking).
* Other values: non-blocking error; stdout is ignored, stderr is written to diagnostic logs, the main flow continues.

#### Common stdout JSON fields

When exit is 0 and stdout is valid JSON, the CLI parses it according to the fields below; otherwise stdout is treated as plain text (only `SessionStart` / `UserPromptSubmit` inject plain-text stdout into the conversation as additional context).

| Field                | Description                                                                                                                                                                                   |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `continue`           | When `false`, requests stopping subsequent execution                                                                                                                                          |
| `stopReason`         | With `continue: false`, explains the reason to the Agent                                                                                                                                      |
| `suppressOutput`     | When `true`, do not display the hook output to the user                                                                                                                                       |
| `systemMessage`      | A hook system message shown to the user only — it is NOT injected into the model context                                                                                                      |
| `decision`           | `"allow"` or `"deny"`, event-specific decision; `"deny"` is equivalent to exit 2. To request user authorization (`"ask"`), use `PreToolUse`'s `hookSpecificOutput.permissionDecision` instead |
| `reason`             | Reason for the decision; shown to the user / model                                                                                                                                            |
| `hookSpecificOutput` | Container for event-specific fields (see each event)                                                                                                                                          |

Event-specific fine-grained control fields (such as `PreToolUse`'s `permissionDecision` or `PostToolUse`'s `updatedToolOutput`) live inside `hookSpecificOutput`. **When emitting `hookSpecificOutput`, you must include `hookEventName`** — otherwise the entire JSON output is rejected and the TUI shows `<hookName> hook error: hookSpecificOutput is missing required field "hookEventName"`. Example:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "ask"
  }
}
```

### Environment Variables

The following environment variables are available when hook scripts execute:

| Variable            | Description                                             |
| ------------------- | ------------------------------------------------------- |
| `QODER_PROJECT_DIR` | Working directory of the current project                |
| `QODER_PLUGIN_ROOT` | Plugin root directory when the hook comes from a plugin |
| `QODER_PLUGIN_DATA` | Plugin data directory when the hook comes from a plugin |

## Event Reference

Events are grouped by purpose. For each event, the matcher field, additional stdin fields, blocking support, and available `hookSpecificOutput` fields are listed.

### Overview

| Event                | matcher matches                             | exit 2 blocks                       | Key input fields                                 |
| -------------------- | ------------------------------------------- | ----------------------------------- | ------------------------------------------------ |
| `SessionStart`       | `source` (startup/resume/clear/compact/new) | —                                   | `source`, `model`                                |
| `SessionEnd`         | `reason`                                    | —                                   | `reason`                                         |
| `UserPromptSubmit`   | —                                           | ✅                                   | `prompt`                                         |
| `PreToolUse`         | tool name                                   | ✅                                   | `tool_name`, `tool_input`                        |
| `PostToolUse`        | tool name                                   | —                                   | `tool_name`, `tool_input`, `tool_response`       |
| `PostToolUseFailure` | tool name                                   | —                                   | `tool_name`, `error`, `error_type`               |
| `PermissionRequest`  | tool name                                   | —                                   | `tool_name`, `tool_input`                        |
| `PermissionDenied`   | tool name                                   | —                                   | `tool_name`, `tool_input`, `reason`              |
| `Stop`               | —                                           | ✅                                   | `stop_hook_active`, `last_assistant_message`     |
| `StopFailure`        | `error_type`                                | —                                   | `error_type`, `error`                            |
| `SubagentStart`      | agent type                                  | —                                   | `agent_id`, `agent_type`                         |
| `SubagentStop`       | agent type                                  | ✅                                   | `agent_id`, `agent_type`, `stop_hook_active`     |
| `PreCompact`         | `trigger`                                   | ✅                                   | `trigger`, `custom_instructions`                 |
| `PostCompact`        | `trigger`                                   | —                                   | `trigger`, `compact_summary`                     |
| `Notification`       | `notification_type`                         | —                                   | `notification_type`, `message`                   |
| `InstructionsLoaded` | `load_reason`                               | —                                   | `file_path`, `memory_type`, `load_reason`        |
| `ConfigChange`       | `source`                                    | ✅ (except `policy_settings` source) | `source`, `file_path`                            |
| `CwdChanged`         | —                                           | —                                   | `old_cwd`, `new_cwd`                             |
| `FileChanged`        | filename basename                           | —                                   | `file_path`, `event`                             |
| `WorktreeCreate`     | —                                           | non-zero exit fails                 | `name`                                           |
| `WorktreeRemove`     | —                                           | —                                   | `worktree_path`                                  |
| `Elicitation`        | `mcp_server_name`                           | ✅                                   | `mcp_server_name`, `message`, `requested_schema` |
| `ElicitationResult`  | `mcp_server_name`                           | ✅                                   | `mcp_server_name`, `action`, `content`           |

### Session Lifecycle

#### SessionStart

Triggered when a session starts.

**matcher field:** Session source

| matcher value | Trigger scenario                   |
| ------------- | ---------------------------------- |
| `startup`     | New session started                |
| `resume`      | Existing session resumed           |
| `clear`       | Reset via `/clear`                 |
| `compact`     | After context compaction completes |
| `new`         | New session (other sources)        |

**Additional input fields:**

```json theme={null}
{
  "source": "startup",
  "model": "Auto"
}
```

**hookSpecificOutput:** `additionalContext` (context injected into the conversation)

> When the hook returns plain text (not JSON), the stdout is also injected into the conversation as context.

#### SessionEnd

Triggered when a session ends.

**matcher field:** End reason

| matcher value                 | Trigger scenario                     |
| ----------------------------- | ------------------------------------ |
| `clear`                       | Ended via `/clear`                   |
| `resume`                      | Switched to another session          |
| `logout`                      | User logged out                      |
| `prompt_input_exit`           | User exited input (Ctrl+D, etc.)     |
| `bypass_permissions_disabled` | Bypass-permissions mode was disabled |
| `other`                       | Other reasons                        |

**Additional input fields:**

```json theme={null}
{
  "reason": "prompt_input_exit"
}
```

#### UserPromptSubmit

Triggered after the user submits a prompt and before the Agent processes it. Can prevent the prompt from entering the conversation.

**Additional input fields:**

```json theme={null}
{
  "prompt": "Write a sorting function for me"
}
```

**Blocking:** exit 2 rejects the prompt; stderr is shown to the user.

**hookSpecificOutput:**

* `additionalContext`: injected alongside the prompt
* `sessionTitle`: a suggested session title

> When the hook returns plain text (not JSON), the stdout is also injected into the conversation as context.

### Tool Calls

#### PreToolUse

Triggered before tool execution. Can block tool execution or modify the input.

**matcher field:** Tool name (e.g. `Bash`, `Write`, `Edit`, `Read`, `Glob`, `Grep`; MCP tool names like `mcp__server__tool`)

**Additional input fields:**

```json theme={null}
{
  "tool_name": "Bash",
  "tool_input": {"command": "rm -rf /tmp/build"},
  "tool_use_id": "toolu_01ABC123"
}
```

> For MCP tools, `mcp_context` (with `server_name`, `tool_name`, connection info) and `original_request_name` are also included.

**Blocking:** exit 2; stderr is returned to the Agent as an error.

**hookSpecificOutput:**

| Field                      | Description                                                                          |
| -------------------------- | ------------------------------------------------------------------------------------ |
| `permissionDecision`       | `"allow"` / `"deny"` / `"ask"`, equivalent to top-level `decision`; takes precedence |
| `permissionDecisionReason` | Reason; takes precedence over top-level `reason`                                     |
| `updatedInput`             | Modified tool input that replaces the original `tool_input`                          |
| `additionalContext`        | Extra context injected into the conversation                                         |

#### PostToolUse

Triggered after a tool executes successfully.

**matcher field:** Tool name

**Additional input fields:**

```json theme={null}
{
  "tool_name": "Write",
  "tool_input": {"file_path": "/path/to/file.ts", "content": "..."},
  "tool_response": {"success": true, "bytes_written": 1024},
  "tool_use_id": "toolu_01ABC123"
}
```

> `tool_response` is an object whose shape depends on the tool. MCP tools also receive `mcp_context` / `original_request_name`.

**hookSpecificOutput:**

| Field                  | Description                                                                |
| ---------------------- | -------------------------------------------------------------------------- |
| `updatedToolOutput`    | Replaces the tool response (works for any tool)                            |
| `updatedMCPToolOutput` | Replaces only MCP tool responses (lower priority than `updatedToolOutput`) |
| `additionalContext`    | Extra context injected into the conversation                               |

#### PostToolUseFailure

Triggered after a tool execution fails.

**matcher field:** Tool name

**Additional input fields:**

```json theme={null}
{
  "tool_name": "Bash",
  "tool_input": {"command": "npm test"},
  "tool_use_id": "toolu_01ABC123",
  "error": "Command exited with non-zero status code 1",
  "error_type": "execution_failed",
  "is_interrupt": false
}
```

**hookSpecificOutput:** `additionalContext`

#### PermissionRequest

Triggered when a tool requires user authorization. Can auto-allow, deny, or modify the input.

**matcher field:** Tool name

**Additional input fields:**

```json theme={null}
{
  "tool_name": "Bash",
  "tool_input": {"command": "rm -rf node_modules"},
  "permission_suggestions": []
}
```

**hookSpecificOutput:** `decision` object whose fields depend on `behavior`.

`behavior: "allow"` (allow execution; optionally rewrite input or persist permissions):

```json theme={null}
{
  "behavior": "allow",
  "updatedInput": { "command": "..." },
  "updatedPermissions": []
}
```

| Field                | Description                                                 |
| -------------------- | ----------------------------------------------------------- |
| `behavior`           | Must be `"allow"`                                           |
| `updatedInput`       | Modified tool input that replaces the original `tool_input` |
| `updatedPermissions` | Persisted permission rule updates                           |

`behavior: "deny"` (reject execution; optionally show a message):

```json theme={null}
{
  "behavior": "deny",
  "message": "...",
  "interrupt": false
}
```

| Field       | Description                                                        |
| ----------- | ------------------------------------------------------------------ |
| `behavior`  | Must be `"deny"`                                                   |
| `message`   | Message shown to the user                                          |
| `interrupt` | Whether to interrupt the current operation and surface to the user |

> `PermissionRequest` hooks do not support `"ask"` behavior. To prompt the user interactively, use `PreToolUse`'s `permissionDecision: "ask"` instead.

#### PermissionDenied

Triggered when the permission classifier denies a tool call. The hook can request a retry.

**matcher field:** Tool name

**Additional input fields:**

```json theme={null}
{
  "tool_name": "Bash",
  "tool_input": {"command": "..."},
  "tool_use_id": "toolu_01ABC123",
  "reason": "Auto mode classifier blocked this call"
}
```

**hookSpecificOutput:** `retry: true` requests a retry of the tool call.

### Agent Flow

#### Stop

Triggered when the main Agent finishes responding with no pending tool calls. Can prevent the Agent from stopping and let it continue working.

**Additional input fields:**

```json theme={null}
{
  "stop_hook_active": false,
  "last_assistant_message": "..."
}
```

| Field                    | Description                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| `stop_hook_active`       | Whether the current turn is being driven by a Stop hook (use this to avoid infinite loops) |
| `last_assistant_message` | The last assistant message before stopping                                                 |

**Blocking:** exit 2; stderr is injected into the conversation as a message and the Agent continues working.

**hookSpecificOutput:** `clearContext: true` to clear the conversation context.

#### StopFailure

Triggered when the Agent stops unexpectedly due to an error. Notification only — output and exit code are ignored.

**matcher field:** `error_type` (e.g. `rate_limit`, `server_error`)

**Additional input fields:**

```json theme={null}
{
  "error_type": "rate_limit",
  "error": "...",
  "error_details": "...",
  "last_assistant_message": "..."
}
```

`error_type` values: `rate_limit` / `authentication_failed` / `billing_error` / `invalid_request` / `server_error` / `max_output_tokens` / `unknown`.

#### SubagentStart

Triggered when a sub-agent starts.

**matcher field:** Agent type name

**Additional input fields:**

```json theme={null}
{
  "agent_id": "a1b2c3d4",
  "agent_type": "task"
}
```

**hookSpecificOutput:** `additionalContext`

#### SubagentStop

Triggered when a sub-agent completes. Can prevent the sub-agent from stopping (similar to `Stop`).

**matcher field:** Agent type name

**Additional input fields:**

```json theme={null}
{
  "agent_id": "a1b2c3d4",
  "agent_type": "task",
  "stop_hook_active": false,
  "agent_transcript_path": "...",
  "last_assistant_message": "..."
}
```

**Blocking:** exit 2; stderr is injected into the sub-agent's conversation.

**hookSpecificOutput:** `clearContext: true` to clear the sub-agent's context.

### Context Compaction

#### PreCompact

Triggered before context compaction. Can block compaction.

**matcher field:** Trigger method

| matcher value | Trigger scenario                                                  |
| ------------- | ----------------------------------------------------------------- |
| `manual`      | User runs `/compact` manually                                     |
| `auto`        | Triggered automatically when the context window is near its limit |

**Additional input fields:**

```json theme={null}
{
  "trigger": "manual",
  "custom_instructions": "Preserve all tool call results"
}
```

**Blocking:** exit 2 prevents this compaction.

#### PostCompact

Triggered after context compaction completes.

**matcher field:** Trigger method (same as PreCompact)

**Additional input fields:**

```json theme={null}
{
  "trigger": "manual",
  "compact_summary": "Compaction summary..."
}
```

**hookSpecificOutput:** `additionalContext`

### Notifications

#### Notification

Triggered when a user-facing notification is emitted (permission requests, idle prompts, elicitation, etc.).

**matcher field:** Notification type

| matcher value          | Trigger scenario             |
| ---------------------- | ---------------------------- |
| `permission_prompt`    | Tool permission request      |
| `idle_prompt`          | Idle prompt                  |
| `auth_success`         | Successful authentication    |
| `elicitation_dialog`   | MCP elicitation dialog opens |
| `elicitation_response` | User responds to elicitation |
| `elicitation_complete` | Elicitation flow completes   |

**Additional input fields:**

```json theme={null}
{
  "notification_type": "permission_prompt",
  "message": "Agent is requesting permission to run: rm -rf node_modules",
  "title": "Permission Required",
  "details": {}
}
```

**hookSpecificOutput:** `additionalContext`

### Context and Configuration Loading

#### InstructionsLoaded

Triggered when an instruction / memory file is loaded. Notification only — output and exit code are ignored.

**matcher field:** `load_reason` (e.g. `session_start`, `include`)

**Additional input fields:**

```json theme={null}
{
  "file_path": "/abs/path/AGENTS.md",
  "memory_type": "project",
  "load_reason": "session_start",
  "globs": ["**/AGENTS.md"],
  "trigger_file_path": "...",
  "parent_file_path": "..."
}
```

`load_reason` values: `session_start` / `nested_traversal` / `path_glob_match` / `include` / `compact`.

#### ConfigChange

Triggered when a configuration file changes during a session.

**matcher field:** Config source

| matcher value      | Trigger scenario                                      |
| ------------------ | ----------------------------------------------------- |
| `user_settings`    | User-level `~/.qoder/settings.json`                   |
| `project_settings` | Project-level `${project}/.qoder/settings.json`       |
| `local_settings`   | Project-local `${project}/.qoder/settings.local.json` |
| `policy_settings`  | Policy configuration                                  |
| `skills`           | Skill directory changes                               |
| `agents`           | Custom agent directory changes                        |

**Additional input fields:**

```json theme={null}
{
  "source": "user_settings",
  "file_path": "/abs/path/settings.json"
}
```

**Blocking:** exit 2 prevents the change from being applied to the current session. **Exception**: when `source` is `policy_settings`, hooks still fire for audit purposes but the change is enforced and cannot be blocked.

### Working Directory and Files

#### CwdChanged

Triggered when the working directory changes.

**Additional input fields:**

```json theme={null}
{
  "old_cwd": "/old",
  "new_cwd": "/new"
}
```

**hookSpecificOutput:**

| Field               | Description                                               |
| ------------------- | --------------------------------------------------------- |
| `additionalContext` | Context injected into the conversation                    |
| `watchPaths`        | Absolute paths to register with the `FileChanged` watcher |

#### FileChanged

Triggered when a watched file changes.

**matcher field:** Basename of the changed file (supports exact match, `|` multi-value, regex)

**Additional input fields:**

```json theme={null}
{
  "file_path": "/abs/path/file.ts",
  "event": "change"
}
```

`event` values: `change` / `add` / `unlink`.

**hookSpecificOutput:** `additionalContext`, `watchPaths` (same as CwdChanged)

### Worktree Isolation

#### WorktreeCreate

Triggered when an isolated worktree needs to be created. The hook must return the absolute path of the worktree; any non-zero exit code is treated as failure.

**Additional input fields:**

```json theme={null}
{
  "name": "feature-x"
}
```

**Returning the path:** write the absolute path to stdout, or place it in `hookSpecificOutput.worktreePath`.

#### WorktreeRemove

Triggered when a worktree is being removed. Notification only — failures are surfaced via stderr.

**Additional input fields:**

```json theme={null}
{
  "worktree_path": "/abs/path/worktree"
}
```

### MCP Interaction

#### Elicitation

Triggered when an MCP server requests user input (elicitation). The hook can auto-accept, decline, or cancel.

**matcher field:** `mcp_server_name`

**Additional input fields:**

```json theme={null}
{
  "mcp_server_name": "my-server",
  "message": "Please confirm",
  "mode": "...",
  "url": "...",
  "elicitation_id": "...",
  "requested_schema": {}
}
```

**Blocking:** exit 2 declines the elicitation.

**hookSpecificOutput:**

| Field     | Description                           |
| --------- | ------------------------------------- |
| `action`  | `"accept"` / `"decline"` / `"cancel"` |
| `content` | Input content provided when `accept`  |

#### ElicitationResult

Triggered after the user responds to an elicitation. The hook can override the response.

**matcher field:** `mcp_server_name`

**Additional input fields:**

```json theme={null}
{
  "mcp_server_name": "my-server",
  "action": "accept",
  "content": {},
  "mode": "...",
  "elicitation_id": "..."
}
```

**Blocking:** exit 2 rewrites action to `decline`.

**hookSpecificOutput:** `action`, `content` (override the response)

## Practical Examples

### Desktop Notifications

Pop up a desktop notification when the Agent needs authorization or sends a notification.

Script `~/.qoder/hooks/notify.sh` (macOS):

```bash theme={null}
#!/bin/bash
input=$(cat)
ntype=$(echo "$input" | jq -r '.notification_type')

if [ "$ntype" = "permission_prompt" ]; then
  osascript -e 'display notification "Authorization required" with title "Qoder CLI"'
else
  osascript -e 'display notification "New notification" with title "Qoder CLI"'
fi

exit 0
```

Configuration:

```json theme={null}
{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/notify.sh"
          }
        ]
      }
    ]
  }
}
```

### Auto-Lint After Writing Files

Run lint checks automatically every time the Agent writes or edits a file.

Script `${project}/.qoder/hooks/auto-lint.sh`:

```bash theme={null}
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')

case "$file_path" in
  *.js|*.ts|*.jsx|*.tsx)
    npx eslint "$file_path" --fix 2>/dev/null
    ;;
esac

exit 0
```

Configuration: event `PostToolUse`, matcher `Write|Edit`, command `.qoder/hooks/auto-lint.sh`.

### Keep the Agent Working

When the Agent stops, check whether there are unfinished tasks; if so, inject a message to keep the Agent working.

Script `~/.qoder/hooks/check-continue.sh`:

```bash theme={null}
#!/bin/bash
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
  echo "Uncommitted changes detected, please complete git commit" >&2
  exit 2
fi

exit 0
```

Configuration: event `Stop`, command `~/.qoder/hooks/check-continue.sh`.
