> ## 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 inject custom logic at key lifecycle points of an AI session, enabling audit logging, security controls, context injection, and dynamic behavior modification.

<div id="event-overview" />

<div id="eventoverview" />

## Event Overview

| Event                | Trigger                       | Controllable Behavior                    |
| -------------------- | ----------------------------- | ---------------------------------------- |
| `PreToolUse`         | Before tool invocation        | Intercept / allow / modify input         |
| `PostToolUse`        | After tool succeeds           | Audit / inject context / override output |
| `PostToolUseFailure` | After tool fails              | Error handling / logging                 |
| `UserPromptSubmit`   | Before user prompt is sent    | Inject context / intercept               |
| `SessionStart`       | Session begins                | Initialize / inject context              |
| `SessionEnd`         | Session ends                  | Cleanup / logging                        |
| `Stop`               | AI stops generating           | Prevent stop, force continuation         |
| `SubagentStart`      | Subagent starts               | Observe / log                            |
| `SubagentStop`       | Subagent stops                | Observe / log                            |
| `PreCompact`         | Before context compaction     | Observe / log                            |
| `PostCompact`        | After context compaction      | Observe / log                            |
| `CwdChanged`         | Working directory changes     | Observe / log                            |
| `InstructionsLoaded` | Instruction file loaded       | Observe / log                            |
| `FileChanged`        | File created/modified/deleted | Observe / log                            |
| `PermissionRequest`  | Permission requested          | Auto-approve / deny permission requests  |

For complete event type definitions, see [Hooks Reference](/en/cli/sdk/python/references#hooks-reference).

<div id="configuration" />

## Configuration

Configure hooks via `QoderAgentOptions.hooks`:

```python theme={null}
from qoder_agent_sdk import query, QoderAgentOptions, HookMatcher

async for msg in query(
    prompt="perform task",
    options=QoderAgentOptions(
        hooks={
            "PreToolUse": [HookMatcher(matcher="Bash", hooks=[my_hook])],
            "PostToolUse": [HookMatcher(hooks=[audit_hook])],
            "SessionEnd": [HookMatcher(hooks=[log_hook])],
        },
    ),
):
    ...  # process messages
```

<div id="matcher" />

### Matcher

The `matcher` field is a regex pattern — hooks only fire when the tool name matches:

```python theme={null}
hooks={
    "PreToolUse": [
        HookMatcher(matcher="Bash", hooks=[bash_audit]),                # Bash only
        HookMatcher(matcher="File.*|Write|Edit", hooks=[file_audit]),   # File operations
        HookMatcher(hooks=[general_log]),                               # All tools (no matcher)
    ],
}
```

<div id="callback-functions" />

<div id="callbackfunctions" />

### Callback Functions

Each hook callback receives the event input, tool use ID, and context:

```python theme={null}
HookCallback = Callable[
    [HookInput, str | None, HookContext],
    Awaitable[HookJSONOutput],
]
```

<div id="inputs" />

#### Inputs

All events share common fields: `hook_event_name` (event type), `session_id` (session ID), `transcript_path` (transcript file path), `cwd` (working directory). Each event also has event-specific fields, such as `tool_name` and `tool_input` for `PreToolUse`.

For complete input type definitions, see [Hooks Reference](/en/cli/sdk/python/references#basehookinput).

<div id="outputs" />

#### Outputs

Callbacks return a dict that controls behavior through these fields:

* `continue_: False` — Terminate the session (serialized as JSON `"continue"`)
* `decision: "block"` + `reason` — Block tool execution or prevent the AI from stopping
* `hookSpecificOutput` — Event-specific output, such as modifying tool input (`updatedInput`), overriding tool output (`updatedToolOutput`), or injecting context (`additionalContext`)

For complete output type definitions, see [Hooks Reference](/en/cli/sdk/python/references#hookjsonoutput).

<div id="examples" />

## Examples

<div id="security-interception-pretooluse" />

<div id="securityinterceptionpretooluse" />

### Security Interception (PreToolUse)

Block dangerous shell commands:

```python theme={null}
async def security_hook(inp: HookInput, tid: str | None, ctx: HookContext) -> HookJSONOutput:
    if inp.get("tool_name") == "Bash":
        cmd = (inp.get("tool_input") or {}).get("command", "")
        if "rm -rf" in cmd:
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": "Destructive delete operations are not allowed",
                },
            }
    return {}
```

<div id="redact-sensitive-information-posttooluse" />

<div id="redactsensitiveinformationposttooluse" />

### Redact Sensitive Information (PostToolUse)

Override tool output to replace AK/Token and other sensitive information:

```python theme={null}
import re

async def secret_redact_hook(inp: HookInput, tid: str | None, ctx: HookContext) -> HookJSONOutput:
    if inp.get("hook_event_name") != "PostToolUse":
        return {}

    response = inp.get("tool_response", "")
    content = response if isinstance(response, str) else json.dumps(response)

    redacted = re.sub(r"(?:LTAI|AKID)[A-Za-z0-9]{16,}", "<REDACTED_AK>", content)
    redacted = re.sub(r"Bearer\s+[A-Za-z0-9\-._~+/]+=*", "Bearer <REDACTED>", redacted)

    if redacted == content:
        return {}

    return {
        "hookSpecificOutput": {
            "hookEventName": "PostToolUse",
            "updatedToolOutput": redacted,
        },
    }
```

<div id="truncate-long-output-posttooluse" />

<div id="truncatelongoutputposttooluse" />

### Truncate Long Output (PostToolUse)

Trim overly long Bash output, keeping head and tail:

```python theme={null}
async def bash_summarize_hook(inp: HookInput, tid: str | None, ctx: HookContext) -> HookJSONOutput:
    if inp.get("hook_event_name") != "PostToolUse":
        return {}
    if inp.get("tool_name") != "Bash":
        return {}

    content = str(inp.get("tool_response") or "")
    THRESHOLD = 50 * 1024
    if len(content) <= THRESHOLD:
        return {}

    head = content[:8 * 1024]
    tail = content[-4 * 1024:]
    omitted = len(content) - len(head) - len(tail)
    return {
        "hookSpecificOutput": {
            "hookEventName": "PostToolUse",
            "updatedToolOutput": f"{head}\n\n[... OMITTED {omitted} chars ...]\n\n{tail}",
        },
    }
```

<div id="force-continuation-stop" />

<div id="forcecontinuationstop" />

### Force Continuation (Stop)

Prevent the AI from stopping when the task is incomplete:

```python theme={null}
async def keep_going(inp: HookInput, tid: str | None, ctx: HookContext) -> HookJSONOutput:
    if inp.get("hook_event_name") != "Stop":
        return {}

    if not is_task_complete():
        return {"decision": "block", "reason": "Please continue completing the remaining tasks"}
    return {}
```

<div id="auto-approve-permissions-permissionrequest" />

<div id="autoapprovepermissionspermissionrequest" />

### Auto-Approve Permissions (PermissionRequest)

Automatically approve Read tool permission requests:

```python theme={null}
async def auto_approve_read(inp: HookInput, tid: str | None, ctx: HookContext) -> HookJSONOutput:
    if inp.get("hook_event_name") != "PermissionRequest":
        return {}

    if inp.get("tool_name") == "Read":
        return {
            "hookSpecificOutput": {
                "hookEventName": "PermissionRequest",
                "decision": {"behavior": "allow"},
            },
        }
    return {}
```

> For the complete permission model, see the [Permissions documentation](/en/cli/sdk/python/permissions).

<div id="audit-and-security-controls-combined" />

<div id="auditandsecuritycontrolscombined" />

### Audit and Security Controls (Combined)

Combine audit logging with security interception:

```python theme={null}
import json
import logging
import re
from qoder_agent_sdk import query, QoderAgentOptions, HookMatcher
from qoder_agent_sdk.types import HookInput, HookContext, HookJSONOutput

async def security_hook(inp: HookInput, tid: str | None, ctx: HookContext) -> HookJSONOutput:
    if inp.get("hook_event_name") != "PreToolUse":
        return {}

    # Audit log
    logging.info(json.dumps({
        "event": "tool_call",
        "tool": inp.get("tool_name"),
        "input": inp.get("tool_input"),
    }))

    # Security check: block curl to external domains
    if inp.get("tool_name") == "Bash":
        cmd = str((inp.get("tool_input") or {}).get("command", ""))
        if re.search(r"curl\s+https?://(?!localhost)", cmd):
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": "HTTP requests to external domains are not allowed",
                },
            }

    return {}


async def main():
    async for msg in query(
        prompt="run deployment",
        options=QoderAgentOptions(
            hooks={
                "PreToolUse": [HookMatcher(hooks=[security_hook])],
            },
        ),
    ):
        pass  # process messages
```

<div id="notes" />

## Notes

* Hook callbacks should return quickly to avoid blocking AI execution.
* `matcher` uses Python regex syntax (the `re` module), matching the `tool_name` field.
* `continue_: False` terminates the session — only effective for `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `UserPromptSubmit`, `Stop`, and `SubagentStop` events. Observation events (e.g., `SessionEnd`, `CwdChanged`) ignore this field.
* When multiple hooks return conflicting `decision` values, `"deny"` / `"block"` takes precedence (strictest rule wins).
* When multiple hooks set `updatedToolOutput`, the **last non-empty value** wins. For chained transforms (e.g., redact then truncate), execute them sequentially within a single callback.
* The Python SDK uses trailing-underscore field names (`continue_`) to avoid conflicts with Python keywords. The SDK automatically converts them to wire-protocol names (`continue`) during serialization.
