Hooks run custom scripts at key points in the Qoder CN CLI agent lifecycle—such as blocking dangerous commands, sending notifications on task completion, or auto-linting written files.
Hooks are defined in a JSON configuration file and take effect immediately—no code changes required.
Quick start
This example blocks the Agent from running rm -rf commands.
Step 1: Create the script
mkdir -p ~/.qoder-cn/hooks
cat > ~/.qoder-cn/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-cn/hooks/block-rm.sh
Step 2: Edit the configuration file
Add the following to ~/.qoder-cn/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.qoder-cn/hooks/block-rm.sh"
}
]
}
]
}
}
Step 3: Verify the result
Start Qoder CN CLI and ask the Agent to run a command containing rm -rf. The hook blocks execution and notifies the Agent.
Configuration
Configuration file locations
Hook configurations are loaded from the following three files. The settings from all three levels are merged and executed:
~/.qoder-cn/settings.json # User-level: applies to all projects.
${project}/.qoder/settings.json # Project-level: applies to the current project. Can be committed to Git to share with your team.
${project}/.qoder/settings.local.json # Project-level (local): has the highest priority. Recommended to add to .gitignore.
Configuration format
{
"hooks": {
"EventName": [
{
"matcher": "match condition",
"hooks": [
{
"type": "command",
"command": "command to run",
"timeout": 60
}
]
}
]
}
}
Fields:
|
Field |
Required |
Description |
|
|
Yes |
Must be set to |
|
|
Yes |
The shell command to run. |
|
|
No |
The timeout in seconds. Default: 60. |
|
|
No |
Match condition. If omitted, the hook applies to all instances. |
A single event supports multiple matcher groups, each containing multiple hook commands.
Matcher rules
The matcher field determines when a hook triggers. Each event matches against a different field.
|
Syntax |
Description |
Example |
|
Leave blank or |
Matches all. |
Triggers for all tools. |
|
Exact value |
Exact match. |
|
|
|
Matches multiple values. |
|
|
Regular expression |
Regular expression match. |
|
Writing hook scripts
Hook scripts receive a JSON payload on stdin and use exit codes and stdout to control behavior. Each event adds its own fields to the common structure below.
Input
All events include the following fields on stdin:
|
Field |
Description |
|
|
The ID of the current session. |
|
|
The current working directory. |
|
|
The name of the event that triggered the hook. |
Parse the input with jq:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
Output
A hook controls behavior through its exit code: 0 for success, 2 to block the operation (supported events only), and any other non-zero value for a non-blocking error. Some events also parse a JSON object on stdout for fine-grained control when the exit code is 0.
Environment variables
Hook scripts can access the following environment variables:
|
Variable |
Description |
|
|
The working directory of the current project. |
Supported events
Qoder CN CLI supports the following hook events across the session lifecycle.
SessionStart
Triggers when a session starts.
Matcher field: Session source
|
Matcher value |
Trigger scenario |
|
|
A new session starts. |
|
|
An existing session is resumed. |
|
|
Context compaction is complete. |
Additional input fields:
{
"source": "startup",
"model": "Auto"
}
SessionEnd
Triggers when a session ends.
Matcher field: End reason
|
Matcher value |
Trigger scenario |
|
|
The user closes the prompt (e.g., by pressing Ctrl+D). |
|
|
The session ends for other reasons. |
Additional input fields:
{
"reason": "prompt_input_exit"
}
UserPromptSubmit
Triggers after a user submits a prompt but before the Agent processes it.
Additional input fields:
{
"prompt": "Write a sorting function for me"
}
PreToolUse
Triggers before a tool runs. This hook can block tool execution.
matcher: Tool name (such as Bash, Write, Edit, Read, Glob, Grep, or an MCP tool name such as mcp__server__tool)
Additional input fields:
{
"tool_name": "Bash",
"tool_input": {"command": "rm -rf /tmp/build"},
"tool_use_id": "toolu_01ABC123"
}
Block tool execution: Exit with code 2. Content on stderr is sent to the Agent as an error message.
PostToolUse
Triggers 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
Triggers after a tool fails to execute.
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
Triggers when the Agent finishes its response with no pending tool calls. Use this hook to prevent the Agent from stopping and prompt it to continue.
Prevent the Agent from stopping: Exit with code 2. Content from stderr is injected into the conversation, prompting the Agent to continue.
SubagentStart / SubagentStop
Triggers when a subagent starts or stops. SubagentStop works like Stop and can prevent a subagent from stopping.
Matcher field: Agent type name
Additional input fields:
{
"agent_id": "a1b2c3d4",
"agent_type": "task"
}
PreCompact
Triggers before context compaction.
Matcher field: Trigger method
|
Matcher value |
Trigger scenario |
|
|
The user manually executes |
|
|
Compaction is triggered automatically when the context window is full. |
Additional input fields:
{
"trigger": "manual",
"custom_instructions": "Keep all tool call results"
}
Notification
Triggers on notification events such as permission requests or task completions.
Matcher field: Notification type
|
Matcher value |
Trigger scenario |
|
|
A permission request notification. |
|
|
A notification for a result generated by the Agent. |
Additional input fields:
{
"message": "Agent is requesting permission to run: rm -rf node_modules",
"title": "Permission Required",
"notification_type": "permission"
}
PermissionRequest
Triggers when a tool requires user authorization to run.
Matcher field: Tool name
Additional input fields:
{
"tool_name": "Bash",
"tool_input": {"command": "rm -rf node_modules"}
}
Use cases
Send desktop notifications
Send a macOS desktop notification when the Agent completes a task or requests authorization. Save the following script to ~/.qoder-cn/hooks/notify.sh:
#!/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 CN CLI"'
else
osascript -e 'display notification "Authorization required" with title "Qoder CN CLI"'
fi
exit 0
Configuration:
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "~/.qoder-cn/hooks/notify.sh"
}
]
}
]
}
}
Automatically lint modified files
Auto-lint JavaScript and TypeScript files after the Agent writes or edits them. Save the following script to ${project}/.qoder-cn/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
Set event to PostToolUse, matcher to Write|Edit, and command to .qoder-cn/hooks/auto-lint.sh.
Keep the Agent running
Check for uncommitted Git changes when the Agent stops, and prompt it to continue if any exist. Save the following script to ~/.qoder-cn/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 commit your changes." >&2
exit 2
fi
exit 0
Set event to Stop and command to ~/.qoder-cn/hooks/check-continue.sh.