The hook
Yesterday in QW-01, a status file lied. Today it's a counter. Same class of error, different scale. The first card costs you time; this one can cost you thirty rows you'll never get back.
What broke
May 16, morning. I want to purge orphan sessions surfaced by a daily probe. Standard SQL probe: SELECT COUNT(*) FROM seances WHERE cours_id NOT IN (SELECT id FROM cours) returns 351. I draft the DELETE, I review it, I go get coffee. Ten minutes later, out of habit, I rerun the probe before hitting ENTER. Answer: 381. Thirty rows appeared during the break, because a session-generation cron ran once in the meantime.
Had I fired the DELETE confident in the 351 I had in my head, it would have swept a perimeter that was already out of phase. Not catastrophic on an orphan audit — but on a DELETE FROM contacts WHERE statut='liste_rouge' calibrated in the morning and executed in the afternoon, an entire data category vanishes when an unexpected import slipped in. That day I got lucky. Next time I won't.
The cost isn't in the thirty wrong rows of the day. It's in the trust you grant to a count you saw twenty minutes ago, treating it as valid while the system kept breathing without you. It's exactly the same mechanic as QW-01 — the agent-pilot believing their own summary. Except here the summary is a number, and the stale summary becomes a command.
The hook that refuses stale DELETE
Drop in ~/.claude/hooks/pre-bulk-mutation-count-staleness.sh. Blocks any bulk mutation that doesn't carry an explicit "I just recounted" marker. The bypass is named: no silent drift, intent is inscribed in the command itself.
#!/usr/bin/env bash
# Block bulk DELETE/UPDATE without a fresh count marker in the SQL.
# Bypass: append `-- count-fresh:YYYYMMDD-HHMM` to your statement.
# The hook trusts your honesty on freshness — its job is to break
# the autopilot, not to police you. The marker forces you to type
# the act of recounting; the discipline of typing it truthfully is on you.
case "$CLAUDE_TOOL_INPUT" in
*DELETE\ FROM*|*UPDATE\ *SET*)
[[ "$CLAUDE_TOOL_INPUT" =~ count-fresh:([0-9]{8}-[0-9]{4}) ]] \
|| { echo "BLOCKED: bulk mutation without count-fresh:YYYYMMDD-HHMM marker"; exit 1; }
;;
esac
Two conditions, ten lines, an error message that tells you exactly what to write to pass. The hook doesn't guess your intent — it requires you to spell it out. It doesn't police you either: paste yesterday's timestamp and you go through. The doctrine doesn't replace the discipline, it makes it visible.
ROI
The hook doesn't save time, it closes an incident class. A bulk mutation that misjudges its perimeter costs anywhere from ten minutes (clean rollback from a snapshot) to half a day (manual line-by-line reconstruction with log cross-checking) — and that's the cases where you notice. In the others, production has quietly lost an entire data category and nobody knows. The real win isn't measurable in minutes. It's the disappearance of a kind of incident that should never have existed.
Apply now
Drop the hook in ~/.claude/hooks/, make it executable, register it in your settings.json under hooks.preToolUse. Next time your agent (or yourself) prepares a DELETE against a count seen twenty minutes ago, the hook replies "BLOCKED" and asks you to re-probe before signing. Append -- count-fresh:20260516-1107 to your SQL once the probe is fresh, and the command goes through. An agent's context is, by construction, stale — this hook doesn't only protect you, it protects your agent against its own mental cache.
Your quick win takes five minutes — the time to copy the hook, test it against a fake DELETE, and let it guard your sleep tonight.
Quick Win Card series, episode 02. Counterpart Toolkit v0.7, amendment R7 promoted 20/05/2026. Doctrine repo: github.com/michelfaure/doctrine-counterpart. Direct sequel to QW-01 — same class of error, different scale.

Top comments (0)