Simple TOML hooks for Claude Code, Cursor, Windsurf, Gemini CLI - Command blocking, auto-formatting, stop-time automation
- 🦀 Built with Rust - Low overhead, lightweight single binary, blazing fast (<10ms startup)
- ⚡ Kill Command Blocking - Blocks
kill,pkill,killall,taskkilland suggests safe-kill - 🗑️ RM Command Blocking - Blocks
rm,rmdir,del,eraseand suggests safe-rm - 💾 DD Command Blocking - Optionally blocks
ddto prevent disk overwrite accidents - 🌳 AST-based Parsing - Uses tree-sitter-bash for accurate command analysis with wrapper/subshell detection (sudo,
sudo -n,sudo --user,timeout --signal,command rm, bash -c, pipes) - 🔧 Custom Command Filters - Define custom filters with regex support
- 📁 Extension Hooks - Execute external tools (formatters, linters) on file modifications, with lint output passed to AI agent (Claude Code only)
- ⏹️ Stop Hooks - Run commands when agent loop ends (notifications, git commit with git-sc, cleanup)
- 🧹 Project-wide Lint on Stop - Auto-detect project type (
Cargo.toml,tsconfig.json, etc.) and run lint/typecheck, feeding errors back to the AI agent - ⏱️ Hook Timeout - Configurable timeout for hook commands (default: 60s), kills hung processes with SIGKILL
- 📂 Project Config Merge - Place
.claw-hooks.tomlin your project root to override/extend global settings per project - 🔌 Multi-Agent Support - Works with Claude Code, Cursor, Windsurf, and Gemini CLI
Native hooks require complex Python/Bash scripts for simple tasks. claw-hooks reduces this to simple TOML configuration.
Claude Code - Blocking rm command requires a Python script:
#!/usr/bin/env python3
import json
import sys
def main():
input_data = json.loads(sys.stdin.read())
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
if tool_name == "Bash":
command = tool_input.get("command", "")
dangerous = ["rm ", "rm -", "rmdir"]
if any(cmd in command for cmd in dangerous):
result = {
"decision": "block",
"message": "🚫 Dangerous command blocked"
}
print(json.dumps(result))
sys.exit(2)
print(json.dumps({"decision": "approve"}))
sys.exit(0)
if __name__ == "__main__":
main()Then configure in settings.json:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "python3 /path/to/hook.py"}]
}]
}
}Cursor/Windsurf - Similar complexity with different JSON structures to parse.
Alternative: Regex one-liner - Harder to maintain and limited functionality:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "jq -r '.tool_input.command // \"\"' | grep -qE '^rm(dir)?\\b' && { echo '🚫 Dangerous command blocked' >&2; exit 2; }; exit 0"
}]
}]
}
}Problems with regex approach:
- ❌ Doesn't catch
sudo rm,cd /tmp && rm, or commands in pipes - ❌ Hard to add multiple blocked commands
- ❌ No custom messages per command type
- ❌ Requires jq dependency
- ❌ Different regex needed for each agent's JSON structure
Extension hooks (formatters/linters) - Even more complex:
# Regex one-liner attempt - becomes unmaintainable
jq -r '.tool_input.file_path // ""' | xargs -I{} sh -c 'case "{}" in *.rs) rustfmt "{}" ;; *.py) ruff format "{}" && ruff check --fix "{}" ;; *.ts|*.tsx) biome format --write "{}" && biome lint --write "{}" ;; esac'Or with a Python script:
#!/usr/bin/env python3
import json
import sys
import subprocess
import os
def main():
input_data = json.loads(sys.stdin.read())
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
if tool_name in ["Write", "Edit", "MultiEdit"]:
file_path = tool_input.get("file_path", "")
ext = os.path.splitext(file_path)[1]
commands = {
".rs": ["rustfmt {}"],
".py": ["ruff format {}", "ruff check --fix {}"],
".ts": ["biome format --write {}", "biome lint --write {}"],
".tsx": ["biome format --write {}", "biome lint --write {}"],
}
if ext in commands:
for cmd in commands[ext]:
subprocess.run(cmd.format(file_path), shell=True)
print(json.dumps({"decision": "approve"}))
if __name__ == "__main__":
main()Block dangerous commands with 2 lines:
rm_block = true
rm_block_message = "🚫 Use safe-rm instead"Extension hooks with simple map:
[extension_hooks]
".css" = ["biome format --write {file}", "biome lint --write {file}"]
".py" = ["ruff format --check {file}", "ruff check --preview --select=I,F,DOC {file}"]
".rs" = ["rustfmt {file}"]
".ts" = ["biome check {file}"]
".tsx" = ["biome check {file}"]Rules:
- Each extension hook command template must contain exactly one
{file}placeholder. - Paths containing parent-directory traversal segments (e.g.,
../) are rejected. - Paths containing shell redirection metacharacters (
<,>) are rejected.
Why it works better:
- ✅ AST-based parsing with tree-sitter-bash for accurate command detection
- ✅ Quote-aware (detects commands, ignores arguments in quotes)
- ✅ Detects
sudo rm,sudo -n rm,sudo --user root rm,timeout --signal TERM 10 rm,command rm,cd /tmp && rm, commands in pipes - ✅ Handles wrappers and subshells (sudo, timeout, command, bash -c, xargs)
- ✅ Single binary, no Python/jq dependencies
Configure once:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "claw-hooks hook"}]
}]
}
}| Feature | Native Hooks | claw-hooks |
|---|---|---|
| Block dangerous commands | 25+ lines Python per command | 1 line TOML |
| Custom filters | New script per filter | Add to [[custom_filters]] |
| Extension hooks (formatters) | Complex file detection script | [extension_hooks] map |
| Lint output to agent | Manual JSON construction | Automatic (Claude Code only)* |
| Multi-agent support | Different scripts per agent | Single binary with --format |
| Stop hooks (lint, notifications, etc.) | Custom scripts per use case | [[stop_hooks]] config |
* Lint/formatter output is automatically passed to Claude Code via additionalContext, enabling the agent to fix warnings.
- OS: macOS, Linux, Windows
- Dependencies: None (single binary)
brew install owayo/claw-hooks/claw-hooksgit clone https://github.com/owayo/claw-hooks.git
cd claw-hooks
cargo build --releaseBinary: target/release/claw-hooks
macOS (Apple Silicon)
curl -L https://github.com/owayo/claw-hooks/releases/latest/download/claw-hooks-aarch64-apple-darwin.tar.gz | tar xz
sudo mv claw-hooks /usr/local/bin/macOS (Intel)
curl -L https://github.com/owayo/claw-hooks/releases/latest/download/claw-hooks-x86_64-apple-darwin.tar.gz | tar xz
sudo mv claw-hooks /usr/local/bin/Linux (x86_64)
curl -L https://github.com/owayo/claw-hooks/releases/latest/download/claw-hooks-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv claw-hooks /usr/local/bin/Linux (ARM64)
curl -L https://github.com/owayo/claw-hooks/releases/latest/download/claw-hooks-aarch64-unknown-linux-gnu.tar.gz | tar xz
sudo mv claw-hooks /usr/local/bin/Windows
Download claw-hooks-x86_64-pc-windows-msvc.zip from Releases, extract, and add to PATH.
# Generate default configuration
claw-hooks init
# Test with a safe command (allowed)
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git status"}}' | claw-hooks hook
# Output: {"decision":"approve"}
# Test with a dangerous command (blocked)
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | claw-hooks hook
# Output: {"decision":"block","message":"🚫 Use safe-rm instead..."}| Command | Description |
|---|---|
hook (alias: run) |
Process hook events from stdin |
init |
Generate default configuration |
check |
Validate configuration |
version |
Show version |
| Option | Short | Description |
|---|---|---|
--format |
-f |
Input format: claude (default), cursor, windsurf, gemini |
--config |
-c |
Path to configuration file |
--help |
-h |
Show help |
# Process Claude Code hooks (default)
claw-hooks hook
# Process Cursor hooks
claw-hooks hook --format cursor
# Process Windsurf hooks
claw-hooks hook --format windsurf
# Process Gemini CLI hooks
claw-hooks hook --format gemini
# Use custom config
claw-hooks hook --config /path/to/config.tomlAdd to ~/.claude/settings.json (user) or .claude/settings.json (project):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "claw-hooks hook" }]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{ "type": "command", "command": "claw-hooks hook" }]
}
],
"Stop": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "claw-hooks hook" }]
}
]
}
}Add to ~/.cursor/hooks.json (user) or <project>/.cursor/hooks.json (project):
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{ "command": "claw-hooks hook --format cursor" }
],
"afterFileEdit": [
{ "command": "claw-hooks hook --format cursor" }
],
"stop": [
{ "command": "claw-hooks hook --format cursor" }
]
}
}Add to ~/.codeium/windsurf/hooks.json (user) or .windsurf/hooks.json (project):
{
"hooks": {
"pre_run_command": [
{ "command": "claw-hooks hook --format windsurf", "show_output": true }
],
"post_write_code": [
{ "command": "claw-hooks hook --format windsurf", "show_output": true }
],
"post_cascade_response": [
{ "command": "claw-hooks hook --format windsurf", "show_output": true }
]
}
}Add to ~/.gemini/settings.json (user) or .gemini/settings.json (project):
{
"hooks": {
"BeforeTool": [
{
"matcher": "run_shell_command",
"hooks": [{ "type": "command", "command": "claw-hooks hook --format gemini" }]
}
],
"AfterTool": [
{
"matcher": "write_file|replace",
"hooks": [{ "type": "command", "command": "claw-hooks hook --format gemini" }]
}
],
"AfterAgent": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "claw-hooks hook --format gemini" }]
}
]
}
}Default location: ~/.config/claw-hooks/config.toml (all platforms)
# Command blocking
rm_block = true # Block rm/rmdir/del/erase (default: true)
kill_block = true # Block kill/pkill/killall/taskkill (default: true)
dd_block = true # Block dd command (default: true)
# Custom messages (recommended: use with safe-rm/safe-kill tools)
# safe-rm: https://github.com/owayo/safe-rm
# safe-kill: https://github.com/owayo/safe-kill
rm_block_message = "🚫 Use safe-rm instead: safe-rm <file> (validates Git status and path containment). Only clean/ignored files in project allowed."
kill_block_message = "🚫 Use safe-kill instead: safe-kill <PID> or safe-kill -n <name> (like pkill). Use -s <signal> for signal."
dd_block_message = "🚫 dd command blocked for safety."
# Debug logging
debug = false
# log_path = "~/.config/claw-hooks/logs" # default: same directory as config.toml
# Hook command timeout in seconds (default: 60)
# Commands exceeding this timeout will be killed (SIGKILL)
# hook_timeout = 60
# Custom command filters (regex supported)
[[custom_filters]]
command = "yarn"
message = "Use `pnpm` instead of `yarn`"
# Args mode: command (regex) + args matching
[[custom_filters]]
command = "npm"
args = ["install", "i", "add"] # Blocks: npm install, npm i, npm add
message = "Use `pnpm` instead of `npm`"
[[custom_filters]]
command = "pip3?" # Regex: matches pip or pip3
args = ["install", "uninstall"]
message = "Use `uv pip` instead"
# Regex-only mode (when args is not specified)
[[custom_filters]]
command = "python[23]? -m pip" # More complex patterns
message = "Use `uv pip` instead"
[[custom_filters]]
command = "docker"
args = ["rm", "rmi", "system prune"] # Blocks: docker rm, docker rmi
message = "Ask the user to run this command manually"
# Extension hooks (triggered on file write/edit)
# Map format: ".ext" = ["cmd1 {file}", "cmd2 {file}"]
# Output (stdout/stderr) is passed to AI agent as additionalContext (Claude Code only)
# Each command template must contain exactly one {file}
# Parent-directory traversal paths (../) are rejected for safety
# Shell redirection metacharacters (<, >) in file paths are rejected for safety
[extension_hooks]
".css" = ["biome format --write {file}", "biome lint --write {file}"]
".py" = ["ruff format --check {file}", "ruff check --preview --select=I,F,DOC {file}"]
".rs" = ["rustfmt {file}"]
".ts" = ["biome check {file}"]
".tsx" = ["biome check {file}"]
# Stop hooks (triggered when agent loop ends)
# All commands in the array are executed in parallel.
# [[stop_hooks]]
# commands = ["afplay /System/Library/Sounds/Glass.aiff"] # macOS notification sound
# [[stop_hooks]]
# commands = ["notify-send 'Agent completed'"] # Linux notification
# Conditional stop hooks (project-wide lint on stop)
# Detects project type by file existence and tool availability.
# On failure, the result is returned to the AI agent so it can fix the issues.
# condition fields (AND logic): file_exists, command_exists
[[stop_hooks]]
commands = ["cargo clippy --all-targets --all-features -- -D warnings", "cargo fmt --check"]
condition = { file_exists = "Cargo.toml" }
[[stop_hooks]]
commands = ["pnpm exec tsc --noEmit"]
condition = { file_exists = "tsconfig.json" }
[[stop_hooks]]
commands = ["ruff format .", "ruff check --preview --fix --select=I,F,DOC --unsafe-fixes"]
condition = { file_exists = "pyproject.toml", command_exists = "ruff" }
[[stop_hooks]]
commands = ["biome check --write ."]
condition = { file_exists = "package.json" }claw-hooks uses a global configuration file (~/.config/claw-hooks/config.toml) by default. You can customize behavior per project in three ways:
1. .claw-hooks.toml — Auto-detected project config (recommended)
Place a .claw-hooks.toml in your project root. claw-hooks automatically detects it in the current working directory and merges it with the global config. No --config flag needed.
# my-project/.claw-hooks.toml
# Override: disable dd blocking for this project
dd_block = false
# Override: project-specific extension hooks (replaces global)
[extension_hooks]
".rs" = ["rustfmt {file}"]
".ts" = ["biome check {file}"]
# Merge: additional stop hooks (added to global stop hooks)
[[stop_hooks]]
commands = ["pnpm exec tsc --noEmit"]
condition = { file_exists = "tsconfig.json" }Merge rules:
| Field | Rule | Behavior |
|---|---|---|
extension_hooks |
Replace | Project definition completely replaces global |
custom_filters |
Replace | Project definition completely replaces global |
stop_hooks |
Merge | Both global and project hooks are executed |
rm_block, kill_block, dd_block |
Replace | Project value takes precedence |
*_block_message, hook_timeout |
Replace | Project value takes precedence |
debug, log_path, nano_buddy |
Global only | Not allowed in project config |
Omitted fields keep the global value. Setting an empty array (e.g., custom_filters = []) explicitly clears the global value.
Validate with claw-hooks check — it reports if a project config was found and whether it's valid.
2. --config — Full config replacement
Use --config to specify a complete configuration file, replacing the global config entirely:
# my-project/.claude/claw-hooks.toml
rm_block = true
kill_block = true
dd_block = false # Allow dd in this project
[extension_hooks]
".rs" = ["rustfmt {file}"]// my-project/.claude/settings.json
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "claw-hooks hook --config .claude/claw-hooks.toml" }]
}],
"PostToolUse": [{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{ "type": "command", "command": "claw-hooks hook --config .claude/claw-hooks.toml" }]
}],
"Stop": [{
"matcher": "",
"hooks": [{ "type": "command", "command": "claw-hooks hook --config .claude/claw-hooks.toml" }]
}]
}
}3. Conditional stop hooks — Automatic project detection
Stop hooks with file_exists conditions automatically adapt to the project type based on the working directory. A single global config can handle multiple project types:
# ~/.config/claw-hooks/config.toml
# Runs only in Rust projects (where Cargo.toml exists)
[[stop_hooks]]
commands = ["cargo clippy -- -D warnings"]
condition = { file_exists = "Cargo.toml" }
# Runs only in TypeScript projects (where tsconfig.json exists)
[[stop_hooks]]
commands = ["pnpm exec tsc --noEmit"]
condition = { file_exists = "tsconfig.json" }All three approaches can be combined: use the global config for shared rules, .claw-hooks.toml for project-specific overrides, and conditional stop hooks for automatic project-type detection.
Stop hooks with a condition field run lint/typecheck commands based on the project type. All commands in the commands array are executed in parallel. When any command fails (non-zero exit), all failure outputs are collected and returned to the AI agent as a block reason, prompting it to fix the issues.
Timeout handling: When a command exceeds hook_timeout, claw-hooks kills the process (SIGKILL) and logs a timeout notice, but does not count it as a blocking failure. This prevents session shutdown from stalling on slow commands. Normal command failures — including those that explicitly exit with code 124 — still block as usual.
Stop hook fields:
| Field | Type | Default | Description |
|---|---|---|---|
commands |
string[] |
(required) | Commands to execute (in parallel within the same stage) |
condition |
object |
(none) | Execution condition (AND logic: file_exists, command_exists) |
stage |
1-5 |
5 |
Execution order. Lower stages run first. Hooks in the same stage run in parallel. |
report |
bool |
(auto) | Whether to report results to the AI agent. Default: true if condition is set, false otherwise. |
Condition fields (AND logic — all specified conditions must be true):
| Field | Description |
|---|---|
file_exists |
Run only when this file exists in the working directory |
command_exists |
Run only when this command is available in PATH (Windows PATHEXT is respected; explicit paths like ./tool or /usr/bin/tool are also supported) |
# Stage-based execution: analysis → lint → commit
[[stop_hooks]]
commands = ["astro-sight impact --dir . --git"]
stage = 1 # Run first
report = true # Return results to AI
[[stop_hooks]]
commands = ["cargo clippy --all-targets --all-features -- -D warnings", "cargo fmt --check"]
condition = { file_exists = "Cargo.toml" }
stage = 3
# report not set → condition present → true (default)
[[stop_hooks]]
commands = ["pnpm exec tsc --noEmit"]
condition = { file_exists = "tsconfig.json" }
stage = 3
[[stop_hooks]]
commands = ["git-sc --all --yes --quiet"]
# stage not set → 5 (last)
# report not set → no condition → false (fire-and-forget)Stage execution order: Stages are executed sequentially from 1 to 5. All hooks in the same stage run in parallel. A stage completes before the next one begins.
Report behavior: When report = true (or defaulting to true via condition), command failures are collected and returned to the AI agent as a block reason. When report = false (or defaulting to false without condition), failures are logged but do not block — fire-and-forget style.
# More examples:
# Python: run ruff format/check when pyproject.toml exists and ruff is installed
[[stop_hooks]]
commands = ["ruff format .", "ruff check --preview --fix --select=I,F,DOC --unsafe-fixes"]
condition = { file_exists = "pyproject.toml", command_exists = "ruff" }
# JavaScript/TypeScript: run biome check when package.json exists
[[stop_hooks]]
commands = ["biome check --write ."]
condition = { file_exists = "package.json" }claw-hooks passes the following environment variables to stop hook child processes:
| Variable | Description |
|---|---|
CLAW_HOOKS_STOP_ACTIVE |
Always set to 1. Prevents recursive stop hook execution when a child process triggers another claw-hooks stop event. |
CLAW_HOOKS_AGENT_MESSAGE |
The AI agent's last message before stopping (if available). Contains what the agent was working on. |
CLAW_HOOKS_AGENT_MESSAGE is populated from:
- Claude Code:
last_assistant_messagefield in the Stop event - Windsurf:
responsefield in thepost_cascade_responseevent - Gemini CLI:
prompt_responsefield in theAfterAgentevent - Cursor: Not available
This is useful for tools that benefit from knowing the agent's context. For example, git-sc uses this to generate more accurate commit messages:
[[stop_hooks]]
commands = ["git-sc --all --yes --quiet"]When git-sc runs as a stop hook, it reads CLAW_HOOKS_AGENT_MESSAGE and includes the agent's context in the AI prompt, resulting in commit messages that reflect the intent of the changes rather than just the raw diff.
Custom filters support two modes:
Regex mode (default): When only command is specified, it's treated as a regex pattern.
[[custom_filters]]
command = "python[23]? -m pip" # Complex regex pattern
message = "Use uv pip instead"Args mode: When args is specified, command is treated as a regex pattern (matched against the command name) and any of the args triggers the filter.
[[custom_filters]]
command = "npm" # Regex pattern for command name
args = ["install", "i", "add"] # First argument must match one of these
message = "Use pnpm instead"
[[custom_filters]]
command = "pip3?" # Matches both pip and pip3
args = ["install", "uninstall"] # First argument must match one of these
message = "Use uv pip instead"Both modes detect commands even when chained with ;, &&, ||, or |:
# Blocked: yarn is detected after semicolon
echo "install"; yarn install
# → {"decision":"block","message":"Use `pnpm` instead of `yarn`"}
# Allowed: "yarn" is inside quotes (not a command), pnpm is OK
echo "not yarn install"; pnpm install
# → {"decision":"approve"}Commands inside quotes are ignored (they're arguments, not commands).
Each AI agent sends different JSON structures. claw-hooks uses --format to determine parsing.
Uses the official Claude Code hooks specification:
Supported hook events: PreToolUse, PostToolUse, Stop, Notification, UserPromptSubmit, SessionStart, SessionEnd
No event type in JSON. Detected by field presence:
| JSON Fields | Detected Hook | Internal Mapping |
|---|---|---|
command |
beforeShellExecution |
PreToolUse + Bash |
file_path / filePath |
afterFileEdit |
PostToolUse + Write |
status |
stop |
Stop |
Uses agent_action_name field:
| agent_action_name | Internal Mapping |
|---|---|
pre_run_command |
PreToolUse + Bash |
post_write_code |
PostToolUse + Write |
post_cascade_response |
Stop |
Uses hook_event_name and tool_name fields:
// BeforeTool event (shell command)
{
"hook_event_name": "BeforeTool",
"tool_name": "run_shell_command",
"tool_input": { "command": "..." },
"session_id": "..."
}
// AfterTool event (file write)
{
"hook_event_name": "AfterTool",
"tool_name": "write_file",
"tool_input": { "file_path": "..." }
}
// AfterAgent event (agent loop ends)
{
"hook_event_name": "AfterAgent"
}| hook_event_name | tool_name | Internal Mapping |
|---|---|---|
BeforeTool |
run_shell_command |
PreToolUse + Bash |
AfterTool |
write_file |
PostToolUse + Write |
AfterAgent |
- | Stop |
Output format uses allow/deny instead of approve/block:
- Allow:
{"decision":"allow"} - Deny:
{"decision":"deny","reason":"..."}
graph LR
subgraph Before Command
CC1[Claude: PreToolUse + Bash]
CU1[Cursor: beforeShellExecution]
WS1[Windsurf: pre_run_command]
GE1[Gemini: BeforeTool + run_shell_command]
end
CH1[🛡️ Validate & suggest alternatives]
CC1 --> CH1
CU1 --> CH1
WS1 --> CH1
GE1 --> CH1
subgraph After File Save
CC2[Claude: PostToolUse + Write/Edit]
CU2[Cursor: afterFileEdit]
WS2[Windsurf: post_write_code]
GE2[Gemini: AfterTool + write_file]
end
CH2[🔧 Run commands by extension]
CC2 --> CH2
CU2 --> CH2
WS2 --> CH2
GE2 --> CH2
subgraph Agent Stop
CC3[Claude: Stop]
CU3[Cursor: stop]
WS3[Windsurf: post_cascade_response]
GE3[Gemini: AfterAgent]
end
CH3[⏹️ Lint / notifications / cleanup]
CC3 --> CH3
CU3 --> CH3
WS3 --> CH3
GE3 --> CH3
{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf /tmp/test" },
"session_id": "abc123"
}Approve: {"decision":"approve"}
Approve with lint output (Claude Code PostToolUse only):
{
"decision": "approve",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "[rustfmt {file}] warning: unused variable..."
}
}The additionalContext field passes lint warnings/errors to Claude Code, allowing it to fix issues automatically. This feature is only available for Claude Code's PostToolUse hooks.
Block: {"decision":"block","message":"Use safe-rm instead..."}
Claude Code / Cursor / Windsurf:
| Code | Meaning |
|---|---|
0 |
Allow |
2 |
Block |
Gemini CLI (different semantics):
| Code | Meaning |
|---|---|
0 |
Success (decision in JSON: allow or deny) |
2 |
System error (stderr used as reason) |
Gemini CLI expects exit code 0 for all decisions, including blocks. The decision field in the JSON response determines whether the action is allowed or denied.
| Metric | Value |
|---|---|
| Startup time | <10ms |
- Rust 1.85+
- Cargo
cargo build # Debug
cargo build --release # Releasecargo test
cargo test -- --nocapture # Verbosecargo clippy --all-targets --all-features -- -D warnings
cargo fmt --checkContributions welcome! Please submit a Pull Request.
