If you want Claude to always write tests before implementation, don’t just ask. Enforce it with a hook.
The Hook
Add to .claude/settings.json:
{
"hooks": {
"preToolExecution": [
{
"matcher": { "tool": "Write", "path": "src/**/*.ts" },
"command": "python3 .claude/scripts/tdd-guard.py",
"timeout": 5000
}
]
}
}
Guard Script
#!/usr/bin/env python3
import sys
import json
import os
input_data = json.loads(sys.stdin.read())
file_path = input_data.get("path", "")
# Only check source files, not test files
if ".test." in file_path or ".spec." in file_path or "/tests/" in file_path:
sys.exit(0)
# Check if a corresponding test file exists
test_variants = [
file_path.replace(".ts", ".test.ts"),
file_path.replace(".ts", ".spec.ts"),
file_path.replace("/src/", "/tests/"),
]
for test_path in test_variants:
if os.path.exists(test_path):
sys.exit(0)
print(json.dumps({
"blocked": True,
"reason": "Write the test file first. No test found for: " + file_path
}))
sys.exit(1)
What Happens
When Claude tries to write src/auth/login.ts without src/auth/login.test.ts existing, the hook blocks the write and tells Claude to create the test first. Claude then writes the test, and on the next attempt the source file goes through.
Tip
Combine with the TDD workflow tip. Add “Write tests first, then implement” to your CLAUDE.md, and use this hook as the enforcement backstop.