
Coding with AI Agents Without Losing Control of Your Code
An AI agent doesn't wreck your project in a single commit. The damage builds up: a PR you approved without really reading it, a dependency it "fixed" its own way, a project rule it didn't know about. By the time you notice, the code works, but you can no longer explain why.
I use agents every day, including on the code of this site. These are the rules that let me move fast without handing over the wheel.
The risk isn't bad code, it's code you didn't read
Bad code shows up in review. The problem is plausible code: it compiles, passes lint, and looks like what you would have written. That's the code that slips through.
So I stopped asking "how much can the agent do on its own?" and started asking something else: how much can I verify afterwards? A task I can't review in ten minutes is too big to delegate in one go.
1. Written context, not context in your head
The agent starts every session with no memory of your project. If a rule only exists in your head, the agent will break it, with total confidence.
On this site, the context lives in a single file, AGENTS.md, read by any tool (Claude Code, Copilot or whatever comes next). It holds the stack, the commands, the conventions and, above all, the pitfalls that already cost me hours:
## Code conventions
- Pages, layouts and routes read posts only through `lib/content` — never call the content API directly.
- Tailwind only — no CSS-in-JS, no inline `style` props.
- Never hardcode user-visible strings; use `useTranslation`.
Known pitfalls (each one cost real debugging time):
- **Never `await app.listen()` at module top level** in the API. Vercel intercepts `listen()`,
the promise never resolves, and every request hangs until it times out with no logs.
My rule: every time I correct the agent twice on the same thing, the correction becomes a line in that file. The file grows from the project's real mistakes, not from generic best practices copied off the internet.
2. Technical limits, not promises
"Don't touch production" written in a prompt is a request. A permission denied in the config is a limit. I prefer the limit.
This is a real excerpt from this repository's .claude/settings.json:
{
"permissions": {
"allow": ["Bash(yarn lint:check)", "Bash(yarn build)", "Bash(yarn api:test)", "Bash(git diff *)"],
"deny": [
"Read(./.env.local)",
"Read(./.env.development.local)",
"Read(./.env.production.local)"
]
}
}
Reads and checks run without asking me. The agent can't even open the secret files. On top of that, a personal layer outside git blocks production database writes, environment variable changes and production deploys. The agent prepares the command; I'm the one who presses the button.
This isn't distrust. It's the same principle you apply to a new teammate: broad read access, production writes only after review.
3. Small tasks with an objective way to verify them
Ask the agent for work that ends in something checkable: a passing test, a build that succeeds, a page that returns 200. "Improve the project's architecture" has no end and no criteria. "Fix the Dependabot alerts, running lint, typecheck, tests and build" has both.
An example from today. I asked the agent to fix the site's security alerts. It forced new versions of several dependencies, and one of them (OpenTelemetry 2.x) broke the build:
TypeError: Cannot read properties of undefined (reading 'AlwaysOn')
at async .../next-contentlayer2/dist/index-cjs.cjs:72:49
> Build failed because of webpack errors
Because the build was part of the task, the error showed up before the PR, not in production. The agent dropped that upgrade, dismissed the alert with the reason on record and moved on to the rest.
# Not fixed (dismissed as tolerable risk)
- @opentelemetry/core and @opentelemetry/propagator-jaeger: the fix is 2.x only, and forcing it breaks the contentlayer build (Cannot read properties of undefined (reading 'AlwaysOn')). Build-time tracing inside contentlayer2, which has no newer release.
- braces, http-cache-semantics, sprintf-js: no patched version published; dev/build-time chains (chokidar, node-gyp, argparse).
# Verification
- yarn lint:check, site and API typechecks, yarn api:test (54 passed)
- yarn build against the production content API: contentlayer generated 9 documents, build OK
- next start: /pt, /en, /pt/curriculum, /pt/about and an English post return 200
4. Review the diff like it came from a colleague
Same case: after the merge, GitHub reopened twelve alerts that had been marked as fixed but never were. The agent had trusted GitHub's status, and so had I. We only caught it because I asked for the alerts to be checked after the deploy, instead of calling the job done at the merge.
The lesson became a rule: the agent checks the result at the source, not in the summary. The version in the lockfile, not the alert status. Production logs, not "the deploy passed".
In review, I read three things before anything else:
- What changed outside the scope. A file I didn't expect in the diff is the first sign of trouble.
- What was removed. Agents delete a test "that was failing" with frightening ease.
- What was decided for me. A major dependency bump, a public API change, a database migration: all of that belongs in the PR description, not buried in the diff.
Common mistakes
// ❌ Wrong: "refactor the payments module and adjust whatever you think is needed"
// ✅ Correct: "extract the interest calculation into a pure function, with tests for the 3 current cases"
// ❌ Wrong: approving because CI passed
// ✅ Correct: approving because you read the diff and can explain every file
// ❌ Wrong: correcting the agent in the chat and forgetting about it
// ✅ Correct: two corrections = one new line in AGENTS.md
// ❌ Wrong: letting AI write end-user text without review
// ✅ Correct: AI does the creative part, code draws whatever has to be exact
I learned that last one from this blog's covers. I used to let an AI draw the whole cover, title included, and the titles came out with typos. Now the illustration comes from the AI and the title is drawn by a canvas from the post's text. Each one does what it does well.
When to delegate and when not to
| Delegate | Do it yourself (or watch closely) |
|---|---|
| Dependency updates with tests and build | Architecture decisions you'll maintain for years |
| Boilerplate that follows an existing pattern | Authentication and authorization code |
| Digging through logs and forming hypotheses | Any write to production |
| A first draft of tests for legacy code | Business rules only you know |
| Translating and proofreading text | Anything that reaches users without review |
The right column isn't "no AI allowed". It's where the cost of a plausible mistake is too high to catch only at the end.
Where to start
If you use agents today without any of these rules, start small:
- Create an
AGENTS.mdwith five lines: the stack, the verification commands and the three rules you repeat most in code review. - Configure your tool's permissions: allow lint, tests and build; block reading
.envfiles and any production command. - On your next task, decide how you'll verify it before you ask. If you can't say how you'll check the result, the task isn't ready to delegate.
The agent speeds you up a lot. But your name is still on the commit that goes to production.
André Luiz Vieira
Frontend & Mobile Engineer
I live in Curitiba, Brazil, and I've spent five years building React and React Native products for banks, credit unions and e-commerce. Here I write about what I learn along the way.
Join the conversation
Questions, ideas or a different take? Comments run on GitHub Discussions; sign in with GitHub to post.