Skip to main content
Control & Safety

Hooks

Hooks allow you to 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
PermissionDeniedPermission request deniedObserve / audit the denial
WorktreeCreate (TypeScript SDK)Managed worktree requestedReplace the built-in physical creation
WorktreeRemove (TypeScript SDK)Hook-created worktree is removedReplace the built-in physical deletion
See SDK References for the full event type definitions.

Worktree replacement hooks (TypeScript)

WorktreeCreate and WorktreeRemove are resource replacement hooks, not read-only notifications. If WorktreeCreate is registered, the callback must physically create the worktree and return its path. The paired WorktreeRemove callback must physically delete that hook-created resource. Qoder CLI still owns the session cwd switch, transcript relocation, Resume state, and Exit lifecycle.
const hooks = {
  WorktreeCreate: [{
    hooks: [async (input) => {
      const worktreePath = await createWorktree(input.name);
      return {
        hookSpecificOutput: {
          hookEventName: 'WorktreeCreate',
          worktreePath,
        },
      };
    }],
  }],
  WorktreeRemove: [{
    hooks: [async (input) => {
      await removeWorktree(input.worktree_path);
      return {};
    }],
  }],
};
Here, createWorktree and removeWorktree are application-provided helpers that perform the Git or host-specific operation; the SDK does not implement them.

Configuration

Configure hooks in the hooks field of options:
import { query } from '@qodercn-ai/qodercn-agent-sdk';
import type { HookCallback, HookCallbackMatcher } from '@qodercn-ai/qodercn-agent-sdk';

const result = query({
  prompt: 'perform task',
  options: {
    hooks: {
      PreToolUse: [{ matcher: 'Bash', hooks: [myHook] }],
      PostToolUse: [{ hooks: [auditHook] }],
      SessionEnd: [{ hooks: [logHook] }],
    },
  },
});

for await (const message of result) {
  // process messages
}

Matcher

The matcher field is a regex pattern — hooks only fire when the tool name matches:
const result = query({
  prompt: 'perform task',
  options: {
    hooks: {
      PreToolUse: [
        { matcher: 'Bash', hooks: [bashAuditHook] },           // Bash only
        { matcher: 'File.*|Write|Edit', hooks: [fileAuditHook] },  // File operations
        { hooks: [generalLogHook] },                            // All tools (no matcher)
      ],
    },
  },
});

Callback Functions

Each hook callback receives the event input, the tool-use ID, and a context (an abort signal in TypeScript):
type HookCallback = (
  input: HookInput,
  toolUseID: string | undefined,
  options: { signal: AbortSignal },
) => Promise<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. See SDK References for full input type definitions.

Outputs

The callback returns an object / dict that controls behavior via:
  • continue: false — ends the session (Python field name continue_, serialized to JSON "continue")
  • decision: "block" + reason — Block tool execution or prevent AI from stopping
  • hookSpecificOutput — Event-specific output, such as modifying tool input (updatedInput), overriding tool output (updatedToolOutput), or injecting context (additionalContext)
See SDK References for full output type definitions.

Example

Security Interception (PreToolUse)

Block dangerous shell commands:
const securityHook: HookCallback = async (input) => {
  if (input.hook_event_name !== 'PreToolUse') return {};

  if (input.tool_name === 'Bash') {
    const cmd = String((input.tool_input as any)?.command ?? '');
    if (cmd.includes('rm -rf')) {
      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:
const secretRedactHook: HookCallback = async (input) => {
  if (input.hook_event_name !== 'PostToolUse') return {};

  const content = typeof input.tool_response === 'string'
    ? input.tool_response
    : JSON.stringify(input.tool_response);

  const redacted = content
    .replace(/(?:LTAI|AKID)[A-Za-z0-9]{16,}/g, '<REDACTED_AK>')
    .replace(/Bearer\s+[A-Za-z0-9\-._~+/]+=*/g, 'Bearer <REDACTED>');

  if (redacted === content) return {};

  return {
    hookSpecificOutput: {
      hookEventName: 'PostToolUse',
      updatedToolOutput: redacted,
    },
  };
};

Truncate Long Output (PostToolUse)

Trim overly long Bash output, keeping head and tail:
const bashSummarizeHook: HookCallback = async (input) => {
  if (input.hook_event_name !== 'PostToolUse') return {};
  if (input.tool_name !== 'Bash') return {};

  const content = String(input.tool_response ?? '');
  const THRESHOLD = 50 * 1024;
  if (content.length <= THRESHOLD) return {};

  const head = content.slice(0, 8 * 1024);
  const tail = content.slice(-4 * 1024);
  const omitted = content.length - head.length - tail.length;
  return {
    hookSpecificOutput: {
      hookEventName: 'PostToolUse',
      updatedToolOutput: `${head}\n\n[... OMITTED ${omitted} chars ...]\n\n${tail}`,
    },
  };
};

Force Continuation (Stop)

Prevent the AI from stopping when the task is incomplete:
const keepGoingHook: HookCallback = async (input) => {
  if (input.hook_event_name !== 'Stop') return {};

  if (!isTaskComplete()) {
    return {
      decision: 'block',
      reason: 'Please continue completing the remaining tasks',
    };
  }
  return {};
};

Auto-Approve Permissions (PermissionRequest)

Automatically approve Read tool permission requests:
const autoApproveRead: HookCallback = async (input) => {
  if (input.hook_event_name !== 'PermissionRequest') return {};

  if (input.tool_name === 'Read') {
    return {
      hookSpecificOutput: {
        hookEventName: 'PermissionRequest',
        decision: { behavior: 'allow' },
      },
    };
  }
  return {};
};
For the complete permission model, see Permissions.

Audit and Security Controls (Combined)

Combine audit logging with security interception:
import { query } from '@qodercn-ai/qodercn-agent-sdk';
import type { HookCallback } from '@qodercn-ai/qodercn-agent-sdk';
import * as fs from 'fs';

const auditLog = fs.createWriteStream('audit.log', { flags: 'a' });

const securityHook: HookCallback = async (input, toolUseID) => {
  if (input.hook_event_name === 'PreToolUse') {
    // Audit log
    auditLog.write(JSON.stringify({
      event: 'tool_call',
      tool: input.tool_name,
      input: input.tool_input,
      timestamp: new Date().toISOString(),
    }) + '\n');

    // Security check: block curl to external domains
    if (input.tool_name === 'Bash') {
      const cmd = String((input.tool_input as any)?.command ?? '');
      if (/curl\s+https?:\/\/(?!localhost)/.test(cmd)) {
        return {
          hookSpecificOutput: {
            hookEventName: 'PreToolUse',
            permissionDecision: 'deny',
            permissionDecisionReason: 'HTTP requests to external domains are not allowed',
          },
        };
      }
    }
  }

  return {};
};

const result = query({
  prompt: 'run deployment',
  options: {
    hooks: {
      PreToolUse: [{ hooks: [securityHook] }],
    },
  },
});

for await (const message of result) {
  // process messages
}

Notes

  • Hook callbacks should return quickly to avoid blocking AI execution.
  • matcher matches the tool_name field; regex syntax follows each language (JavaScript regex in TypeScript, the re module in Python).
  • continue: false (Python: continue_: False) ends the session—effective only for PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, Stop, and SubagentStop; observational events (e.g. SessionEnd, CwdChanged) ignore it.
  • 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.