The guard passed every unit test. Then we asked a fresh Claude Code session to write a file that should have tripped it. The Write succeeded, and no hook fired.
What the guard was for
On July 21 at about 3:28 PM, a session ran a leading-wildcard LIKE '%...' scan serially over six cache tables in our shared production database. The worst table took 139 seconds and 835K logical reads. The app’s own cache DELETEs hit the 30-second ODBC timeout, and every customer account page errored for about five minutes. A “read-only probe” still takes locks and input/output, and all our clones point at one live database.
So we wrote a PreToolUse hook. It reads the raw JSON payload on stdin and blocks when two things appear together. One is a prod-DB connection marker: our credential env vars, a DSN string, pyodbc, sqlcmd, Invoke-Sqlcmd. The other is a LIKE followed by a quoted string starting with %. It has no dependencies, just grep -E on the payload, so it runs the same for everyone from the checked-in repo. The registration matches Bash|Write|Edit|MultiEdit.
It started life on one desktop. This week we shipped it from all three repos. One of them, the Node API that talks to the same live database, had no .claude directory at all.
Two probes that walked past it
The unit tests fed the hook literal SQL like WHERE cacheKey LIKE '%foo'. That is the shape the regex was written against, so of course it passed.
Live testing meant prompting fresh sessions to write a script with the wildcard first in the pattern, then report verbatim whether the Write was blocked. Two probes:
- Node with
mssql. The script did.input('pattern', sql.NVarChar, '%session')and then.query('... WHERE cacheKey LIKE @pattern'). TheLIKEis followed by@pattern, not a quote, so the scan regex never sees it. Worse, the connection marker never matched either. That API readsDB_SERVER,DB_NAMEand so on, and none of our markers listed those. The port would have shipped a hook that could not fire in that repo. - PowerShell with
Invoke-Sqlcmd. The script built$pattern = '%' + $KeySuffixand putLIKE $(pattern)in the query with-Variable @("pattern='$pattern'"). The Write went through.
Chasing sigils
Our first move after probe 2 was to fix the regex. We had already handled the bare-literal case, so we widened it to parameterized ones: LIKE followed by a placeholder sigil. We wrote that regex into a replay harness that rebuilds the real PreToolUse payload for the Write and pipes it through the hook, and we ran it against the PowerShell payload:
conn: MATCHscan: no matchparam_like: no matchwildcard_literal: MATCHparam_like was LIKE[[:space:]]*[@:?\$][A-Za-z0-9_]. It covers @name, :name, ? and $name. sqlcmd variable substitution is $(name), a dollar sign followed by a paren, so it slid straight past. We had spent a cycle building the second special case for a guard whose whole job was to stop chasing special cases. Every driver spells its placeholder differently. That is the wrong shape for the match, and we would have kept finding new sigils in live tests, one production near-miss at a time.
The last line of that output was the useful one. wildcard_literal was N?['"][[:space:]]*%, a quote followed by %, with no LIKE in front. It matched the PowerShell payload, because the leading wildcard exists as a literal ('%') in the script whether or not the SQL text ever contains it. It also matches the Node payload’s '%session'.
Matching where the wildcard is born
The match no longer depends on the shape of the SQL. The condition is a connection marker plus a string literal that begins with %, anywhere in the payload, with no requirement that the literal sit next to LIKE. The wildcard has to originate somewhere as a quoted value, even when the SQL only ever sees a placeholder. Matching that origin covers @pattern, $(pattern), :pattern and whatever the next driver invents.
The connection markers gained the Node/mssql shape, so the guard can fire in the API repo. Then we re-ran both probes in fresh sessions, because passing the unit tests had already proven nothing once. Both were blocked.
What it still does not catch
The hook’s header already admits a known limit. CHARINDEX and PATINDEX over a whole table wedge prod just as well, but they have heavy legitimate bounded use, and matching them would false-positive constantly. Markdown files are exempt so incident write-ups can quote the pattern. It is a nudge with teeth, and the wildcard-literal match is broader, so it will occasionally fire on a harmless '%', which we accept.
What the replay caught
A unit test written by the person who wrote the regex checks the regex against the author’s imagination. Both bypasses came from an agent writing code the way agents actually write it, and only a payload replay from a real session caught them. Before trusting a guard on a shared production system, check it against payloads from real sessions, and treat the examples its author wrote as the easy half of the test.