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
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 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):
~/.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 level (local), recommended to add to .gitignore

Configuration Format

{
  "hooks": {
    "EventName": [
      {
        "matcher": "match condition",
        "hooks": [
          {
            "type": "command",
            "command": "command to execute",
            "timeout": 600
          }
        ]
      }
    ]
  }
}
A single event can contain multiple matcher groups, and each group can contain multiple hook entries. Group (HookDefinition) fields:
FieldRequiredDescription
matcherNoMatch condition; matches all if omitted
hooksYesArray of hook entries in this group
asyncNoWhen 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 via type. Different types support different fields.

command (run a shell command)

{
  "type": "command",
  "command": "~/.qoder/hooks/check.sh",
  "timeout": 600,
  "shell": "bash",
  "env": { "FOO": "bar" }
}
FieldRequiredDescription
commandYesThe shell command to execute
timeoutNoTimeout in seconds, default 600
shellNo"bash" or "powershell"; system default if omitted
envNoExtra environment variables, merged with the system environment
ifNoConditional filter, e.g. "ToolName" or "ToolName(arg_pattern)", fires only when the tool name / argument matches
asyncNoWhen true, this single hook runs in the background; overrides the group-level async
asyncRewakeNoWhen 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
rewakeMessageNoWith asyncRewake, overrides the prefix of the injected system message
rewakeSummaryNoWith asyncRewake, overrides the one-line summary (max 300 chars)
onceNoWhen true, the hook is removed from the registry after its first successful execution; only effective for session-scoped hooks
statusMessageNoCustom description shown in the spinner / status line
argsNoOptional 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 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.
{
  "type": "command",
  "command": "/usr/bin/python3",
  "args": ["${QODER_PLUGIN_ROOT}/scripts/check.py", "--strict"]
}
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.

http (send an HTTP request)

The hook input is POSTed as JSON to the URL; the response is expected to be a JSON HookOutput.
{
  "type": "http",
  "url": "https://example.com/hook",
  "headers": { "Authorization": "Bearer ${MY_TOKEN}" },
  "timeout": 600
}
FieldRequiredDescription
urlYesURL that receives the POST
headersNoCustom request headers; values support ${ENV_VAR} interpolation
allowedEnvVarsNoWhitelist of environment variables allowed to be interpolated in headers; all are allowed if omitted
timeoutNoTimeout in seconds, default 600
if / once / statusMessageNoSame 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.
{
  "type": "prompt",
  "prompt": "Decide whether the command is safe; when not safe, return ok=false and provide a reason.",
  "model": "haiku",
  "timeout": 30
}
FieldRequiredDescription
promptYesThe prompt template sent to the evaluator. The serialized event JSON is appended to it
modelNoModel override; uses the session default if omitted
timeoutNoTimeout in seconds, default 30
if / once / statusMessageNoSame as command
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.
{
  "type": "agent",
  "prompt": "Review the following changes: $ARGUMENTS",
  "tools": ["Read", "Grep"],
  "maxTurns": 50,
  "timeout": 60
}
FieldRequiredDescription
promptYesVerification prompt; supports the $ARGUMENTS placeholder (replaced with the hook input JSON)
toolsNoWhitelist 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
maxTurnsNoMax agentic turns, default 50
modelNoModel override
timeoutNoTimeout in seconds, default 60
if / once / statusMessageNoSame as command
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:
SyntaxMeaningExample
Omitted or "*"Match allAll tools fire
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
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:
if valueMeaning
"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:
FieldDescription
session_idCurrent session ID
transcript_pathPath to the current transcript file
cwdCurrent working directory
hook_event_nameName of the triggered event
permission_modeCurrent permission mode (when the event provides one)
agent_idCurrent agent ID (when the event provides one)
agent_typeCurrent agent type (when the event provides one)
Different events append additional fields on top of these (see each event description). Parse input with jq:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')

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).
FieldDescription
continueWhen false, requests stopping subsequent execution
stopReasonWith continue: false, explains the reason to the Agent
suppressOutputWhen true, do not display the hook output to the user
systemMessageA 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
reasonReason for the decision; shown to the user / model
hookSpecificOutputContainer for event-specific fields (see each event)
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:
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "ask"
  }
}

Environment Variables

The following environment variables are available when hook scripts execute:
VariableDescription
QODER_PROJECT_DIRWorking directory of the current project
QODER_PLUGIN_ROOTPlugin root directory when the hook comes from a plugin
QODER_PLUGIN_DATAPlugin 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 available hookSpecificOutput fields are listed.

Overview

Eventmatcher matchesexit 2 blocksKey input fields
SessionStartsource (startup/resume/clear/compact/new)source, model
SessionEndreasonreason
UserPromptSubmitprompt
PreToolUsetool nametool_name, tool_input
PostToolUsetool nametool_name, tool_input, tool_response
PostToolUseFailuretool nametool_name, error, error_type
PermissionRequesttool nametool_name, tool_input
PermissionDeniedtool nametool_name, tool_input, reason
Stopstop_hook_active, last_assistant_message
StopFailureerror_typeerror_type, error
SubagentStartagent typeagent_id, agent_type
SubagentStopagent typeagent_id, agent_type, stop_hook_active
PreCompacttriggertrigger, custom_instructions
PostCompacttriggertrigger, compact_summary
Notificationnotification_typenotification_type, message
InstructionsLoadedload_reasonfile_path, memory_type, load_reason
ConfigChangesource✅ (except policy_settings source)source, file_path
CwdChangedold_cwd, new_cwd
FileChangedfilename basenamefile_path, event
WorktreeCreatenon-zero exit failsname
WorktreeRemoveworktree_path
Elicitationmcp_server_namemcp_server_name, message, requested_schema
ElicitationResultmcp_server_namemcp_server_name, action, content

Session Lifecycle

SessionStart

Triggered when a session starts. matcher field: Session source
matcher valueTrigger scenario
startupNew session started
resumeExisting session resumed
clearReset via /clear
compactAfter context compaction completes
newNew session (other sources)
Additional input fields:
{
  "source": "startup",
  "model": "Auto"
}
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
matcher valueTrigger scenario
clearEnded via /clear
resumeSwitched to another session
logoutUser logged out
prompt_input_exitUser exited input (Ctrl+D, etc.)
bypass_permissions_disabledBypass-permissions mode was disabled
otherOther reasons
Additional input fields:
{
  "reason": "prompt_input_exit"
}

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:
{
  "prompt": "Write a sorting function for me"
}
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:
{
  "tool_name": "Bash",
  "tool_input": {"command": "rm -rf /tmp/build"},
  "tool_use_id": "toolu_01ABC123"
}
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:
FieldDescription
permissionDecision"allow" / "deny" / "ask", equivalent to top-level decision; takes precedence
permissionDecisionReasonReason; takes precedence over top-level reason
updatedInputModified tool input that replaces the original tool_input
additionalContextExtra context injected into the conversation

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": {"success": true, "bytes_written": 1024},
  "tool_use_id": "toolu_01ABC123"
}
tool_response is an object whose shape depends on the tool. MCP tools also receive mcp_context / original_request_name.
hookSpecificOutput:
FieldDescription
updatedToolOutputReplaces the tool response (works for any tool)
updatedMCPToolOutputReplaces only MCP tool responses (lower priority than updatedToolOutput)
additionalContextExtra context injected into the conversation

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",
  "error_type": "execution_failed",
  "is_interrupt": false
}
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:
{
  "tool_name": "Bash",
  "tool_input": {"command": "rm -rf node_modules"},
  "permission_suggestions": []
}
hookSpecificOutput: decision object whose fields depend on behavior. behavior: "allow" (allow execution; optionally rewrite input or persist permissions):
{
  "behavior": "allow",
  "updatedInput": { "command": "..." },
  "updatedPermissions": []
}
FieldDescription
behaviorMust be "allow"
updatedInputModified tool input that replaces the original tool_input
updatedPermissionsPersisted permission rule updates
behavior: "deny" (reject execution; optionally show a message):
{
  "behavior": "deny",
  "message": "...",
  "interrupt": false
}
FieldDescription
behaviorMust be "deny"
messageMessage shown to the user
interruptWhether to interrupt the current operation and surface to the user
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:
{
  "tool_name": "Bash",
  "tool_input": {"command": "..."},
  "tool_use_id": "toolu_01ABC123",
  "reason": "Auto mode classifier blocked this call"
}
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:
{
  "stop_hook_active": false,
  "last_assistant_message": "..."
}
FieldDescription
stop_hook_activeWhether the current turn is being driven by a Stop hook (use this to avoid infinite loops)
last_assistant_messageThe last assistant message before stopping
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": "rate_limit",
  "error": "...",
  "error_details": "...",
  "last_assistant_message": "..."
}
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:
{
  "agent_id": "a1b2c3d4",
  "agent_type": "task"
}
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:
{
  "agent_id": "a1b2c3d4",
  "agent_type": "task",
  "stop_hook_active": false,
  "agent_transcript_path": "...",
  "last_assistant_message": "..."
}
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
matcher valueTrigger scenario
manualUser runs /compact manually
autoTriggered automatically when the context window is near its limit
Additional input fields:
{
  "trigger": "manual",
  "custom_instructions": "Preserve all tool call results"
}
Blocking: exit 2 prevents this compaction.

PostCompact

Triggered after context compaction completes. matcher field: Trigger method (same as PreCompact) Additional input fields:
{
  "trigger": "manual",
  "compact_summary": "Compaction summary..."
}
hookSpecificOutput: additionalContext

Notifications

Notification

Triggered when a user-facing notification is emitted (permission requests, idle prompts, elicitation, etc.). matcher field: Notification type
matcher valueTrigger scenario
permission_promptTool permission request
idle_promptIdle prompt
auth_successSuccessful authentication
elicitation_dialogMCP elicitation dialog opens
elicitation_responseUser responds to elicitation
elicitation_completeElicitation flow completes
Additional input fields:
{
  "notification_type": "permission_prompt",
  "message": "Agent is requesting permission to run: rm -rf node_modules",
  "title": "Permission Required",
  "details": {}
}
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:
{
  "file_path": "/abs/path/AGENTS.md",
  "memory_type": "project",
  "load_reason": "session_start",
  "globs": ["**/AGENTS.md"],
  "trigger_file_path": "...",
  "parent_file_path": "..."
}
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 valueTrigger scenario
user_settingsUser-level ~/.qoder/settings.json
project_settingsProject-level ${project}/.qoder/settings.json
local_settingsProject-local ${project}/.qoder/settings.local.json
policy_settingsPolicy configuration
skillsSkill directory changes
agentsCustom agent directory changes
Additional input fields:
{
  "source": "user_settings",
  "file_path": "/abs/path/settings.json"
}
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:
{
  "old_cwd": "/old",
  "new_cwd": "/new"
}
hookSpecificOutput:
FieldDescription
additionalContextContext injected into the conversation
watchPathsAbsolute 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:
{
  "file_path": "/abs/path/file.ts",
  "event": "change"
}
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:
{
  "name": "feature-x"
}
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:
{
  "worktree_path": "/abs/path/worktree"
}

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:
{
  "mcp_server_name": "my-server",
  "message": "Please confirm",
  "mode": "...",
  "url": "...",
  "elicitation_id": "...",
  "requested_schema": {}
}
Blocking: exit 2 declines the elicitation. hookSpecificOutput:
FieldDescription
action"accept" / "decline" / "cancel"
contentInput 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:
{
  "mcp_server_name": "my-server",
  "action": "accept",
  "content": {},
  "mode": "...",
  "elicitation_id": "..."
}
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):
#!/bin/bash
input=$(cat)
ntype=$(echo "$input" | jq -r '.notification_type')

if [ "$ntype" = "permission_prompt" ]; then
  osascript -e 'display notification "Authorization required" with title "Qoder CLI"'
else
  osascript -e 'display notification "New notification" with title "Qoder CLI"'
fi

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

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:
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')

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