Skip to main content

Hooks

Hooks let you inject custom logic at key points in the Qoder CN IDE lifecycle without modifying any code. By editing a JSON configuration file, you can:
  • Intercept dangerous operations before a tool call.
  • Automatically run a linter after every file write to keep code style consistent.
  • Get a desktop notification when the Agent finishes a task—no need to watch the IDE.
Unlike prompt instructions, Hooks are deterministic: once an event fires, the script runs regardless of how the model interprets things.

Supported events

Qoder CN IDE supports five hook events:
Event nameTriggerBlockable
UserPromptSubmitAfter the user submits a prompt, before the Agent processes itYes
PreToolUseBefore a tool call is executedYes
PostToolUseAfter a tool call succeedsNo
PostToolUseFailureAfter a tool call failsNo
StopWhen the Agent completes its responseNo

Quick start

The following example intercepts dangerous commands like rm -rf to demonstrate the hook configuration flow.

Step 1: Create the script

mkdir -p ~/.lingma/hooks
cat > ~/.lingma/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 ~/.lingma/hooks/block-rm.sh

Step 2: Add the configuration

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

Step 3: Verify

Restart the IDE. In the Qoder CN panel, ask the Agent to run a command containing rm -rf. The hook blocks execution and reports the error back to the Agent.

Common scenarios

ScenarioEventDescription
Block dangerous commandsPreToolUseBlock Agent from running commands like rm -rf, DROP TABLE, etc.
Validate file pathsPreToolUseRestrict Agent file writes to designated directories
Auto-lint / formatPostToolUseAutomatically run ESLint / Prettier after each file write
Audit loggingPostToolUseLog all Agent tool calls for security auditing
Failure monitoring / alertsPostToolUseFailureSend alerts or log errors when tool calls fail
Prompt content reviewUserPromptSubmitDetect sensitive information (passwords, keys, etc.) in user input
Auto-inject contextUserPromptSubmitAppend project guidelines or coding conventions to prompts
Desktop notificationStopShow a system notification when the Agent completes

How it works

Using hooks boils down to three steps: write a script → register the configuration → run automatically. When the Agent reaches a lifecycle point (e.g., before a tool call), the plugin checks whether a hook is registered for that event:
  1. The IDE loads all hook configurations at startup.
  2. During Agent execution, a specific event (e.g., PreToolUse) is reached.
  3. All hook groups for that event are traversed, and each matcher is compared against the current context.
  4. Matched hooks execute their shell scripts in order.
  5. Each script reads event context (JSON) from stdin and returns decisions via exit code and stdout.
  6. The IDE decides subsequent behavior (allow or block) based on the return values.

Prerequisites

  • jq: Example scripts depend on jq to parse JSON. On macOS, run brew install jq. On Linux, run apt install jq.
  • Script permissions: All hook scripts must be executable (chmod +x).

Creating hooks

Step 1: Determine the requirements—pick an event and matcher

First, decide where to intercept and what to match:
I want to intercept / handle "what operation" at "what point"?
                ↓                              ↓
             pick an event               write a matcher

Requirement

Event

matcher

Check before Agent runs any shell command

PreToolUse

"Bash"

Process after Agent writes or edits a file

PostToolUse

"Write | Edit"

Log when an Agent tool call fails

PostToolUseFailure

"Bash" or omit

Screen all user-submitted prompts

UserPromptSubmit

Omit (matches all)

Trigger a notification when the Agent stops

Stop

Omit (matches all)

Intercept MCP tools only

PreToolUse

"mcp__.*"

Step 2: Write the hook script

A hook script is a standard shell script that follows this protocol:
  • Input: Receive event context as JSON via stdin.
  • Output: Control behavior via exit code.
exit 0   →  Allow (continue)
exit 2   →  Block (stop the operation; stderr is injected into the conversation)
other    →  Error (continue; stderr is shown to the user)
Script template:
#!/bin/bash

# 1. Read JSON input from stdin
input=$(cat)

# 2. Use jq to extract fields you care about
#    Different events expose different fields; see the Event reference 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 goes back to the Agent
    echo "Operation denied: $command" >&2
    exit 2
  fi
fi

# 4. Allow
exit 0
Beyond exit codes, you can output JSON on exit 0 for finer control:
#!/bin/bash
input=$(cat)

# Emit JSON for fine-grained control (only takes effect on exit 0)
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"This operation is not allowed"}}'
exit 0

Step 3: Register in the configuration file

Write the script path under the corresponding event:
{
  "hooks": {
    "EventName": [
      {
        "matcher": "match condition (optional)",
        "hooks": [
          {
            "type": "command",
            "command": "script path"
          }
        ]
      }
    ]
  }
}

Step 4: Test and debug

You can pipe a fake payload to the script for testing:
# Simulate a PreToolUse event
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"},"hook_event_name":"PreToolUse"}' \
  | ~/.lingma/hooks/block-rm.sh
echo "Exit code: $?"
Check stderr output (block message):
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' \
  | ~/.lingma/hooks/block-rm.sh 2>&1

Configuring hooks

Configuration file locations

Hook configuration is loaded from the following files. Multiple configurations are merged and executed (priority from lowest to highest):
LocationScopePriorityShareableDescription
~/.lingma/settings.jsonUser1 (Lowest)NoPersonal configuration; applies to all projects
.lingma/settings.jsonProject2YesCommittable to Git for team sharing
.lingma/settings.local.jsonProject (Local)3NoPersonal dev overrides; add to .gitignore
The current version does not support hot reloading. Restart the IDE after modifying configuration.

Configuration format

{
  "hooks": {
    "EventName": [
      {
        "matcher": "match condition",
        "hooks": [
          {
            "type": "command",
            "command": "command to run"
          }
        ]
      }
    ]
  }
}
FieldRequiredDescription
typeYesMust be "command"
commandYesThe shell command or script path to execute
timeoutNoTimeout in seconds. Default: 30
matcherNoFilter for triggering. If omitted, matches all invocations of the event
A single event can have multiple matcher groups, and each group can contain multiple hook commands.

matcher rules

A matcher narrows when a hook fires. Matched fields vary by event (see per-event descriptions).

Syntax

Meaning

Example

Omit or "*"

Match all

Triggers for all tools

Exact value

Exact match

"Bash" triggers only for the Bash tool

| separated

Match multiple values

"Write | Edit" triggers for Write or Edit

Regular expression

Regex match

"mcp__.*" matches all MCP tools

Tool name mapping

Qoder CN supports two sets of tool names—native and compatible. Either can be used when configuring hooks; they are normalized at runtime for matching. For example, matcher: "Bash" and matcher: "run_in_terminal" are equivalent.
Native nameCompatible nameDescription
run_in_terminalBashExecute a shell command
read_fileReadRead file content
create_fileWriteCreate or write to a file
search_replaceEditEdit a file
delete_file-Delete a file
grep_codeGrepSearch file content
search_fileGlobMatch file names
list_dirLSList directory contents
taskTaskStart a subtask / sub-agent
mcp__<server>__<tool>Same as nativeAn MCP tool

Writing hook scripts

Hook scripts receive JSON via stdin and control behavior through exit code and stdout. This section describes the input/output format shared by all events; per-event fields appear in the Event reference.

Input

Hook scripts receive JSON via stdin. All events include these common fields:
FieldDescription
session_idCurrent session ID
cwdCurrent working directory
hook_event_nameName of the triggered event
transcript_pathFile path to the session context JSON
Different events add additional fields (see the Event reference). Parse input with jq:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')

Output

Hooks signal decisions via exit code and stdout. Exit code determines basic behavior:
Exit CodeMeaningBehavior
0SuccessContinues execution; parses stdout JSON
2BlockStops the operation; stderr is injected into the conversation (blockable events only)
OtherErrorContinues execution; stderr is shown to the user only
stdout JSON (optional; only takes effect on exit 0):
{
  "continue": true,
  "stopReason": "",
  "suppressOutput": false,
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow | deny | ask",
    "permissionDecisionReason": "Reason"
  }
}

Event reference

UserPromptSubmit

  • Trigger: After the user submits a prompt, before the Agent processes it
  • Blockable: Yes (exit 2 cancels the request)
  • Additional fields: prompt (the raw text entered by the user)

PreToolUse

  • Trigger: Before the Agent executes a tool call
  • Blockable: Yes (exit 2 rejects the tool call)
  • Matcher field: tool_name
  • Additional fields: tool_name, tool_input

PostToolUse

  • Trigger: After a tool call succeeds
  • Blockable: No
  • Matcher field: tool_name
  • Additional fields: tool_name, tool_input, tool_response

PostToolUseFailure

  • Trigger: After a tool call fails
  • Blockable: No
  • Matcher field: tool_name
  • Additional fields: tool_name, tool_input, error

Stop

  • Trigger: When the Agent completes its response
  • Blockable: No
  • Additional fields: stop_hook_active, last_assistant_message

FAQ

Why don't my configuration changes take effect?

Hot reloading is not supported. Restart the IDE after modifying settings.json.

What happens if a hook script times out?

The default timeout is 30 seconds. A timeout is treated as an error (execution continues; stderr is shown to the user).

How is the execution order determined for multiple hooks?

Same-event hooks run in declared order. Across files, priority applies: user (lowest) → project → project-local (highest).

What environment variables can hook scripts access?

Hooks run in the project root (cwd) as the current user, inheriting the IDE process environment.

How do I view hook execution logs?

Hook stderr output is sent back to the Agent or user. For detailed logs, search Qoder CN runtime logs for entries prefixed with [hook].