Hooks let you run custom logic at key points during Agent execution in the Qoder IDE and JetBrains plugin — no source code changes required. Edit a JSON config file to:
- Block dangerous operations before a tool runs
- Auto-lint after every file write to enforce code style
- Send a desktop notification when the Agent finishes, so you don’t have to watch the IDE
Unlike prompt instructions, hooks are deterministic — when the event fires, your script runs. No model interpretation, no drift.
Supported Events
The IDE / JB plugin currently supports five hook events:
| Event | When It Fires | Blockable |
|---|
| UserPromptSubmit | After the user submits a prompt, before the Agent processes it | Yes |
| PreToolUse | Before a tool executes | Yes |
| PostToolUse | After a tool executes successfully | No |
| PostToolUseFailure | After a tool execution fails | No |
| Stop | When the Agent completes its response | No |
Quick Start
Here is an example that blocks rm -rf commands:
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
Add the config
Add the following to ~/.qoder/settings.json:{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/block-rm.sh"
}
]
}
]
}
}
Verify it works
Open your IDE and ask the Agent to run a command containing rm -rf in the Qoder plugin panel. The hook blocks execution and feeds the error message back to the Agent.
Use Cases
| Scenario | Event | Description |
|---|
| Block dangerous commands | PreToolUse | Prevent the Agent from running rm -rf, DROP TABLE, etc. |
| Validate file paths | PreToolUse | Restrict the Agent to creating or editing files only within a designated directory |
| Auto-lint / format | PostToolUse | Run ESLint / Prettier automatically after every file write |
| Audit logging | PostToolUse | Record every tool invocation for security audits |
| Failure alerting | PostToolUseFailure | Send an alert or write an error log when a tool call fails |
| Prompt content screening | UserPromptSubmit | Detect sensitive data (passwords, keys, etc.) in user input |
| Auto-inject context | UserPromptSubmit | Automatically append project conventions or coding standards to every prompt |
| Desktop notification | Stop | Show a system notification when the Agent finishes |
How It Works
The hook lifecycle comes down to three steps: write a script, register it in config, and it takes effect automatically.
When the Agent reaches a lifecycle event (such as “before a tool call”), the plugin checks whether any hooks are registered for it:
- The plugin loads all hook configurations at startup.
- During Agent execution, the plugin encounters a lifecycle event (e.g.
PreToolUse).
- The plugin iterates through every hook group registered for that event and evaluates the
matcher against the current context.
- Hooks with a matching matcher run their shell scripts in order.
- Each script receives event context as JSON via
stdin and returns a decision through its exit code and stdout.
- The plugin reads the result and decides what to do next — proceed or block.
Prerequisites
- jq: The example scripts use jq to parse JSON. Install it with
brew install jq on macOS or apt install jq on Linux.
- Script permissions: Every hook script must be executable (
chmod +x).
Creating Hooks
1. Decide What You Need: Pick an Event and a Matcher
Start by deciding where to intervene and what to match:
I want to intercept/handle [what operation] at [what point]?
↓ ↓
write a matcher pick an event
| Requirement | Event | Matcher |
|---|
| Check before the Agent runs any shell command | PreToolUse | "Bash" |
| Process after the Agent writes or edits a file | PostToolUse | "Write | Edit" |
| Log when any tool call fails | PostToolUseFailure | "Bash" or omit |
| Screen every user prompt | UserPromptSubmit | Omit (matches all) |
| Trigger a notification when the Agent stops | Stop | Omit (matches all) |
| Intercept only MCP tools | PreToolUse | "mcp__.*" |
2. Write the Hook Script
A hook script is a standard shell script that follows this protocol:
Input: JSON event context delivered via stdin.
Output: Determined by the exit code.
exit 0 → Allow (continue execution)
exit 2 → Block (stop the action; stderr is injected into the conversation)
other → Error (continue execution; stderr is shown to the user)
Script template:
#!/bin/bash
# 1. Read the JSON input from stdin
input=$(cat)
# 2. Extract the fields you care about with jq
# Different events provide different fields — see the "Hook Events" section
tool_name=$(echo "$input" | jq -r '.tool_name')
tool_input=$(echo "$input" | jq -r '.tool_input')
# 3. Write your logic
if [ "$tool_name" = "Bash" ]; then
command=$(echo "$input" | jq -r '.tool_input.command')
# Check for dangerous operations
if echo "$command" | grep -qE 'rm\s+-rf|DROP\s+TABLE'; then
# Block: exit 2 + stderr message is fed back to the Agent
echo "Operation denied: $command" >&2
exit 2
fi
fi
# 4. Allow
exit 0
You can also output JSON on stdout when exiting with exit 0 for finer-grained control:#!/bin/bash
input=$(cat)
# Output JSON for fine-grained control (only parsed when exit code is 0)
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"This operation is not allowed"}}'
exit 0
3. Register the Script in Your Config
Add the script path under the corresponding event in your settings file:
{
"hooks": {
"EventName": [
{
"matcher": "match condition (optional)",
"hooks": [
{
"type": "command",
"command": "path/to/script"
}
]
}
]
}
}
4. Test and Debug
You can test your scripts directly from the terminal by piping JSON input:
# Simulate a PreToolUse event
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"},"hook_event_name":"PreToolUse"}' \
| ~/.qoder/hooks/block-rm.sh
echo "Exit code: $?"
Check the stderr output (the block message):
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' \
| ~/.qoder/hooks/block-rm.sh 2>&1
Configuring Hooks
Config File Locations
Hook configurations are loaded from the following files. When hooks are defined at multiple levels, they are merged and executed together (listed from lowest to highest priority):
| Location | Scope | Priority | Shareable | Description |
|---|
~/.qoder/settings.json | User (global) | 1 (lowest) | No | Personal config, applies to all projects |
.qoder/settings.json | Project | 2 | Yes | Commit to Git and share with your team |
.qoder/settings.local.json | Project (local) | 3 | No | Gitignored; for your personal dev setup |
The IDE / JB plugin and the CLI share the same config files. Hot reload is not yet supported — restart the IDE after editing hook configurations for changes to take effect.
{
"hooks": {
"EventName": [
{
"matcher": "match condition",
"hooks": [
{
"type": "command",
"command": "command to execute"
}
]
}
]
}
}
| Field | Required | Description |
|---|
type | Yes | Must be "command" |
command | Yes | Shell command or path to the script to run |
timeout | No | Timeout in seconds (defaults to 30). Custom values are not yet supported; configurable timeouts are coming in a future release |
matcher | No | Match condition. If omitted, the hook fires on every occurrence of that event |
You can define multiple matcher groups under a single event, and each group can contain multiple hook commands.
Matcher Rules
matcher determines when a hook fires. What it matches against depends on the event (see each event’s description).
| Pattern | Meaning | Example |
|---|
Omit or "*" | Match everything | All tools trigger the hook |
| Exact value | Exact match | "Bash" fires only for the Bash tool |
|-separated | Match multiple values | "Write | Edit" fires for Write or Edit |
| Regex | Regex match | "mcp__.*" matches all MCP tools |
Qoder supports two sets of tool names — native names and Claude Code-compatible names. You can use either in your matchers; the plugin maps them internally. For example, matcher: "Bash" is equivalent to matcher: "run_in_terminal".
| Qoder native name | Compatible name | Description |
|---|
run_in_terminal | Bash | Execute shell commands |
read_file | Read | Read file contents |
create_file | Write | Create / write a file |
search_replace | Edit | Edit a file |
delete_file | - | Delete a file |
grep_code | Grep | Search file contents |
search_file | Glob | Match files by name |
list_dir | LS | List a directory |
task | Task | Launch a subtask / sub-agent |
Skill | - | Invoke a skill |
search_web | WebSearch | Web search |
fetch_content | WebFetch | Fetch web page content |
todo_write | TodoWrite | Write a TODO |
ask_user_question | - | Ask the user a question |
search_memory | - | Search memory |
update_memory | - | Update memory |
switch_mode | - | Switch mode |
create_plan | - | Create a plan |
run_preview | - | Preview a web app |
mcp__<server>__<tool> | same | MCP tools |
Writing Hook Scripts
Hook scripts receive JSON input via stdin and communicate results through their exit code and stdout. This section covers the input/output format common to all events. For event-specific fields, see Hook Events.
Your hook script receives JSON data via stdin. Every event includes these common fields:
| Field | Description |
|---|
session_id | Current session ID |
cwd | Current working directory |
hook_event_name | Name of the event that triggered this hook |
transcript_path | Path to the session context JSON file |
Each event adds its own fields on top of these (see the individual event descriptions).
Parse the input with jq:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
Output
Hooks communicate results through their exit code and stdout.
Exit code determines the basic behavior:
| Exit code | Meaning | Behavior |
|---|
0 | Success | Continue execution; stdout JSON is parsed |
2 | Block | Stop the action; stderr is injected into the conversation (only for blockable events) |
| Other | Error | Non-blocking error; stderr is shown to the user; execution continues |
stdout JSON (only parsed when exit code is 0) provides fine-grained control for certain events. See each event’s description for the supported fields. When the exit code is non-zero, stdout is ignored.
Environment Variables
When a hook script runs, the plugin injects environment variables that your script can reference. The full list of injected environment variables is pending confirmation.
Hook Events
UserPromptSubmit
Fires after the user submits a prompt in the IDE plugin panel, before the Agent begins processing it. Use it for prompt screening, content filtering, or auto-injecting context.
Matcher: None. This event fires for all user input.
Extra input fields:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "UserPromptSubmit",
"prompt": "Write me a sorting function"
}
Blocking the prompt: Exit with code 2. The stderr content is displayed to the user as an error, and the Agent does not process the prompt.
stdout JSON fields (when exit code is 0):
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "## Current Git status\n..."
}
}
| Field | Type | Description |
|---|
hookSpecificOutput.hookEventName | string | Fixed to "UserPromptSubmit" |
hookSpecificOutput.additionalContext | string | Additional context injected into the Agent’s conversation |
Example: Auto-append project conventions to every prompt
#!/bin/bash
input=$(cat)
prompt=$(echo "$input" | jq -r '.prompt')
# Automatically append a coding standards reminder
echo '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"Follow the coding standards defined in the project .editorconfig."}}'
exit 0
Fires before a tool executes. Can block tool execution. This is the most commonly used hook event — ideal for blocking dangerous commands, validating file paths, or enforcing permissions.
Matcher: Tool name (e.g. Bash, Write, Edit, Read, Glob, Grep, or MCP tool names like mcp__server__tool).
Extra input fields:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf /tmp/build" }
}
Blocking tool execution: Exit with code 2. The stderr content is returned to the Agent as an error. See the Quick Start for a full example.
stdout JSON fields (when exit code is 0):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Safe read operation",
"updatedInput": { "command": "npm test --coverage" },
"additionalContext": "Added coverage flag"
}
}
| Field | Type | Description |
|---|
hookSpecificOutput.hookEventName | string | Fixed to "PreToolUse" |
hookSpecificOutput.permissionDecision | string | "allow" (proceed), "deny" (reject), or "ask" (prompt the user) |
hookSpecificOutput.permissionDecisionReason | string | Reason for the decision, shown to the Agent / user when denying or asking |
hookSpecificOutput.updatedInput | object | Modified tool input parameters (optional; use this to rewrite the tool call) |
hookSpecificOutput.additionalContext | string | Additional context (optional) |
PostToolUse
Fires after a tool executes successfully. Not blockable. Use it for auto-linting, logging, or result analysis.
Matcher: Tool name.
Extra input fields:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "PostToolUse",
"tool_name": "Write",
"tool_input": { "file_path": "/path/to/file.ts", "content": "..." },
"tool_response": "File written successfully"
}
stdout JSON fields (when exit code is 0):
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"feedback": "File formatted with Prettier. 3 issues auto-fixed."
}
}
| Field | Type | Description |
|---|
hookSpecificOutput.hookEventName | string | Fixed to "PostToolUse" |
hookSpecificOutput.feedback | string | Feedback displayed to the user (e.g. a lint results summary) |
PostToolUseFailure
Fires when a tool call fails. Not blockable. Use it for error monitoring, retry suggestions, or logging.
Matcher: Tool name.
Extra input fields:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "PostToolUseFailure",
"tool_name": "Bash",
"tool_input": { "command": "npm test" },
"error": "Command exited with non-zero status code 1"
}
| Field | Type | Description |
|---|
error | string | The error message from the failed tool execution |
Stop
Fires after the Agent completes its response (i.e. the Agent has no more tool calls to make). Not blockable. Use it for desktop notifications, logging, or task status reports.
The Stop event does not support blocking in the current version — you cannot use exit 2 to keep the Agent working. This capability is planned for a future release.
Matcher: None. This event fires whenever the Agent stops.
Extra input fields:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "Stop",
"stop_hook_active": true,
"last_assistant_message": "I've finished writing the sorting function."
}
stdout JSON fields (when exit code is 0; planned for a future release):
{
"decision": "block",
"reason": "Tests failing. Fix them before completing."
}
| Field | Type | Description |
|---|
decision | string | "block" (prevent the Agent from stopping and make it continue working) |
reason | string | Reason for blocking; injected into the conversation as a message |
Work with Hooks
Block Dangerous Commands
Check for destructive operations like rm -rf or DROP TABLE before the Agent runs a shell command.
Script ~/.qoder/hooks/block-dangerous.sh:
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command')
# Check for dangerous patterns
if echo "$command" | grep -qE 'rm\s+-rf|DROP\s+TABLE|mkfs|dd\s+if='; then
echo "Dangerous command blocked: $command" >&2
exit 2
fi
exit 0
Config: event PreToolUse, matcher Bash , command ~/.qoder/hooks/block-dangerous.sh.
Auto-Lint After File Writes
Automatically run a linter 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 lint JS/TS files
case "$file_path" in
*.js|*.ts|*.jsx|*.tsx)
npx eslint "$file_path" --fix 2>/dev/null
;;
esac
exit 0
Config: event PostToolUse, matcher Write|Edit, command .qoder/hooks/auto-lint.sh.
Write to a log file whenever one of the Agent’s tool calls fails, making it easier to troubleshoot.
Script ~/.qoder/hooks/log-failure.sh:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
error=$(echo "$input" | jq -r '.error')
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $tool_name failed: $error" >> ~/.qoder/hooks/failure.log
exit 0
Config: event PostToolUseFailure, no matcher (matches all tools), command ~/.qoder/hooks/log-failure.sh.
Desktop Notification on Completion
Show a system notification when the Agent finishes a task. Great for long-running tasks.
Script ~/.qoder/hooks/notify-done.sh (macOS):
#!/bin/bash
input=$(cat)
message=$(echo "$input" | jq -r '.last_assistant_message // "Task complete"' | head -c 100)
osascript -e "display notification \"$message\" with title \"Qoder Agent\""
exit 0
Config: event Stop, no matcher, command ~/.qoder/hooks/notify-done.sh.
Prompt Content Screening
Screen user prompts for sensitive data (passwords, keys, etc.) before submission to prevent accidental leaks.
Script ~/.qoder/hooks/check-prompt.sh:
#!/bin/bash
input=$(cat)
prompt=$(echo "$input" | jq -r '.prompt')
# Check for possible credentials
if echo "$prompt" | grep -qiE '(password|secret|api_key|token)\s*[:=]\s*\S+'; then
echo "Detected possible sensitive data in your prompt. Please review and resubmit." >&2
exit 2
fi
exit 0
Config: event UserPromptSubmit, no matcher, command ~/.qoder/hooks/check-prompt.sh.
Full Config Example
Here is a complete configuration with hooks for all five events:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/check-prompt.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/block-dangerous.sh"
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/validate-file-path.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/auto-lint.sh"
}
]
}
],
"PostToolUseFailure": [
{
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/log-failure.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/notify-done.sh"
}
]
}
]
}
}
Things to Keep in Mind
- Timeout handling: Hook scripts have a default timeout of 30 seconds. If a script times out, it is killed and treated as an allow (proceed). Configurable timeouts are coming in a future release.
- Error handling: If a script exits with an unexpected code (anything other than 0 or 2), the error message is shown to the user but the Agent continues without interruption.
- Script permissions: Make sure your scripts are executable (
chmod +x).
- Config merging: When the same event has hooks defined at multiple config levels, they execute in order from lowest to highest priority. If any hook blocks (exit 2), the remaining hooks for that event are skipped.
- jq dependency: The example scripts rely on
jq to parse JSON. Make sure it is installed on your system (brew install jq on macOS, apt install jq on Linux).