AI-Assisted Coding
Practical techniques for shipping real software with AI coding agents.
ADRs Are AI Agent Fuel
Architecture Decision Records give AI coding assistants the one thing your code can’t: why you chose what you chose.
Tip #002The 5-Second AI Hallucination Detector
AI-generated code can call APIs that never existed. A type checker in pure-check mode catches it in seconds.
Tip #003Run Two AI Agents at Once with Git Worktrees
Two agents in one folder overwrite each other’s files. Git worktrees give each its own directory and branch on the same repo.
Tip #101Review the Tests First, Split Big Diffs
Read the tests before the implementation to learn intent first — then keep diffs near 100 lines and split anything approaching 1,000 by stacking, file group, or vertical slice.
Tip #102Big Refactor? Write a Codemod, Not Hand Edits
When a refactor touches hundreds of lines, stop hand-editing — write a codemod, sed script, or AST transform so one rule applies everywhere and the diff stays reviewable.
Tip #103That Ugly Code Is Load-Bearing
Chesterton’s Fence for AI-assisted refactoring: run git blame and answer 3 questions before you let an agent delete code it doesn’t understand.
Tip #104Early Returns Kill Nesting, Named Constants Kill Magic Numbers
Two thirty-second refactors that make AI-generated code readable: flip nested ifs into guard clauses, and pull unexplained numbers into named constants.
Tip #105Never Cast an Env Var to Bool
bool(\“false\”) is True — a raw cast silently inverts your kill switch. Parse booleans through one shared true/false vocabulary for env vars and config alike.
Tip #106A Malformed Config Value Should Log and Fall Back
Route every setting through one typed reader that logs a bad value and degrades to a documented default — so a typo becomes a log line, not an outage.
Tip #107Result And Wait Are Deadlocks Waiting
Why blocking on .Result or .Wait deadlocks C# web apps under load — and how to go async all the way down, with cancellation tokens where they matter.
Tip #109Your Site Screams AI-Generated: Grep the Three Tells
The AI-slop look is greppable: purple gradients, centered-everything, one uniform 16px+ radius — three commands find it, three fixes break it.
Tip #110outline: none Without a Replacement Focus Style
One line of CSS locks keyboard users out of your app — how :focus-visible gives them back a focus ring without cluttering the mouse UI.
Tip #111Server State Belongs in a Cache Layer, Not Global State
Backend-owned data doesn’t belong in your global store — put it in a data-fetching cache layer and get caching, dedup, and invalidation for free.
Tip #112AI-Generated UIs Skip the Empty States — Prompt for Every One
AI builds your UI’s happy path; users live on the sad path. A checklist for prompting loading, empty, error, and overflow states — every single time.
Tip #113Never Launch a Goroutine Without an Owner
Goroutines are not garbage collected — every `go` statement in AI-generated Go code needs a cancellation path, a channel owner, and a WaitGroup, or it leaks until the service falls over.
Tip #115Use ?? For Defaults, Not ||
Why `config.port || 3000` silently discards 0, empty string, and false — and how nullish coalescing (`??`) only falls back on null/undefined.
Tip #116forEach Doesn’t Await Anything
Why an async callback inside forEach silently races ahead — and how to choose between for...of + await and Promise.all deliberately.
Tip #117Reject Double Bang in Generated Kotlin
Why every !! in AI-generated Kotlin is a deferred crash, and how explicit nullables and sealed results make the empty case impossible to skip.
Tip #118Ban GlobalScope.launch in Code Reviews
Unscoped coroutines leak work and swallow cancellation — make GlobalScope.launch an automatic review reject and keep every coroutine tied to an owner.
Tip #121That Innocent `if exists` Check Is a Race Condition
Why check-then-act filesystem code is a TOCTOU race, and how one parameter — exist_ok=True — deletes the bug and a line of code at the same time.
Tip #122sum() Streams Lazily, sum([]) Builds First
Drop the square brackets inside sum() and a million-item memory spike becomes a lazy stream — same result, one character less.
Tip #123Two Ifs In A Comprehension? Give It A Function Name
When a list comprehension grows a second condition, expand it into a named generator function — readable, testable, and a pattern your coding agent will copy.
Tip #124Threads Won’t Fix CPU-Bound Python
Threads or asyncio for I/O-bound work, ProcessPoolExecutor for CPU-bound — the one-line decision AI assistants get wrong, and how to check which bound you’re fighting.
Tip #125String += In A Loop Is O(n²), Use Join
Why += on strings inside a loop is quadratic, and the two linear fixes — ''.join() on a list of parts, or io.StringIO for streaming builds.
Tip #126Always Chain Exceptions With 'from e'
Re-raising a wrapped exception without 'from e' buries the original traceback — two words keep the stack trace pointing at the real cause.
Tip #127Protocol Over Inheritance: Structural Test Seams in Python
Use typing.Protocol to swap real dependencies for test fakes with zero base-class coupling — structural typing the checker still verifies.
Tip #130Reject AI-Generated Rails Callbacks That Do Slow Work
AI assistants love after_save callbacks that call APIs — one review rule keeps saves fast, tests offline, and slow work retrying safely in jobs.
Tip #131Enforce Uniqueness in the Database, Not Just the Model
Rails' validates_uniqueness_of is race-prone under concurrent load — keep it for friendly errors, but add a database unique index for the guarantee.
Tip #132Every unsafe Block Needs a Safety Comment
An unsafe block suspends the compiler’s checks — write a // SAFETY comment stating the invariant it upholds, and let reviewers and AI agents verify instead of guess.
Tip #133Never Build Shell Commands As Strings
One unquoted variable can delete the wrong directory. Use Bash arrays for argument lists, quote every expansion, and start scripts with set -euo pipefail.
Tip #134Swap Offset Pagination for Keyset Pagination
On a large, changing table, LIMIT/OFFSET silently skips and repeats rows — keyset pagination pages by the last row’s key instead and stays consistent while the table mutates underneath you.
Tip #135Audit AI-Generated Swift for Task.detached
See Task.detached in AI-generated Swift? Stop and check it — a detached task silently drops cancellation, priority, and actor isolation, and it’s almost always the wrong default.
Tip #136Give Every Suppression an Expiry Date: ts-expect-error over ts-ignore
@ts-ignore stays silent forever; @ts-expect-error errors the moment the underlying bug is fixed, so type suppressions can’t rot in your codebase.
Tip #55Comment The Why, Never The What
Delete the comments that restate your code and keep the one kind that never rots — why-comments that record intent for humans and AI assistants alike.
Tip #56Never Leave Commented-Out Code
Delete dead code instead of commenting it out — git remembers every line, and your AI assistant reasons better over files with zero graveyards.
Tip #57Keep a Rules File for Your Agent
Document your conventions once in a rules file — CLAUDE.md, AGENTS.md, .cursor/rules — and every AI session follows them without you retyping a thing.
Tip #58Document Gotchas Inline, Where They Bite
The bug that cost you a day belongs as a comment right where the next person will hit it — not in a wiki nobody opens.
Tip #59Docs Are Executable Context Now
Nobody read your docs — until your coding agent started reading every word. Why decision records are the highest-leverage context you can write, and how to keep one in 10 minutes.
Tip #60The ADR Alternatives Section Is Gold
Record the options you rejected — pros, cons, and one line on why each lost — so your AI coding agent stops re-proposing ideas you already ruled out.
Tip #61Onboard AI To A New Codebase Like A Human
Give your coding agent the same onboarding a new hire gets — a fixed five-step recon order plus one traced request — so its code matches your conventions.
Tip #62Trace One Real Request End-To-End
The fastest way to understand any codebase: follow one real request from the entry point to the database and back — then hand that map to your AI agent.
Tip #63Record the Local Dialect So AI Code Blends In
A 3-item conventions checklist — naming, patterns, house rules — that makes AI-generated code match your codebase instead of a generic default.
Tip #64The SMIG Rule: Situation, Mechanism, Implication, Gotcha
A four-part rule for onboarding docs and code tours: skip what the code shows, spend your words on implications and gotchas.
Tip #65Don’t Dump A File Tree And Call It Onboarding
A folder listing tells your coding agent what exists, not what to act on — replace the raw dump with five narrated moves that trace the real path through the system.
Tip #66Tour Files: Persona-Targeted Code Walkthroughs
Turn onboarding into a guided route: .tour files anchor step-by-step walkthroughs to real files and lines, one tour per persona, played back inside the editor.
Tip #67Verify Every Claim Against the Code
READMEs drift; the code doesn’t. How to onboard to any repo by tracing one real request end to end and verifying every claim against source.
Tip #68Run Your Native Test Suite Under Sanitizers
Why ASan, UBSan, and TSan catch the C/C++ bugs a green line-coverage report ships — and how to wire them into the tests you already have.
Tip #69Trap Every Crash Before You Fix It
Write the failing regression test before the fix, run it under a sanitizer, and the bug can never silently come back.
Tip #70Don’t Trust In-Memory Database Tests
In-memory database fakes skip SQL translation — the query that passes in tests can still break in production. When and how to test against the real engine.
Tip #71Assert All Four UI States
Most widget tests only check the happy render — here’s how to assert loading, empty, error, and success, with the mocking patterns that make each state reachable.
Tip #72Never waitForTimeout — Wait for the Actual Network
Fixed sleeps make E2E tests slow AND flaky at the same time — replace every one with a wait on the real condition: a network response or an auto-retrying assertion.
Tip #73Make a Flaky Test Fail Ten Times on Purpose
Reproduce a flaky test locally with --repeat-each=10 (and its pytest/Go equivalents) instead of burning a week of CI retries.
Tip #74Inject Clocks, Random Seeds, and Temp Dirs
Flaky tests almost always read a hidden input — the real clock, a shared RNG, or a common temp dir. Inject all three and the same inputs give the same result on every run.
Tip #75Slice Tests, Not a Full Boot
Why @SpringBootTest should be the exception, not the default — use @WebMvcTest and @DataJpaTest slices for faster feedback, and teach your AI assistant the same habit.
Tip #76Shuffle Your Test Suite to Expose Order-Dependent Tests
Run your suite in random order to surface shared-state bugs on your machine — with the exact shuffle and seed flags for Vitest, Jest, pytest, Go, and RSpec.
Tip #77A Test That Passes Against Empty Code Tests Nothing
If your suite stays green after you delete the production code, it asserts nothing. The five-minute audit, the placeholder smells, and the tools that automate the check.
Tip #78Never Launch Real Coroutines in Tests
Why real dispatchers make async tests flake in CI — and how injected TestDispatchers, virtual time, and full Flow lifecycle tests make them deterministic.
Tip #79One Fixture, Three Databases
Parametrize one pytest fixture across SQLite, PostgreSQL, and MySQL and every test that uses it runs once per backend — 3x coverage from one decorator.
Tip #80Tag the Slow Tests, Get Your Inner Loop Back
Use pytest markers to split slow integration tests from fast unit tests, so every save runs in under a second while CI runs everything.
Tip #81Always Mock With Autospec True
A plain Mock says yes to methods that don’t exist — patch with autospec=True so your tests fail when AI-generated code misuses a real API.
Tip #82Async Mocks: Assert Awaited, Not Called
assert_called_once passes even when your code forgot the await — assert_awaited_once is the assertion that actually catches the bug.
Tip #83Never Disable Warnings in Pytest
Deprecation warnings are early failure signals — why silencing pytest warnings trades a noisy test run today for a production break tomorrow, and how to filter with precision instead.
Tip #84pytest --lf: Re-run Only What Broke
Shrink your debug loop from minutes to seconds with pytest --lf, -x, --ff, and --sw — and why it matters even more when an AI agent is doing the debugging.
Tip #85100% Test Coverage Is a Red Flag, Not a Badge
Why chasing 100% coverage suite-wide wastes effort — target ~80% overall and push auth, payments, and data mutation toward 100% branch coverage instead.
Tip #86Test Background Jobs for Idempotency and Retries
Queues deliver at-least-once, so every job will eventually run twice — here’s how to test that a duplicate run charges once, emails once, and recovers cleanly after a crash.
Tip #87Flaky Tests Trace To Time, IO, Or Randomness
A test that passes sometimes almost always touches real time, real files, or real randomness — inject all three and the flake disappears.
Tip #88Make Docs Executable: Doc Tests
Compile the examples in your docs as part of the test suite — Rust doc tests, Python doctest, and pytest patterns that make stale documentation fail CI.
Tip #89Run Shell Scripts Through ShellCheck First
Lint AI-generated bash with ShellCheck before you test it — most shell bugs are static quoting and portability issues a linter catches in seconds.
Tip #90Test Shell Scripts From a Path With Spaces
Run your shell scripts from a temp directory whose path contains a space — quoting and cwd bugs break loudly, for free, before they break in production.
Tip #91Test Data Invariants at the Database Layer
Why app-layer validation isn’t enough — prove unique keys, foreign keys, and check constraints on a real, disposable database that matches production.
Tip #92Fake the Network, Persistence, Clocks — and Permission State
You already fake the network and the database in tests — inject a fake for permission state too, so the denied branch stops shipping broken.
Tip #93Prove The Bug Before You Fix It
Make your AI coding agent write a failing test that reproduces the bug before touching the fix — proof it exists, proof it’s gone, and a permanent guard.
Tip #94Have a Separate Subagent Write the Reproduction Test
Split the reproduction test from the fix — a fresh subagent writes the failing test without seeing the fix, so shared blind spots can’t rubber-stamp a broken patch.
Tip #95The Tautology Mock
When a mock returns the exact value the test asserts, the test passes with zero production code running — how to spot tautology mocks and replace them with real implementations or in-memory fakes.
Tip #96Keep Tests DAMP, Not DRY
Why duplication is fine in tests: DAMP (Descriptive And Meaningful Phrases) beats DRY when a failing test has to explain itself.
Tip #97Have the Agent Discover the Project’s Own Commands
Stop your AI agent from guessing pytest or npm test — make it read package.json, pyproject.toml, the Makefile, and CI to find the repo’s canonical commands.
Tip #98Never Trust AI-Written Tests To Validate AI-Written Code
When one model writes both the code and its tests, they can share the same wrong assumption — a green suite proves consistency, not correctness. Here’s how to break the loop.
Tip #99A Raw Owning Pointer in C++ Is a Review Blocker
Why a raw owning pointer should fail C++ code review — and the RAII checklist (unique_ptr, shared_ptr, span, rule of zero) to enforce on AI-generated code.
TipCheck context.mounted After Every await
Why Flutter apps crash after slow async calls — and the one-line context.mounted guard that fixes use-after-dispose for good.
TipOne Model Writes, a Different Model Reviews
Why the model that wrote your code can’t be trusted to review it — and how a two-model write/review loop with a human final call catches far more bugs.
TipJava Optional Is a Return Type Only
Optional was designed for one job — method return types. Why fields, parameters, and unchecked .get() fight the design, and the patterns to use instead.