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

# Claude Code hooks compatibility

Piebald supports Claude Code-compatible command hooks. Hooks are shell commands that run at specific points during a chat, receive JSON on `stdin`, and can return JSON on `stdout` to do the following:

* Add context to the chat
* Prevent prompts from being sent
* Automatically approve or deny tools
* Modify tool call input before tools are executed
* Force the model to continue iterating when it stops

Piebald's Claude Code hooks compatibility is limited to command hooks—HTTP-based, prompt-based, and agent-based hooks aren't supported yet.  `if` conditions, `CLAUDE_ENV_FILE`, transcript files, and async hooks are not supported, and `statusMessage` and `suppressOutput` are ignored.

We also don't support the following Claude Code hook types, which don't have dirrect equivalents in Piebald: `SessionEnd`, `Notification`, `ConfigChange`, `CwdChanged`, `FileChanged`, `InstructionsLoaded`, `WorktreeCreate`, `WorktreeRemove`, `Elicitation`, `ElicitationResult`, `TaskCreated`, `TaskCompleted`, `TeammateIdle`, and `PermissionRequest`.

If you need any of those features, [let us know](https://github.com/Piebald-AI/piebald-issues/issues/new).

## Quick Start

Piebald uses Claude Code's hook format.  We don't have a custom hooks format, so all of your Claude Code hooks should work without modification in Piebald (except such as make use of functionality we don't yet support).

To get started with Claude Code hooks, create a `.claude/settings.json` file in your project:

```json theme={null}
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/check-bash.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}
```

Then create the hook script:

```bash theme={null}
#!/usr/bin/env bash
set -euo pipefail

input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // ""')

if [[ "$command" == *"rm -rf /"* ]]; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Refusing to run a dangerous delete command."
    }
  }'
  exit 0
fi

jq -n '{
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "allow"
  }
}'
```

Make it executable:

```bash theme={null}
chmod +x .claude/hooks/check-bash.sh
```

To reload the hooks cache after changing your project `.claude/settings.json`, you'll need to create a new chat in that project. After changing global hooks in `~/.claude/settings.json`, restart Piebald to reload the global hooks cache.

## Configuration Files

Hooks are loaded from Claude Code-compatible settings files:

| Location                                | Scope                                   |
| --------------------------------------- | --------------------------------------- |
| `~/.claude/settings.json`               | Global hooks for all projects           |
| `<project>/.claude/settings.json`       | Project hooks                           |
| `<project>/.claude/settings.local.json` | Local project hooks, usually gitignored |

Later configs append hook handlers. For example, if both global and project files define `PreToolUse`, both sets of handlers can run.

A hook group has a `matcher` and a list of hook handlers:

```json theme={null}
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "submit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/check-prompt.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
```

| Field           | Description                                                                         |
| --------------- | ----------------------------------------------------------------------------------- |
| `matcher`       | Regex matched against the event's value. `null`, `""`, or `"*"` matches everything. |
| `type`          | Only `"command"` is supported. Other types are ignored for now.                     |
| `command`       | Shell command to run.                                                               |
| `timeout`       | Timeout in seconds. Defaults to `60`.                                               |
| `statusMessage` | Accepted for compatibility, but not currently shown in the UI.                      |

Matchers are case-sensitive regexes. Invalid regexes won't match.

## Supported Events

| Event                | Can Affect Behavior                   | When It Runs                                                                 |
| -------------------- | ------------------------------------- | ---------------------------------------------------------------------------- |
| `SessionStart`       | Adds context                          | When a chat is created. Also runs after compaction with `source: "compact"`. |
| `UserPromptSubmit`   | Can block or add context              | Before a user message is sent.                                               |
| `PreToolUse`         | Can allow, deny, ask, or modify input | After Piebald evaluates tool approval, before the tool executes.             |
| `PostToolUse`        | Adds context for the next turn        | After a tool completes successfully.                                         |
| `PostToolUseFailure` | Adds context                          | After a tool fails.                                                          |
| `Stop`               | Can continue generation or force stop | When a normal chat generation would finish.                                  |
| `StopFailure`        | Observational                         | When generation fails with an error.                                         |
| `PreCompact`         | Observational                         | Before context compaction.                                                   |
| `PostCompact`        | Observational                         | After context compaction.                                                    |
| `SubagentStart`      | Adds context                          | Before a subagent's first message.                                           |
| `SubagentStop`       | Can continue generation or force stop | When a subagent generation would finish.                                     |

`SessionEnd` isn't supported because Piebald sessions don't necessarily ever end like they do in Claude Code.

## Hook Execution

Hook commands run through the platform shell:

| Platform    | Shell                                        |
| ----------- | -------------------------------------------- |
| macOS/Linux | `/bin/bash -c`, falling back to `/bin/sh -c` |
| Windows     | `cmd /C`                                     |

Hook input JSON is sent to the command's `stdin` and reads `stdout` and `stderr`.

Hook commands receive these environment variables:

| Variable             | Value                                  |
| -------------------- | -------------------------------------- |
| `CLAUDE_PROJECT_DIR` | The resolved project or chat directory |
| `CLAUDE_CODE_REMOTE` | Always `false`                         |

Matched hook handlers run in parallel. If multiple hooks return decisions, Piebald resolves the decision according to the event rules below.

## Common Input

Every hook receives a JSON object with common fields:

```json theme={null}
{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "transcript_path": null,
  "cwd": "/Users/alex/project",
  "hook_event_name": "PreToolUse",
  "permission_mode": "default"
}
```

| Field             | Description                                                        |
| ----------------- | ------------------------------------------------------------------ |
| `session_id`      | Stable deterministic ID for the Piebald chat.                      |
| `transcript_path` | Always `null`; Piebald doesn't write Claude Code transcript files. |
| `cwd`             | Resolved project directory or the chat's current directory.        |
| `hook_event_name` | Name of the event being run.                                       |
| `permission_mode` | One of `default`, `acceptEdits`, `bypassPermissions`, or `plan`.   |

## Output Format

Hooks can print JSON to `stdout`:

```json theme={null}
{
  "continue": true,
  "stopReason": "optional reason",
  "additionalContext": "Text to add to the model context.",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Allowed by policy.",
    "updatedInput": {
      "command": "ls -la"
    },
    "decision": "block",
    "reason": "Explain what the model should do next.",
    "additionalContext": "More context for the model."
  }
}
```

Piebald also accepts plain text on `stdout` for successful hooks. If a hook exits with code `0` and stdout isn't valid JSON, the text is treated as `additionalContext`.

### Exit Codes

| Exit Code     | Behavior                                                                                 |
| ------------- | ---------------------------------------------------------------------------------------- |
| `0`           | Success. JSON stdout is parsed; plain text stdout becomes additional context.            |
| `2`           | Blocking signal for events that support blocking, such as `UserPromptSubmit` and `Stop`. |
| Other nonzero | Logged and ignored.                                                                      |

## UserPromptSubmit

`UserPromptSubmit` runs after you press Enter but before the message is actually sent.

Matcher value: `submit`

Input includes:

```json theme={null}
{
  "hook_event_name": "UserPromptSubmit",
  "prompt": "The user's message text"
}
```

To block the prompt, return:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "decision": "block",
    "reason": "Please remove the production secret before sending."
  }
}
```

You can also exit with code `2`; `stderr` will be used as the block reason.

To add context to the message, return `additionalContext`:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Remember: this repository uses pnpm."
  }
}
```

## PreToolUse

`PreToolUse` runs after a tool call is generated but before it's executed. It can override approval decision that would usually be applied by the selected permission mode.

Matcher value: the Claude Code tool name, such as `Bash`, `Read`, `Edit`, or `mcp__github__create_issue`.

Input includes:

```json theme={null}
{
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "pnpm test",
    "description": "",
    "timeout": 60000,
    "run_in_background": false
  },
  "tool_use_id": "toolu_123"
}
```

Return a permission decision:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "ask",
    "permissionDecisionReason": "This command touches generated files."
  }
}
```

Supported decisions:

| Decision | Behavior                                                           |
| -------- | ------------------------------------------------------------------ |
| `allow`  | Auto-approve the tool call.                                        |
| `deny`   | Deny the tool call. Uses `permissionDecisionReason` as the reason. |
| `ask`    | Require manual approval in the UI.                                 |

If multiple hooks return conflicting decisions, the most restrictive decision wins, so deny > ask > allow.

A `PreToolUse` hook can also modify tool input with `updatedInput`:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": {
      "command": "pnpm test -- --runInBand",
      "description": "",
      "timeout": 60000,
      "run_in_background": false
    }
  }
}
```

<Warning>
  Matched hooks run in parallel. If multiple hooks return `updatedInput`, the last returned update wins. Avoid configuring multiple hooks that modify the same tool input.
</Warning>

PreToolUse runs during the approval evaluation. If a hook returns `ask`, and the user manually approves the tool later, `PreToolUse` isn't run again for that approval.

## PostToolUse

`PostToolUse` runs after a tool completes successfully.

Matcher value: the Claude Code tool name.

Input includes:

```json theme={null}
{
  "hook_event_name": "PostToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "pnpm test",
    "description": "",
    "timeout": 60000,
    "run_in_background": false
  },
  "tool_response": "Test output...",
  "tool_use_id": "toolu_123"
}
```

`PostToolUse` can't prevent a tool that already ran. If it returns `decision: "block"`, the reason is queued as context for the model's next turn:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "decision": "block",
    "reason": "Tests failed. Fix the failing test before continuing."
  }
}
```

You can also return `additionalContext` to add information for the next model turn.

## PostToolUseFailure

`PostToolUseFailure` runs when a tool fails.

Matcher value: the Claude Code tool name.

Input includes:

```json theme={null}
{
  "hook_event_name": "PostToolUseFailure",
  "tool_name": "Bash",
  "tool_input": {
    "command": "pnpm test",
    "description": "",
    "timeout": 60000,
    "run_in_background": false
  },
  "tool_use_id": "toolu_123",
  "error": "Command failed",
  "is_interrupt": false
}
```

Outputs are observational. `additionalContext` is queued for the model.

## Stop and SubagentStop

`Stop` runs when a normal chat generation would finish. `SubagentStop` uses the same behavior for subagent chats.

Matcher value:

| Event          | Matcher   |
| -------------- | --------- |
| `Stop`         | `""`      |
| `SubagentStop` | `default` |

Input includes:

```json theme={null}
{
  "hook_event_name": "Stop",
  "stop_hook_active": false,
  "last_assistant_message": "The assistant's final response text"
}
```

For `SubagentStop`, input also includes:

```json theme={null}
{
  "agent_id": "550e8400-e29b-41d4-a716-446655440000",
  "agent_type": "default",
  "agent_transcript_path": null
}
```

To make the model continue, return `decision: "block"` with a reason:

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "Stop",
    "decision": "block",
    "reason": "Run the test suite before stopping."
  }
}
```

The reason is injected as a new user message and continues generation.

To force an immediate stop, return `continue: false`:

```json theme={null}
{
  "continue": false,
  "stopReason": "Enough work has been completed."
}
```

`continue: false` takes precedence over `decision: "block"`.

<Note>
  `continue: true` doesn't mean "keep generating". It's treated as no opinion. Use `decision: "block"` or exit code `2` to request continuation.
</Note>

Stop hook continuations are capped at 3 per generation loop to prevent infinite loops.

## SessionStart

`SessionStart` runs when a chat starts.

Matcher value:

| Source         | Matcher   |
| -------------- | --------- |
| New chat       | `startup` |
| Compacted chat | `compact` |

Input includes:

```json theme={null}
{
  "hook_event_name": "SessionStart",
  "source": "startup",
  "model": "claude-sonnet-4-5"
}
```

Return `additionalContext` to queue context for the chat.

## Compaction Hooks

`PreCompact` runs before compaction, and `PostCompact` runs after compaction.

Matcher value: `manual` or `auto`

`PreCompact` input includes:

```json theme={null}
{
  "hook_event_name": "PreCompact",
  "trigger": "manual",
  "custom_instructions": "Optional user instructions"
}
```

`PostCompact` input includes:

```json theme={null}
{
  "hook_event_name": "PostCompact",
  "trigger": "manual",
  "compact_summary": "The generated compaction summary"
}
```

These hooks are observational. Their outputs don't change compaction behavior.

## StopFailure

`StopFailure` runs when generation fails.

Matcher value: the classified error type, such as `rate_limit`, `authentication_failed`, `timeout`, `network_error`, `cancelled`, or `unknown`.

Input includes:

```json theme={null}
{
  "hook_event_name": "StopFailure",
  "error": "rate_limit",
  "error_details": "Full error details",
  "last_assistant_message": "Last assistant text, if available"
}
```

Outputs are ignored.

## SubagentStart

`SubagentStart` runs after a subagent chat is created and before its first message.

Matcher value: `default`

Input includes:

```json theme={null}
{
  "hook_event_name": "SubagentStart",
  "agent_id": "550e8400-e29b-41d4-a716-446655440000",
  "agent_type": "default"
}
```

Return `additionalContext` to inject context into the subagent's first message.

## Tool Name Mapping

Tool names are automatically mapped to Claude Code tool names:

| Piebald Tool         | Claude Code Hook Name |
| -------------------- | --------------------- |
| `RunTerminalCommand` | `Bash`                |
| `ReadFile`           | `Read`                |
| `WriteFile`          | `Write`               |
| `EditFile`           | `Edit`                |
| `Glob`               | `Glob`                |
| `Grep`               | `Grep`                |
| `LaunchSubagent`     | `Agent`               |
| `WebFetch`           | `WebFetch`            |
| `WebSearch`          | `WebSearch`           |
| `AskUserQuestion`    | `AskUserQuestion`     |
| `ProposePlanToUser`  | `ExitPlanMode`        |
| `TodoWrite`          | `TodoWrite`           |
| MCP `server__tool`   | `mcp__server__tool`   |

`Task` is accepted as an alias for `Agent` in matchers.

## Tool Input Reshaping

Before tool input is sent to hooks, it's somewhat reshaped to match Claude Code's field names:

| Tool                    | Reshaping                                                          |
| ----------------------- | ------------------------------------------------------------------ |
| `Bash`                  | Adds `description`, `timeout`, and `run_in_background` defaults.   |
| `Read`, `Write`, `Edit` | Renames `path` to `file_path` and resolves it to an absolute path. |
| `Agent`                 | Adds `description` and `subagent_type: "default"`.                 |
| Other tools             | Passed through unchanged.                                          |

When `PreToolUse` returns `updatedInput`, Piebald converts the Claude Code-shaped input back into Piebald's internal shape before executing the tool.

## Example: Block Production Commands

```json theme={null}
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/no-prod.sh"
          }
        ]
      }
    ]
  }
}
```

```bash theme={null}
#!/usr/bin/env bash
set -euo pipefail

input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // ""')

if [[ "$command" == *"kubectl"* && "$command" == *"prod"* ]]; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "ask",
      permissionDecisionReason: "Production command requires manual approval."
    }
  }'
else
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "allow"
    }
  }'
fi
```

## Example: Ask The Model To Continue Until Tests Pass

```json theme={null}
{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/check-tests.sh",
            "timeout": 120
          }
        ]
      }
    ]
  }
}
```

```bash theme={null}
#!/usr/bin/env bash
set -euo pipefail

if pnpm test >/tmp/piebald-test-output.txt 2>&1; then
  exit 0
fi

jq -n --rawfile output /tmp/piebald-test-output.txt '{
  hookSpecificOutput: {
    hookEventName: "Stop",
    decision: "block",
    reason: ("Tests are failing. Fix them before stopping.\n\n" + $output)
  }
}'
```
