Build1 publisher2 min readPublished
PreToolUse hooks now enforce the rules one developer's coding agent kept skipping
One developer moved about a dozen agent rules out of an instructions file and into Claude Code PreToolUse hooks after watching them slip in long contexts. Each hook blocks a tool call before it executes, though it guards only the command shapes its pattern matches.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- Each hook is a small program that receives the agent's pending tool call as JSON on stdin and returns a decision, with a reason, before the tool runs.
- One guard blocks merge commands outright, because the author holds that merging is a human's decision and that an agent able to merge will eventually do it at 2am.
- Local type-checking is blocked entirely, after a cold run across the monorepo cost about 50 seconds of CPU and one process peaked at 5.6 GB.
- A timestamp guard exists because the analytics language reads bare literals in the project timezone, a ten-hour shift that once made a feature flag look like it was ramping.
- Writing 'do not write comments' twice in capitals did not hold reliably, while a hook that blocks multi-line comment blocks works every time, the author reports.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure An agent can rewrite a file with sed -i and skip every check attached to the edit tools, so each protected rule has to be enforced against shell commands as well.
- constraint Each guard becomes code that needs its own tests, because it blocks only the command shapes it was written to recognise and can also refuse harmless ones.
- cost Blocking local type-checks moves type errors to CI, so an agent finds them only after it pushes.
- decision Every rule now needs a placement decision: outcomes that are irreversible, expensive or silently wrong go into hooks, and prose keeps the guidance an agent can afford to miss.
The case rests on one developer's experience, written up on dev.to. The author wrote that the agent follows the instructions file "most of the time," and that the misses come when "the file is long, the task is interesting, and the rule is the least salient thing in a 30k-token context" [1]. The post does not count the misses or compare models, so the pattern is a field report from one codebase. The design argument stands without the count. "A rule in prose competes with every other sentence in the file for attention. A rule in a hook does not compete with anything. It just runs," the author wrote [2].
The detail I would copy is where the refusal goes. The hook's reason is returned to the model, not to the operator [6]. A precise reason lets the agent correct itself and carry on in a few seconds. A bare "blocked" leaves it to guess, flail or ask a human [6]. "The reason string is the whole product," the author wrote [7]. I think that is correct. The type-check guard's reason tells the agent that CI type-checks every push, so the agent commits instead of grinding through a local run [9].
Blocking type-checks outright looks extreme until the memory is counted. Several agents in separate worktrees will start the check at once on the author's 24 GB laptop [9]. At the reported 5.6 GB peak, four concurrent runs take 22.4 GB, and a fifth would need 28 GB [3].
The sample database guard blocks a command when it contains "psql" and, uppercased, "DELETE" [8]. A DELETE sent through another client passes it. A read-only psql query naming a deleted_at column is refused, because "DELETED_AT" contains "DELETE" [1]. The post shows that code as an example of the technique [3]. Its production database guard, as described, also covers truncates, cache flushes and a session-level SET through a connection pooler [14].
The timestamp guard has the same limit. Its regex wants a column name containing timestamp or _at, then a comparison operator, then a quoted date with no explicit offset; the whole hook is forty lines including the explanation [11]. The author wrote that the bug "cannot reach me any more, from any session, in any file or shell command" [12]. A clause written as `timestamp BETWEEN '2026-07-03 00:00' AND ...` gets through, because no comparison operator follows the column name [2].
What to watch
- A measured test of instruction-file adherence at different context lengths, which would show whether the 30k-token failure pattern holds outside one codebase.
- Whether the author's guards move from substring and regex matching to parsing the SQL or shell command, the change that would close the gaps in the published examples.