Skip to main content
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
Step 2: Edit the configuration file Add the following to ~/.qoder/settings.json:
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):

Configuration Format

A single event can contain multiple matcher groups, and each group can contain multiple hook entries. Group (HookDefinition) fields:

Hook Entry Types

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

command (run a shell command)

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). 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.
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.
When the shell command prefix is enabled, only shell-form hooks that use a bash-compatible shell run through the configured executable wrapper. Exec-form hooks and PowerShell hooks are launched directly and are not prefixed.

http (send an HTTP request)

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

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.
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.
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: 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:

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.

Input

Hook scripts receive JSON data via stdin. All events include the following common fields: Different events append additional fields on top of these (see each event description). Parse input with jq:

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). 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:

Environment Variables

The following environment variables are available when hook scripts execute:

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

Session Lifecycle

SessionStart

Triggered when a session starts. matcher field: Session source Additional input fields:
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 Additional input fields:

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:
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:
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:

PostToolUse

Triggered after a tool executes successfully. matcher field: Tool name Additional input fields:
tool_response is an object whose shape depends on the tool. MCP tools also receive mcp_context / original_request_name.
hookSpecificOutput:

PostToolUseFailure

Triggered after a tool execution fails. matcher field: Tool name Additional input fields:
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:
hookSpecificOutput: decision object whose fields depend on behavior. behavior: "allow" (allow execution; optionally rewrite input or persist permissions):
behavior: "deny" (reject execution; optionally show a message):
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:
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:
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:
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:
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:
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 Additional input fields:
Blocking: exit 2 prevents this compaction.

PostCompact

Triggered after context compaction completes. matcher field: Trigger method (same as PreCompact) Additional input fields:
hookSpecificOutput: additionalContext

Notifications

Notification

Triggered when a user-facing notification is emitted (permission requests, idle prompts, elicitation, etc.). matcher field: Notification type Additional input fields:
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:
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 Additional input fields:
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:
hookSpecificOutput:

FileChanged

Triggered when a watched file changes. matcher field: Basename of the changed file (supports exact match, | multi-value, regex) Additional input fields:
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:
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:

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:
Blocking: exit 2 declines the elicitation. hookSpecificOutput:

ElicitationResult

Triggered after the user responds to an elicitation. The hook can override the response. matcher field: mcp_server_name Additional input fields:
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):
Configuration:

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:
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:
Configuration: event Stop, command ~/.qoder/hooks/check-continue.sh.