Quick Start
The following example demonstrates how to use a Hook to block dangerous commands — automatically preventing execution when the Agent attempts to runrm -rf.
Step 1: Create the script
~/.qoder/settings.json:
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
| 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 viatype. Different types support different fields.
command (run a shell command)
| 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 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). 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 whenUnder${QODER_PLUGIN_ROOT}or${QODER_PLUGIN_DATA}is used outside a plugin only matches the${...}form (to avoid false positives on literal$VARtext). Plugin authors who want preflight coverage should prefer the${...}form.
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):
commandis a shell snippet. The CLI runsbash -c "<command>"(or PowerShell). Pipes, redirection, glob expansion, and${VAR}env-var expansion all work. - Exec form (when
argsis set):commandis the path/name of a single executable, and each element ofargsis one literal argv entry. The CLI runs the binary directly without a shell — no quoting, splitting, or globbing is performed. Theshellfield is ignored whenargsis set.
- 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.
.bat/.cmdscripts 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.| 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.
| 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 |
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 theStructuredOutput tool with { ok: boolean, reason?: string }: ok=true allows, ok=false blocks.
| 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 |
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_patterninside parentheses uses glob matching (not regex), and is checked against the tool’s primary argument (e.g., Bash’scommand, file tools’file_path).
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.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) |
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 (onlySessionStart / 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) |
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:| 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 availablehookSpecificOutput 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) |
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 |
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:additionalContext: injected alongside the promptsessionTitle: 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,Blocking: exit 2; stderr is returned to the Agent as an error. hookSpecificOutput:mcp_context(withserver_name,tool_name, connection info) andoriginal_request_nameare also included.
| 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:hookSpecificOutput:tool_responseis an object whose shape depends on the tool. MCP tools also receivemcp_context/original_request_name.
| 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:additionalContext
PermissionRequest
Triggered when a tool requires user authorization. Can auto-allow, deny, or modify the input. matcher field: Tool name Additional input fields:decision object whose fields depend on behavior.
behavior: "allow" (allow execution; optionally rewrite input or persist permissions):
| 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):
| Field | Description |
|---|---|
behavior | Must be "deny" |
message | Message shown to the user |
interrupt | Whether to interrupt the current operation and surface to the user |
PermissionRequesthooks do not support"ask"behavior. To prompt the user interactively, usePreToolUse’spermissionDecision: "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: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:| 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 |
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:additionalContext
SubagentStop
Triggered when a sub-agent completes. Can prevent the sub-agent from stopping (similar toStop).
matcher field: Agent type name
Additional input fields:
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 |
PostCompact
Triggered after context compaction completes. matcher field: Trigger method (same as PreCompact) Additional input fields: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 |
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| 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 |
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:| 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:
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: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:
| 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:
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):
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:
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:
Stop, command ~/.qoder/hooks/check-continue.sh.