Docs
Refer friends. Keep the rewards coming!Your friend can unlock up to 10M tokens · earn up to 30% revenue share.
+500K TokensGenerate link

Hooks

Run your own logic when CrabCode hits key events: shell commands, HTTP callbacks, LLM verifiers. Configured in settings.json.

What it is

A hook is a callback attached to a CrabCode lifecycle event. On trigger, CrabCode feeds a JSON payload to your hook via stdin (or HTTP POST); the response decides what happens next — allow, block, modify input, or feed extra context back to the model.

Four hook types:

typeFormGood for
commandRun a shell commandLinting, logging, blocking dangerous ops
httpPOST to a URLCentral audit, forward to a SaaS
promptEvaluate a prompt with a small modelLightweight semantic checks ("is this a secret?")
agentSpawn an agentic verifierPost-task verification ("did tests actually run?")

When you see this doc

  • The "Add" page in /hooks (read-only, points you at settings.json)
  • A validation error pointing at the hooks field

Supported events

EventWhen
SessionStartSession boots
SessionEndSession shutdown (tight cap, default 1.5s)
SetupOnce at --init / --maintenance startup
UserPromptSubmitUser hits Enter on input
PreToolUseModel wants to call a tool, before execution
PostToolUseTool finished successfully
PostToolUseFailureTool failed / timed out / was interrupted
Stop / StopFailureModel finished a turn / turn ended on API error
NotificationSystem notification (e.g. waiting on user input)
SubagentStart / SubagentStopSubagent lifecycle
PermissionRequest / PermissionDeniedPermission prompt / denial
PreCompact / PostCompactBefore / after conversation compaction
CwdChanged / FileChangedCwd or watched file changed
WorktreeCreate / WorktreeRemovegit worktree created / removed
Elicitation / ElicitationResultMCP elicitation flow
TeammateIdle / TaskCreated / TaskCompletedMulti-agent collaboration
ConfigChange / InstructionsLoadedSettings change / CRABCODE.md load

The authoritative list is the dropdown inside /hooks — SDK and runtime share the same source.

Example config

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.crabcode/hooks/check-bash.sh",
            "if": "Bash(git push *)",
            "timeout": 5
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "prettier --write \"$FILE_PATH\"" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Verify that tests ran and passed; otherwise raise an error.",
            "timeout": 60
          }
        ]
      }
    ]
  }
}
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.crabcode/hooks/check-bash.sh",
            "if": "Bash(git push *)",
            "timeout": 5
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "prettier --write \"$FILE_PATH\"" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Verify that tests ran and passed; otherwise raise an error.",
            "timeout": 60
          }
        ]
      }
    ]
  }
}
  • matcher — string / regex matching event-relevant values (tool name for PreToolUse / PostToolUse). Omit to match all
  • if — permission-rule syntax (e.g. Bash(git push *)); non-matching tool calls skip spawn entirely, saving fork cost
  • timeout — per-execution seconds; killed on timeout
  • shellcommand type only: bash or powershell (defaults to bash, follows $SHELL)

Hook protocol (command type)

  • stdin — JSON object with event-specific fields. Common keys:
    • tool_name, tool_input, tool_use_id (PreToolUse / PostToolUse / PostToolUseFailure)
    • session_id, hook_event_name
    • PostToolUse also has response; PostToolUseFailure adds error, error_type, is_interrupt, is_timeout
    • PreCompact carries compaction details; UserPromptSubmit carries the raw prompt text
  • stdout — optional JSON response. Common shapes:
    • {"continue": false, "stopReason": "..."} — terminate this turn
    • {"decision": "block", "reason": "..."} — block the action
    • {"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "..."}} — explicit permission denial
    • {"async": true, "asyncTimeout": 30} — switch to background async mode
  • Exit codes (semantics vary per event; the /hooks per-event help is authoritative):
    • 0 — pass; stdout shown or hidden depending on event
    • 2 — block / feed stderr back to the model; used by PreToolUse, UserPromptSubmit, Stop
    • other — show stderr to the user, conversation continues

HTTP hooks POST the same JSON to url; headers accepts $VAR_NAME placeholders resolved against the allowedEnvVars allow list.

Full stdin / stdout schema

stdin common fields (every event): session_id, hook_event_name, cwd, transcript_path (path to the current session transcript).

Event-specific fields:

  • PreToolUse / PostToolUse / PostToolUseFailure: tool_name, tool_input, tool_use_id; PostToolUse adds response; PostToolUseFailure adds error, error_type, is_interrupt, is_timeout
  • UserPromptSubmit: prompt (raw user input)
  • PreCompact / PostCompact: trigger, custom_instructions, ...
  • FileChanged: changed_paths, change_kind
  • PermissionRequest: the pending tool / permission rule
  • InstructionsLoaded: file_path, memory_type (User / Project / Local / Managed), load_reason (session_start / nested_traversal / path_glob_match / include / compact), optional globs, trigger_file_path, parent_file_path

stdout response fields (all optional; no output = default pass): continue (false ends this turn), suppressOutput (hide stdout), stopReason, decision ("approve" / "block"), reason, systemMessage (user-side warning), async + asyncTimeout (background mode), hookSpecificOutput (event-typed advanced controls).

hookSpecificOutput shapes:

  • PreToolUse: permissionDecision (allow / deny / ask), permissionDecisionReason, updatedInput (rewrite tool args), additionalContext
  • UserPromptSubmit / SessionStart / Setup / SubagentStart / PostToolUse / PostToolUseFailure / Notification: additionalContext
  • SessionStart / CwdChanged / FileChanged: watchPaths (register absolute paths to watch)
  • PermissionRequest: decision: { behavior: "allow" | "deny", ... }
  • Elicitation / ElicitationResult: action (accept / decline / cancel) + content
  • WorktreeCreate: worktreePath

Failure behavior

CrabCode treats hook failures as fail-soft:

  • Exit 0 → pass
  • Exit 2 → block / feed stderr back to the model (PreToolUse, UserPromptSubmit, Stop, TeammateIdle, TaskCreated, TaskCompleted, ...)
  • Any other non-zero → marked non_blocking_error: the user sees a system notice (command name + stderr), session continues
  • Timeout → process killed, treated as non-zero
  • Bad JSON output → also non_blocking_error with a schema-error notice

Only exit code 2 (or an explicit {"decision":"block"} / permissionDecision:"deny") actually stops the model. Everything else just logs and proceeds.

Debugging

  • Enable debug logs: launch CrabCode with --debug. Hook execution details (command line, stdout, stderr, exit code, duration) land in ~/.crabcode/debug/<session-id>.txt. Override the directory with CRABCODE_DEBUG_LOGS_DIR; pass -d2e / --debug-to-stderr to stream to stderr directly
  • Latest run: ~/.crabcode/debug/latest is a symlink to the most recent debug log
  • Inspect stdin from the hook's POV: in your script, cat > /tmp/hook-input.json then exit 0 to capture the payload for offline analysis
  • Hung hooks: per-hook cap is TOOL_HOOK_EXECUTION_TIMEOUT_MS = 10 min; SessionEnd default is 1.5s (override via CRABCODE_SESSIONEND_HOOKS_TIMEOUT_MS). On timeout the process is killed
  • Debugging HTTP hooks: point url at httpbin.org/post or a local listener to verify the payload shape; $VAR_NAME placeholders in headers only resolve against the allowedEnvVars allow list

Real-world examples

Example 1: block git push --force to main — a PreToolUse hook reads stdin, pulls tool_input.command, and exit 2s on a force-push match:

bash
# ~/.crabcode/hooks/guard-force-push.sh
#!/usr/bin/env bash
payload=$(cat)
cmd=$(echo "$payload" | jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'git push.*(--force|-f)\s.*(main|master)'; then
  echo "Refusing force-push to main/master" >&2
  exit 2
fi
# ~/.crabcode/hooks/guard-force-push.sh
#!/usr/bin/env bash
payload=$(cat)
cmd=$(echo "$payload" | jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'git push.*(--force|-f)\s.*(main|master)'; then
  echo "Refusing force-push to main/master" >&2
  exit 2
fi

Paired with settings.json:

json
{ "hooks": { "PreToolUse": [
  { "matcher": "Bash", "hooks": [
    { "type": "command", "command": "~/.crabcode/hooks/guard-force-push.sh", "timeout": 3 }
  ]}
]}}
{ "hooks": { "PreToolUse": [
  { "matcher": "Bash", "hooks": [
    { "type": "command", "command": "~/.crabcode/hooks/guard-force-push.sh", "timeout": 3 }
  ]}
]}}

Example 2: auto-format after writePostToolUse matches Edit|Write, grabs the freshly-written tool_input.file_path, feeds it to prettier:

json
{ "hooks": { "PostToolUse": [
  { "matcher": "Edit|Write", "hooks": [
    { "type": "command",
      "command": "FILE=$(jq -r '.tool_input.file_path'); [ -n \"$FILE\" ] && prettier --write \"$FILE\" 2>&1 || true",
      "timeout": 10 }
  ]}
]}}
{ "hooks": { "PostToolUse": [
  { "matcher": "Edit|Write", "hooks": [
    { "type": "command",
      "command": "FILE=$(jq -r '.tool_input.file_path'); [ -n \"$FILE\" ] && prettier --write \"$FILE\" 2>&1 || true",
      "timeout": 10 }
  ]}
]}}

Limits and caveats

  • PreToolUse default timeout 10 min — but the session blocks while it runs. Keep hooks light
  • Prefer absolute paths: ~ doesn't expand in some envs
  • Hooks run as your shell user: full local exec privileges — trust the script
  • PostToolUseFailure is your cleanup net: API errors / tool timeouts / Esc interrupts all land here — good place to delete temp files or git stash pop
  • async / asyncRewake: set "async": true to run in the background without blocking; asyncRewake wakes the model on exit code 2