Lab · Intermediate · 60 min
A Hook That Blocks Commits When Tests Were Skipped
Write a pre-tool hook that intercepts git commit, checks that the test command ran successfully after the last edit, and blocks with a clear message if not.
Claude Code · Cursor
Show all 4 stepsHide list
Step 1 of 4
Lab brief
Goal
A deterministic guardrail that the agent cannot talk its way around, working in Claude Code and Cursor.
Failure mode
Memory file says 'always run tests before committing'; the agent commits anyway after a long session because the instruction scrolled out of attention.
Staff move
Move must-always rules from prompts into hooks, keep the hook fast, and make the block message tell the agent exactly what to do next.
Step 2 of 4
1. Hook logic
- A post-edit hook (PostToolUse / afterFileEdit) touches .agent/state/dirty
- A post-shell hook records success when the test command exits 0 and removes the dirty marker
- A pre-shell hook (PreToolUse on Bash / beforeShellExecution) matches git commit
- If the dirty marker exists, block: 'Tests have not passed since your last edit. Run pnpm test:unit, fix failures, then commit.'
Step 3 of 4
2. Wire it up
| Tool | Where | How it blocks |
|---|---|---|
| Claude Code | .claude/settings.json → hooks.PreToolUse with matcher Bash | Exit code 2 (stderr shown to the model) or a JSON deny decision |
| Cursor | .cursor/hooks.json → beforeShellExecution | Exit code 2 or a deny permission in the JSON response |
Fail safe
If the hook crashes, decide deliberately whether that allows or blocks. In Cursor, set failClosed for security-critical hooks.
Step 4 of 4
3. Verify
- Ask the agent to make a change and commit without testing — expect a block and a self-correction
- Break a test on purpose — expect the agent to fix code, not delete the test (add a second hook if it tries)
- Measure hook latency; keep it under ~100 ms