Skip to content

Use Claude Code stop hooks for deterministic QA enforcement

pattern

Claude Code wastes tokens on reminder instructions to run tests and often skips QA checks entirely after context compaction

hooksciclaude-codetestingqamakefile
22 views

Problem

When working with Claude Code on long sessions, you end up burning tokens on repeated instructions like "make sure you run the tests" and "check linting before you finish." Worse, after a context compaction event (when Claude Code summarizes and truncates the conversation to free up context), these instructions are often lost entirely. The agent then proceeds without running tests, pushing broken code or missing regressions that a simple make test would have caught.

Solution

Configure a Stop Hook that runs deterministic QA checks after every agent turn. Stop hooks execute automatically when Claude Code finishes responding, so they enforce quality gates regardless of what the agent remembers.

Hook configuration in .claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "cd $PROJECT_DIR && make check 2>&1 | tail -20"
          }
        ]
      }
    ]
  }
}

Makefile with deterministic QA targets:

# Makefile
.PHONY: check test lint typecheck

check: lint typecheck test

lint:
	npx eslint . --max-warnings 0

typecheck:
	npx tsc --noEmit

test:
	npx vitest run --reporter=verbose

For projects using Go:

# Makefile
.PHONY: check test lint vet

check: lint vet test

lint:
	golangci-lint run ./...

vet:
	go vet ./...

test:
	go test ./... -count=1 -race

Hook output feeds back into the conversation. If the check fails, Claude Code sees the error output and can self-correct:

Stop hook output:
FAIL src/auth.test.ts > login > should reject expired tokens
  AssertionError: expected 200 to be 401
  ❯ src/auth.test.ts:42:18

make: *** [test] Error 1

Claude Code receives this output and fixes the failing test before the user ever sees it.

Why It Works

Stop hooks are deterministic -- they run the same command every time regardless of context window state, token budget, or what the agent "remembers." Makefile targets are also deterministic: make check always runs the same linting, type checking, and test suite. This creates a closed feedback loop where the agent writes code, the hook runs QA, and failures feed back into the conversation for the agent to fix. The loop converges toward passing code without any natural language instructions about testing, freeing up context window space for actual problem-solving.

Context

  • Stop hooks survive context compaction because they are configured in settings files, not in the conversation
  • The hook output is piped back into the conversation, giving Claude Code the chance to self-correct before the turn ends
  • Use tail -20 to limit output size and avoid flooding the context with verbose test logs
  • The matcher field is empty ("") to match all responses; you can narrow it with regex to only trigger on code-editing turns
  • This pattern replaces verbose CLAUDE.md instructions like "always run tests before finishing" that waste tokens and get lost after compaction
  • Works with any build system -- just replace make check with npm test, cargo test, pytest, or whatever your project uses
About this share
Contributormblode
Repositorymblode/shares
CreatedFeb 10, 2026
View on GitHub