01. The incident
Stopping a dev server is easy: pkill -f "ng serve". Except the command an agent runs
is itself a shell whose command line contains ng serve, so the pattern matches the
agent's own shell and kills the tool call with exit 143. The rules file said not to do it. It
happened 181 times in one week.
Now pkill -f and pgrep -f are denied in settings, and the rule says stop
a server by port with fuser -k 4200/tcp. It has not happened since. That is the whole
argument of this post in one example: an instruction an agent may skip under load is a script's job.
02. Four hooks
| Event | Hook | Blocks when |
|---|---|---|
| Before any shell command | git guard | a commit or push on main; a commit naming a local-only harness file; a push whose range carries one |
| After every edit or write | Prettier | never blocks; formatting just stops being a review finding |
| When an implementer agent stops | report check | its last message lacks Files:, Checks:, Deferred: and Plan edits: |
| When the spec-writer stops | word cap | today's spec exceeds 1,500 words, tables included |
the shape of a blocking hook// the tool call arrives as JSON on stdin; exit 2 blocks it
const { tool_input } = JSON.parse(await readStdin());
const cmd = tool_input.command ?? '';
if (/\bgit\b[^|;&]*\b(commit|push)\b/.test(cmd) && currentBranch(dirOf(cmd)) === 'main') {
process.stderr.write('make the branch first: git switch -c <name> origin/main');
process.exit(2);
}
Each hook replaced prose. The report check exists because manual checks used to be deferred
silently; now the word Deferred: is there every time. The word cap exists because a
prose cap nobody checked produced a 16,800-word spec. The git guard replaced four paragraphs
that four files repeated, slightly differently.
03. The deny list
Twenty-one denied commands, and each line has a story:
git reset --hard: in a checkout shared with other sessions, one moved the wrong branch. Every commit, push or reset now prints the current branch in the same command.- Stash pop, drop and clear: the stashes hold uncommitted local tooling, and stash indices shift, so a stash is cited by hash and only applied.
rm -rf,git clean, force push: obvious, and still worth writing down.- Queueing a pipeline: outward-facing, a human's call.
- The issue tracker MCP server's six write tools: a shell deny list does not cover MCP tool names, so a denied CLI command had a second door until those were denied too.
04. How to start
- Grep your rules file for "never" and "always". Each hit is a hook candidate.
- Start with the git guard. It is the one with the worst failure mode.
- Make the hook's message an instruction ("make the branch first"), because the agent reads it and tries again.
- Recheck after a platform upgrade: one matcher we rely on works but is not documented.
If a rule can be a script, it is a script.