A couple days ago I had a design review for a new capability in our system. Then I had AI turn the design doc into tasks and send agents to work on what they could in parallel to unblock other tasks. They created code reviews for me to look at and it was terrifying.
No, all the tests were written and passing, and technically all these PRs were “green” and ready to merge. But the code quality wasn’t up to the standards we’d expect from a human during peer review.
The AI agents were perfectly happy to just add more code to existing classes and methods without much consideration for the overall state of the codebase. Need a different path for some new behavior? A new argument and an if statement will do just fine.
They kept adding onto the existing mess, and now you also have a huge PR that nobody wants to look at. And if nobody looks at it, nobody really knows what’s in the codebase anymore.
You could argue that in the AI era it’s not necessary to know what’s in the codebase. But as someone who still goes on-call every few weeks and can get paged at 3am, I think I’d rather know.
Wrong layer of fixing it
The obvious fix is to put another instruction in AGENTS.md, CLAUDE.md, or whatever instruction file your coding agent uses.
I try not to rely on those too much. They’re useful context. I just don’t want to rely on context for something I want enforced every time. In longer sessions I’ve seen agents drift from instructions that were perfectly clear earlier. And yes, I try to use smaller sessions, but that’s not always possible. What I really want is a checkpoint.
Most of my AI setup already consists of hooks and loops. I have a “Don’t ask” gate, for example. Whenever an agent asks me whether it should continue with something obvious, the hook tells it to continue without making me answer the question again. So I added another hook. Before writing code, stop and decide whether the existing structure can absorb the change or whether it needs to change first.
I built this version in Claude Code, but Codex has lifecycle hooks too. The configuration differs between agents, but the checkpoint is the same.
The first hook stops the edit
In Claude Code, PreToolUse runs before a tool call such as Edit or Write, and it can block the call. That’s exactly what I wanted.
The first time Claude tries to modify a source file in a session, my hook stops it and gives it something roughly like this:
BLOCKED: no boundary plan exists for this session.
Before editing source code:
1. Decide whether this change should EXTEND the current design or REFACTOR it first.
2. Consider at least one cleaner boundary: a new type, strategy, collaborator, or small refactor.
3. Pick the recommended approach.
4. Define the smallest reviewable first slice.
5. Explicitly defer anything that does not belong in that slice.
Then it writes the plan. Only after that does it get to touch the source code. This sounds almost stupidly simple. It also works surprisingly well.
The useful part isn’t the five-line plan. An agent can obviously write five lines of nonsense if it wants to. The useful part is forcing the decision to happen before it commits to an implementation. Stopping the first edit gives it one chance to look at the existing code and say, “Actually, maybe adding boolean enableNewBehavior here is a terrible idea.” Which is exactly what I want.
Then I added another hook because agents still get carried away
The first hook helps with the initial design. It doesn’t stop a small change from slowly turning into a monster. In fact, yesterday an agent tried to hand me a PR with about 17 changed files. I said, ‘no, thanks.’ Then I added another hook after source edits.
It looks at the current diff and counts production code, excluding tests. My current warning thresholds are more than 200 added non-test lines or more than 8 non-test files touched. Those numbers are completely arbitrary. I’ll probably adjust them as I use this more.
Once the diff crosses one of those thresholds, the agent gets another message:
Reviewability check:
This change now spans 9 non-test files and 237 added non-test lines.
Does it still change exactly one thing?
Is refactoring being mixed into the feature implementation?
If the change now contains multiple independent ideas, split them.
In Claude Code I do this with PostToolUse. It runs after the tool has already succeeded, so this one is a reminder, not an undo button.
Having smaller changes means I can actually review every line and catch gaps, if any. That’s what we’d usually tell a coworker anyway when they try to refactor the whole codebase in one commit. Or you just say LGTM and move on with your life if you’re not on-call that week. And yes, I’ve had such coworkers. I’ve also been one of those coworkers when I was a newer dev.
How you can set it up
My actual hooks have helper functions for source paths, test detection, session markers, Git state, cooldowns, and some internal repo-specific stuff. I could clean all of that up and paste it into this newsletter. I don’t think that would help most people.
Your source file patterns are probably different. Your test paths are probably different. Your default branch might be main, master, or something custom. And if you’re on Windows, copying a Bash script can be a bad experience. I think the more useful version is to let your coding agent generate the hooks for its own environment.
Give it this (fully AI-generated based on my existing hooks, though those were also AI-generated):
Prompt
Create two lifecycle hooks using the hook system supported by the coding agent I am currently running.
Before implementing anything, inspect the current hook configuration/schema for this agent. Do not assume Claude Code hook names or configuration apply to another agent.
The first hook should run before source-code edits.
Before the first source-code edit in a task or session, require a short boundary plan. The plan must say whether the change should extend the existing design or refactor first, consider at least one cleaner-boundary alternative, choose a recommended approach, define the smallest reviewable first slice, and state what is deferred.
Verify that the hook actually fires for the file-write tool this agent uses. If the hook system can’t intercept that tool, tell me instead of generating a configuration that only appears to enforce it.
Keep the plan scoped to the current task/session using whatever stable session or task identifier the agent exposes. Store it somewhere that is not itself treated as a gated source file.
If a valid plan does not exist, block the source-code edit and explain exactly what the agent needs to write before retrying.
The second hook should run after source edits.
After source edits, measure the current change against the merge-base with the repository's default branch. Include both tracked changes and new untracked source files. Exclude tests.
When the change exceeds either 200 added non-test lines or 8 non-test files, add context asking whether the change still represents one cohesive change and whether refactoring has been mixed with feature work.
Make both thresholds configurable with environment variables and add a 30-minute per-session cooldown to the warning.
Fail open if required tooling is unavailable. Quote shell variables correctly. Do not assume the default branch is origin/main if it can be detected.
Generate the hook configuration required by this agent.
Before finishing, syntax-check the scripts and show me how to test each hook with representative hook input.
Finally, explain any ways these hooks can be bypassed, including source files modified through shell commands or external processes instead of the agent's normal edit tool.
Hopefully, your AI agent can figure it out. If it can’t, give it this whole article.
There are some holes
Plan quality is still on the honor system. The hook can prove that the agent wrote a plan. It can’t prove the plan isn’t garbage.
And a hook attached only to the normal edit tools only sees those tools. Shell commands and outside processes can modify files too. Claude Code now has a FileChanged hook as well, which can detect changes made through shell commands or outside processes.
The bigger lesson for me is that whenever I catch myself giving an agent the same feedback over and over, I should probably stop repeating myself and turn it into a checkpoint.
Have you added any guardrails around your coding agents yet? What behavior do you keep having to correct?
Cheers!
Evgeny Urubkov (@codevev)