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.
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.
Any you need any of those features, let us know.
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:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/check-bash.sh",
"timeout": 30
}
]
}
]
}
}
Then create the hook script:
#!/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:
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:
{
"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.
Every hook receives a JSON object with common fields:
{
"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. |
Hooks can print JSON to stdout:
{
"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:
{
"hook_event_name": "UserPromptSubmit",
"prompt": "The user's message text"
}
To block the prompt, return:
{
"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:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Remember: this repository uses pnpm."
}
}
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:
{
"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:
{
"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:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"command": "pnpm test -- --runInBand",
"description": "",
"timeout": 60000,
"run_in_background": false
}
}
}
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.
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:
{
"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:
{
"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:
{
"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:
{
"hook_event_name": "Stop",
"stop_hook_active": false,
"last_assistant_message": "The assistant's final response text"
}
For SubagentStop, input also includes:
{
"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:
{
"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:
{
"continue": false,
"stopReason": "Enough work has been completed."
}
continue: false takes precedence over decision: "block".
continue: true doesn’t mean “keep generating”. It’s treated as no opinion. Use decision: "block" or exit code 2 to request continuation.
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:
{
"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:
{
"hook_event_name": "PreCompact",
"trigger": "manual",
"custom_instructions": "Optional user instructions"
}
PostCompact input includes:
{
"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:
{
"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:
{
"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 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.
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
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/no-prod.sh"
}
]
}
]
}
}
#!/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
{
"hooks": {
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/check-tests.sh",
"timeout": 120
}
]
}
]
}
}
#!/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)
}
}'