Skip to main content

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

Event Overview

EventTriggerControllable Behavior
PreToolUseBefore tool invocationIntercept / allow / modify input
PostToolUseAfter tool succeedsAudit / inject context / override output
PostToolUseFailureAfter tool failsError handling / logging
UserPromptSubmitBefore user prompt is sentInject context / intercept
SessionStartSession beginsInitialize / inject context
SessionEndSession endsCleanup / logging
StopAI stops generatingPrevent stop, force continuation
SubagentStartSubagent startsObserve / log
SubagentStopSubagent stopsObserve / log
PreCompactBefore context compactionObserve / log
PostCompactAfter context compactionObserve / log
CwdChangedWorking directory changesObserve / log
InstructionsLoadedInstruction file loadedObserve / log
FileChangedFile created/modified/deletedObserve / log
PermissionRequestPermission requestedAuto-approve / deny permission requests
For complete event type definitions, see Hooks Reference.

Configuration

Configure hooks via QoderAgentOptions.hooks:
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

Matcher

The matcher field is a regex pattern — hooks only fire when the tool name matches:
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)
    ],
}

Callback Functions

Each hook callback receives the event input, tool use ID, and context:
HookCallback = Callable[
    [HookInput, str | None, HookContext],
    Awaitable[HookJSONOutput],
]

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.

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.

Examples

Security Interception (PreToolUse)

Block dangerous shell commands:
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 {}

Redact Sensitive Information (PostToolUse)

Override tool output to replace AK/Token and other sensitive information:
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,
        },
    }

Truncate Long Output (PostToolUse)

Trim overly long Bash output, keeping head and tail:
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}",
        },
    }

Force Continuation (Stop)

Prevent the AI from stopping when the task is incomplete:
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 {}

Auto-Approve Permissions (PermissionRequest)

Automatically approve Read tool permission requests:
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.

Audit and Security Controls (Combined)

Combine audit logging with security interception:
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

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.