Skip to main content

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:
LocationScope
~/.claude/settings.jsonGlobal hooks for all projects
<project>/.claude/settings.jsonProject hooks
<project>/.claude/settings.local.jsonLocal 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
          }
        ]
      }
    ]
  }
}
FieldDescription
matcherRegex matched against the event’s value. null, "", or "*" matches everything.
typeOnly "command" is supported. Other types are ignored for now.
commandShell command to run.
timeoutTimeout in seconds. Defaults to 60.
statusMessageAccepted for compatibility, but not currently shown in the UI.
Matchers are case-sensitive regexes. Invalid regexes won’t match.

Supported Events

EventCan Affect BehaviorWhen It Runs
SessionStartAdds contextWhen a chat is created. Also runs after compaction with source: "compact".
UserPromptSubmitCan block or add contextBefore a user message is sent.
PreToolUseCan allow, deny, ask, or modify inputAfter Piebald evaluates tool approval, before the tool executes.
PostToolUseAdds context for the next turnAfter a tool completes successfully.
PostToolUseFailureAdds contextAfter a tool fails.
StopCan continue generation or force stopWhen a normal chat generation would finish.
StopFailureObservationalWhen generation fails with an error.
PreCompactObservationalBefore context compaction.
PostCompactObservationalAfter context compaction.
SubagentStartAdds contextBefore a subagent’s first message.
SubagentStopCan continue generation or force stopWhen 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:
PlatformShell
macOS/Linux/bin/bash -c, falling back to /bin/sh -c
Windowscmd /C
Hook input JSON is sent to the command’s stdin and reads stdout and stderr. Hook commands receive these environment variables:
VariableValue
CLAUDE_PROJECT_DIRThe resolved project or chat directory
CLAUDE_CODE_REMOTEAlways 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:
{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "transcript_path": null,
  "cwd": "/Users/alex/project",
  "hook_event_name": "PreToolUse",
  "permission_mode": "default"
}
FieldDescription
session_idStable deterministic ID for the Piebald chat.
transcript_pathAlways null; Piebald doesn’t write Claude Code transcript files.
cwdResolved project directory or the chat’s current directory.
hook_event_nameName of the event being run.
permission_modeOne of default, acceptEdits, bypassPermissions, or plan.

Output Format

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 CodeBehavior
0Success. JSON stdout is parsed; plain text stdout becomes additional context.
2Blocking signal for events that support blocking, such as UserPromptSubmit and Stop.
Other nonzeroLogged 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

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:
DecisionBehavior
allowAuto-approve the tool call.
denyDeny the tool call. Uses permissionDecisionReason as the reason.
askRequire 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:
EventMatcher
Stop""
SubagentStopdefault
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:
SourceMatcher
New chatstartup
Compacted chatcompact
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 Name Mapping

Tool names are automatically mapped to Claude Code tool names:
Piebald ToolClaude Code Hook Name
RunTerminalCommandBash
ReadFileRead
WriteFileWrite
EditFileEdit
GlobGlob
GrepGrep
LaunchSubagentAgent
WebFetchWebFetch
WebSearchWebSearch
AskUserQuestionAskUserQuestion
ProposePlanToUserExitPlanMode
TodoWriteTodoWrite
MCP server__toolmcp__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:
ToolReshaping
BashAdds description, timeout, and run_in_background defaults.
Read, Write, EditRenames path to file_path and resolves it to an absolute path.
AgentAdds description and subagent_type: "default".
Other toolsPassed 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)
  }
}'