Hooks let you insert custom logic at key points during QwenWork execution without changing application code. By editing a JSON configuration file, you can:
- Block dangerous operations before a tool runs.
- Run lint automatically after a file is written.
- Show a desktop notification when the Agent finishes.
Unlike prompt instructions, Hooks are deterministic: whenever a configured event fires, the script runs.
Quick Start
The following example blocks commands containing rm -rf.
1. Create the script
mkdir -p ~/.qwenwork/hooks
cat > ~/.qwenwork/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 ~/.qwenwork/hooks/block-rm.sh
2. Add the configuration
Add the following to ~/.qwenwork/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.qwenwork/hooks/block-rm.sh"
}
]
}
]
}
}
3. Verify the result
Open QwenWork and ask the Agent to execute a command containing rm -rf. The Hook blocks the command and returns the error message to the Agent.
Configuration file
| Location | Scope | Description |
|---|
~/.qwenwork/settings.json | User level | Personal configuration that applies to all QwenWork sessions |
Hot reload is not currently supported. Restart QwenWork after changing the configuration file.
{
"hooks": {
"EventName": [
{
"matcher": "match condition",
"hooks": [
{
"type": "command",
"command": "command to execute",
"timeout": 60
}
]
}
]
}
}
| Field | Required | Description |
|---|
type | Yes | Must be "command" |
command | Yes | Shell command to execute |
timeout | No | Timeout in seconds; the default is 60 |
matcher | No | Match condition; omit it to match every event instance |
An event can contain multiple matcher groups, and each group can contain multiple Hook commands.
Matcher rules
| Pattern | Meaning | Example |
|---|
Leave empty or "*" | Match all | Run for every tool |
| Exact value | Exact match | "Bash" matches only the Bash tool |
Values separated by | | Match several values | "Write | Edit" matches Write or Edit |
| Regular expression | Pattern match | "mcp__.*" matches MCP tools |
Write a Hook Script
A Hook script receives JSON through stdin and controls behavior using its exit code and stdout. This section describes the input and output shared by all events. Event-specific fields are documented under Hook Events.
QwenWork does not inject environment variables into Hook scripts. Read the session ID, working directory, tool information, and other event data from the stdin JSON payload.
All events include the following common fields:
| Field | Description |
|---|
session_id | Current session ID |
cwd | Current working directory |
hook_event_name | Name of the event that fired |
Different events add their own fields to this payload. Use jq to parse the input:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
Output
Exit code 0 indicates success. Exit code 2 blocks supported events and injects stderr into the conversation. Other exit codes are treated as non-blocking errors.
When a script exits with 0, supported events can parse JSON from stdout for more precise control. Stdout is ignored when the exit code is not 0.
Hook Events
SessionStart
Fires when a session starts.
Matcher: session source
| Matcher value | Scenario |
|---|
startup | Start a new session |
resume | Resume an existing session |
compact | Context compaction has completed |
{
"source": "startup",
"model": "Auto"
}
SessionEnd
Fires when a session ends.
Matcher: end reason
prompt_input_exit: The user exits input, for example with Ctrl+D.
other: Any other reason.
{
"reason": "prompt_input_exit"
}
UserPromptSubmit
Fires after the user submits a prompt and before the Agent starts processing it.
{
"prompt": "Write a sorting function"
}
Fires before a tool runs and can block the tool call.
Matcher: tool name, such as Bash, Write, Edit, Read, Glob, or Grep. MCP tools use names such as mcp__server__tool.
{
"tool_name": "Bash",
"tool_input": {"command": "rm -rf /tmp/build"},
"tool_use_id": "toolu_01ABC123"
}
To block the tool, exit with code 2. Stderr is returned to the Agent as an error.
PostToolUse
Fires after a tool completes successfully.
Matcher: tool name
{
"tool_name": "Write",
"tool_input": {"file_path": "/path/to/file.ts", "content": "..."},
"tool_response": "File written successfully",
"tool_use_id": "toolu_01ABC123"
}
PostToolUseFailure
Fires after a tool fails.
Matcher: tool name
{
"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
Fires when the main Agent finishes a response and has no pending tool calls. The Hook can prevent the Agent from stopping so it continues working.
To prevent the Agent from stopping, exit with code 2. Stderr is injected into the conversation as a message and the Agent continues.
SubagentStart / SubagentStop
Fire when a sub-agent starts or finishes. Like Stop, SubagentStop can prevent a sub-agent from stopping.
Matcher: Agent type
{
"agent_id": "a1b2c3d4",
"agent_type": "task"
}
PreCompact
Fires before context compaction.
Matcher: trigger type
| Matcher value | Scenario |
|---|
manual | The user runs /compact manually |
auto | The context window is full and compaction starts automatically |
{
"trigger": "manual",
"custom_instructions": "Preserve all tool results"
}
Notification
Fires for notifications such as permission requests and completed tasks.
Matcher: notification type
permission: Permission request notification.
result: Agent result notification.
{
"message": "Agent is requesting permission to run: rm -rf node_modules",
"title": "Permission Required",
"notification_type": "permission"
}
PermissionRequest
Fires when a tool needs user authorization.
Matcher: tool name
{
"tool_name": "Bash",
"tool_input": {"command": "rm -rf node_modules"}
}
Use Cases
Desktop notifications
Show a desktop notification when the Agent completes a task or needs authorization.
Create ~/.qwenwork/hooks/notify.sh on 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 "QwenWork"'
else
osascript -e 'display notification "Authorization required" with title "QwenWork"'
fi
exit 0
Configure the Hook:
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "~/.qwenwork/hooks/notify.sh"
}
]
}
]
}
}
Run lint after file changes
Run lint automatically whenever the Agent writes or edits a file.
Create ${project}/.qwenwork/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
Configure the PostToolUse event with matcher Write|Edit and command .qwenwork/hooks/auto-lint.sh.
Ask the Agent to continue
When the Agent tries to stop, check for unfinished work and inject a message that tells it to continue.
Create ~/.qwenwork/hooks/check-continue.sh:
#!/bin/bash
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
echo "Uncommitted changes detected. Complete the git commit." >&2
exit 2
fi
exit 0
Configure the Stop event with command ~/.qwenwork/hooks/check-continue.sh.