Repository Guardrails for AI-Generated Code
Use isolated workspaces, merge-boundary enforcement, and explicit policy files so coding-agent changes stay reviewable and low-risk.
Ask a coding agent to add bounded retry logic to one Java client, and you can still get a 14-file diff.
The retry code is there. So are a dependency change, a new interface, moved tests, one config edit, and a bootstrap tweak nobody asked for. That result does not look ridiculous. It looks plausible, which is worse. The agent fixed the local problem by expanding the job until the problem went away.
I keep coming back to this kind of run because it shows the failure more clearly than most AI coding demos do. We talk about prompts, context, and model quality. Those matter. But many bad runs are simpler than that.
The repository had no deterministic way to say no.
A change budget helps, but I want the bigger control stack around the diff.
Prompts are guidance
Teams usually start with the polite fix. Add a stronger system prompt. Add AGENTS.md. Add one rule about touching fewer files, another about public APIs, and one more about architecture. That is little more than wishful thinking in the end.
If the same model defines the scope, edits the code, explains the result, and defends the extra changes, you do not have an independent control point.
GitHub’s own responsible-use guidance for Copilot inline suggestions is pretty clear about this. Users still need to review and validate generated code, and the system may miss larger design or architectural issues. Its review guidance for Copilot pull requests says the same thing in a different place: review the pull request thoroughly before merge, and if the repository requires approvals, another reviewer still has to approve it.
That is exactly what is needed.
The phrase “human in the loop” usually hides way too much. A tired engineer reading a 40-file diff at the end is not a control plane. It is a human catching the mess late.
The repository needs a rejection path
The appeal of AI coding is real. The first pass is cheaper. Starting work is easier. We can try more things in less time.
Review still moves at human speed. And is tiring.
Someone still has to read the diff and decide whether the system deserves trust afterward. That means checking hidden contracts, failure paths, dependency choices, configuration fallout, tests, and the edits that look harmless until they hit production. If the change touches security, infrastructure, or public behavior, review gets expensive very quickly.
Sonar’s State of Code survey put numbers on that review bottleneck. It found that 96 percent of developers do not fully trust AI-generated code, only 48 percent say they always verify it before committing, and 38 percent say AI-generated code takes more effort to review than code written by human colleagues.
That sounds right to me. Demos measure generation speed. Real teams pay review cost.
Large diffs are where this gets really expensive. Files changed. CI passed. The task is marked done. Then a reviewer has to figure out whether the pull request contains one change or four.
That is why I vouch for a repository-side rejection path. The question I care about is simple: can the repository stop the change before review becomes manual guesswork?
The stack I actually recommend
I do keep the control stack simple and boring. You have read that before from me. Start with an isolated workspace. A separate worktree, branch, container, or ephemeral environment is the minimum boundary of trust. Git already gives us a good primitive here. The official git-worktree documentation describes worktrees as multiple working trees attached to the same repository. That is boring infrastructure, which is exactly what I love. A bad local run should be cheap to inspect and cheap to throw away.
After that, put a fast local gate in front of the commit. This is where local hooks make sense. They are good at rejecting mistakes early, which keeps review queues cleaner and teaches the agent that the repository has real edges. Keep this layer narrow: staged secret scanning, staged diff policy checks, a few fast source-pattern rules, maybe one small structural rule if the project is small enough.
The authoritative gate belongs at the merge boundary. Compilation, tests, architecture checks, dependency policy, security analysis, branch rules, and required reviewers belong there. On GitHub.com that usually means required checks, protected branches or rulesets, codeowners, and dependency review. The repository decides whether the change lands, not the local shell session.
Sensitive paths need their own treatment. Workflow files, build policy, deployment descriptors, dependency manifests, security configuration, and suppression files should not be ordinary edit surfaces just because an agent can see them.
Exceptions should stay visible. New suppressions, policy-file changes, and bypassed gates should read like explicit risk decisions, not like formatting noise.
Tool roles matter more than tool count
In the end, not the number of scanners you pile into the pipeline counts but which job they do.
Secret scanning is a special case, so I keep Gitleaks in the stack. It is focused, and it already ships with a staged pre-commit mode. Secrets are different from general code smells. They deserve a tool that looks at the exact thing about to be committed.
Semgrep’s pre-commit workflow fits the fast policy layer well because it can reject obvious bad patterns without a full build. This is a good place for rules like disabling TLS verification, calling shell execution APIs, or reaching across boundaries the code should not cross. If your organization has a few specific footguns, this is one of the fastest ways to catch them.
Architecture boundaries deserve explicit tools instead of vague expectations. ArchUnit is strong when the architecture matters enough to express as tests. Checkstyle ImportControl is smaller and more mechanical. Both are better than a review comment that says, “we usually do not do that here.”
Guard Your Code: Enforcing Architecture Boundaries in Quarkus with ArchUnit
Architecture is a constraint. Good constraints free teams to move fast without breaking the system. This tutorial shows how to codify architectural boundaries in a Quarkus app using ArchUnit, borrowing Boundary–Control–Entity (BCE) ideas popularized by Adam Bien. We’ll build a tiny “orders” service, express a few pragmatic rules, and enforce them with t…
Maven Enforcer is a good addition for traditional Java applications. If the agent quietly adds a repository, widens dependency drift, or breaks a build rule the team depends on, I want the build to reject it immediately.
I would still keep SpotBugs and Find Security Bugs in the tool-chain also. They are imperfect, like every static analysis tool, but they still find problems that look fine in a diff and less fine after compilation.
OWASP Dependency-Check belongs in CI or scheduled verification, not in pre-commit. Its first run may take 20 minutes or more because it downloads and processes NVD data. That belongs in the remote gate. I would not put it in a hook that people will start bypassing out of self-defense.
Every team can start smaller. Just raising “more warnings” is not a security plan.
A few rules worth being stubborn about
Some rules are small, but they change the quality of the whole stack.
Scan staged content, not the working tree. Partial staging is normal with Git. If the hook inspects one version while Git commits another, the control is already lying to you.
Keep local hooks fast. A pre-commit gate that feels like a build pipeline teaches people to reach for --no-verify. The local layer is for speed. The authoritative layer is CI and merge policy.
Fail closed on tool errors. If a scanner is missing, a ruleset is broken, or a hook cannot run the check it was supposed to run, that is a failed control.
Pin tool versions and rulesets. Otherwise the same change passes on one machine and fails on another, which is a fast way to turn policy into folklore.
Treat new suppressions as policy exceptions. I do not want an agent adding a broad @SuppressWarnings, a // nosemgrep, a new SpotBugs suppression, or a changed exclusion file without extra scrutiny. Suppressions are decisions to accept risk or noise.
Protect the policy surface itself. Hook scripts, workflow definitions, architecture tests, scanner configs, and dependency-policy files should not be ordinary collateral damage in a feature diff. If the agent changes the control system, that should be obvious and intentional. Maybe even add the configuration of those tools to the respective ignore-lists for your agents.
Baseline legacy findings and reject new ones. Older repositories often have too much existing noise for a clean fail-on-anything policy. Fine. Review the baseline once, then block new violations instead of pretending the only choices are perfection or surrender.
Java teams already own most of the right instincts
One reason I like this topic for Java teams is that the answer is less exotic than the AI conversation makes it sound.
You do not need a new spiritual framework for agent governance. You need explicit boundaries, deterministic checks, and a clear merge policy. Most Java teams already know those ideas. They may just need to apply them more deliberately now that code arrives faster. Don’t fear to keep something alive that is working and established just because a new tool arrives.
If an agent only looks productive because it can expand scope faster than humans can review it, the main goal has to be to strengthen the enforcement layer you already trust. Give tasks boundaries. Give the workspace an isolation story. Give the repository a rejection path. Give reviewers diffs that still fit in human working memory.
Review cost is the metric
If I had to remove one success metric from the current AI coding conversation, I would remove generated volume.
I do not care how many files the agent touched if half the runs get discarded. I do not care how quickly the draft appeared if a senior engineer still needs 45 minutes to decide whether the accepted diff contains unrelated changes. I do not care how many suggestions were accepted if the team learns to distrust every large pull request.
The numbers I would actually watch are much less exciting:
Files changed per accepted task
Percentage of runs discarded
Human review time per accepted change
Rate of policy or hook violations
Frequency of protected-path touches
How often a “small task” needs scope elevation
Those numbers tell me whether the system is getting easier to review with confidence.
That is the bar I care about. Not how much code the agent can generate, but whether a busy team can still understand the accepted change in one pass.
The point
I am not trying to make coding agents perfectly obedient. I want their mistakes to stay cheap. That goal feels realistic to me, and it is enough.
We need systems where agent mistakes stay small, visible, and reversible. The workspace is isolated. The hook layer is fast. The merge boundary is real. The tools have clear jobs. The protected paths are obvious. The reviewer is separate from the author. The accepted diff is still small enough that a human can understand it without heroic effort.
For me, that is the first version of AI-assisted development worth trusting.
Treat every agent commit as untrusted until the repository gives you a reason to trust it.



