Under the Hood: How AI Coding Tools Actually Work
This course teaches transferable skills for steering AI coding tools well — the mental model and controls stay the same no matter which tool or company you use.
Two failures that look like competence
You typed a prompt, hit enter, and the AI confidently wrote code that imported a package that does not exist. Here is the machinery that produced that — and the exact controls that would have caught it.
An AI tool wrote import faiss_cpu_optimized — a package that does not exist on PyPI. Attackers pre-register plausible names like this to sneak malware into your dependencies — an attack class called slopsquatting.
In a widely-reported incident, an agent with unrestricted write access to a production database fired a destructive command despite a code-freeze instruction — wiping more than 1,200 users’ data.
Reference phantom packages
of AI-generated code samples import packages that do not exist. (Lasso Security / arXiv, 2024)
Contain security flaws
of AI-generated code ships with a security vulnerability. (Veracode, 2025)
“Writes code with complete conviction — including bugs or nonsense — and won’t tell you something is wrong unless you catch it.”
What an LLM actually does (and doesn’t)
Think of the model as a fluent improviser who never breaks character: it always produces confident, plausible output — with no internal fact-checker. Here is the four-step pipeline behind every response.
Tokenization
Text becomes subword chunks. The model has no concept of a whole word.
Embeddings
Each token becomes a vector. Meaning turns into geometry.
Attention
Weighs which tokens matter for predicting the next one.
Sampling
Draws one token from a probability distribution — so the same prompt can give different output each run.
It produces what is statistically likely — not what is true. That single fact explains hallucinated packages, invented methods, and plausible-but-wrong logic.
import faiss_cpu_optimized # looks real, is not on PyPI
The model saw thousands of import <something>_optimized lines during training.
faiss_cpu_optimized is a statistically plausible name, so it generated it.
Zero check that the package actually exists — that is the whole failure.
Every turn resends everything
The context window is everything the model can see this turn — system instructions, your rules files, the conversation so far, files you pulled in. What is not in the window does not exist for that turn.
Your first prompt goes in.
Your prompt + the reply + your new prompt — all resent.
The full conversation grows every exchange until it hits the window limit.
Prompt caching saves the computed state of stable content so only new messages are processed fresh — cutting latency and cost. But it silently resets on any of these:
/compactCompaction rewrites the conversation, so the cached prefix no longer matches.switch modelA different model has a different cache — the whole stable context is reprocessed.change MCPAdding or removing an MCP server alters the tool list baked into the prefix.TTL expiryCaches live for a limited time; after it lapses the state is rebuilt.new sessionA fresh session starts with nothing cached.Four ways context goes bad
More context is not always better. A lean, relevant window beats a bloated one — here are the four failure modes to watch for (Drew Breunig, 2025).
Poisoning
A hallucination or error enters the context and gets referenced again and again.
Distraction
Context grows so long the model over-focuses on it and neglects what it learned in training.
Confusion
Superfluous content in the context drags the quality of the response down.
Clash
New information or tools conflict with things already in the context.
You didn’t send a prompt; you started a loop
“My preferred definition of an LLM agent is something that runs tools in a loop to achieve a goal.”
Reframe it: not “I sent the model a question” but “I started a loop, and I can intervene at each of these four stages.” Step through the agent loop below.
Both incidents from the first screen broke down in Stage 3. The fabricated install and the destructive database command both happened when a tool ran without a gate or a scope to stop it.
The controls — and a 3-question diagnostic
The harness is the director and stage crew around the improviser: it is how you add control. Each loop stage exposes its own levers.
SetupModel choice, rules files (such as AGENTS.md or CLAUDE.md), MCP servers, and ignore filters.AssemblyExplicit file references (@-mentions) and input hooks.Model ↔ ToolPermission modes (plan / ask / auto), tool scoping, and pre/post-tool hooks.EndSession boundaries and handoff artifacts for the next run.“If you give an agent a way to verify its work, it more often than not fixes its own mistakes and prevents regressions.”
Here is a post-tool hook that catches the “invented method” class automatically — it re-runs your type-checker after every edit.
// settings for your harness — run the type-checker after each edit/write
{ "hooks": { "PostToolUse": [
{ "matcher": "edit|write", "command": "run-your-typecheck" }
] } }
A rule that fires after a tool runs.
Whenever the agent edits or writes any file…
…automatically run your type-checker.
If the AI invented a method that does not exist on a real class, the check fails immediately and the agent sees it — no human needed.
When AI output disappoints, stop guessing (“try a better prompt?”) and diagnose the failing stage.
The model fabricated something · context was missing or stale · the agent overstepped.
Rules · ignore filters · file refs · model selection · permission mode · tool scope · hooks · handoffs.
Setup · Assembly · Model ↔ Tool · End.
Check your mental model
Three diagnostic scenarios. Pick the fix a working engineer would reach for.
The agent keeps calling a config method that does not exist on your real class. Which control best prevents this next time?
Mid-session you switch models to save cost, and suddenly latency spikes on the very next turn. Why?
Your agent read a wrong fact early in the session and now keeps repeating it across turns. Which context-failure mode is this?
Next: Module 2 — Research → Plan → Implement, the operating pattern that puts these controls into practice.
The Operating Pattern: Research → Plan → Implement
Workflow beats prompting. A structured flow turns the model's slot-machine inconsistency into results you can review and repeat.
Same prompt, two very different pulls
The prompt "Add anchor links to the titles in the topics list page" was run twice by the same model. Same words, same code, two unrelated results.
Run 1
Touched every anchor on the page — not just the titles that were asked for.
Run 2
Invented an unrequested table of contents nobody asked for — different styling, different regex.
That inconsistency isn't the model being dumb. It's the workflow being absent — and here's the mechanism behind it.
Your sentence has many valid readings. The model must pick one, and it can't ask you about every fork.
It works from a partial view of the codebase, so it fills the blanks with plausible guesses.
No matter how detailed the prompt, one shot at a big task is a gamble. The bigger the task, the bigger the risk of a bad pull.
"GitHub Copilot provides better results when assigned clear, well-scoped tasks." — GitHub best practices, 2026
Building a house, not pouring concrete and hoping
You survey the land, draw blueprints a contractor can build from, then construct. You don't pour the foundation and then decide where the kitchen goes. Three phases, three distinct jobs.
Research
Survey the site. Build a mental model of the problem.
Deliverable: understanding + a findings summary.
Plan
Draw the blueprints. Compress intention into scope.
Deliverable: scope + acceptance criteria.
Implement
Construct. Execute with discipline — mostly mechanical.
Deliverable: working code + verification.
Each phase feeds the next, and skipping one creates rework. But each column can hold several activities for big work or collapse to almost nothing for a one-line change. You scale the effort to the task.
Research is questioning, not outsourced thinking
"The goal is to build a mental model of the problem space — not to produce output." — AgentPatterns, 2026
Research means asking follow-up questions until you understand, studying what already exists before proposing anything. A research prompt looks like a question, not a request for code.
How does authentication work in this app?
You're not asking it to fix anything yet.
You're asking it to read the code, find the existing patterns, and report back.
Now your plan is grounded in what's actually there, not in guesses.
In one real run, this exact question revealed the app had no client-side auth at all — a static site fronted by an upstream layer. A naive "fix the login" plan would have chased a problem that didn't exist.
The blueprint is a "Done when" list
A plan's core artifact is a list of checkable outcomes you can review before any code exists. Here's a real one.
## Fragment Anchors Plan
Done when:
- Hash links work for page titles in the topics list
- Anchor IDs follow a consistent kebab-case naming pattern
- Deep links survive hash-router navigation
- No scope creep to other pages or unrelated features
A title for the work — small and specific.
Everything below is a checkable outcome, not a vibe.
The feature itself works on the right page.
IDs are kebab-case — predictable, not random each run.
Links still work after hash-router navigation.
A guardrail against scope creep — the fix for that runaway table of contents.
A day-one teammate could finish this in about a day using only the plan plus repo access. That means small scope and enough concrete detail — paths, interfaces, Done-when — that they're never forced back into research.
Grounding changes the answer. The same auth prompt produced materially different plans with vs. without evidence attached.
The model assumes how things work and plans against its guess.
Feed it a captured network trace and a working request example — the plan matches reality. Then review it: does it address the original problem? Did I make a wrong assumption?
With good blueprints, construction is mechanical
"Implementation becomes mechanical when the research and plan are solid." — AgentPatterns, 2026
Each implementation prompt cites the plan plus the work item, so the model's job is execution, not re-architecture. Quality gates run automated-first, then human review against "Done when".
Lint, type-check, tests. Let the agent fix its own hygiene.
You check outcomes, not syntax. The plan already told you what "done" means.
A small miss → adjust the prompt or chunk the scope. A systemic miss → revise the plan or return to research. Don't stack hacks.
The cost of being wrong is asymmetric. Where the error lives decides how much it hurts.
Research error
Re-read a file. Seconds.
Plan error
Rewrite a paragraph. Minutes.
Implementation error
Revert, re-plan, re-implement — and burn context.
An AI plan is cheap. The wrong plan is expensive. Verify before you execute.
The handoff: passing blueprints between crews
Run each phase in a fresh session and carry a handoff artifact between them. Watch a research-to-implement handoff play out.
A good handoff is Distilled (conclusions only), Validation-carrying (the criteria that prove the next step worked), and Durable (survives compaction — re-loaded next session). If implementation fails, update the plan and retry. No need to re-research.
Scaling up: when the work is too big for one plan
Most product requirements don't arrive pre-broken-down for an agent. Extend the flow with two moves before you build.
Have the AI gap-find your own plan before you commit a sprint to it.
Split into vertical-slice stories, not horizontal layers. Aim for 5–8 stories, each ~1–2 days, dependencies explicit and ordered.
A vertical slice is one behavior end-to-end; its tasks are the dependency-ordered build steps (model → schema → API → UI). Write the acceptance criteria as Given/When/Then.
Bad story
"Add comment functionality."
No observable outcome. The model will fill the gaps — slot machine again.
Good story
"Given a logged-in user, When they submit the comment form, Then the comment appears below the post with a timestamp."
Review this task breakdown for gaps and risks. For each: what could go wrong,
wrong assumptions, conflicts with existing architecture, scope creep, and per-task
risk (low/med/high). Flag any story likely over 2 days and suggest how to split it.
Focus on what could go wrong, not what could go right.
You're deliberately asking the model to attack your own plan.
You want the failure modes named — assumptions, conflicts, creep — not reassurance.
Oversized stories get flagged now, while splitting is cheap.
Decomposition errors are cheap to fix now, expensive once implementation starts.
If you can't write the Given/When/Then, the story isn't ready. This one habit — even on a single epic's worth of stories — is the biggest lever on whether an agent builds the right thing.
Check your instinct
No definitions here — just judgment calls. Pick what a disciplined engineer would actually do.
A one-line copy change in a footer: "© 2025" should read "© 2026". How much Research → Plan → Implement rigor does this deserve?
On a large feature, the agent's implementation has come out wrong for the third time. Each retry, you tweaked the prompt a little more. What's the best next move?
You have limited time to catch mistakes. An error in which phase is the most expensive to discover late?
Set the Agent Up to Win
Tools, rules, context, and a build that boots on the first try — the workspace is the agent's competence.
Your new colleague's first day
An agent that keeps asking the same question, retries the same command, or edits the wrong folder isn't broken — it's under-briefed. Everything it needs to succeed is something you can set up once.
Picture onboarding a new hire. Leave them the right setup and they ship in an hour; leave them nothing and they spend the day guessing. An agent is that new hire every session — with no memory of yesterday unless you wrote it down.
Working tools and a harness that can read the repo and run commands.
Rules and docs that name the conventions and the gotchas up front.
A build anyone — teammate, CI, or agent — can reproduce from a clean checkout.
The permission mode is your oversight dial
Before rules and docs, know the one control you touch every session. The permission mode sets a ceiling on what the agent may do without stopping to ask. The names differ by tool; the four categories are the same.
Reasons, never edits. Use it to survey the code or to lock a plan before any code moves.
Pauses for approval before each action. Use it on new or unfamiliar work.
Edits flow automatically; still pauses for riskier operations like running shell commands. Use it for known-shape changes.
Full agentic execution, subject to safety checks. Use it for scoped tasks you have supervised before.
Match the mode to what you want to pause for — friction is a dial, not a personality. In most tools a single cycle key (like Shift+Tab) rotates through the modes.
Rules capture the correction you keep repeating
If you're telling the agent the same thing twice — "use our logger, not raw print statements", "tests live in a dedicated folder", "don't touch the legacy billing code" — that repetition is a signal. Capture it as a durable rules file such as AGENTS.md or CLAUDE.md.
The rules lifecycle — step through it
"Would removing this cause the agent to make mistakes? If not, cut it." Start with what your next task needs — not a comprehensive repo-wide manifest.
A rules file is one member of a family of durable configuration you can leave behind:
rules file
Standing conventions the agent loads every session.
MCP
Model Context Protocol: connects outside tools and data sources to the agent.
hook
A script that runs automatically at a set point, e.g. formatting on every save.
subagent
A helper agent the main agent delegates a focused task to.
skill
A packaged, reusable procedure the agent loads on demand for a recurring task.
Write the behavior, not the intent
A rule the agent can act on names the exact behavior. Vague intent forces it to guess — and it guesses differently every time.
Specific
Use 2-space indentation · Run the test suite before committing · API handlers live in src/api/handlers/
Vague
Format code properly · Test your changes · Keep files organized
What earns a place — and what doesn't
Include
Build and test commands it can't guess · style rules that differ from defaults · repo etiquette (branch naming, PR conventions) · non-obvious gotchas · pointers to other files.
Exclude
Anything it can infer by reading code · standard language conventions it already knows · long tutorials (link instead) · info that changes often · duplicated file contents · "write clean code."
One more thing worth writing down explicitly: what the agent is allowed to read. That is controlled by the tool's ignore or deny config — not by .gitignore, which only affects discovery, not agent access.
# in your agent config
permissions:
deny:
- "legacy/billing/**"
- "**/*.env"
# or, tool-specific: .cursorignore
legacy/billing/
*.env
This is where you fence off files, not .gitignore.
The deny list is the agent's real read-access wall.
Keep it out of the legacy billing folder entirely.
And never let it read secrets like environment files.
Some tools use a dedicated ignore file instead —
same idea: name the folders that are off-limits,
so a curious agent can't wander into sensitive code.
Docs that pass the fresh-agent test
Judge an onboarding doc by one test: could a fresh session build and run one test using only this doc, in under ten minutes? Four moves measurably lift output — the deltas are from a study across 2,500+ repos.
Numbered workflows
Not "follow our deploy process" but "1. run preflight, 2. wait for green, 3. merge, 4. watch the dashboard 10 min."
+25% correctness · +20% completeness
Decision tables
Where two valid patterns exist, name which is current and which is legacy, in a table.
+25% best-practices adherence
Real code snippets
Copy a real 3–10 line shape from production once, with a pointer to more.
+20% code reuse
Every "don't" gets a "do"
Not "don't use the old package manager" but "use pnpm 9.x"; not "don't build HTTP clients directly" but "use the shared client from lib/http."
Halves warning-overhead failure
Two anti-patterns that look like "more help" but hurt
Over-reading
Piles of unmapped docs; the agent reads everything "just in case" — one case: 12 docs, ~80,000 tokens for a two-line change.
−25% completeness · Fix: a short root that says which sub-doc to read for which task
Warning-only docs
30+ "don'ts" with no "do"; the agent walks the whole warning list before every single action.
~2× task time · Fix: pair every "don't" with a "do"
Both backfire for the same reason: frontier models reliably follow only ~150–200 instructions. As the count climbs, adherence degrades across all of them. Every line competes for the same budget.
Short root, progressive disclosure, pointers not copies
Structure the docs so the agent loads only what a task needs. A short root file acts as a table of contents; progressive disclosure keeps each topic in its own file.
Table of contents plus the handful of always-on rules.
One topic per file; the agent loads only what it needs right now.
Link the scripts in package.json; don't paste them. The pointer never rots — the copy always does.
# AGENTS.md
- This service uses pnpm, not npm.
- Build commands live in package.json
scripts — see there, do not duplicate.
- Never modify legacy/billing/ —
pre-2022, business-critical.
- If you are touching payments,
read agent_docs/payments.md first.
Four lines do the work of a page.
One fact the agent could never guess: the package manager.
One pointer: find the build commands where they already live, don't recreate them (the copy would rot).
One paired don't/do-by-pointing: the dragon — where not to go, and why it matters.
One map entry: which sub-doc to open for which task.
And keep docs living: any PR that changes a build, convention, or deploy step updates the doc in the same diff. Flag any doc unchanged for 90 days while the repo kept moving.
A deterministic build is a contract
"Local green is not enough." A build is deterministic only when a teammate, CI, or an agent can reproduce it from a clean checkout. Environment drift is why the same clone passes on two laptops and fails in CI. More discipline won't fix it; a process will.
Declare the canonical setup → build → test path in the repo
Prove it in clean environments — not just one laptop
Fix the shared path when local, CI, and agent results disagree
Different artifacts for different consumers
For humans — a narrative quickstart to get oriented.
The runnable path itself — often a devcontainer plus a lockfile so installs are idempotent.
Session-persistent facts for the agent.
The most common silent blocker is a private registry: name the feed URL and the auth step in your docs — even if it's just "ask this person for the credential."
Agents don't ask questions to be polite — they ask because the info isn't there. If an agent asks the same thing twice, that answer belongs in the docs; if it retries a command, the prerequisites aren't clear enough. Verify by prompting a fresh, zero-context session: "Set up the environment and run the tests," and watch for churn. No churn → verified. Churn → you just found the gap.
Check your instinct
Four situations. Decide what a set-up-to-win engineer does before you reveal the answer.
Your agent keeps re-asking where the integration tests live, and it retries the setup command twice every session. What is this telling you, and what's the fix?
You want to be helpful, so you add 35 "don't do X" rules to the root doc. What most likely happens?
You're reviewing a rule you added six months ago. How do you decide whether it stays?
Your build commands change often. Which belongs in AGENTS.md — a 30-line copy of the build scripts, or a one-line pointer to where they live?
Reviewing & Driving AI-Authored Code
Bad human code looks bad. AI code is a high-quality counterfeit — built to pass inspection. Reviewing it takes a technique, not a vibe.
The counterfeit that passes at the counter
A cashier catches a crude fake in a glance. What gets through is the good counterfeit — right paper, right ink, matching the real bills around it. AI code fails the same way: it compiles, passes lint, reads like its neighbors, and is still wrong.
Scrolling and vibe-checking catch clumsy mistakes. AI errors are engineered to look competent, so they slip past exactly that. You need a different technique than “does this look okay?”
“AI-generated code is more dangerous to review than bad human-written code, because it fails in ways that look like competence.”
“Participants who had access to an AI assistant wrote significantly less secure code — and were also more likely to believe they wrote secure code.”
The volume problem
median PR review time, year-over-year
of PRs now merge with zero review
growth in PR size
more issues than human-written code
Faros AI, 22,000-developer dataset, 2025. Volume went up; review capacity didn’t. So your review time has to become effective, not performative.
The five ways a counterfeit gets through
Every AI failure you review falls into one of these five families. Learn the tell for each and review stops being a vibe and becomes a checklist.
Fake / hallucinated APIs
An import or method call that exists in no version of the library. Broken-import is a productivity bug; slopsquatting is a security one — attackers pre-register the invented name with malware.
Tell: check every new import against the lockfile and the library’s current docs.
Plausible-but-wrong logic
Matches the surrounding style but breaks an invariant: an off-by-one in a paginator, a guard clause with the comparison flipped, a filter that silently drops good data.
Tell: re-derive the invariant from the spec and check the diff against that — do not pattern-match the diff against the code around it.
Missing error handling
The happy path works. The null, error, and timeout paths silently swallow the problem or crash the process.
Tell: for each new external call ask “what happens on null / error / timeout?” If the answer isn’t in the diff, the path is missing.
Vacuous tests
Tests that pass without ever exercising the code they claim to cover — the counterfeit with a fake anti-forgery strip.
Tell: delete the implementation line the test covers. If the test still passes, it’s vacuous.
Comprehension debt
A big diff accepted without reading; abstractions you can’t explain in plain English.
Tell: “Never commit code you can’t explain.” — Addy Osmani
Vacuous tests, up close
The most seductive counterfeit: a green test suite that proves nothing. Here a payment test looks thorough and asserts nothing real.
charge = jest.fn().mockResolvedValue({ ok: true });
processOrder(order);
expect(charge).toHaveBeenCalledWith(order.total);
Replace the payment client with a mock that always says “ok.”
Run the order.
Assert only that the mock got called the way we wired it. It never checks that the order was marked paid, saved, or that a receipt was sent — processOrder could be empty and this still passes.
Its siblings — same trick, different disguise
snapshot-only
A snapshot test confirms the DOM shape didn’t change — a broken discount still matches the old shape.
doesn’t-throw
“It didn’t crash” is not “it did the right thing.”
coverage-padding
Chasing a coverage number with toBeDefined(), which happily accepts 0, NaN, or an empty object.
mirror test
Asserts a helper was called — not that the result it returned is correct.
Delete the behavior the test claims to cover. Does the test still pass? If yes, it was testing nothing.
Spot the counterfeit
This is plausible-but-wrong logic in the wild. It reads like tidy cleanup and matches the style around it. One line silently destroys valid data. Click the line you’d flag.
A routine that removes “empty” data series:
series_to_remove = []
for name, data in curves.items():
has_events = any(d.get("event") == 1 for d in data)
if not has_events:
series_to_remove.append(name)
for name in series_to_remove:
del curves[name]
Don’t inspect every bill equally
Sustained vigilance across a whole diff fails — attention runs out before the lines do. Tier the diff first, then read the high-value lines closely.
Tier 1 — read every line
Spec interpretation (asked vs. built), interfaces, error paths, security-sensitive logic, and every AI-authored test.
Tier 2 — sample
Boilerplate, generated code, scaffolding, repetitive transforms. Spot-check 1 in 5 or 1 in 10; stop when you’ve seen enough.
Tier 3 — trust the signal
Type-check output, the linter, import resolution, tests that genuinely exercise the path. Don’t re-read what the harness already proves.
When you choose not to read every line, say so in the review.
“Sampled the migrations, stopped after three.” · “Trusted the type-check for the rename.” Now the author and the next reviewer know exactly what was actually read.
Driving a ticket: rig the checks before the writing
You don’t just review AI code — you drive tickets end-to-end. The daily loop:
Ticket
Plan
Implement
Describe
Review
PR
An “agent-sized” ticket fits one PR (target under ~400 lines changed), has acceptance criteria the harness can verify, and needs no simultaneous migration or cross-service deploy to test locally.
The single highest-leverage move: “Give the agent a way to verify its work.” Wire it before the agent starts writing — in three layers.
“Add integration tests for the order flow before declaring done; confirm the modal renders on a mobile viewport.”
A rules-file line backed by a verification hook the agent can’t skip.
The test command, a curl probe on a health route, or a browser-automation tool for UI.
# AGENTS.md
Before saying you are done: run `./scripts/check`.
If it fails, fix and re-run.
A rules file the agent reads on every task.
This turns “please verify” — a polite request the model may skip — into a standing contract.
Paired with a stop-hook, the agent literally cannot declare done on a red check.
You sign for every claim in the PR
Real cases include fabricated screenshots (a UI result the agent had no way to actually run) and fabricated commit hashes narrated in a summary. For every claim — test names, file references, “I checked X” — open the file and verify. If the agent drafted the description, edit at least one section before you sign your name.
A PR template tuned for AI diffs — one that lives on the source branch of your code host — turns the vague “looks good” into five targeted defenses:
## What & why — one paragraph + linked ticket
## Test plan
- Happy / Error / Edge: one concrete case each
## AI authorship
- [ ] Tools used: <which>
- [ ] I read every line of this diff myself
## Reviewer focus
- Read closely / Sample / Trust-signal: <files>
## Pre-flight
- [ ] New imports verified (no hallucinated packages)
- [ ] Searched repo for existing utilities
What & why — anchors the diff to the actual ask.
Test plan — defends against vacuous tests: name a real case per path.
AI authorship — defends against reviewer mis-calibration. The “I read every line” box is the thing your signature attests to.
Reviewer focus — defends against rubber-stamping by pre-tiering the diff for the next reader.
Pre-flight — catches hallucinated imports and duplicated utilities before review even starts.
Review quality decays with size. A 200-line PR reviewed twice in 20 min beats a 1,200-line PR reviewed once in 90 min.
“A fresh context improves code review since the model won’t be biased toward code it just wrote.”
Approve-with-comments by default; request-changes only for real blockers (it hard-blocks merge); reject rarely and always with a rationale — never a vibe rejection.
Check yourself
Four situations from the daily loop. Pick the move a calibrated reviewer makes.
A test mocks the payment client to return success and asserts the client was called. Your teammate says it’s vacuous. What single check proves it?
You’re handed a 900-line AI PR to review. What’s the best first move?
The AI’s PR description says “verified the search filters correctly, see screenshot.” You know the agent had no running app. What do you do?
You’re about to hand an agent a ticket. What’s the single highest-leverage thing you can do for quality?
The Deterministic Safety Net
Quality, testing, and post-deploy nets that catch mistakes automatically — before, during, and after you ship.
Cheap levers beat heroic review
Picture a trapeze artist working above a safety net. The performer is non-deterministic — even a great one slips. The net is deterministic: same spot every time, it never has to recognize the fall, it just catches.
Fast, creative, and fallible. You will never make it perfect.
Mechanical and boring on purpose. It never moves, so it always catches.
"None of the things around volume of code or number of coding agents correlated with productivity. The only signal was code quality. Great code in the pattern means great code out." — Eno Reyes, CTO of Factory. Your codebase is the agent's training signal at inference time: it reads your existing files to learn your style, and the linter is its feedback loop when it gets it wrong.
LLMs are slow, expensive, and non-deterministic. Linters are fast and deterministic. Use each for what it is good at — and the net matters more, not less, when an AI is writing the code.
Formatter vs linter: determinism is the product
Two tools, two jobs. Running one through the other is the classic anti-pattern — keep the layers separate.
Formatter (mechanical)
Whitespace, line breaks, quote style. Reprints the AST deterministically in milliseconds, near-zero config. Prettier, Black, gofmt, ruff format.
Linter (judgment)
Logic, naming, dead code, bug patterns. Emits diagnostics, takes seconds to minutes, hundreds of toggles. ESLint, Pylint, golangci-lint, ruff check.
Adopt a vendor default and move on. Determinism buys you identical output for identical input (whoever or whatever wrote it), smaller diffs, invisible style so review focuses on logic, and faster onboarding.
The right gate at the right stage
Formatter + fast lints. Zero-cost feedback while you type.
Staged files only. Keep it under ~3 seconds or people bypass it.
Slower lints and the type-check, before code leaves your machine.
Everything, whole repo, check-only. This CI gate is the one that actually blocks a merge.
# CI: verify only — never rewrite files
prettier --check .
eslint .
# Agent post-edit hook: format every file it writes
on_file_write: prettier --write $FILE
In CI, run the formatter in --check mode: it reports if anything is misformatted but changes nothing.
Run the linter across the whole repo. Either check fails the build if it finds a problem.
Separately, wire a hook so the agent formats every file the instant it writes it — turning a polite request into a hard guarantee.
Paying down lint debt: the ratchet
You want a strict new rule. The codebase has 3,300 existing violations. Three ways forward — only one ships fast without blocking everyone.
Big-bang
Fix everything in one PR. Only viable when the count is small — otherwise a merge nightmare.
Boy-scout
Fix violations on files you happen to touch. Slow and uneven — old code you never open never improves.
Ratchet ✓
Record today's counts, block any increase, and lower the cap as people fix old ones. New errors can never merge.
"When we ratchet errors, we ensure they trend downward in a steady, irreversible process… a new error cannot merge." One team went 3,300 → 0 in ten months this way. A mass-reformat need not wreck git blame: record the reformat commit hash in a blame-ignore file and blame skips over it.
Testing: draw the line, keep the harness deterministic
The test pyramid tells you how to spend your testing budget — most tests fast and low, a few broad and slow.
~10%
Fast and isolated. One function, no outside world. Milliseconds each.
An integration test checks the contract between 2+ components across one boundary; a failure pinpoints the module.
An E2E test drives a full business outcome, UI to database. Broad, slow, fragile — use sparingly.
AI-assisted test generation is a four-phase loop
Read the source, its collaborators, and existing example tests.
Draft the assertions, fixtures, and edge cases before writing code.
Compile, run, score coverage, feed the errors back and iterate.
Assert on behavior, not implementation. Curate the golden set.
determinism
Seeded fixtures, dynamic ports, random container names, fixed model versions, frozen clocks, named goldens. Randomness belongs inside the system under test — never in the setup.
real coverage
Coverage % is not quality: one study found 31% of generated tests had no assertions. Require the focal method actually called, ≥1 behavior-checking assertion, and ≥1 negative path.
Flaky tests: own them, don't re-run them
A flaky test passes and fails on the same code. At scale the noise is enormous.
of CI pass-to-fail transitions are flakes, not real regressions.
of tests are flaky at scale.
dev-hours per year lost to flakiness.
Properties of a high-signal test
isolatedOwns its own state; no dependence on other tests or run order.stable selectorsTarget data-testids or ARIA roles — never brittle CSS paths.condition waitsWait for a state to be true, never a fixed sleep timer.one scenarioDeterministic input, one behavior per test.Triage decision rules
No team eliminates flakiness. The ones that ship reliably have named owners, SLAs, and the discipline to fix-or-delete — instead of a "just re-run it" culture that only compounds the flake rate over time.
A smoke test is a behavioral assertion — not /health
Improve one piece of the net each iteration (n+1 > n). Since the AI ships more code than review can catch, the post-deploy net has to absorb the difference.
# What most pipelines call a "smoke test"
curl https://api.example.com/health
# -> 200 OK -> "deploy succeeded"
# An actual behavioral smoke test
POST /checkout {cart} -> 200 {order_id: "…"}
Hitting /health only proves the process booted and routes answer — that is liveness.
Nothing has been asserted about whether the service does its job.
A real smoke test exercises one production-critical path and checks the result: checkout returns a well-formed order ID; login returns a session with the expected claims; search returns hits for a known query.
Seven shapes exist — API contract, user journey, integration, data-shape, background job, agent/LLM path, and a negative permission check that asserts an unauthorized request is still rejected.
The hero move: canary deploy → auto-rollback
A canary limits the blast radius: a bad version only touches a sliver of traffic before the gate stops it.
Rollback, reversibility, and safe schema change
Five recovery moves exist — but only two are true rollbacks. Know which lever you are actually pulling.
Deploy prior artifact
A true rollback. Seconds. The move you want.
Switch traffic
Blue/green or canary. Also a true rollback.
Flip a kill switch
Not a rollback, but a kill switch takes effect in ~200ms.
Revert + redeploy
Roll-forward, not rollback. Slower — a full pipeline run.
Compensating action
A new forward action (refund, follow-up email) when nothing can be undone.
None of these undo migrations that already ran, rows the bad code wrote, or emails sent — those need a compensating action.
Two-way doors (reversible) — automate the check; the success condition is the approval. One-way doors (irreversible) — keep a human and pre-design the do-over: soft-delete with a restore window instead of hard delete, a first-class refund flow instead of raw charges. Best of all, decouple deploy from release with a feature flag: ship the code OFF, flip it ON when ready; if it breaks, flip OFF in ~200ms with no redeploy.
Safe schema change: expand → migrate → contract
Two services on their own deploy schedules still share one schema — a shared database still couples them. Never drop or rename a column in the same deploy that removes the code using it.
Expand
Add the new column or table. Nothing reads it yet.
Migrate
Write both old and new; backfill; cut reads over once both sides confirm.
Contract
Remove the old structure — a separate deploy, only after both sides are confirmed.
This is why a schema migration is a one-way door: the migration has already run, so you sequence it in stages instead of relying on rollback.
Check your instincts
Four situations. Pick what a senior engineer would actually do.
You want to add a strict new lint rule, but the codebase already has 3,300 existing violations. What ships fastest without blocking everyone?
A UI test fails about 1 time in 20 and passes on retry. What should team policy be?
Your deploy pipeline curls /health, gets 200 OK, and reports success — but users cannot check out. What is missing?
You need to rename a database column that both an old monolith and a new service still read. What is the safe sequence?
Guardrails & Safe Refactoring
Build like it will go rogue — limit what the agent can do, then shrink what it must reason about.
When is an agent dangerous?
Even with nobody attacking it, an agent that can act — deploy, delete, change data — can do real damage by an honest mistake. It gets far worse when three things line up.
Private data it can read
Your code, database, customer records — anything sensitive in the agent's reach.
Untrusted input in the mix
A web page, a ticket, an email, another tool's output — content the agent didn't write.
A way to act or send out
Run a command, call an API, post data somewhere the outside world can see.
With all three, a booby-trapped page can quietly turn the agent against you — because the model can't reliably tell instructions from content. This is prompt injection, and it echoes the OWASP Top 10 for Agentic Applications (2026). Mindset: build like it will go rogue — not because it usually does, but because the cost when it does is too high to leave to luck.
Watch the guardrail fire
A support ticket carries hidden instructions. The agent can query the database and run commands — the trifecta is live. Play it through and watch the approval hook stop a disaster.
The agent believed the ticket. The hook didn't — it stopped an action, not an intention. That is a guardrail. A politely-worded "please don't" would not have saved you.
Two ways to set a limit — plus a layer underneath
Onboarding metaphor: would you give a new hire production database access on day one? You wouldn't — not from distrust, but because one honest mistake costs too much. You scope access to the task. An agent is that new hire.
Standing limit
A rule obeyed on every action — an
allow list or
deny list. Best for clear-cut rules: never read .env, never rm -rf. If a command is on both lists, block wins.
Approval hook
A checkpoint that stops and asks you before one risky step: installs, deploys, big edits. It can ask, block, or allow.
Sandbox
The layer underneath: it blocks the actual OS call regardless of wording. Narrows the blast radius of writes and exfiltration — but it doesn't stop reads, so it complements your lists, not replaces them.
Why the sandbox matters: lists and hooks depend on recognizing the action — a rephrased command can slip past either. The sandbox recognizes nothing; it just blocks the syscall. Here is what a standing limit looks like in a settings file:
{ "permissions": {
"allow": ["Bash(npm run *)", "Bash(git diff *)"],
"deny": ["Bash(rm *)", "Bash(git push *)",
"Bash(curl *)", "Read(.env*)"]
} }
Open the rulebook for what the agent may do.
Allow: pre-approve the safe, routine commands so you're not clicking "yes" all day.
Deny: hard-block the destructive ones — no delete, no push.
Note we also block curl and hide .env: blocking read of a secret is useless if a script can read it and upload it anyway.
Close the rulebook.
Least privilege is the whole game
"Please don't delete the database" won't stop it — the permissions will. Guardrails are about what the agent can do, not what you asked it to do.
One agent should not hold every permission. Least privilege is the default posture.
A close_ticket tool beats a run-any-SQL tool — a capability you never granted can't be misused.
The common trap: a service account or API token scoped far broader than the person running it.
Managed rules individuals can't loosen: which models are allowed, which MCP servers are approved, spend caps, audit logging. Personal config is the floor; the org layer is the ceiling.
What goes wrong → the guardrail that catches it
| What goes wrong | The guardrail that catches it |
|---|---|
| Runaway loop / surprise overnight bill | Token + time budgets; a "keep going?" approval past a threshold |
| Confidently wrong work slips through | A reviewer agent before it lands, plus a human on anything critical |
| A poisoned tool's text redirects the agent | Locked permissions; read MCP tool descriptions before installing (that text becomes instructions) |
| One agent with every permission ("it's only internal") | Least privilege; scope per task + environment |
| A rephrased command slips past a rule or a tired reviewer | A sandbox — it blocks the OS-level call regardless of wording |
Why big files hurt agents specifically
A god file is a wall you can't tell is load-bearing. Knock it down blindly and the ceiling comes with it.
Above size and complexity thresholds, defect probability rises and the code gets harder to test and understand.
Usable context is often ~64k–200k tokens in practice, not the headline million. A giant file crowds out reasoning — too much context degrades output.
Decomposition is infrastructure for safe AI edits. Smaller files help agents, not only humans. Set a soft cap (~300 lines) and enforce it in CI — "warnings nobody reads are not a policy." But understand why a big file exists before you carve it.
Find the seams, lock behavior, then move
A seam is a place you can alter behavior without editing at that exact spot. Each seam has an
adaptor: constructor injection, a factory, a function parameter, a link-time stub. Classic case: code calls Clock; a seam lets you slot in a FakeClock for tests without touching the call site — that's
dependency inversion.
Write characterization tests that pin the current behavior — happy path, null/empty, boundaries, errors — before any move. Don't assume correctness; lock actual outputs and mark suspect cases // TODO: verify. Skipping this is the #1 driver of bad AI refactors.
Keep the model on judgment, not bulk edits, with a read-only research pass — the same Research → Plan discipline from Module 2, aimed at a scary file:
Read this file. List every top-level function or
class and assign each a single responsibility:
{data access, validation, business rule,
formatting, I/O, framework glue, dead code}.
Propose a target module tree, one responsibility
per module. Flag any symbol that belongs in two
places — those are your decomposition targets.
Do not move anything yet.
"Study the file — don't change it."
"Tell me what each piece is actually for, sorted into a fixed set of jobs."
"Draw me the shape it should be — one job per file."
"Point to anything doing two jobs at once — that's where the seams are."
"And keep your hands off the code until we agree on the plan."
Ship it in small, reversible slices
Pick the strategy by risk. Every slice is a small PR — ≤ ~300 lines, revert-safe (no mid-sequence data migrations), all tests green (including characterization) before merge.
Helper extraction
Large but cohesive, single owner. Mechanical, low risk — just lift the piece out.
Strangler split
Old and new run in parallel behind a façade. Contested ownership; cutover stays reversible.
Module-boundary rewrite
Tangled, no tests. Highest risk — characterization tests first, always.
Six pitfalls to watch
circular depsAdd a shared-abstraction module; verify with a dependency-graph tool.broken importsFaçade re-export plus a @deprecated marker on the old path.merge conflictsShort-lived branches; a single owner during the migration.nano-modulesEach new file earns ≥2 exports used by ≥2 callers — or a justified single responsibility.skipped testsA branch-coverage gate must pass before a refactor lands.re-accumulationCI hard-fails on file size — not a yellow warning everyone ignores.The split isn't done until it's canonical. In the same PR, update the agent docs to name the new module, its owner, and what may not cross the seam — so the next agent respects the boundary you just drew.
Check your judgment
Four scenarios. Pick the move a careful engineer would make.
Your agent reads customer support tickets, can query the database, and can call external APIs. A ticket contains hidden instructions. What's the risk, and the cleanest structural fix?
You're about to let an agent refactor a 2,000-line file with no tests. What's your first move?
A deny rule blocks Read(.env), but you didn't block curl. Is the secret safe?
Which is safer to expose to the agent, and why: a narrow close_ticket tool, or a broad run-any-SQL tool?
Extending the Harness
Teach the harness your team's know-how with custom skills, then shape your data so the agent reads it like a pro.
Tool vs. skill
You keep re-pasting the same five-step procedure into the agent. It works, but it lives in your chat history where a teammate can never find it, and stuffing it into an always-on rules file makes every session pay for it.
A tool is a raw ingredient in the pantry. A skill is the written recipe that turns ingredients into a dish. Fifty recipes in a drawer cost nothing until you open one.
What it is: a capability — one action.
Example: read file, git diff, create issue.
Granularity: atomic — one call.
Where it's from: the harness or an MCP server.
How it runs: the model calls it mid-task.
What it is: know-how — a recipe.
Example: “summarize this diff and flag the risk.”
Granularity: a workflow — many calls plus judgment.
Where it's from: you — markdown plus YAML.
How it runs: loaded by its description when relevant.
A skill uses tools — it's the layer above them, not an alternative. Skills don't give the agent new powers; they give it expertise, loaded on demand so 50 skills cost nothing until one fires.
Anatomy of a skill
Here's a real, tiny SKILL.md. Every part earns its place — the top block is metadata, the bottom is the recipe itself.
---
name: summarize-changes
description: Summarizes uncommitted changes and flags
anything risky. Use when the user asks what
changed, wants a commit message, or reviews a diff.
allowed-tools: Bash(git diff:*)
---
## Current changes
!`git diff HEAD`
## Instructions
Summarize the changes above in two or three
bullets, then list any risks. If the diff is
empty, say so — do not guess.
The
frontmatter
(between the --- lines) is metadata about the skill.
The name and description are the only signal the model sees when deciding whether to use this skill.
So the description says both what it does and when to use it — that's what makes it get picked.
allowed-tools pre-approves one specific command so you're not prompted mid-run. It pre-approves — it does not sandbox; your permission settings still govern the rest.
The !-prefixed line runs live at invocation, so the skill reads the current repo — not a stale pasted snapshot.
The body is the instructions the model follows — and it handles the edge case (empty diff) so the model doesn't invent recovery each run.
The description is the whole ballgame
Among 100+ installed skills, the description is the only thing the model reads to decide. Get it wrong and a perfectly good skill never fires. The formula: what it does + when to use it + key capabilities.
Helps with documents
Processes data
Implements the Project entity model
Summarizes PR diffs and flags risky changes. Use when the user asks what changed, wants a commit message, or asks to review their diff before pushing.
Be pushy with triggers. Models under-trigger far more than they over-trigger, so an explicit “Use when…” beats a subtle implication every time.
Two authoring habits that pay off
“Always output exactly three bullets” breaks the moment the input isn't what you pictured. “Output 1–3 bullets; mention only files that changed” leaves room for judgment — and prefer the positive form, since negation is a weak spot.
! prefix
Let the skill pull the current state at run time instead of pasting a snapshot that's stale by the next invocation.
Keep it small, then author in a loop
Progressive disclosure: the SKILL.md stays tiny; detail lives in bundled files that load only when the task needs them.
Three levels: metadata (always in context) → body (loads when triggered) → bundled files (read or run only when pointed at). That's why a skill can be thorough without being heavy.
Author in a loop
A skill is never right on the first draft. Build it, run it on a real task, then read what the agent actually did — and go again until the runs stop surprising you.
An instruction that just says “summarize the changes above” with no edge handling forces the model to invent recovery when the input isn't the happy case — and inventions diverge run to run. At team scale, formalize the eyeball check into an eval harness: committed fixtures (a rename, an empty diff, a conflict marker) scored by a rubric on every change.
When not to build a skill
Fixed steps? Write a script.
If the steps never change and need no judgment, a plain script is cheaper and deterministic — a release script, a pre-commit that runs lint + typecheck + test.
Needs judgment? Write a skill.
Reach for a skill when it must read context and adapt. They compose: put the deterministic part in scripts/ and let the instructions call it.
“A skill that is only fixed steps is a script wearing a costume.”
A saved query beats a raw log dump
When something breaks, the agent dumps 200 lines of logs into its context and loses the thread. Your logs are a patient's chart: raw notes are a shoebox the agent reads cover-to-cover and still misattributes; a clean, indexed chart lets it find the cause in seconds.
# BEFORE — raw log dump (~200 lines, ~12,000 tokens)
2026-05-19T14:02:11Z svc=checkout ...
svc=payments ... ERROR svc=auth
trace=7f3a48 Read timed out ...
# AFTER — one saved query (~600 tokens, same answer)
SELECT service, COUNT(*) AS error_count
FROM logs WHERE status = 'error'
GROUP BY service ORDER BY error_count DESC LIMIT 10
→ auth-service 184 | payments-api 42 | checkout-api 19
Handed raw logs, the agent fills its context window and loses track of what it was doing.
A saved query returns the same answer, pre-shaped — about 20× fewer tokens.
The answer stays sharp because the agent reads a short summary, not a haystack.
Same investigation, one line back: auth-service is the outlier, not payments-api.
“Agents would fill their context windows with log data and lose track of what they were doing.” — Datadog Engineering, 2026. A saved query is roughly 20× cheaper on tokens and more accurate, because you did the reasoning once and named it.
Four improvements that make data agent-readable
You don't need a new platform — four small changes turn raw output into evidence the agent can reason over.
Add a trace ID
One trace ID per request at the entry point, propagated through every hop and attached to every log line — so the agent can follow a request across services.
Structured logs
Emit
structured logs
with the smallest field set you commit to everywhere: timestamp, level, service, trace_id,
span_id,
tenant_id,
event, message.
Extend /health
Have your
health endpoint
return version, commit, dependency state, and last-deploy time — not just "OK" — so the agent can confirm a deploy and tell what's degraded.
Add a saved query
Wrap the questions you always ask into named views the agent calls by name — no token-burning raw dumps.
The keystone is structured logging — here's the whole idea in two lines:
// Before
console.log('user 123 paid');
// After
logger.info('payment_received', {
tenant_id: '123', trace_id: traceId,
amount: 4250 });
The “before” is a sentence — only a human can parse it, and only by reading it.
The “after” is a structured event the agent can filter (event = payment_received).
It can join across services on trace_id and scope to one customer with tenant_id.
Same information — now machine-readable.
Lock it down before you let it loose
Once an agent can read your systems, two things matter: what it can touch, and what it can see. Read-access lockdown has three tiers — and only one is a real boundary.
Server-side tool disable
Strongest. The server simply doesn't expose write tools — the agent can't call what isn't there.
OAuth scope / IAM
Strong, but only as strong as the OAuth scope grammar your vendor exposes. IAM pins the same way.
Client-side allowlist
Weakest — best-effort, not a security guarantee. Put a scrubber in front regardless.
Classify before the agent reads
Tier 0 · public
Service names, status codes — fine for the agent to see.
Tier 1 · internal
Internal IDs, hostnames, timings — low risk, keep inside.
Tier 2 · sensitive
Customer
PII,
payment fields —
redact
before the agent, via a structured-logging interceptor, not after.
Tier 3 · secrets
Tokens, keys — never logged. If it's in a log today, that's the bug.
One skill fires three ways
A person pulls the trigger, in the loop for every run.
A team member fires it without opening the harness.
Same skill underneath — climbing this list is a policy decision about who pulls the trigger, not new construction.
A real four-agent pipeline once ping-ponged internally for 11 days and ran up a ~$47,000 bill. Guard both failure modes — one run looping (cap max turns, max budget, allowed tools) and the same alert firing over and over (dedup on the alert fingerprint; concurrency limits) — with an org-level spend cap as the last-resort backstop. And an alert needs an owner and a one-page runbook before it needs an agent.
Check your instinct
Four situations. Pick what you'd actually do, then check.
You keep re-pasting the same test-scaffolding procedure to the agent, and a teammate can't find it. Skill or script — and why?
Two skills do similar things; the model keeps picking the wrong one — or neither. Where do you look first?
The agent triages an incident but blames payments-api when the real cause is auth-service two hops away. Root cause and fix?
You wire an alert webhook to fire an agent autonomously. What must you add first?
That's the whole loop
You built a mental model of what's under the hood, then an operating pattern — research, plan, implement. You learned to set up to win, to review and drive instead of rubber-stamp, to string a safety net under your work, to put up guardrails as you refactor, and finally to extend the harness itself with your own skills and readable data.
None of it was about one editor or one codebase. It's a way of working with a capable, tireless, occasionally overconfident collaborator — and it travels with you to whatever tool and whatever code you touch next. Go build something, and keep the loop tight.