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
Hook Entry Types
Each hook entry declares its type viatype. 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 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.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 theStructuredOutput 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_patterninside parentheses uses glob matching (not regex), and is checked against the tool’s primary argument (e.g., Bash’scommand, file tools’file_path).
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 (onlySessionStart / 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 availablehookSpecificOutput fields are listed.
Overview
Session Lifecycle
SessionStart
Triggered when a session starts. matcher field: Session source
Additional input fields:
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: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.
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.
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):
behavior: "deny" (reject execution; optionally show a message):
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:
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: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
Additional input fields:
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
Additional input fields:
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:
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: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:
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.