> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qoder.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Hooks

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                          | Yes       |

## Quick Start

Here is an example that blocks `rm -rf` commands:

<Steps>
  <Step title="Create the script">
    ```bash theme={null}
    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
    ```
  </Step>

  <Step title="Add the config">
    Add the following to `~/.qoder/settings.json`:

    ```json theme={null}
    {
      "hooks": {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "command",
                "command": "~/.qoder/hooks/block-rm.sh"
              }
            ]
          }
        ]
      }
    }
    ```
  </Step>

  <Step title="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.
  </Step>
</Steps>

## 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:

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:

```plaintext theme={null}
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.

```plaintext theme={null}
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:**

```bash theme={null}
#!/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
```

<Tip>
  You can also output JSON on `stdout` when exiting with `exit 0` for finer-grained control:

  ```bash theme={null}
  #!/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
  ```
</Tip>

### 3. Register the Script in Your Config

Add the script path under the corresponding event in your settings file:

```json theme={null}
{
  "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:

```bash theme={null}
# 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):

```bash theme={null}
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-level            | 1 (lowest) | No        | Personal config, applies to all projects |
| `.qoder/settings.json`       | Project-level         | 2          | Yes       | Commit to Git and share with your team   |
| `.qoder/settings.local.json` | Project-level (local) | 3          | No        | Gitignored; for your personal dev setup  |

<Note>
  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.
</Note>

### Config Format

```json theme={null}
{
  "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         |

### 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 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](#hook-events).

### Input

Your hook script receives JSON data via **stdin**. Every event includes these common fields:

| Field                 | Description                                | Always Present |
| --------------------- | ------------------------------------------ | -------------- |
| `session_id`          | Current session ID                         | Yes            |
| `cwd`                 | Current working directory                  | Yes            |
| `hook_event_name`     | Name of the event that triggered this hook | Yes            |
| `transcript_path`     | Path to the session context JSONL file     | Yes            |
| `tool_name`           | Tool name (tool-related events only)       | No             |
| `tool_input`          | Tool input parameters                      | No             |
| `tool_response`       | Tool execution result (PostToolUse only)   | No             |
| `extra.email`         | User's Git email                           | No             |
| `extra.repo`          | Repository path (group/repo format)        | No             |
| `extra.branch`        | Current branch                             | No             |
| `extra.request_time`  | Request time (RFC3339)                     | No             |
| `extra.response_time` | Response time (RFC3339)                    | No             |

Each event adds its own fields on top of these (see the individual event descriptions).

Parse the input with `jq`:

```bash theme={null}
#!/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 the following environment variables that your script can reference:

| Variable                     | Description                                    |
| ---------------------------- | ---------------------------------------------- |
| `QODER_SESSION_ID`           | Session ID                                     |
| `QODER_TOOL_NAME`            | Current tool name                              |
| `QODER_CWD`                  | Working directory                              |
| `QODER_TRANSCRIPT_PATH`      | Transcript file path                           |
| `QODER_TOOL_INPUT_FILE_PATH` | File path the tool operates on (if applicable) |

## 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:**

```json theme={null}
{
  "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):**

```json theme={null}
{
  "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**

```bash theme={null}
#!/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:**

```json theme={null}
{
  "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](#quick-start) for a full example.

**stdout JSON fields (when exit code is 0):**

```json theme={null}
{
  "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:**

```json theme={null}
{
  "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):**

```json theme={null}
{
  "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:**

```json theme={null}
{
  "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). **Can block the Agent from stopping.** Use it for quality gates, desktop notifications, logging, task status reports, or Harness self-evolution.

**Matcher:** None. This event fires whenever the Agent stops.

**Extra input fields:**

```json theme={null}
{
  "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."
}
```

| Field                    | Type    | Description                                                                                                                                                     |
| ------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `stop_hook_active`       | boolean | `true` when the Agent is retrying after a previous Stop hook block. **Your script must check this field and exit 0 when it is true to prevent infinite loops.** |
| `last_assistant_message` | string  | The Agent's last text response                                                                                                                                  |

**Blocking the Agent from stopping:** Exit with code 2. The block reason is injected into the conversation as a user message, and the Agent continues working.

**stdout JSON fields (when exit code is 0):**

```json theme={null}
{
  "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         |

<Warning>
  **Preventing infinite loops:** When a Stop hook blocks the Agent (exit 2), the Agent retries and the Stop event fires again with `stop_hook_active: true`. Your script **must** check this field and `exit 0` when it is `true`, otherwise the hook will block indefinitely.
</Warning>

## 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`:

```bash theme={null}
#!/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`:

```bash theme={null}
#!/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`:

```bash theme={null}
#!/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):

```bash theme={null}
#!/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`:

```bash theme={null}
#!/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:

```json theme={null}
{
  "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).

## Best Practice Scenarios

### Who Should Use Hooks

Hook value depends on your role. Here's a quick mapping:

#### For Individual Developers

| Scenario                        | Description                                                                          | Practice   |
| ------------------------------- | ------------------------------------------------------------------------------------ | ---------- |
| **Prompt Enhancement**          | Auto-inject project-specific skills and coding standards — no manual input each time | Scenario 1 |
| **Sensitive Data Interception** | Prevent passwords, keys, internal IPs from being sent to the model                   | Scenario 2 |
| **Dangerous Command Blocking**  | Block `rm -rf`, `git push --force`, and other destructive commands                   | Scenario 6 |
| **Harness Self-Evolution**      | Auto-detect reusable lessons at session end and trigger a retrospective              | Scenario 8 |

#### For Teams / Enterprises

| Scenario                       | Description                                                                                     | Practice   |
| ------------------------------ | ----------------------------------------------------------------------------------------------- | ---------- |
| **Rule/Skill Usage Analytics** | Track which Rules and Skills fire across the team, assess asset quality                         | Scenario 3 |
| **File Edit Tracking**         | Record which files the Agent modifies, for change audits and impact analysis                    | Scenario 4 |
| **Global Usage Analytics**     | Collect conversation content, tool calls, model replies — full pipeline for efficiency analysis | Scenario 5 |
| **Safety Controls**            | Enforce a team-wide dangerous-command blocklist                                                 | Scenario 6 |
| **Quality Gates**              | Auto-run build/tests before the Agent finishes; block and fix if they fail                      | Scenario 7 |

<Tip>
  Individual scenarios are typically configured in `~/.qoder/settings.json` (user-level) or `.qoder/settings.local.json` (project-level local). Team scenarios should go in `.qoder/settings.json` (project-level) and be committed to Git to ensure uniform enforcement.
</Tip>

***

### Scenario 1: Prompt Enhancement — Auto-Inject Skills

> **Pain point:** You have to manually specify a Skill every time, or forget to load project-specific context.

> **Solution:** Use a `UserPromptSubmit` hook to **auto-inject a prompt hint** guiding the Agent to use a specific Skill.

**Config:**

```json theme={null}
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/inject-skill-hint.sh"
          }
        ]
      }
    ]
  }
}
```

**Script** `.qoder/hooks/inject-skill-hint.sh`:

```bash theme={null}
#!/bin/sh
# 功能：在 Prompt 提交时，注入 Skill 使用提示（每个会话仅注入一次）
INPUT=$(cat)

# === 会话级去重：同一 session 只注入一次 ===
# 未来可使用 SessionStart 事件（即将支持）替代，届时无需去重逻辑
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')
DEDUP_DIR="/tmp/hook-dedup"
mkdir -p "$DEDUP_DIR"

if [ -n "$SESSION_ID" ] && [ -f "$DEDUP_DIR/skill-hint-$SESSION_ID" ]; then
  exit 0  # 本会话已注入过，跳过
fi
# ============================================

# 读取项目约定的 skill 使用规则（可根据项目自定义）
SKILL_HINT=""

# === 请根据你的项目实际情况修改以下内容 ===
# 示例1：始终提示使用 git-commit skill
# SKILL_HINT="如果用户要求提交代码，请使用 /git-commit skill"

# ============================================

if [ -z "$SKILL_HINT" ]; then
  exit 0
fi

# 标记本会话已注入
[ -n "$SESSION_ID" ] && touch "$DEDUP_DIR/skill-hint-$SESSION_ID"

cat <<EOF
{"hookSpecificOutput": {"additionalContext": "$SKILL_HINT"}}
EOF

exit 0
```

**Key points:**

* `additionalContext` is appended to the user prompt as a **system-reminder** injected into the Agent
* Uses `session_id` + temp files for **session-level deduplication** to avoid injecting the same hint every turn
* The script can read project config files for **project-specific** Skill recommendations
* `exit 0` allows the prompt through; `exit 2` blocks it (e.g. for non-compliant prompts)

***

### Scenario 2: Sensitive Prompt Blocking

> **Pain point:** Users may accidentally include passwords, keys, internal IPs, or personal data in their prompts, risking information leakage.

> **Solution:** Use a `UserPromptSubmit` hook to **detect sensitive content and block the prompt**.

**Config:**

```json theme={null}
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/block-sensitive-prompt.sh"
          }
        ]
      }
    ]
  }
}
```

**Script** `.qoder/hooks/block-sensitive-prompt.sh`:

```bash theme={null}
#!/bin/sh
# 功能：检测用户 Prompt 中的敏感信息，命中则阻断
INPUT=$(cat)

# 提取用户 Prompt 内容
PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // empty')

if [ -z "$PROMPT" ]; then
  exit 0
fi

# === 请根据团队安全规范调整敏感词规则 ===

# 1. 密钥/凭证类模式
SECRET_PATTERNS="password=|passwd=|secret_key=|access_key=|AKIA[0-9A-Z]{16}|token=[a-zA-Z0-9]{20,}"

# 2. 内部网络信息
INTERNAL_PATTERNS="10\.[0-9]+\.[0-9]+\.[0-9]+|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\." 

# 3. 自定义敏感词（团队内部术语、项目代号等）
# CUSTOM_PATTERNS="项目代号X|内部接口地址"

# 合并所有模式
ALL_PATTERNS="$SECRET_PATTERNS|$INTERNAL_PATTERNS"

# 执行检测
MATCH=$(echo "$PROMPT" | grep -oiE "$ALL_PATTERNS" | head -1)

if [ -n "$MATCH" ]; then
  echo "检测到敏感信息: $MATCH" >&2
  exit 2  # 阻断 Prompt 提交
fi

# ============================================

exit 0
```

**Key points:**

* `UserPromptSubmit` is a **blockable event**; `exit 2` directly prevents the prompt from reaching the Agent
* Sensitive patterns support **regular expressions** for flexible matching (e.g. AWS AKIA prefix, internal IP ranges)
* Consider extracting patterns to a **separate config file** (e.g. `.qoder/hooks/sensitive-patterns.txt`) for team-wide maintenance
* For more precise detection, call external tools like `gitleaks` or `trufflehog`

> **Difference from Scenario 1:** Scenario 1 uses `exit 0` + `additionalContext` for prompt **enhancement** (injecting context), while this scenario uses `exit 2` for prompt **blocking** (rejecting non-compliant input). Both can coexist under the same `UserPromptSubmit` event and execute in config order.

***

### Scenario 3: Rule/Skill Usage Analytics

> **Pain point:** Many Rules and Skills are configured but actual usage rates are unknown.

> **Solution:** Use the **Transcript system** + `Stop` hook to **automatically analyze and record** Rule/Skill trigger data after each conversation.

**Config:**

```json theme={null}
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/analyze-rule-skill-usage.sh"
          }
        ]
      }
    ]
  }
}
```

**Script** `.qoder/hooks/analyze-rule-skill-usage.sh`:

```bash theme={null}
#!/bin/sh
# 功能：在 Agent 完成响应时，分析本次会话的 Rule/Skill 使用情况
INPUT=$(cat)

# 从 stdin 提取字段
TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')

if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
  exit 0
fi

# === 📝 分析逻辑 — 请根据实际需求调整 ===

# 1. 提取 session_meta 中的 rules 信息（JSONL 逐行解析）
RULES_META=$(jq -c 'select(.data.meta_type == "rules") | .data.content' "$TRANSCRIPT_PATH" 2>/dev/null | head -1)

# 2. 提取 slash_command 信息（用户通过 / 触发的 Skill）
SLASH_SKILL_META=$(jq -c 'select(.data.meta_type == "slash_command") | .data.content' "$TRANSCRIPT_PATH" 2>/dev/null | head -1)

# 3. 提取 tool_name="Skill" 的调用（Agent 主动触发的 Skill 工具调用）
#    说明：除了用户手动 /skill-name 触发外，Agent 也会自行调用 Skill 工具

AUTO_SKILL_CALLS=$(jq -c 'select(.message.content[ ]? | select(.type == "tool_use" and .name == "Skill"))' "$TRANSCRIPT_PATH" 2>/dev/null)

AUTO_SKILL_COUNT=$(printf '%s' "$AUTO_SKILL_CALLS" | grep -c . 2>/dev/null || echo 0)

AUTO_SKILL_NAMES=$(printf '%s' "$AUTO_SKILL_CALLS" | jq -r '.message.content[ ] | select(.type == "tool_use" and .name == "Skill") | .input.skill' 2>/dev/null | sort -u | jq -R -s 'split("\n") | map(select(. != ""))')


# 4. 统计全部工具调用次数

TOOL_COUNT=$(jq -c 'select(.message.content[ ]?.type == "tool_use")' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')


# 5. 记录到统计文件
STATS_DIR="$HOME/.qoder/stats"
mkdir -p "$STATS_DIR"
DATE=$(date +%Y-%m-%d)
STATS_FILE="$STATS_DIR/usage-${DATE}.jsonl"

jq -n --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg sid "$SESSION_ID" \
  --argjson tc "$TOOL_COUNT" \
  --argjson rules "${RULES_META:-null}" \
  --argjson slash_skill "${SLASH_SKILL_META:-null}" \
  --argjson auto_skill_count "$AUTO_SKILL_COUNT" \
  --argjson auto_skill_names "${AUTO_SKILL_NAMES:-null}" \
  '{timestamp:$ts, session_id:$sid, tool_calls:$tc, rules:$rules, slash_skill:$slash_skill, auto_skill: {count:$auto_skill_count, names:$auto_skill_names}}' >> "$STATS_FILE"

# ============================================

exit 0
```

**Transcript auto-recorded metadata includes:**

* **session\_meta(rules):** All Rules loaded in this session (name, trigger type, file path)
* **session\_meta(slash\_command):** Skills used in this session (name, type, file path)
* **session\_meta(session\_info):** Session mode (agent/plan) and type

> **Advanced usage:** Write a periodic summary script to read `~/.qoder/stats/` JSONL files and generate Rule/Skill usage reports.

***

### Scenario 4: File Edit Tracking

> **Pain point:** Unclear which files the Agent modified in a session and how many times.

> **Solution:** Use a `PostToolUse` hook matching file-edit tools to **log every file change in real time**.

**Config:**

```json theme={null}
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|search_replace|create_file",
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/track-file-changes.sh"
          }
        ]
      }
    ]
  }
}
```

**Script** `.qoder/hooks/track-file-changes.sh`:

```bash theme={null}
#!/bin/sh
# 功能：追踪 Agent 的文件编辑操作
INPUT=$(cat)

SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH="$QODER_TOOL_INPUT_FILE_PATH"

if [ -z "$FILE_PATH" ]; then
  exit 0
fi

# === 📝 文件变更追踪逻辑 ===

# 记录变更日志
CHANGE_LOG="$HOME/.qoder/stats/file-changes.jsonl"
mkdir -p "$(dirname "$CHANGE_LOG")"

TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
BRANCH=$(printf '%s' "$INPUT" | jq -r '.extra.branch // empty')
REPO=$(printf '%s' "$INPUT" | jq -r '.extra.repo // empty')

echo "{\"ts\":\"$TIMESTAMP\",\"session\":\"$SESSION_ID\",\"tool\":\"$TOOL_NAME\",\"file\":\"$FILE_PATH\",\"branch\":\"$BRANCH\",\"repo\":\"$REPO\"}" >> "$CHANGE_LOG"

# ============================================

exit 0
```

**Advanced usage:**

* Use `additionalContext` to **feed change statistics back to the Agent** (e.g. "15 files modified in this session")
* Integrate with your team's code analytics system to track AI-assisted change volume

***

### Scenario 5: Global Usage Analytics

> **Pain point:** No quantitative data on overall Agent usage — can't assess AI-assisted coding efficiency and quality. This includes: what questions users ask, what text the model returns, which tools are called and their results.

> **Solution:** Use a **multi-event hook combo** to build full-pipeline usage data collection. `UserPromptSubmit` captures user questions, `PostToolUse` captures tool call results, `Stop` analyzes the complete session summary via Transcript (model replies, tool call distribution, etc.).

**Config:**

```json theme={null}
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/usage-tracker.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/usage-tracker.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/usage-tracker.sh"
          }
        ]
      }
    ]
  }
}
```

**Script** `~/.qoder/hooks/usage-tracker.sh`:

```bash theme={null}
#!/bin/sh
# 功能：全局使用情况追踪（统一入口，按事件类型分发）
INPUT=$(cat)

EVENT=$(printf '%s' "$INPUT" | jq -r '.hook_event_name // empty')
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# 获取额外上下文
EMAIL=$(printf '%s' "$INPUT" | jq -r '.extra.email // empty')
REPO=$(printf '%s' "$INPUT" | jq -r '.extra.repo // empty')
BRANCH=$(printf '%s' "$INPUT" | jq -r '.extra.branch // empty')

# === 📝 数据采集逻辑 ===

STATS_DIR="$HOME/.qoder/stats"
mkdir -p "$STATS_DIR"
DATE=$(date +%Y-%m-%d)

case "$EVENT" in
  UserPromptSubmit)
    # 采集用户提问内容（截取前 200 字符避免日志过大）
    PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // empty')
    PROMPT_PREVIEW=$(printf '%.200s' "$PROMPT")
    echo "{\"ts\":\"$TIMESTAMP\",\"event\":\"prompt\",\"session\":\"$SESSION_ID\",\"email\":\"$EMAIL\",\"repo\":\"$REPO\",\"branch\":\"$BRANCH\",\"prompt_preview\":\"$PROMPT_PREVIEW\"}" >> "$STATS_DIR/events-${DATE}.jsonl"
    ;;
  PostToolUse)
    TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')
    # 采集工具调用结果（截取前 500 字符）
    TOOL_RESPONSE=$(printf '%s' "$INPUT" | jq -r '.tool_response // empty')
    TOOL_RESPONSE_PREVIEW=$(printf '%.500s' "$TOOL_RESPONSE")
    echo "{\"ts\":\"$TIMESTAMP\",\"event\":\"tool_use\",\"session\":\"$SESSION_ID\",\"tool\":\"$TOOL_NAME\",\"repo\":\"$REPO\",\"tool_response_preview\":\"$TOOL_RESPONSE_PREVIEW\"}" >> "$STATS_DIR/events-${DATE}.jsonl"
    ;;
  Stop)
    # 📊 通过 Transcript 采集完整会话摘要
    TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')
    SUMMARY=""
    if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
      USER_MSG_COUNT=$(jq -c 'select(.type == "user" and (.message.content | type == "string"))' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')
      USER_PROMPTS=$(jq -r 'select(.type == "user" and (.message.content | type == "string")) | .message.content' "$TRANSCRIPT_PATH" 2>/dev/null | head -20)

      ASSISTANT_TEXT_COUNT=$(jq -c 'select(.type == "assistant" and (.message.content[ ]? | select(.type == "text")))' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')

      ASSISTANT_TEXTS=$(jq -r 'select(.type == "assistant") | .message.content[ ]? | select(.type == "text") | .text' "$TRANSCRIPT_PATH" 2>/dev/null | head -20)

      TOOL_CALL_COUNT=$(jq -c 'select(.type == "assistant") | .message.content[ ]? | select(.type == "tool_use")' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')

      TOOL_NAMES=$(jq -r 'select(.type == "assistant") | .message.content[ ]? | select(.type == "tool_use") | .name' "$TRANSCRIPT_PATH" 2>/dev/null | sort | uniq -c | sort -rn | head -10)

      TOOL_SUCCESS=$(jq -c 'select(.type == "user") | .message.content[ ]? | select(.type == "tool_result" and .is_error == false)' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')

      TOOL_ERROR=$(jq -c 'select(.type == "user") | .message.content[ ]? | select(.type == "tool_result" and .is_error == true)' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')

      SUMMARY="user_msgs:${USER_MSG_COUNT},assistant_texts:${ASSISTANT_TEXT_COUNT},tool_calls:${TOOL_CALL_COUNT},tool_success:${TOOL_SUCCESS},tool_error:${TOOL_ERROR}"
    fi
    echo "{\"ts\":\"$TIMESTAMP\",\"event\":\"stop\",\"session\":\"$SESSION_ID\",\"repo\":\"$REPO\",\"summary\":\"$SUMMARY\"}" >> "$STATS_DIR/events-${DATE}.jsonl"

    if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
      SUMMARY_DIR="$STATS_DIR/sessions"
      mkdir -p "$SUMMARY_DIR"
      cat <<SUMMARY_EOF > "$SUMMARY_DIR/${SESSION_ID}.json"
{
  "session_id": "$SESSION_ID",
  "timestamp": "$TIMESTAMP",
  "repo": "$REPO",
  "branch": "$BRANCH",
  "email": "$EMAIL",
  "user_message_count": $USER_MSG_COUNT,
  "assistant_text_count": $ASSISTANT_TEXT_COUNT,
  "tool_call_count": $TOOL_CALL_COUNT,
  "tool_success": $TOOL_SUCCESS,
  "tool_error": $TOOL_ERROR,
  "tool_distribution": "$TOOL_NAMES",
  "user_prompts_preview": $(printf '%s' "$USER_PROMPTS" | head -c 2000 | jq -Rs .),
  "assistant_texts_preview": $(printf '%s' "$ASSISTANT_TEXTS" | head -c 2000 | jq -Rs .),
  "transcript_path": "$TRANSCRIPT_PATH"
}
SUMMARY_EOF
    fi
    ;;
esac

# ============================================

exit 0
```

**Data collection dimensions:**

| Event              | Collected Data                                                                                    | Source                                               |
| ------------------ | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `UserPromptSubmit` | User question (first 200 chars)                                                                   | `prompt` field in stdin JSON                         |
| `PostToolUse`      | Tool name + result (first 500 chars)                                                              | `tool_name` and `tool_response` fields in stdin JSON |
| `Stop`             | Full session summary: user questions, model replies, tool call distribution, success/failure rate | JSONL file at `transcript_path`                      |

> **Why analyze via Transcript in Stop?** `UserPromptSubmit` and `PostToolUse` only capture data from the current interaction, while the Transcript file at `Stop` time contains the complete session history — enabling extraction of model reply text, tool call distribution, and success/failure rates in one pass. See [Transcript File Format](#transcript-file-format) for details.

***

### Scenario 6: Safety Controls — Dangerous Command Blocking

> **Pain point:** The Agent may execute `rm -rf`, `git push --force`, or other dangerous commands.

> **Solution:** Use a `PreToolUse` hook matching `Bash|run_in_terminal` to **intercept dangerous commands before execution**.

**Config:**

```json theme={null}
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|run_in_terminal",
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/block-dangerous-commands.sh"
          }
        ]
      }
    ]
  }
}
```

**Script** `.qoder/hooks/block-dangerous-commands.sh`:

```bash theme={null}
#!/bin/sh
# 功能：拦截危险 Shell 命令
INPUT=$(cat)

# 提取要执行的命令（从 tool_input.command 中读取）
COMMAND=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty')

# === 📝 危险命令黑名单 — 请根据团队规范调整 ===
DANGEROUS_PATTERNS="rm -rf|git push --force|git push -f|DROP TABLE|DROP DATABASE|format |mkfs"

if echo "$COMMAND" | grep -qiE "$DANGEROUS_PATTERNS"; then
  echo "检测到危险命令: $COMMAND" >&2
  exit 2  # 阻断执行
fi

# ============================================

exit 0
```

**Key points:**

* `exit 2` immediately blocks execution; stderr content is **fed back to the Agent** as the block reason
* The Agent will **attempt an alternative** (e.g. a safer command) after being blocked
* For richer feedback, return JSON on stdout:

```json theme={null}
{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"Command contains rm -rf, blocked for safety"}}
```

***

### Scenario 7: Quality Gate Before Agent Completion

> **Pain point:** The Agent claims the task is done, but tests are failing or issues remain.

> **Solution:** Use a `Stop` hook (blockable) to **run quality checks before the Agent finishes**; block and force the Agent to keep working if checks fail.

**Config:**

```json theme={null}
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/quality-gate.sh",
            "timeout": 120
          }
        ]
      }
    ]
  }
}
```

**Script** `.qoder/hooks/quality-gate.sh`:

```bash theme={null}
#!/bin/sh
# 功能：Agent 完成前的质量门禁检查
INPUT=$(cat)

# 检查是否已经是 Stop Hook 触发的循环（防止无限循环）
STOP_HOOK_ACTIVE=$(printf '%s' "$INPUT" | jq -r '.stop_hook_active // false')
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
  exit 0  # 已经是 Stop Hook 触发的重试，放行
fi

# === 📝 质量检查逻辑 — 请根据项目实际情况调整 ===
ERRORS=""

# 1. 检查是否有编译错误
# if ! make build 2>/dev/null; then
#   ERRORS="${ERRORS}\n- 编译失败，请修复编译错误"
# fi

# 2. 检查是否有测试失败
# if ! make test 2>/dev/null; then
#   ERRORS="${ERRORS}\n- 测试未通过，请修复失败的测试"
# fi

# 3. 检查是否有未提交的 TODO 标记
# TODO_COUNT=$(grep -r "TODO(agent)" . --include="*.go" 2>/dev/null | wc -l | tr -d ' ')
# if [ "$TODO_COUNT" -gt 0 ]; then
#   ERRORS="${ERRORS}\n- 发现 $TODO_COUNT 个未完成的 TODO 标记"
# fi

# ============================================

if [ -n "$ERRORS" ]; then
  cat <<EOF
{"decision":"block","reason":"质量检查未通过：$ERRORS\n请修复以上问题后再完成。"}
EOF
  exit 2
fi

exit 0
```

**Key points:**

* `stop_hook_active` is used to **prevent infinite loops** (it is `true` when the Agent retries after being blocked)
* After a Stop hook blocks, the block reason is injected as a **user message** and the Agent continues working
* Set a longer `timeout` (e.g. 120 seconds) since builds and tests may take time

***

### Scenario 8: Harness Self-Evolution — Automated Knowledge Sedimentation

> **Pain point:** Lessons and decisions from each task are scattered in conversation history with no automated sedimentation process.

> **Solution:** Use a `Stop` hook to **automatically trigger a Harness self-evolution flow**, analyzing whether the conversation produced reusable lessons and driving the asset lifecycle.

**Config:**

```json theme={null}
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/harness-evolution.sh"
          }
        ]
      }
    ]
  }
}
```

**Script** `.qoder/hooks/harness-evolution.sh`:

```bash theme={null}
#!/bin/sh
# 功能：Agent 完成响应时，触发 Harness 自进化流程
INPUT=$(cat)

# ⚠️ 【关键】防止无限循环：检查 stop_hook_active 标志
# 当 Stop Hook 阻断 Agent 后，Agent 会重新尝试完成，此时 stop_hook_active=true
# 必须在此处放行，否则会陷入「阻断→重试→再阻断」的死循环
STOP_HOOK_ACTIVE=$(printf '%s' "$INPUT" | jq -r '.stop_hook_active // false')
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
  exit 0  # 已经是 Stop Hook 触发的重试，直接放行
fi

TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')

if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
  exit 0
fi

# === 📝 Harness 自进化检测逻辑 ===

# 1. 统计本次会话的工具调用和文件变更

EDIT_COUNT=$(jq -c 'select(.message.content[ ]?.type == "tool_use")' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')


# 2. 检查是否涉及架构设计、决策讨论、规范制定等关键词
# HAS_DECISION=$(jq -r 'select(.message.content | type == "string") | .message.content' "$TRANSCRIPT_PATH" 2>/dev/null | grep -c '架构\|方案\|规范\|最佳实践' || echo 0)

# 3. 记录会话摘要到 inbox，供后续批量复盘
SEDIMENTATION_DIR="$HOME/.ai/inbox"
mkdir -p "$SEDIMENTATION_DIR"
echo "{\"session_id\":\"$SESSION_ID\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"tool_calls\":$EDIT_COUNT,\"transcript\":\"$TRANSCRIPT_PATH\"}" >> "$SEDIMENTATION_DIR/pending-review.jsonl"

# 4. 可选方案 A：调用外部分析总结系统
# 通过 transcript 和会话上下文调用自己的分析服务
# curl -s -X POST http://localhost:8080/api/analyze \
#   -H 'Content-Type: application/json' \
#   -d "{\"session_id\":\"$SESSION_ID\",\"transcript\":\"$TRANSCRIPT_PATH\"}" \
#   > /dev/null 2>&1 &

# 5. 可选方案 B：通过阻断触发分析总结 Skill
# 取消下面的注释，可让 Agent 被阻断后自动进入复盘 Skill 流程
# 注意：/retro 是自定义的沉淀复盘技能，你可以替换为自己团队的复盘 Skill。
# cat <<'EOF'
# {"decision":"block","reason":"任务已完成。检测到本次对话可能有值得沉淀的经验（文件变更较多）。请运行 /retro 进行复盘，提炼可复用的经验并沉淀到资产体系。"}
# EOF
# exit 2

# ============================================

exit 0
```

**Key points:**

* **Preventing infinite loops (mandatory):** Stop hook scripts **must check `stop_hook_active`**. When the Agent retries after being blocked by a Stop hook, this field is `true` — exit 0 immediately to avoid an infinite loop
* Use `exit 2` + `decision:"block"` to **block the Agent from completing** and force a retrospective
* `pending-review.jsonl` logs sessions for later batch review
* Can be combined with a `/retro` Skill (custom retrospective skill) to form an **auto-detect → remind → sediment** closed loop

**Current implementation options:**

> Hooks currently only support `command` type handlers (executing external scripts), so Harness self-evolution has two implementation paths: (A) call an external analysis service, or (B) block the Agent and trigger a retrospective Skill.

**Future direction:**

> The hook system plans to support `prompt` type and `agent` type handlers. Once available, Harness self-evolution will upgrade from "script-driven" to "Agent-driven", enabling true **end-to-end automated knowledge sedimentation**.

***

## Transcript File Format

The Transcript is a **session log file** automatically generated by Qoder, located at the path pointed to by `transcript_path` (e.g. `~/.qoder/projects/<project>/transcript/<session-id>.jsonl`). Each line is an independent JSON object appended in chronological order, recording the complete session interaction.

### Common Fields Per Line

| Field       | Type   | Description                                                     |
| ----------- | ------ | --------------------------------------------------------------- |
| `type`      | string | Record type: `session_meta` / `user` / `assistant` / `progress` |
| `sessionId` | string | Session ID                                                      |
| `uuid`      | string | Unique ID for this record                                       |
| `timestamp` | string | ISO 8601 timestamp                                              |
| `cwd`       | string | Current working directory                                       |
| `message`   | object | Message content (present for `user`/`assistant` types)          |
| `data`      | object | Metadata (present for `session_meta`/`progress` types)          |

### Record Types

**1. `session_meta` — Session metadata (first line)**

The first line of every Transcript file records the session's basic information:

```json theme={null}
{
  "type": "session_meta",
  "sessionId": "86379a0e-...",
  "data": {
    "meta_type": "session_info",
    "content": {
      "mode": "agent",
      "session_type": "assistant"
    }
  }
}
```

* `data.content.mode`: Session mode (`agent` / `plan` / `ask` / `debug`)
* `data.content.session_type`: Session type (`assistant` / `inline_chat`, etc.)

**2. `user` — User messages**

User messages come in two forms:

**User question** (`message.content` is a string):

```json theme={null}
{
  "type": "user",
  "message": {
    "role": "user",
    "content": "Delete comments from test.py"
  }
}
```

**Tool result** (`message.content` is an array containing `tool_result`):

```json theme={null}
{
  "type": "user",
  "message": {
    "role": "user",
    "content": [
      {
        "type": "tool_result",
        "tool_use_id": "call_d498c5988a...",
        "content": "Contents of /path/to/file.py, from line 1-23 ...",
        "is_error": false
      }
    ]
  },
  "toolUseResult": "Contents of /path/to/file.py ..."
}
```

* `is_error`: Whether the tool execution failed
* `toolUseResult`: Shortcut field for the tool result (same as `content[0].content`)

**3. `assistant` — Model replies**

Model reply `message.content` is always an array containing two element types:

**Text reply** (`type: "text"`):

```json theme={null}
{
  "type": "assistant",
  "message": {
    "role": "assistant",
    "content": [
      {
        "type": "text",
        "text": "I'll help you delete comments from test.py. Let me read the file first.\n\n"
      }
    ]
  }
}
```

**Tool call** (`type: "tool_use"`):

```json theme={null}
{
  "type": "assistant",
  "message": {
    "role": "assistant",
    "content": [
      {
        "type": "tool_use",
        "id": "call_d498c5988a...",
        "name": "read_file",
        "input": {
          "file_path": "/path/to/file.py"
        }
      }
    ]
  }
}
```

* `name`: Tool name (e.g. `read_file`, `search_replace`, `run_in_terminal`, `Skill`)
* `input`: Tool call parameters
* `id`: Corresponds to `tool_use_id` in the subsequent `tool_result`

**4. `progress` — Hook trigger records**

Records when hook scripts fire and which commands run:

```json theme={null}
{
  "type": "progress",
  "data": {
    "type": "hook_progress",
    "hookEvent": "UserPromptSubmit",
    "hookName": "UserPromptSubmit",
    "command": ".qoder/hooks/inject-skill-hint.sh"
  }
}
```

### Session Timeline Example

A typical session's Transcript record order:

```plaintext theme={null}
L1  session_meta          ← Session start, record mode and type
L2  progress              ← UserPromptSubmit Hook fires
L3  user (string)         ← User question: "Delete comments from test.py"
L4  assistant (text)      ← Model reply: "I'll help you..."
L5  assistant (tool_use)  ← Model calls read_file
L6  user (tool_result)    ← read_file returns file content
L7  assistant (text)      ← Model reply: "Now I'll delete..."
L8  assistant (tool_use)  ← Model calls search_replace
L9  user (tool_result)    ← search_replace succeeds
L10 assistant (text)      ← Model reply: "Successfully deleted..."
L11 progress              ← Stop Hook fires
L12 assistant (text)      ← Final reply after Stop Hook
```

### Common jq Extraction Commands

```bash theme={null}
TRANSCRIPT="$TRANSCRIPT_PATH"

# Extract all user questions (filter out tool results)
jq -r 'select(.type == "user" and (.message.content | type == "string")) | .message.content' "$TRANSCRIPT"

# Extract all model text replies
jq -r 'select(.type == "assistant") | .message.content[ ]? | select(.type == "text") | .text' "$TRANSCRIPT"

# Extract all tool calls (tool name + parameters)
jq -c 'select(.type == "assistant") | .message.content[ ]? | select(.type == "tool_use") | {name, input}' "$TRANSCRIPT"

# Count tool call distribution (tool name + count)
jq -r 'select(.type == "assistant") | .message.content[ ]? | select(.type == "tool_use") | .name' "$TRANSCRIPT" | sort | uniq -c | sort -rn

# Extract failed tool calls
jq -c 'select(.type == "user") | .message.content[ ]? | select(.type == "tool_result" and .is_error == true)' "$TRANSCRIPT"

# Get session mode
jq -r 'select(.type == "session_meta") | .data.content.mode' "$TRANSCRIPT"

# Get Hook trigger records
jq -c 'select(.type == "progress" and .data.type == "hook_progress") | {event: .data.hookEvent, command: .data.command}' "$TRANSCRIPT"
```

***

## Design Principles

| Principle                 | Description                                                                       |
| ------------------------- | --------------------------------------------------------------------------------- |
| **Fail fast**             | Hook scripts should be lightweight to avoid blocking the Agent's main flow        |
| **Graceful degradation**  | Abnormal exit codes (not 0 or 2) do not block the Agent, ensuring fault tolerance |
| **Single responsibility** | Each script does one thing; combine multiple hooks for complex logic              |
| **Idempotent design**     | The same event may fire multiple times; scripts should be idempotent              |
| **Loop prevention**       | Stop hooks must check `stop_hook_active` to prevent infinite retries              |

### Recommended Hook Combinations

| Goal                       | Recommended Combination                                                  |
| -------------------------- | ------------------------------------------------------------------------ |
| **Safety controls**        | `PreToolUse(Bash)` dangerous command blocking                            |
| **Quality assurance**      | `Stop` quality gate                                                      |
| **Data-driven insights**   | `UserPromptSubmit` + `PostToolUse` + `Stop` full-pipeline collection     |
| **Harness self-evolution** | `Stop` sedimentation detection + `UserPromptSubmit` Skill guidance       |
| **Team collaboration**     | Project-level `.qoder/settings.json` with unified config, shared via Git |

## Debugging Guide

### Step 1: Confirm the Hook Fires

When a hook **does not fire as expected**, add debug logging at the **very start** of the script:

```bash theme={null}
#!/bin/sh
# ===== Debug mode: confirm hook fires =====
INPUT=$(cat)
printf '[HOOK DEBUG] %s hook triggered, event=%s\n' "$(date '+%H:%M:%S')" "$(printf '%s' "$INPUT" | jq -r '.hook_event_name')" >> /tmp/hook-debug.log
printf '%s' "$INPUT" | jq . >> /tmp/hook-debug.log
echo "---" >> /tmp/hook-debug.log
# ===== Remove debug code when done =====

# ... your actual logic ...
exit 0
```

Then trigger an Agent operation and check the log:

```bash theme={null}
$ tail -f /tmp/hook-debug.log
[HOOK DEBUG] 14:32:01 hook triggered, event=PreToolUse
{
  "session_id": "abc123",
  "hook_event_name": "PreToolUse",
  "tool_name": "run_in_terminal",
  "tool_input": {
    "command": "ls -la"
  },
  ...
}
---
```

> **Troubleshooting:** If no output appears, the hook is not firing. Check your config file location, event name, matcher pattern, and script path.

### Step 2: Reproduce Locally with Real Input

Use the JSON captured in Step 1 to test the script **outside of Qoder**:

```bash theme={null}
# Pipe the captured input to simulate stdin
$ cat /tmp/hook-debug.json | sh .qoder/hooks/pre-tool-check.sh
$ echo $?   # Check exit code: 0=allow, 2=block

# Or manually construct test input
$ echo '{"hook_event_name":"PreToolUse","tool_name":"run_in_terminal","tool_input":{"command":"rm -rf /"}}' | sh .qoder/hooks/pre-tool-check.sh
$ echo $?   # Expected: 2 (block dangerous command)
```

> **Tip:** Save common test cases as files for repeated validation.

### Step 3: Other Debugging Methods

1. **Check the Transcript:** `~/.qoder/projects/<encoded-path>/transcript/<session>.jsonl` — parse with `jq` line by line
2. **Check Hook logs:** Search for `[hook]` prefix in Qoder logs for execution results and timing
3. **Start simple:** First verify the hook fires with a bare `exit 0`, then add business logic incrementally
4. **Clean up:** Remove debug code (`/tmp/hook-debug.log` writes) after debugging to avoid performance impact

## Quick Start Template

### Minimal Config

Save the following to `~/.qoder/settings.json` (user-level) or `<project>/.qoder/settings.json` (project-level):

```json theme={null}
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/pre-tool-check.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/post-edit-track.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".qoder/hooks/on-stop.sh"
          }
        ]
      }
    ]
  }
}
```

### Project Directory Structure

```plaintext theme={null}
<project>/
├── .qoder/
│   ├── settings.json        ← Hook config (Git-shared)
│   ├── settings.local.json  ← Local override config (.gitignore)
│   └── hooks/               ← Hook scripts directory
│       ├── pre-tool-check.sh
│       ├── post-edit-track.sh
│       └── on-stop.sh
└── ...
```
