Hooks

Run your own commands, HTTP calls or model checks at fixed points in the agent loop, with the hook events and JSON protocol your existing hooks already use.

Hooks let you attach your own logic to the agent loop: format a file after every edit, block a command your team never wants run, add context when a session starts, or keep the agent working until the tests pass. Unlike instructions, hooks always run. They don't depend on the model remembering to do something.

Cusp's hooks use the widely used hook format: the same events, configuration, exit codes and JSON output. Hooks you already have in .claude/settings.json work in Cusp.

Configuration

Hooks live under hooks in any settings file. Each event maps to a list of matcher groups, and each group lists handlers.

.cusp/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|MultiEdit|Write|ApplyPatch",
        "hooks": [
          { "type": "command", "command": "bunx biome format --write \"$CUSP_PROJECT_DIR\"", "timeout": 30 }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.cusp/hooks/check-bash.sh" }
        ]
      }
    ]
  }
}

Hooks from every settings level are combined: managed, local, project and user, plus hooks declared by skills and subagents while they're active.

Matchers

matcher selects which tool calls (or which event subtypes) the group applies to.

  • A bare name matches exactly: Bash.
  • A list separated by | matches any of them: Edit|Write.
  • Anything with regex characters is treated as a regular expression: mcp__github__.*.
  • An empty or missing matcher matches everything.

Matchers see canonical tool names. A GPT model's apply_patch call arrives as ApplyPatch, so include it when you match edits.

Narrowing with if

A handler can carry an if condition written as a single permission rule. It only runs when the call matches:

{ "type": "command", "command": "./scripts/guard-push.sh", "if": "Bash(git push *)" }

if is evaluated only on tool events.

Handler types

TypeWhat it doesKey fields
commandRuns a shell command. The event's JSON input arrives on stdincommand, args (exec form without a shell), shell, async, timeout, onFailure
httpPOSTs the JSON input to a URL and reads the response body as outputurl, headers, allowedEnvVars, timeout, onFailure
mcp_toolCalls a tool on a connected MCP serverserver, tool, input
promptAsks a model a yes/no question about the event. $ARGUMENTS is replaced by the JSON inputprompt, model
agentLike prompt, but the model can use tools to investigate before answeringprompt, model

Common fields on every handler: timeout (seconds), statusMessage (shown while it runs), if, and once (run once, then remove; honored for skill hooks).

onFailure: "block" makes a timeout or crash count as a block. By default a failing hook doesn't stop the agent.

Input

Every hook receives a JSON object. Common fields:

FieldMeaning
session_idThe session
transcript_pathPath to the session transcript, when available
cwdThe current working directory
permission_modeThe session's permission mode
hook_event_nameThe event, such as PreToolUse

Tool events add tool_name, tool_input and tool_use_id. PostToolUse adds tool_response.

Environment variables for command hooks:

VariableValue
CUSP_PROJECT_DIR, CLAUDE_PROJECT_DIRThe project root. The hook's working directory
CUSP_HOOK_EVENTThe event name
CUSP_ENV_FILE, CLAUDE_ENV_FILEFor SessionStart, Setup, CwdChanged and FileChanged: a file where the hook can write export NAME=value lines to set environment variables for later shell commands

Output

Exit codes

Exit codeMeaning
0Success. If stdout is JSON, it's read as a decision (below). Otherwise stdout may be added as context for some events
2Block. stderr is the reason. What "block" means depends on the event: a PreToolUse block stops the call and tells the model why; a Stop block keeps the agent working
Anything elseA non-blocking error. The agent continues; the error is shown to you

JSON output

Print a JSON object on stdout with exit code 0 for finer control:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Use the deploy skill instead of calling kubectl directly."
  }
}
FieldEventsEffect
continue: false, stopReasonAnyStop the agent loop entirely
decision: "block", reasonUserPromptSubmit, PostToolUse, Stop, SubagentStop, …Block, with the reason fed back
systemMessageAnyA notice shown to you
hookSpecificOutput.additionalContextSessionStart, UserPromptSubmit, PostToolUse, …Text added to the model's context
hookSpecificOutput.permissionDecisionPreToolUseallow, deny, ask or defer
hookSpecificOutput.updatedInputPreToolUseReplace the tool's input before it runs
hookSpecificOutput.decisionPermissionRequestAnswer the permission prompt: { "behavior": "allow" } or deny

When several hooks answer the same event, the strictest wins: for PreToolUse, deny beats defer, which beats ask, which beats allow. A hook's deny holds in every permission mode, including bypass. Context from multiple hooks is combined in configuration order. Each text field is capped at 10,000 characters.

Events

EventWhen it runsCan block or decide
SessionStartA session starts or resumesAdds context
SetupOne-time repository setup or maintenance runs—
InstructionsLoadedAn instruction file or rule is loaded—
UserPromptSubmitBefore your prompt is sent to the modelBlock the prompt, add context
UserPromptExpansionA skill or command expands into a promptBlock
MessageDisplayBefore an assistant message is shownReplace the displayed text
PreToolUseBefore a tool runsAllow, deny, ask, defer; rewrite input
PermissionRequestA permission prompt is about to be shownAnswer it
PermissionDeniedAuto mode denied a callAsk for a retry
PostToolUseAfter a tool succeedsAdd context, replace output, block
PostToolUseFailureAfter a tool failsAdd context
PostToolBatchAfter a batch of parallel tool calls finishesAdd context
NotificationCusp sends you a notification, for example when it needs input—
SubagentStartA subagent startsAdds context
SubagentStopA subagent finishesBlock to keep it working
TaskCreated, TaskCompletedA task-list item is created or completed—
StopThe agent finishes its turnBlock to keep it working
StopFailureThe turn ended because of an error—
TeammateIdleA teammate agent becomes idle—
ConfigChangeA settings file changes—
CwdChangedThe working directory changesSet environment
DirectoryAddedA directory is added to the workspace—
FileChangedA watched file changes on diskSet environment
WorktreeCreate, WorktreeRemoveA git worktree is created or removedWorktreeCreate can supply the path
PreCompact, PostCompactBefore and after compaction—
PreModelSwitch, PostModelSwitchAround a model changePreModelSwitch can deny it
Elicitation, ElicitationResultAn MCP server asks you for input, and after you answerAccept or decline
SessionEndThe session ends—

Examples

Block edits to generated files

.cusp/hooks/no-generated.sh
#!/usr/bin/env bash
path=$(jq -r '.tool_input.file_path // empty')
if [[ "$path" == *"/generated/"* ]]; then
  echo "Files under generated/ are produced by 'bun run codegen'. Edit the schema instead." >&2
  exit 2
fi
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|MultiEdit|Write|ApplyPatch|EditLines",
        "hooks": [{ "type": "command", "command": "${CUSP_PROJECT_DIR}/.cusp/hooks/no-generated.sh" }]
      }
    ]
  }
}

Keep going until tests pass

A Stop hook that blocks with the test output sends the agent back to work:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "out=$(bun test 2>&1) || { echo \"$out\" | tail -40 >&2; exit 2; }", "timeout": 300 }
        ]
      }
    ]
  }
}

The hook exits 0 when the tests pass, so the turn ends normally. If a failure is outside the agent's control, it will keep trying until you interrupt it; combine this with --max-turns in headless runs.

Add context at session start

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [{ "type": "command", "command": "echo \"Open issues assigned to me:\"; gh issue list --assignee @me --limit 5" }]
      }
    ]
  }
}

Ask a model to check the work

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "The user asked for a change and the agent says it is done. Based on this transcript excerpt, did the agent run the tests and do they pass? $ARGUMENTS"
          }
        ]
      }
    ]
  }
}

Safety

  • Hooks run with your user's permissions, outside the sandbox. Review hook scripts in a repository before trusting it.
  • In an untrusted workspace, project and local settings can't add hooks.
  • Administrators can restrict hooks to managed settings with allowManagedHooksOnly, and restrict HTTP hooks to approved URLs with allowedHttpHookUrls.
  • disableAllHooks: true turns hooks off.

On this page