Skip to main content
Hooks allow you to 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 after a task completes, automatically running lint after writing files, and more. Hooks are defined via JSON configuration files — no code changes required. Simply 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
mkdir -p ~/.qoder/hooks
cat > ~/.qoder/hooks/block-rm.sh << 'EOF'
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command')

if echo "$command" | grep -q 'rm -rf'; then
  echo "Dangerous command blocked: $command" >&2
  exit 2
fi

exit 0
EOF
chmod +x ~/.qoder/hooks/block-rm.sh
Step 2: Edit the configuration file Add the following to ~/.qoder/settings.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}
Step 3: Verify Start Qoder CLI and ask the Agent to run a command containing rm -rf. The Hook will block the execution and notify the Agent.

Configuration

Configuration File Locations

Hook configuration is loaded from the following three files. All three levels are merged and executed together:
~/.qoder/settings.json                    # User-level, applies to all projects
${project}/.qoder/settings.json           # Project-level, applies to the current project; can be committed to git for team sharing
${project}/.qoder/settings.local.json     # Project local-level, highest priority; recommended to add to .gitignore

Configuration Format

{
  "hooks": {
    "EventName": [
      {
        "matcher": "match condition",
        "hooks": [
          {
            "type": "command",
            "command": "command to execute",
            "timeout": 60
          }
        ]
      }
    ]
  }
}
Field descriptions:
FieldRequiredDescription
typeYesFixed value: "command"
commandYesThe shell command to execute
timeoutNoTimeout in seconds (default: 60)
matcherNoMatch condition; matches all if omitted
Multiple matcher groups can be configured under a single event, and each group can contain multiple hook commands.

Matcher Rules

matcher filters the scope of hook triggers. Different events match different fields (see individual event descriptions).
SyntaxMeaningExample
Omitted or "*"Match allAll tools trigger
Exact valueExact match"Bash" matches only the Bash tool
| separatedMatch multiple values"Write|Edit" matches Write or Edit
Regular expressionRegex match"mcp__.*" matches all MCP tools

Writing Hook Scripts

Hook scripts receive JSON input via stdin and control behavior through exit codes and stdout. This section describes the common input/output format for all events. Additional fields specific to each event are described in Supported Events.

Input

Hook scripts receive JSON data via stdin. All events include the following common fields:
FieldDescription
session_idCurrent session ID
cwdCurrent working directory
hook_event_nameName of the triggered event
Different events append additional fields on top of these (see individual event descriptions). Parse input using jq:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')

Output

Hooks control behavior through exit codes and stdout. exit 0 indicates success. Qoder CLI parses stdout; some events (such as UserPromptSubmit, SessionStart) support plain-text context injection or fine-grained JSON control — see individual event descriptions for details. exit 2 indicates a blocking error, effective only for events that support blocking. stdout is ignored; stderr is fed back to the Agent as an error message. The exact effect depends on the event: PreToolUse blocks tool execution, UserPromptSubmit rejects the prompt, Stop prevents the Agent from stopping, and so on. Other exit codes are treated as non-blocking errors that do not affect the execution flow; stderr is only recorded in the logs.

Environment Variables

The following environment variables are available when hook scripts execute:
VariableDescription
QODER_PROJECT_DIRWorking directory of the current project

Supported Events

Qoder CLI supports the following Hook events, covering all stages of the session lifecycle.

SessionStart

Triggered when a session starts. matcher field: Session source
matcher valueTrigger scenario
startupNew session started
resumeExisting session resumed
compactAfter context compaction completes
Additional input fields:
{
  "source": "startup",
  "model": "Auto"
}

SessionEnd

Triggered when a session ends. matcher field: End reason
matcher valueTrigger scenario
prompt_input_exitUser exits input (Ctrl+D, etc.)
otherOther reasons
Additional input fields:
{
  "reason": "prompt_input_exit"
}

UserPromptSubmit

Triggered after the user submits a prompt but before the Agent processes it. You can inject additional context for the Agent or validate and block specific types of prompts. Does not support matcher — all configured hooks are executed. In addition to common fields, UserPromptSubmit also receives a prompt field containing the text submitted by the user:
{
  "prompt": "Write a sorting function for me"
}
Output control: When exit 0, there are two ways to add context for the Agent: output plain text directly to stdout, or inject via the additionalContext field in a JSON response. Plain text is simpler, but make sure it does not start with {, otherwise it will be parsed as JSON. To block a prompt, use exit 2 (stderr content is shown to the user), or exit 0 with a JSON output containing decision: "block". exit 2 is suitable for pure blocking scenarios; the JSON approach is more flexible, allowing the same script to conditionally block or pass through.
FieldDescription
decisionSet to "block" to prevent prompt processing and remove it from context; omit to allow through
reasonReason shown to the user when blocked; not added to context
additionalContextContext string injected for the Agent (passed via hookSpecificOutput)
{
  "decision": "block",
  "reason": "Reason for blocking",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Injected context content"
  }
}
JSON format is not mandatory. For simple cases, exit 0 with plain text output is sufficient to inject context. JSON is only needed when you need to block the prompt or require fine-grained control.
For complete examples, see Inject Context on Prompt Submit and Block Prompts Containing Sensitive Information.

PreToolUse

Triggered before tool execution. Can block tool execution. matcher field: Tool name (e.g. Bash, Write, Edit, Read, Glob, Grep; MCP tool names like mcp__server__tool) Additional input fields:
{
  "tool_name": "Bash",
  "tool_input": {"command": "rm -rf /tmp/build"},
  "tool_use_id": "toolu_01ABC123"
}
Blocking tool execution: exit code 2; stderr content is returned to the Agent as an error. For a complete example, see Quick Start.

PostToolUse

Triggered after a tool executes successfully. matcher field: Tool name Additional input fields:
{
  "tool_name": "Write",
  "tool_input": {"file_path": "/path/to/file.ts", "content": "..."},
  "tool_response": "File written successfully",
  "tool_use_id": "toolu_01ABC123"
}

PostToolUseFailure

Triggered after a tool execution fails. matcher field: Tool name Additional input fields:
{
  "tool_name": "Bash",
  "tool_input": {"command": "npm test"},
  "tool_use_id": "toolu_01ABC123",
  "error": "Command exited with non-zero status code 1",
  "is_interrupt": false
}

Stop

Triggered when the Agent finishes responding (main Agent, with no pending tool calls). Can prevent the Agent from stopping and let it continue working. Preventing the Agent from stopping: exit code 2; stderr content is injected into the conversation as a message, and the Agent continues working.

SubagentStart / SubagentStop

Triggered when a sub-agent starts and completes. SubagentStop is similar to Stop and can prevent the sub-agent from stopping. matcher field: Agent type name Additional input fields:
{
  "agent_id": "a1b2c3d4",
  "agent_type": "task"
}

PreCompact

Triggered before context compaction. matcher field: Trigger method
matcher valueTrigger scenario
manualUser manually runs /compact
autoAutomatically triggered when context window is full
Additional input fields:
{
  "trigger": "manual",
  "custom_instructions": "Preserve all tool call results"
}

Notification

Triggered on notification events (permission requests, task completion, etc.). matcher field: Notification type
matcher valueTrigger scenario
permissionPermission request notification
resultAgent result notification
Additional input fields:
{
  "message": "Agent is requesting permission to run: rm -rf node_modules",
  "title": "Permission Required",
  "notification_type": "permission"
}

PermissionRequest

Triggered when a tool requires user authorization to execute. matcher field: Tool name Additional input fields:
{
  "tool_name": "Bash",
  "tool_input": {"command": "rm -rf node_modules"}
}

Practical Examples

Desktop Notifications

Pop up a desktop notification when the Agent completes a task or requires authorization. Script ~/.qoder/hooks/notify.sh (macOS):
#!/bin/bash
input=$(cat)
message=$(echo "$input" | jq -r '.message')

if echo "$message" | grep -q "^Agent"; then
  osascript -e 'display notification "Task completed" with title "Qoder CLI"'
else
  osascript -e 'display notification "Task requires authorization" with title "Qoder CLI"'
fi

exit 0
Configuration:
{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/notify.sh"
          }
        ]
      }
    ]
  }
}

Auto-Lint After Writing Files

Automatically run lint checks every time the Agent writes or edits a file. Script ${project}/.qoder/hooks/auto-lint.sh:
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')

# Only check JS/TS files
case "$file_path" in
  *.js|*.ts|*.jsx|*.tsx)
    npx eslint "$file_path" --fix 2>/dev/null
    ;;
esac

exit 0
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:
#!/bin/bash
# Check for uncommitted git changes
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
  echo "Uncommitted changes detected, please complete git commit" >&2
  exit 2
fi

exit 0
Configuration: event Stop, command ~/.qoder/hooks/check-continue.sh.

Inject Context on Prompt Submit

Automatically inject the current git branch information as Agent context before each user query. Script ~/.qoder/hooks/inject-branch.sh:
#!/bin/bash
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -n "$branch" ]; then
  echo "Current git branch: $branch"
fi
exit 0
Configuration: event UserPromptSubmit, command ~/.qoder/hooks/inject-branch.sh.

Block Prompts Containing Sensitive Information

Intercept prompts containing sensitive keywords such as passwords and keys, preventing them from being sent to the Agent. Script ~/.qoder/hooks/block-sensitive.sh:
#!/bin/bash
input=$(cat)
prompt=$(echo "$input" | jq -r '.prompt')

if echo "$prompt" | grep -qi 'password\|secret\|credential'; then
  echo "Prompt contains sensitive information and has been blocked" >&2
  exit 2
fi

exit 0
Configuration: event UserPromptSubmit, command ~/.qoder/hooks/block-sensitive.sh.