Документация доступна только на английском языке.

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.

Nib'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 Nib, except for two events that never fire (see Events).

Configuration

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

.nib/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|ApplyPatch",
        "hooks": [
          { "type": "command", "command": "bunx biome format --write \"$NIB_PROJECT_DIR\"", "timeout": 30 }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.nib/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: a handler with if on any other event never runs. It matches the way a permission rule does: Bash(rm *) also catches sudo rm … and bash -c 'rm …', Edit(*.ts) also catches Write and a shell redirect into a .ts file, and WebFetch(domain:github.com) matches that host only (write WebFetch(domain:*.github.com) for its subdomains). A rule that doesn't parse doesn't narrow: the hook runs.

One exception: when a tool can't say what a call touched after the call has run, PostToolUse handlers run without checking their if, so such a hook may fire on a call its if would have excluded.

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, asyncRewake, 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, continueOnBlock
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).

Not every type runs on every event. prompt and agent handlers run on the tool events, the prompt events, Stop, SubagentStop, PermissionDenied and the task events; PermissionRequest takes every type but agent; SessionStart and Setup take only command and mcp_tool; the other events take command, http and mcp_tool.

A command hook's line runs in bash (/bin/sh where bash isn't installed) on macOS and Linux, and in Git Bash on Windows, or in PowerShell there when Git Bash isn't installed. shell: "powershell" runs it in PowerShell (pwsh, else Windows PowerShell), and shell: "bash" only in a POSIX shell. With args, command itself runs, with no shell.

async runs a command in the background. With asyncRewake, a background command that exits with code 2 wakes the agent with its output. continueOnBlock makes a prompt hook that blocks return its reason to the agent, which carries on, instead of ending the turn; agent hooks always do.

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
NIB_PROJECT_DIR, CLAUDE_PROJECT_DIRThe project root. The hook's working directory
NIB_HOOK_EVENTThe event name
NIB_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. An allow skips a routine prompt, never one that needs you
hookSpecificOutput.updatedInputPreToolUseReplace the tool's input before it runs
hookSpecificOutput.decisionPermissionRequestAnswer the permission prompt: { "behavior": "allow" } or deny. An allow can't approve a prompt that needs you; a deny can answer any

When several hooks answer the same event, the strictest wins: for PreToolUse, deny beats ask, which beats allow, and defer means no opinion: it counts only when every hook defers, so one hook's defer never cancels another's ask. A hook's deny holds in every permission mode, including bypass. So does an ask, except that bypass honours it only from your own hooks and your organization's (your user settings, managed settings, the command line, or the program embedding Nib), not from a repository's (its project or local settings), a plugin's, a skill's or an agent's. Context from multiple hooks is combined in configuration order. A text field over 10,000 characters is cut to its first 2,000, with a note saying so.

hookSpecificOutput.sessionTitle, initialUserMessage and reloadSkills (SessionStart, and sessionTitle on UserPromptSubmit too) and the top-level terminalSequence are accepted so existing hooks keep working, but not applied yet. suppressOutput is accepted and has no effect. suppressOriginalPrompt changes nothing: a blocked prompt's notice never repeats the prompt.

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, add context
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; block: the turn stops
NotificationNib 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 completedBlock: the item isn't created, or stays open, and the reason goes to the agent
StopThe agent finishes its turnBlock to keep it working
StopFailureThe turn ended because of an error—
ConfigChangeA settings file changesBlock: the session keeps the previous settings until the file changes again (managed settings can't be blocked)
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 replaces git's worktree creation: it must print the new worktree's path, or creation fails. WorktreeRemove runs only for worktrees a hook made, and a failure fails the removal
PreCompact, PostCompactBefore and after compactionPreCompact can block the 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—

MessageDisplay and TeammateIdle are accepted in a hooks file so that existing configurations load, but they never fire. If your settings have a hook for either, Nib shows a warning naming it when the session starts.

Examples

Block edits to generated files

.nib/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|Write|ApplyPatch|EditLines",
        "hooks": [{ "type": "command", "command": "${NIB_PROJECT_DIR}/.nib/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

  • Your hooks run with your user's permissions, outside the sandbox; a Hub extension's hooks run in the extension's 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 in managed settings turns every hook off. Set anywhere else, it leaves only managed hooks running.

На этой странице