The complete guide to Claude Code setup (Opus 4.6, Sonnet 4.6, Haiku 4.5). 1M token context window. 100+ hours saved. 25 hook events. Agent teams and task management. Production-tested patterns for skills, hooks, and MCP integration.
Boris Cherny – author of Programming TypeScript and Anthropic employee – says his number one tip for Claude Code is: “Give Claude a way to verify its work – this 2-3x the quality of the final result.” This chapter implements that insight with three components: a /verify command for interactive verification, a verify-app agent for deep automated checks, and a Stop hook that nudges verification when source code changes.
Purpose: Give Claude a systematic way to verify its own work before the user sees it Source: Boris Cherny’s Claude Code tips + production patterns Difficulty: Beginner to Intermediate Prerequisites: Chapter 13: Claude Code Hooks, Chapter 36: Agents and Subagents, Chapter 47: Adoptable Rules and Commands
Without verification, Claude’s workflow is:
Make changes --> Move on
The user becomes the verifier. They test the endpoint, check the UI, run the test suite, and report back. This creates a slow feedback loop with round-trips between Claude and the user.
With a verification loop, the workflow becomes:
Make changes --> Verify they work --> Fix issues --> Report clean result
Claude catches its own mistakes before the user ever sees them. The key to making this work is reducing friction. If verification requires remembering a manual checklist, it won’t happen consistently. A single command – /verify – makes it automatic.
This chapter builds three components at increasing levels of automation:
| Component | What It Does | When It Runs |
|---|---|---|
/verify command |
Interactive verification with modes | User types /verify |
verify-app agent |
Deep 3-tier verification as a subagent | Spawned by Task() call |
| Stop hook | Nudges verification after code changes | Automatically after turns |
The /verify command is a slash command (see Chapter 47) that uses dynamic context injection to pre-compute useful information before Claude processes the prompt. This means Claude sees the git diff, server health, and branch name immediately – no round-trips needed.
~/.claude/commands/verify.md # Global (all projects)
.claude/commands/verify.md # Project-specific (overrides global)
---
allowed-tools: Bash, Read, Grep, Glob
description: Verify recent changes with auto-detected scope
---
# Verification: $ARGUMENTS mode
## Pre-computed Context (dynamic injection)
**Branch**: !`git branch --show-current`
**Changed files**:
!`git diff HEAD --name-only`
**Unstaged changes**:
!`git diff --stat`
**Server health** (if running):
!`curl -s -o /dev/null -w "%{http_code}" http://localhost:${PORT:-8080}/health 2>/dev/null || echo "not running"`
## Verification Instructions
Based on the mode argument ("$ARGUMENTS"), run the appropriate verification:
### Mode: quick (default if no argument)
1. Check server health endpoint (if server is running)
2. Verify no syntax errors in changed files (node --check, python -m py_compile, etc.)
3. Report: files changed, health status, any obvious issues
### Mode: deep
1. Everything in "quick" mode
2. Run the project test suite (npm test, pytest, go test, etc.)
3. Check for lint/format issues (if linter configured)
4. Verify no files exceed 500 lines (modularity check)
5. Report: test results, lint status, modularity violations
### Mode: auto (recommended)
1. Detect what changed from the git diff above
2. If only docs/config changed: quick mode
3. If source code changed: deep mode
4. If test files changed: run those specific tests
5. Report: what was detected, what was verified, results
## Output Format
Summarize verification results as:
- PASS: What verified successfully
- FAIL: What failed (with fix suggestions)
- SKIP: What was skipped and why
The !`command` syntax (see Chapter 46: Advanced Configuration Patterns) runs shell commands at command invocation time – before Claude processes the prompt. When a user types /verify, Claude Code:
git branch --show-current and injects the resultgit diff HEAD --name-only and injects the file listcurl against the health endpoint and injects the status codeThis eliminates the first round of tool calls Claude would otherwise need. Instead of: “Let me check what changed… [Bash] git diff… OK now let me check health… [Bash] curl…”, Claude sees the diff and health status immediately and jumps straight to verification.
Critical: The correct syntax is !`command` (exclamation mark followed by a backtick-wrapped command). An earlier version of this chapter used the $!command!$ syntax, which does not work. The preprocessor looks for !`...` patterns – an exclamation mark immediately followed by a backtick-delimited command. Keep injections as bare text in the markdown:
<!-- CORRECT: !`command` syntax -->
**Branch**: !`git branch --show-current`
<!-- WRONG: $!...!$ does not work -->
**Branch**: $!git branch --show-current!$
<!-- WRONG: code fence prevents execution -->
**Branch**: `!`git branch --show-current``
# Auto-detect mode (recommended)
/verify auto
# Quick health check only
/verify quick
# Full verification with tests
/verify deep
# No argument defaults to quick
/verify
The /verify command is for interactive use – the user types it. The verify-app agent is for programmatic use – Claude spawns it as a subagent via Task() when it wants to verify its own work without user intervention.
~/.claude/agents/verify-app.md # Global (all projects)
.claude/agents/verify-app.md # Project-specific (overrides global)
---
model: sonnet
allowed-tools: Bash, Read, Grep, Glob
permissionMode: auto
memory: project
---
# verify-app: 3-Tier Verification Agent
You are a verification agent. Your job is to verify that recent code changes
work correctly. You run three tiers of checks, each building on the last.
If an earlier tier fails, skip later tiers and report immediately.
## Tier 1: Static Analysis (always runs)
Detect the tech stack from the project root:
- Node.js: check for package.json
- Python: check for requirements.txt, pyproject.toml, or setup.py
- Go: check for go.mod
- Rust: check for Cargo.toml
Then run static checks appropriate to the stack:
- Node.js: `node --check` on changed .js files, `npx tsc --noEmit` if tsconfig exists
- Python: `python -m py_compile` on changed .py files
- Go: `go vet ./...`
- All: Check for files >500 lines, check for conflict markers (<<<<<<)
## Tier 2: Health Endpoints (if server is running)
Check if a dev server is running:
- Try common ports: 8080, 3000, 5173, 4000, 8000
- Hit /health or / endpoint
- If server responds: verify HTTP 200
- If server is not running: skip this tier (not a failure)
## Tier 3: Test Suite (if tests exist)
Detect and run tests:
- Node.js: `npm test` (if test script exists in package.json)
- Python: `pytest` (if pytest installed) or `python -m unittest discover`
- Go: `go test ./...`
- If no test infrastructure: skip this tier
## Output Format
Report results as a structured summary:
```
VERIFICATION RESULTS
====================
Tier 1 (Static): PASS / FAIL (details)
Tier 2 (Health): PASS / FAIL / SKIP (details)
Tier 3 (Tests): PASS / FAIL / SKIP (details)
Issues Found:
- [list any failures with file paths and error messages]
Suggested Fixes:
- [actionable fix for each issue]
```
| Situation | Use | Why |
|---|---|---|
| User wants to verify interactively | /verify |
Direct, fast, user sees results in-line |
| Claude wants to self-check | verify-app |
Fresh context, doesn’t pollute main window |
| After a multi-step plan | verify-app |
Subagent verifies all changes at once |
| Quick sanity check mid-conversation | /verify quick |
Lightweight, no subagent overhead |
| CI-like verification before commit | verify-app |
Thorough, 3-tier, reports structured |
Claude (or a rule) can spawn the verification agent like this:
Task(subagent_type: "verify-app",
prompt: "Verify the changes I just made to src/routes/auth.routes.js
and src/services/auth.service.js. Check that the server is healthy
and tests pass.")
The agent gets a fresh context window, runs all three tiers, and reports back without consuming the main conversation’s context budget.
The Stop hook is the gentlest component. It runs after every Claude turn and checks whether source code was changed. If so, it suggests running /verify. It does not force verification – it nudges.
Create this file at ~/.claude/hooks/stop-verify-nudge.sh:
#!/bin/bash
# Stop hook: suggest /verify when source code changes detected
# Runs after each Claude turn completes
# Count changed files (staged + unstaged)
CHANGED_FILES=$(git diff --name-only HEAD 2>/dev/null | wc -l)
# Count source code files specifically
SRC_CHANGES=$(git diff --name-only HEAD 2>/dev/null | grep -cE '\.(js|ts|py|go|rs|java|rb|php|jsx|tsx|vue|svelte)$')
# Build suggestion message
if [ "$SRC_CHANGES" -gt 0 ]; then
echo "[$SRC_CHANGES source file(s) changed] Consider running /verify to check your work."
elif [ "$CHANGED_FILES" -gt 5 ]; then
echo "[$CHANGED_FILES files changed] Consider running /verify quick for a health check."
fi
# Always exit 0 -- hook should never block Claude
exit 0
Make it executable:
chmod +x ~/.claude/hooks/stop-verify-nudge.sh
Add the Stop hook to your settings file. For global use, edit ~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/stop-verify-nudge.sh"
}
]
}
]
}
}
For project-specific use, add the same block to .claude/settings.json in your repo.
git diff --name-only HEAD to see what changed/verify/verify quickThe hook is async – it does not block Claude’s response. See Chapter 13 for details on hook execution.
The Stop hook could force verification by returning an error or blocking the response. It does not do this for three reasons:
All three components support both global and project-level installation.
~/.claude/ .claude/
commands/verify.md <--> commands/verify.md
agents/verify-app.md <--> agents/verify-app.md
hooks/stop-verify-nudge.sh hooks/stop-verify-nudge.sh
settings.json settings.json
Global (~/.claude/): Generic, auto-detects tech stack, works with any project. Install once, use everywhere.
Project (.claude/): Can add project-specific checks – custom health endpoints, specific test suites, Sacred compliance validation, linter configurations. Gets committed to the repo so the whole team benefits.
Override rule: When both exist, the project-level version takes precedence. This lets you install a generic global /verify and override it in specific projects that need custom checks.
.claude/commands/verify.md that adds project-specific checksThese are the copy-paste versions of all three components.
Save to ~/.claude/commands/verify.md:
---
allowed-tools: Bash, Read, Grep, Glob
description: Verify recent changes with auto-detected scope
---
# Verification: $ARGUMENTS mode
## Context
**Branch**: !`git branch --show-current`
**Changed files**:
!`git diff HEAD --name-only`
**Server**: !`curl -s -o /dev/null -w "%{http_code}" http://localhost:${PORT:-8080}/health 2>/dev/null || echo "not running"`
## Instructions
Mode "$ARGUMENTS" (default: quick):
- **quick**: Health check + syntax check on changed files
- **deep**: Quick + test suite + lint + modularity (500-line limit)
- **auto**: Detect from changed files (docs=quick, source=deep, tests=run them)
Report as PASS / FAIL / SKIP for each check.
Save to ~/.claude/agents/verify-app.md:
---
model: sonnet
allowed-tools: Bash, Read, Grep, Glob
permissionMode: auto
memory: project
---
# Verification Agent
Run 3-tier verification on recent changes:
1. **Static** (always): syntax check changed files, conflict markers, file size >500L
2. **Health** (if server running): hit health endpoint on ports 8080/3000/5173/8000
3. **Tests** (if test infra exists): npm test / pytest / go test
Auto-detect tech stack from package.json / requirements.txt / go.mod / Cargo.toml.
Report: PASS/FAIL/SKIP per tier, list issues, suggest fixes.
Save to ~/.claude/hooks/stop-verify-nudge.sh and run chmod +x on it:
#!/bin/bash
CHANGED=$(git diff --name-only HEAD 2>/dev/null | wc -l)
SRC=$(git diff --name-only HEAD 2>/dev/null | grep -cE '\.(js|ts|py|go|rs|java|rb|jsx|tsx)$')
if [ "$SRC" -gt 0 ]; then
echo "[$SRC source file(s) changed] Consider running /verify to check your work."
elif [ "$CHANGED" -gt 5 ]; then
echo "[$CHANGED files changed] Consider running /verify quick."
fi
exit 0
Add to ~/.claude/settings.json (merge with existing hooks if present):
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/stop-verify-nudge.sh"
}
]
}
]
}
}
!`command`) in the commandThe !`git diff HEAD --name-only` syntax runs at command invocation time, not when Claude processes the prompt. This eliminates an entire round-trip. Without it, Claude would need to call Bash("git diff HEAD --name-only") as its first action, wait for the result, then proceed. With dynamic injection, the diff is already in the prompt. Zero round-trips for context gathering.
One important caveat: the !`...` preprocessor only finds injections written as bare text in the markdown. If the injection is nested inside another code fence or escaped, it renders as literal text instead of being executed. See the “How Dynamic Injection Works” section above for the correct format. Note that an earlier draft of this chapter used the $!command!$ syntax, which does not work – the correct syntax is !`command`.
Stop hooks run after Claude’s response is complete. They cannot modify the response – they can only append output that Claude sees on the next turn. This is by design. A synchronous verification gate would slow every response, even ones that don’t change code. The async nudge only costs anything when it has something useful to say.
Requiring the user to specify “this is a Node.js project, run npm test” defeats the purpose. Both the command and agent detect the tech stack from project files (package.json, go.mod, etc.) and choose the right tools. This means /verify works in any project without configuration.
Not every project has a running server. Not every project has tests. The 3-tier design means:
A “SKIP” is not a failure. It means the check was not applicable.
/verify command reduces a multi-step checklist to one action. Dynamic injection (!`command`) pre-computes context so Claude can verify immediately with zero round-trips.verify-app agent runs in a fresh context window, so verification does not consume your main conversation’s context budget. Spawn it via Task() after multi-step changes.Previous: 49: Workflow Resilience Patterns