Claude Hookbook

Hooks / testing

Run the nearest test after an edit

Finds the test file that belongs to the file Claude just edited and runs it, in the background.

PostToolUseEdit|Writereview riskneeds manual check

What it does

Maps the edited file to its test twin using common conventions (foo.ts -> foo.test.ts, foo.py -> test_foo.py), runs that test with the right runner (vitest/jest via npx, or pytest), and reports failures back to Claude. Runs async so editing never waits on a test suite. Path mapping is heuristic - extend the case list for your layout.

When it runs

EventPostToolUse
MatcherEdit|Write
Scopeproject
Platformsmacos, linux
Asyncruns in the background, reports on exit
Timeout120s
Requiresjq
Failure modewarn
Verified againstClaude Code hooks reference, event/matcher schema checked 2026-09-10
Test statusneeds manual check - run the test steps below before relying on it

Config

Merge this into .claude/settings.json - or use the generator to combine hooks.

{
  "hooks": {
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-nearest-test.sh",
            "timeout": 120,
            "async": true
          }
        ],
        "matcher": "Edit|Write"
      }
    ]
  }
}

Script

Save as .claude/hooks/run-nearest-test.sh and chmod +x it.

#!/bin/bash
# run-nearest-test: run the test file paired with the edited file (async).
set -u
file=$(jq -r '.tool_input.file_path // empty')
[ -z "$file" ] && exit 0
dir=$(dirname "$file"); base=$(basename "$file"); stem="${base%.*}"
candidates=""
case "$base" in
  *.test.*|*.spec.*|test_*.py) exit 0 ;;  # edited file IS a test; skip
  *.ts|*.tsx|*.js|*.jsx) candidates="$dir/$stem.test.ts $dir/$stem.test.tsx $dir/$stem.test.js $dir/$stem.spec.ts" ;;
  *.py) candidates="$dir/test_$stem.py $dir/${stem}_test.py" ;;
  *) exit 0 ;;
esac
test_file=""
for c in $candidates; do [ -f "$c" ] && test_file="$c" && break; done
[ -z "$test_file" ] && exit 0
cd "${CLAUDE_PROJECT_DIR:-$PWD}" || exit 0
case "$test_file" in
  *.py)
    command -v pytest >/dev/null 2>&1 || exit 0
    if ! pytest -q "$test_file" >/dev/null 2>&1; then
      echo "Tests failed in $test_file after editing $file" >&2
      pytest -q "$test_file" 2>&1 | tail -20 >&2
      exit 2
    fi
    ;;
  *)
    if ! npx vitest run "$test_file" >/dev/null 2>&1 && ! npx jest "$test_file" >/dev/null 2>&1; then
      echo "Tests failed in $test_file after editing $file" >&2
      exit 2
    fi
    ;;
esac
exit 0

Install

  1. Save the script to .claude/hooks/run-nearest-test.sh and chmod +x it.
  2. Add the config fragment to .claude/settings.json. async: true is intentional - the hook runs in the background and reports on exit.

Test it

Sources

Related hooks

Typecheck after TypeScript changes

Runs tsc --noEmit in the background after Claude edits TypeScript, so type errors surface even when tests don't cover the file.

PostToolUseEdit|Writereview riskneeds manual check

Full check before task completion

Blocks Claude from marking a task completed while your project check fails.

TaskCompletedblocking riskneeds manual check

Pro Pack (later)

Framework bundles, cross-platform scripts, tested blocking policies, and a test harness. Free recipes and the generator stay free.

Waitlist opens soon.