How to Auto-Run Tests with Claude Code Hooks: PostToolUse, Stop, and Minimal Verification

A practical guide to auto-running tests with Claude Code Hooks: when to use PostToolUse, when to use Stop, how to choose minimal test commands by file type, and how to avoid triggering the full suite after every small edit.

Using Claude Code hooks to run tests automatically is useful for one common problem: you may have written “run tests after changes” in CLAUDE.md, but Claude Code may forget, run too much, or summarize a failure too loosely.

Hooks turn “please remember” into “run this when the event happens.” For testing, the two most useful events are PostToolUse and Stop.

Quick Answer

Use PostToolUse when you want a lightweight check right after Claude edits or writes a file. Limit the matcher to tools such as Edit|Write.

Use Stop when you want a final reminder or narrow verification before Claude finishes a turn. Do not put the full test suite there by default.

A stable setup usually looks like this:

  1. PostToolUse runs lightweight checks, such as formatting, type checking, or file-type-based focused tests.
  2. Stop prints a final verification checklist or runs a narrow final script.
  3. Test failures return a short summary, not a full log dump.
  4. Project hooks live in .claude/settings.json, with real logic in .claude/hooks/.
  5. Test the setup in a small repository before using it across a team.

Decide What Should Run

Do not start with:

1
npm test

The closer a command is to each edit, the shorter and more deterministic it should be.

Scenario Suggested check
A frontend component changed Related unit test, typecheck, lint
A backend function changed Related package unit test
A config file changed Config validation or build dry run
Markdown changed Link or front matter check
Turn is ending Minimal final verification reminder
Commit or release Full tests and build, usually manual or CI

Hooks should not replace CI. They are a local safety net that helps Claude notice obvious problems quickly.

Do not pack complex logic into a one-line settings.json command.

1
2
3
4
5
6
your-repo/
  .claude/
    settings.json
    hooks/
      run-related-tests.sh
      final-check.sh

On Windows, use PowerShell scripts if that is what the team normally runs:

1
2
3
4
5
6
your-repo/
  .claude/
    settings.json
    hooks/
      run-related-tests.ps1
      final-check.ps1

Use PostToolUse After Edits

PostToolUse fires after a tool call. For automated tests, usually match file-writing tools only.

.claude/settings.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-related-tests.sh"
          }
        ]
      }
    ]
  }
}

This registers the hook. The file-to-test logic belongs in the script.

Bash Example

.claude/hooks/run-related-tests.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env bash
set -euo pipefail

INPUT="$(cat)"
FILE_PATH="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')"

if [ -z "$FILE_PATH" ]; then
  exit 0
fi

case "$FILE_PATH" in
  *.ts|*.tsx)
    echo "Running TypeScript checks for $FILE_PATH"
    npm run typecheck -- --pretty false
    ;;
  *.test.ts|*.spec.ts|*.test.tsx|*.spec.tsx)
    echo "Running focused test for $FILE_PATH"
    npm test -- "$FILE_PATH" --runInBand
    ;;
  *.py)
    echo "Running Python checks for $FILE_PATH"
    python -m pytest -q
    ;;
  *.md)
    echo "Markdown changed: skip code tests"
    ;;
  *)
    echo "No related test rule for $FILE_PATH"
    ;;
esac

Then make it executable:

1
chmod +x .claude/hooks/run-related-tests.sh

PowerShell Example

.claude/hooks/run-related-tests.ps1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$inputJson = [Console]::In.ReadToEnd()
if ([string]::IsNullOrWhiteSpace($inputJson)) {
  exit 0
}

$payload = $inputJson | ConvertFrom-Json
$filePath = $payload.tool_input.file_path

if ([string]::IsNullOrWhiteSpace($filePath)) {
  exit 0
}

switch -Regex ($filePath) {
  '\.tsx?$' {
    Write-Output "Running TypeScript checks for $filePath"
    npm run typecheck -- --pretty false
    break
  }
  '\.(test|spec)\.tsx?$' {
    Write-Output "Running focused test for $filePath"
    npm test -- $filePath --runInBand
    break
  }
  '\.py$' {
    Write-Output "Running Python checks for $filePath"
    python -m pytest -q
    break
  }
  '\.md$' {
    Write-Output "Markdown changed: skip code tests"
    break
  }
  default {
    Write-Output "No related test rule for $filePath"
  }
}

Corresponding settings:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "powershell.exe -ExecutionPolicy Bypass -File \"$CLAUDE_PROJECT_DIR/.claude/hooks/run-related-tests.ps1\""
          }
        ]
      }
    ]
  }
}

Test the script manually before wiring it into hooks.

Keep Failure Output Short

Hook output enters Claude Code’s workflow. If a test fails, Claude may use that output to continue fixing the issue. Avoid dumping huge logs.

For example:

1
npm test -- "$FILE_PATH" --runInBand 2>&1 | tail -n 80

Hooks should run deterministic commands. Avoid interactive prompts, random ports, flaky services, or commands that require manual confirmation.

Use Stop for Final Checks

PostToolUse is for immediate checks. Stop is better for “before this turn ends, confirm what remains.”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/final-check.sh"
          }
        ]
      }
    ]
  }
}

.claude/hooks/final-check.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env bash
set -euo pipefail

if git diff --quiet; then
  echo "No working tree changes. Skip final checks."
  exit 0
fi

echo "Working tree changed. Suggested final checks:"
echo "- run the focused test command for touched files"
echo "- run typecheck if code changed"
echo "- run full test suite before commit or deploy"

Start with reminders before automatically running expensive commands.

When Not to Auto-Run Tests

Be careful when:

  • unit tests take more than about 30 seconds;
  • tests need databases, browsers, queues, or external APIs;
  • tests mutate local data;
  • logs are huge;
  • there is no stable focused test command;
  • Claude often edits many unrelated files at once.

In those cases, make the hook suggest a command instead of running it.

Risk-Based Strategy

Risk Automated action
Low formatting, single-file lint, Markdown checks
Medium typecheck, related unit tests, config validation
High full tests, E2E, build, migration checks

Low-risk checks can run in PostToolUse. Medium checks can run by file type. High-risk checks should usually stay in CI or require human confirmation.

CLAUDE.md and Hooks

CLAUDE.md is good for principles:

1
2
3
After code changes, prefer the smallest relevant verification.
When tests fail, fix the cause instead of bypassing the test.
Do not run full E2E for a small change unless the user asks.

Hooks are good for execution:

1
2
After Edit or Write, run run-related-tests.sh.
On Stop, print final verification suggestions.

Use both: CLAUDE.md says how to think; hooks make the action happen.

Troubleshooting

If a hook does not run:

  1. Run /hooks in Claude Code and check registration.
  2. Validate settings.json.
  3. Check event names such as PostToolUse and Stop.
  4. Check the matcher, such as Edit|Write.
  5. Check script paths.
  6. On macOS/Linux, confirm execute permissions.
  7. On Windows, check PowerShell execution policy and quoting.
  8. Start with echo hook fired.
  9. Do not start with complex test commands.
  10. If output is too long, summarize it.

Team Rollout

For team repositories:

  1. Start with final-check reminders.
  2. Automate low-risk checks such as formatting, Markdown, and typecheck.
  3. Add related unit tests by directory or file type.
  4. Leave full tests to CI or manual confirmation.
  5. Provide both sh and ps1 scripts if the team uses mixed systems.

References

Summary

Auto-running tests with Claude Code hooks is not about putting npm test into a config file. It is about splitting verification into small, relevant, repeatable checks.

Use PostToolUse for lightweight checks after edits, Stop for final reminders or narrow verification, and keep full test suites in CI or manual pre-release checks. Put principles in CLAUDE.md, actions in .claude/hooks/, and verify registration with /hooks.

记录并分享
Built with Hugo
Theme Stack designed by Jimmy