Anatoly integrates directly with Claude Code via its hooks system, creating a real-time write -> audit -> fix automation loop. Every time Claude Code edits a file, Anatoly reviews it in the background. When Claude Code finishes its task, Anatoly intercepts the stop signal, collects findings, and injects them back as feedback -- forcing Claude Code to fix the issues before completing.
Table of Contents#
- What are Claude Code Hooks?
- Overview
- Initialization
- How It Works
- Hook State
- Anti-Loop Protection
- Configuration
- Disabling Hooks
What are Claude Code Hooks?#
Claude Code hooks are shell commands that execute automatically at specific points in Claude Code's lifecycle. They let external tools observe and control what Claude Code does -- inspecting tool calls, blocking actions, or injecting context.
Hook events#
Claude Code fires hooks on several events. The ones Anatoly uses:
| Event | When it fires | Blocking? | Matcher |
|---|---|---|---|
| PreToolUse | Before a tool executes | Yes | Tool name (Bash, Edit|Write, ...) |
| PostToolUse | After a tool succeeds | No | Tool name |
| Stop | When Claude Code finishes responding | Yes | None |
Other events exist (SessionStart, UserPromptSubmit, Notification, etc.) but are not used by Anatoly.
Configuration#
Hooks live in .claude/settings.json under a hooks key. Each event maps to an array of matcher groups, each containing a hooks array of handlers:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "my-script.sh",
"async": true,
"timeout": 600
}
]
}
]
}
}matcher-- regex matched against the tool name (for tool events) or event source.type-- handler type. Anatoly uses"command"(shell command).async-- iftrue, the hook runs in the background and does not block Claude Code.timeout-- maximum execution time in seconds (default: 600).
Protocol#
Input: Claude Code sends a JSON payload on stdin with event context:
{
"session_id": "abc123",
"hook_event_name": "PostToolUse",
"tool_name": "Edit",
"tool_input": { "file_path": "src/foo.ts", "old_string": "...", "new_string": "..." }
}For Stop events, the payload includes a stop_hook_active flag indicating whether this is a re-entry after a previous block.
Output: The hook communicates back via exit code and stdout:
| Exit code | Meaning | Stdout |
|---|---|---|
| 0 | Success | Optional JSON parsed by Claude Code |
| 2 | Block | stderr message shown to Claude |
| Other | Non-blocking error | Ignored (logged in verbose mode) |
To block Claude Code from finishing (Stop event) or to reject a tool call (PreToolUse), the hook exits 0 and writes a JSON decision to stdout:
{
"decision": "block",
"reason": "Explanation injected into Claude's context"
}The "block" decision prevents Claude Code from completing and feeds the reason back as instructions, prompting Claude to address the issue before trying again.
Overview#
The integration uses two Claude Code hook types:
| Hook | Trigger | Anatoly Command | Behavior |
|---|---|---|---|
| PostToolUse | After each Edit or Write tool call |
anatoly hook on-edit |
Spawns a background review for the edited file |
| Stop | When Claude Code is about to finish | anatoly hook on-stop |
Waits for pending reviews, blocks with findings if issues are detected |
The result is a closed-loop workflow: Claude writes code, Anatoly audits it silently in the background, and any findings are fed back as actionable instructions before Claude finishes.
Initialization#
Run the init command to generate the hooks configuration:
npx anatoly hook initThis creates or updates .claude/settings.json with the required hooks:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx anatoly hook on-edit",
"async": true
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "npx anatoly hook on-stop",
"timeout": 180
}
]
}
]
}
}If .claude/settings.json already contains a hooks key, the init command prints the configuration to stdout instead of overwriting, so you can merge it manually.
How It Works#
PostToolUse Hook (on-edit)#
Triggered after every Edit or Write tool call. The hook runs asynchronously so it does not block Claude Code.
Flow:
- Reads the JSON payload from stdin. Claude Code provides
{ tool_name, tool_input: { file_path, ... } }. - Extracts
file_pathfromtool_input(or top-level). - Applies filters -- exits silently if:
- No
file_pathin the payload. - The file is not a TypeScript file (
.ts,.tsx,.mts,.cts). - The file no longer exists on disk (deleted).
- An
anatoly runis already active (lock file held). - The file's SHA-256 hash matches the existing cached review (no changes since last review).
- No
- Checks for an already-running review for the same file (debounce). If one exists, sends
SIGTERMto the previous process. - Spawns a detached child process:
anatoly review --file <path> --no-cache. - Records the PID and status in hook state, then exits immediately.
The review runs entirely in the background. Claude Code is not blocked at any point.
Stop Hook (on-stop)#
Triggered when Claude Code is about to finish its task. This hook is synchronous -- it blocks Claude Code until it completes (up to the 180-second timeout).
Flow:
- Reads stdin JSON and checks for
stop_hook_active(anti-loop flag -- see below). - Loads hook state and checks
stop_countagainstmax_stop_iterations. - Waits for all running reviews to complete (polls every 500ms, 120-second global timeout from
startTimeacross all pending reviews). - Reads completed
.rev.jsonfiles and filters symbols bymin_confidence. - Collects findings where any symbol has:
correctionother thanOKutilityofDEADduplicationofDUPLICATEoverengineeringofOVER
- If findings exist, outputs a JSON response using the Stop hook protocol:
{
"decision": "block",
"reason": "Anatoly Review Findings:\n..."
}The "block" decision prevents Claude Code from stopping and injects the reason as context, prompting Claude to fix the reported issues.
If no findings are detected, the hook exits with code 0 and Claude Code finishes normally.
Hook State#
Hook state is persisted to .anatoly/hook-state.json and tracks all in-flight reviews across a session.
interface HookState {
session_id: string; // Unique ID per session
reviews: Record<string, HookReview>; // Keyed by relative file path
stop_count: number; // Number of times the Stop hook has fired
}
interface HookReview {
pid: number; // OS process ID of the review
status: 'running' | 'done' | 'error' | 'timeout';
started_at: string; // ISO 8601 timestamp
rev_path: string; // Path to the .rev.json output
}Key behaviors:
- Orphan detection: On load, any review marked
runningwhose PID is no longer alive is reclassified aserror. - Atomic writes: State is written atomically via a temp-file-and-rename pattern to prevent corruption from concurrent access.
- Fresh state on corruption: If the state file is missing or unparseable, a fresh state is initialized automatically.
- Session scoping: Each state has a
session_idgenerated fromDate.now()plus a random suffix.
Anti-Loop Protection#
Without safeguards, the hook loop could run indefinitely: Claude fixes issues, the Stop hook fires again with new findings, and so on. Anatoly uses two layers of protection:
-
stop_count/max_stop_iterations: The hook state tracks how many times the Stop hook has fired. Oncestop_countreachesmax_stop_iterations(default: 3, configurable 1--10), the Stop hook exits silently and allows Claude Code to finish. -
stop_hook_activeflag: Claude Code setsstop_hook_active: truein the stdin payload when re-entering the Stop hook after a block. If detected, Anatoly exits immediately to avoid double-processing.
Configuration#
The following .anatoly.yml settings affect hook behavior:
llm:
min_confidence: 70 # Only report findings with confidence >= this value (0-100)
max_stop_iterations: 3 # Maximum Stop hook cycles before allowing Claude to finish (1-10)| Setting | Default | Description |
|---|---|---|
llm.min_confidence |
70 |
Minimum confidence threshold for reporting a finding |
llm.max_stop_iterations |
3 |
Maximum number of write-audit-fix cycles |
Disabling Hooks#
To disable the integration, remove the hooks section from .claude/settings.json. No changes to Anatoly configuration are needed -- the hooks are entirely opt-in through the Claude Code settings file.