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 -20to limit output size and avoid flooding the context with verbose test logs - The
matcherfield 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 checkwithnpm test,cargo test,pytest, or whatever your project uses