Документация доступна только на английском языке.
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.
{
"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
| Type | What it does | Key fields |
|---|---|---|
command | Runs a shell command. The event's JSON input arrives on stdin | command, args (exec form without a shell), shell, async, asyncRewake, timeout, onFailure |
http | POSTs the JSON input to a URL and reads the response body as output | url, headers, allowedEnvVars, timeout, onFailure |
mcp_tool | Calls a tool on a connected MCP server | server, tool, input |
prompt | Asks a model a yes/no question about the event. $ARGUMENTS is replaced by the JSON input | prompt, model, continueOnBlock |
agent | Like prompt, but the model can use tools to investigate before answering | prompt, 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:
| Field | Meaning |
|---|---|
session_id | The session |
transcript_path | Path to the session transcript, when available |
cwd | The current working directory |
permission_mode | The session's permission mode |
hook_event_name | The event, such as PreToolUse |
Tool events add tool_name, tool_input and tool_use_id. PostToolUse adds tool_response.
Environment variables for command hooks:
| Variable | Value |
|---|---|
NIB_PROJECT_DIR, CLAUDE_PROJECT_DIR | The project root. The hook's working directory |
NIB_HOOK_EVENT | The event name |
NIB_ENV_FILE, CLAUDE_ENV_FILE | For 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 code | Meaning |
|---|---|
0 | Success. If stdout is JSON, it's read as a decision (below). Otherwise stdout may be added as context for some events |
2 | Block. 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 else | A 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."
}
}| Field | Events | Effect |
|---|---|---|
continue: false, stopReason | Any | Stop the agent loop entirely |
decision: "block", reason | UserPromptSubmit, PostToolUse, Stop, SubagentStop, … | Block, with the reason fed back |
systemMessage | Any | A notice shown to you |
hookSpecificOutput.additionalContext | SessionStart, UserPromptSubmit, PostToolUse, … | Text added to the model's context |
hookSpecificOutput.permissionDecision | PreToolUse | allow, deny, ask or defer. An allow skips a routine prompt, never one that needs you |
hookSpecificOutput.updatedInput | PreToolUse | Replace the tool's input before it runs |
hookSpecificOutput.decision | PermissionRequest | Answer 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
| Event | When it runs | Can block or decide |
|---|---|---|
SessionStart | A session starts or resumes | Adds context |
Setup | One-time repository setup or maintenance runs | — |
InstructionsLoaded | An instruction file or rule is loaded | — |
UserPromptSubmit | Before your prompt is sent to the model | Block the prompt, add context |
UserPromptExpansion | A skill or command expands into a prompt | Block, add context |
PreToolUse | Before a tool runs | Allow, deny, ask, defer; rewrite input |
PermissionRequest | A permission prompt is about to be shown | Answer it |
PermissionDenied | Auto mode denied a call | Ask for a retry |
PostToolUse | After a tool succeeds | Add context, replace output, block |
PostToolUseFailure | After a tool fails | Add context |
PostToolBatch | After a batch of parallel tool calls finishes | Add context; block: the turn stops |
Notification | Nib sends you a notification, for example when it needs input | — |
SubagentStart | A subagent starts | Adds context |
SubagentStop | A subagent finishes | Block to keep it working |
TaskCreated, TaskCompleted | A task-list item is created or completed | Block: the item isn't created, or stays open, and the reason goes to the agent |
Stop | The agent finishes its turn | Block to keep it working |
StopFailure | The turn ended because of an error | — |
ConfigChange | A settings file changes | Block: the session keeps the previous settings until the file changes again (managed settings can't be blocked) |
CwdChanged | The working directory changes | Set environment |
DirectoryAdded | A directory is added to the workspace | — |
FileChanged | A watched file changes on disk | Set environment |
WorktreeCreate, WorktreeRemove | A git worktree is created or removed | WorktreeCreate 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, PostCompact | Before and after compaction | PreCompact can block the compaction |
PreModelSwitch, PostModelSwitch | Around a model change | PreModelSwitch can deny it |
Elicitation, ElicitationResult | An MCP server asks you for input, and after you answer | Accept or decline |
SessionEnd | The 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
#!/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 withallowedHttpHookUrls. disableAllHooks: truein managed settings turns every hook off. Set anywhere else, it leaves only managed hooks running.