One broken minified JavaScript file got past us once. The informal check in my head was not a machine proving the result still parsed, and by the time anyone noticed, the broken file was already committed and already deployed. I was the check. I was wrong, and nothing caught it until after it had already shipped.
The failure exposed a broader design problem. A rule I have to remember is not a repository rule. A rule an AI agent has to rediscover at the start of every task is not much better. We work from four clones, each another place where a good convention can become an absent control.
I stopped trying to make the team, or an agent, better at remembering the check. I made the repository install the check.
The gap was between two steps
The diagnostic path was simple. We had a minification path and the ability to test JavaScript syntax, but not one enforced path binding the two together. A compressed asset can look complete, especially when review means reading one long line of generated code. Our actual requirement was stricter: after minification, Node must be able to parse it.
That distinction led to a small wrapper rather than another reminder. The new ai/scripts/compile-js.sh is 61 lines. It runs terser, then makes node -c verify the resulting JavaScript. The two commands now belong to one named operation. If either command fails, there is no successful compilation path to pretend was complete.
The wrapper fixed the immediate failure mode, but only for someone who chose to run it. That was not sufficient. A safety check that can be bypassed accidentally by taking a familiar path is still relying on memory.
Why the obvious fixes lost
Documentation can tell a developer or agent to syntax-check dated JavaScript before commit. It cannot make that check occur when the commit is made.
I also considered putting the whole test in remote CI and stopping there. CI would catch the bad artifact later, but later is the wrong side of a commit boundary for this problem. The useful failure is local, specific, and immediate. The person or agent making the commit should see that a dated JS file is syntactically broken before it enters history.
A conventional Git hook installed manually into .git/hooks lost for the same reason. It works only on the checkout where someone remembered the installation step. With four active clones, the next clone is not an edge case. It is normal work.
So the control needed two layers. It needed a pre-commit gate that could reject the offending change. It also needed an installation path that ran without anyone remembering it.
The hook that installs the hook
On April 25, we closed TASK-6861 with 137 lines across four files. The 48-line ai/scripts/git-hooks/pre-commit script blocks any commit containing a syntactically broken dated JS file. That is the enforcement point. It makes the policy executable at the point where an invalid change would otherwise become a commit.
The other half lives in .claude/hooks/install-pre-commit.sh, an 18-line installer. A SessionStart configuration in .claude/settings.json runs it for the developer role. The installer sets Git’s core.hooksPath for that clone, pointing Git at the repository-managed hook path rather than a manually populated local directory.
That configuration detail is the whole design. The pre-commit script is checked into the repository, but checked-in code does nothing if Git has no reason to call it. core.hooksPath makes the hook discoverable. SessionStart makes that configuration happen again wherever the repository is opened in a developer session. A new clone is no longer a blank spot in the safety model.
This is not an elaborate policy engine. It is an 18-line shell script assigning one Git configuration value, paired with a hook that checks a narrow file class. The sequence matters: session begins, Git configures the hook path for this clone, a commit is attempted, the hook checks the dated JS file, and a broken file is rejected.
Each part has a different job. terser creates the minified output. node -c verifies syntax. The pre-commit hook makes the verification mandatory when relevant files are committed. SessionStart propagates that mandatory check across the four clones we actually use.
The rule is now part of the environment
I used to think a good guardrail was a carefully written instruction. This incident changed the test. A guardrail is only real when a fresh working copy receives it before the next safety-sensitive action.
That is especially important when AI agents are involved. An agent can read a runbook, follow a checklist, and still take a path that does not include the check. I do not want a safety property to depend on whether an agent happened to load the right context or whether I remembered to mention an old failure. We can make the repository carry the condition forward instead.
The durable pattern is not “add a hook.” It is to trace every required safety rule to the moment it must act, then make its installation part of the normal startup path. The pre-commit hook is 48 lines. The installer that makes a fourth clone inherit it without anyone remembering is 18. Neither number is impressive. Together they are the difference between a rule I have to remember and one I don’t.