Skip to main content
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:
EventWhen It FiresBlockable
UserPromptSubmitAfter the user submits a prompt, before the Agent processes itYes
PreToolUseBefore a tool executesYes
PostToolUseAfter a tool executes successfullyNo
PostToolUseFailureAfter a tool execution failsNo
StopWhen the Agent completes its responseNo

Quick Start

Here is an example that blocks rm -rf commands:
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
2

Add the config

Add the following to ~/.qoder/settings.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}
3

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

ScenarioEventDescription
Block dangerous commandsPreToolUsePrevent the Agent from running rm -rf, DROP TABLE, etc.
Validate file pathsPreToolUseRestrict the Agent to creating or editing files only within a designated directory
Auto-lint / formatPostToolUseRun ESLint / Prettier automatically after every file write
Audit loggingPostToolUseRecord every tool invocation for security audits
Failure alertingPostToolUseFailureSend an alert or write an error log when a tool call fails
Prompt content screeningUserPromptSubmitDetect sensitive data (passwords, keys, etc.) in user input
Auto-inject contextUserPromptSubmitAutomatically append project conventions or coding standards to every prompt
Desktop notificationStopShow 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:
  1. The plugin loads all hook configurations at startup.
  2. During Agent execution, the plugin encounters a lifecycle event (e.g. PreToolUse).
  3. The plugin iterates through every hook group registered for that event and evaluates the matcher against the current context.
  4. Hooks with a matching matcher run their shell scripts in order.
  5. Each script receives event context as JSON via stdin and returns a decision through its exit code and stdout.
  6. 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
RequirementEventMatcher
Check before the Agent runs any shell commandPreToolUse"Bash"
Process after the Agent writes or edits a filePostToolUse"Write | Edit"
Log when any tool call failsPostToolUseFailure"Bash" or omit
Screen every user promptUserPromptSubmitOmit (matches all)
Trigger a notification when the Agent stopsStopOmit (matches all)
Intercept only MCP toolsPreToolUse"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):
LocationScopePriorityShareableDescription
~/.qoder/settings.jsonUser (global)1 (lowest)NoPersonal config, applies to all projects
.qoder/settings.jsonProject2YesCommit to Git and share with your team
.qoder/settings.local.jsonProject (local)3NoGitignored; 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.

Config Format

{
  "hooks": {
    "EventName": [
      {
        "matcher": "match condition",
        "hooks": [
          {
            "type": "command",
            "command": "command to execute"
          }
        ]
      }
    ]
  }
}
FieldRequiredDescription
typeYesMust be "command"
commandYesShell command or path to the script to run
timeoutNoTimeout in seconds (defaults to 30). Custom values are not yet supported; configurable timeouts are coming in a future release
matcherNoMatch 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).
PatternMeaningExample
Omit or "*"Match everythingAll tools trigger the hook
Exact valueExact match"Bash" fires only for the Bash tool
|-separatedMatch multiple values"Write | Edit" fires for Write or Edit
RegexRegex match"mcp__.*" matches all MCP tools

Tool Name Mapping

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 nameCompatible nameDescription
run_in_terminalBashExecute shell commands
read_fileReadRead file contents
create_fileWriteCreate / write a file
search_replaceEditEdit a file
delete_file-Delete a file
grep_codeGrepSearch file contents
search_fileGlobMatch files by name
list_dirLSList a directory
taskTaskLaunch a subtask / sub-agent
Skill-Invoke a skill
search_webWebSearchWeb search
fetch_contentWebFetchFetch web page content
todo_writeTodoWriteWrite 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>sameMCP 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.

Input

Your hook script receives JSON data via stdin. Every event includes these common fields:
FieldDescription
session_idCurrent session ID
cwdCurrent working directory
hook_event_nameName of the event that triggered this hook
transcript_pathPath 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 codeMeaningBehavior
0SuccessContinue execution; stdout JSON is parsed
2BlockStop the action; stderr is injected into the conversation (only for blockable events)
OtherErrorNon-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..."
  }
}
FieldTypeDescription
hookSpecificOutput.hookEventNamestringFixed to "UserPromptSubmit"
hookSpecificOutput.additionalContextstringAdditional 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

PreToolUse

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"
  }
}
FieldTypeDescription
hookSpecificOutput.hookEventNamestringFixed to "PreToolUse"
hookSpecificOutput.permissionDecisionstring"allow" (proceed), "deny" (reject), or "ask" (prompt the user)
hookSpecificOutput.permissionDecisionReasonstringReason for the decision, shown to the Agent / user when denying or asking
hookSpecificOutput.updatedInputobjectModified tool input parameters (optional; use this to rewrite the tool call)
hookSpecificOutput.additionalContextstringAdditional 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."
  }
}
FieldTypeDescription
hookSpecificOutput.hookEventNamestringFixed to "PostToolUse"
hookSpecificOutput.feedbackstringFeedback 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"
}
FieldTypeDescription
errorstringThe 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."
}
FieldTypeDescription
decisionstring"block" (prevent the Agent from stopping and make it continue working)
reasonstringReason 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.

Log Tool Failures

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