CompanionExisting code: adopt and rescue
Every chapter of this guide starts from a clean folder: grill, plan, build test-first, gate, ship. Real life often starts from a folder that already exists — full of code nobody fully understands, with no tests, no gates, and users who depend on it anyway. The industry word for that folder is brownfield: not an empty lot, but a site where something already stands and cannot simply be knocked down. This page is the missing on-ramp: how to bring an existing codebase into the guide's machinery without breaking what works. The order of moves is the whole content — assess, freeze, pin, gate, then change — and every instinct you'll have (fix that obvious mess first, add the overdue feature first) is a version of doing them backwards.
You didn't start from zero
Two very different roads lead to the same folder, and it's worth knowing which one you're on:
The inherited codebase
A departed freelancer's project, a predecessor's system, an open-source app you forked. Somebody understood this once; that somebody isn't here. The knowledge gap is the main problem — the code may actually be decent.
Your own vibe-coded mess
The salon owner's real story, more often than not: before finding any of this guide's machinery, she and an agent built the booking app in a glorious three-day sprint — no tests, no tickets, no review, features piled on features until change number forty broke change number twelve. It works. Mostly. Nobody can say why.
The arrivals differ emotionally — inheriting invites too much caution, owning invites too little ("I wrote it, I'll just quickly…") — but the protocol below is identical, because the technical situation is: running code, unknown behaviour, no net. One honest reframe helps with arrival B: the mess is not evidence that agentic coding failed. It's evidence of rung-1 usage — full-speed generation with none of the harness. The same agent that built the mess is about to be the most effective rescue tool you have.
Assess before touching — the read-only survey
Rule one: the first agent session changes nothing. Run it in plan mode (or state "read-only — do not modify any file" as the first line), and have it do what agents do better than anyone: read the entire codebase without getting bored and report what's actually there.
Say this inside Claude Code (it's a prompt, not a shell command):
Read-only survey — change nothing. Map this repo and report:
1. What runs: entry points, how to start it, what it serves.
2. What's tested: which tests exist, do they pass, what do
they actually cover.
3. The money paths: which code handles bookings, payments,
auth, and customer data.
4. Red flags: secrets in the code, dead code, dependencies
with known problems, TODO/FIXME clusters.
5. Draw the floor plan: the real modules, what each owns, and
where they reach into each other.
Two documents fall out of this survey, and both are Section-10 artifacts generated from the code instead of written before it. First, the floor plan (Section 10): the modules as they really are, doors and all the places code reaches around the doors. Second, a draft AGENTS.md — have the agent propose one from what it found (how to run the app, how to run the tests, the conventions the code already follows), then cut it to the lean standard of Section 2. On a greenfield project these files describe intent; here they describe reality, which makes them more valuable, not less: every future session starts oriented instead of rediscovering the repo from scratch.
Freeze — no new features while there's no net
The moment the survey confirms "no meaningful tests," a temporary law takes effect: no new features until a net exists. Every feature added to an unpinned codebase is built on behaviour nobody can verify, makes the eventual pinning harder, and — the subtle cost — trains you to keep working at rung 1 because it keeps appearing to work. The freeze isn't punishment; it's triage: the fastest route to shipping features safely runs through a few days of net-building first.
For changes that genuinely can't wait — the salon's booking form is throwing errors today — apply the smallest-blast-radius rule: the fix that touches the fewest files and changes the least behaviour wins, even when a "proper" fix is visible from where you stand. Blast radius in an untested codebase isn't measured in files changed but in behaviours you might have changed without knowing — and without tests, that second number is unknowable. The proper fix goes on the ticket list, tagged "after the net exists." Write the freeze into the draft AGENTS.md so every agent session honours it too: "No new features. No refactoring beyond the task at hand. Smallest possible diffs."
Characterization tests — pin what it does, bugs included
An ordinary test asserts what the code should do. A characterization test asserts what the code currently does — including behaviour that is probably a bug. That inversion, from Michael Feathers' Working Effectively with Legacy Code, is the single most important idea on this page, because in an unknown codebase you can't yet tell load-bearing quirks from accidents. Maybe the app books appointments on public holidays because nobody thought about holidays — or because the owner works holidays and told the previous developer so. Pin the behaviour first; argue about correctness later, one deliberate change at a time.
Why you'd ever write a test for a bug. The tests aren't a statement that the behaviour is good — they're a tripwire that tells you when it changes. Once "books on holidays" is pinned, a refactor that silently stops holiday bookings turns a test red, and you get to decide on purpose: bug fixed (update the test, gladly) or feature broken (thank the tripwire). Without the pin, the same change ships silently and a regular customer's Boxing Day appointment quietly vanishes. In a codebase with no documentation, current behaviour is the only spec that provably exists — characterization tests write it down.
Here's the good news of the whole page: agents are unusually good at this. Writing characterization tests by hand is tedious archaeology — read code, guess inputs, record outputs, repeat — precisely the tireless-reading work agents excel at, with none of the design judgment that makes greenfield TDD prompts tricky:
Say this inside Claude Code:
Write characterization tests for the booking module. Pin
CURRENT behaviour exactly as it is — do not fix or improve
anything, even where behaviour looks wrong. Where it looks
wrong, add a comment: "PINNED: possibly a bug — books on
public holidays." Cover the money paths first: create,
cancel, reschedule, overlap handling, price calculation.
For the truly tangled core — the 800-line function nobody dares open — use the golden-master variant: don't write assertions about the logic at all. Feed it a broad batch of recorded inputs, capture every output verbatim into files, commit those files, and let the test simply diff future output against the recording. Crude, and exactly right for code you can't yet reason about: any change to the tangle now announces itself, character by character. Both kinds of pin are blackbox tests — they hold the outside steady precisely so the inside becomes safe to rebuild later.
Adopt the gates one at a time — ratchet, don't boil the ocean
Point Section 7's full gate list at a rescued codebase and everything fails at once — strict types: hundreds of errors; linter: thousands of warnings; coverage: near zero. Turning it all on today means either a red wall nobody respects or a month of cleanup nobody finishes. Adopt gates in order of what each one buys you, and make each a required check (the machinery from Setup, part two) before adding the next:
The build passes
One command compiles and starts the app, in CI, on the current code. Sounds trivial; in a vibe-coded repo it often isn't — and until it's true, nothing else can be checked mechanically.
One smoke test
A single end-to-end test of the most important money path: open the app, book an appointment, see it confirmed. One test, guarding the one thing that must never silently break.
CI on every PR
Build + smoke test + the growing characterization suite run on every pull request, red blocks merge. From this rung on, the net is enforced, not remembered.
Coverage on changed code only
The ratchet's key trick: require tests for lines touched by this PR, not for the whole repo. Old code is grandfathered; anything you touch gets pinned or tested as the price of touching it. Coverage climbs exactly where the risk is — where change happens.
The rest, gradually
Strict types (per-folder if needed), lint rules, Section 7's non-functional gates, security scans — each turned on for new code first, ratcheted over old code module by module.
The principle under the ladder is the ratchet: every gate is set to "no worse than today," then tightened as the code improves — never loosened. It's the same move as Section 7's Lighthouse budgets ("no worse than today," not perfection), applied to code quality generally. A ratchet turns cleanup from a project you'd have to schedule into a by-product of ordinary work.
Strangler fig — for the messes refactoring can't reach
Some cores are beyond incremental repair: the booking logic is one 2,000-line file where date handling, pricing, emails and database calls interleave, and every pulled thread unravels three others. For these, stop repairing and start strangling — Martin Fowler's strangler-fig pattern: build clean new modules beside the old code, route traffic to them one path at a time, and let the old code die of disuse.
The tree behind the name. A strangler fig starts life as a seed high in a host tree's crown, grows roots downward around the trunk, and eventually stands on its own — by which time the host inside has died and rotted away, leaving a living tree the shape of the old one. The software version: the old system keeps running and serving users the entire time (that's the point — no big-bang rewrite, no launch-day cliff), while the new system grows around it path by path. There is never a day the app is down for rebuilding; there's just a shrinking share of requests still handled by old code.
Concretely for the salon app: cancellations are the worst-understood path, so the agent builds a clean cancellation module — grilled, planned, test-first, everything Sections 3–6 prescribe, because new code gets the full greenfield treatment even inside a rescue. One small edit to the old code routes cancellation requests to the new module. The characterization tests from above are the referee: they pinned how cancellations behaved before, so they can confirm the new module matches (except where you deliberately fixed a pinned bug — updating that pin is now a conscious decision with a diff). Then the next path, and the next. Each step is small, shippable, and reversible — the route back is re-pointing one call.
Strangle or refactor in place? Refactor in place when the code is understandable but untidy — clear modules, bad naming, some duplication — because there the pins plus small diffs are cheap and safe. Strangle when understanding the old code costs more than rewriting the behaviour: interleaved concerns, no seams to test at, changes that keep causing distant breakage. The tell is your own recent history — if the last three "small fixes" each broke something unrelated, stop fixing that code and start replacing it.
Salvage or restart — the honest fork
Sometimes the rescue costs more than the thing being rescued. Ask three questions, and answer them from the survey rather than from hope:
Does any part have tests worth keeping?
Real assertions on real behaviour — even a handful — are evidence somebody understood that part once, and a nucleus to grow the net from. Zero tests anywhere shifts the maths toward restarting.
Is the domain logic recoverable?
Can you (or the agent) point at the code that encodes actual business rules — pricing, slot allocation, cancellation windows? Recoverable rules are the valuable part of any codebase. If the rules are smeared untraceably through UI handlers, the code holds little worth carrying.
How much of it is glue?
Framework wiring, boilerplate, configuration, UI scaffolding — an agent regenerates glue in hours, better than the original. A codebase that's 80% glue and 20% muddled logic is cheap to rebuild and expensive to rescue.
Mostly-glue, no tests, unrecoverable logic → restart. And here's the reframe that takes the sting out: restarting onto the Section 13 template, carrying the knowledge, is not starting over. The first build — even the vibe-coded mess — settled the expensive questions: what the product does, which features earn their keep, where the sharp edges live (the salon's walk-in-versus-appointment rules took three rewrites to get right; that knowledge survives). Before retiring the old code, harvest it deliberately: have the agent extract a PRD from what the app actually does, keep the floor plan from the survey, and keep the characterization tests of the money paths — on the rebuild they flip from tripwire to target: the new implementation must make the pins pass (minus the pinned bugs you're glad to drop). Then build on the golden-path template with all of it in hand. The mess's last job is to be the spec for its replacement — weeks of learning, preserved; only the tangle left behind.
Trust, recalibrated — working without the net
One more adjustment, and it governs everything above until the gates exist: the guide's normal risk profile doesn't apply here yet. Section 6 lets you skim low-risk diffs and Section 14 tunes a trust dial — but every calibration in those chapters assumes the machinery: tests that catch regressions, CI that blocks bad merges, staging that absorbs mistakes. In a codebase with none of that, there is no such thing as a low-risk change, because nothing exists to tell you a change was risky until a user does. So until the net is real, run at the ladder's bottom rung on purpose:
- Read-only first. Survey sessions before any writing session — always.
- One file at a time. Small diffs aren't a preference here; they're the only changes whose blast radius you can even estimate.
- A human reads every diff. Yes, every line, the rung-2 discipline the rest of the guide works to escape. Here it's correct: your eyes are temporarily the only gate that exists.
- No AFK runs. Unattended agents in an unpinned codebase is the highest-risk combination this guide can describe.
The restrictions are the strongest argument for building the net quickly — they lift one by one, mechanically, as the machinery arrives. Smoke test green in CI? Trivial diffs can be skimmed. Money paths pinned? The agent can refactor inside them under test. Full gates plus staging? You're back on the guide's normal calibration — Section 14's trust dial, now standing on machinery instead of hope. That's the quiet promise of this page: a rescue doesn't end when the code is clean. It ends when working in this repo feels like every other chapter of the guide — and from there, the loop is just the loop.
Quick check: you inherit the salon's old booking app — no tests, customers using it daily — and the owner's first request is a small new feature, "just add a tip option to checkout." What do you actually do first?
Not the feature. First the read-only survey (what runs, what's tested — here, nothing — where the money paths are), which takes an afternoon, not a month. Then the freeze conversation with the owner: no new features until a minimal net exists, because checkout is a money path and an unpinned change there can silently corrupt real bookings. Then characterization tests pin checkout's current behaviour, CI starts running them on every PR — and then the tip option, built test-first as a small diff, lands safely inside a week. The honest sell to the owner: the "detour" is the fast path. Adding the feature first works right up until it doesn't, and "it doesn't" means a customer charged wrongly with no test to have caught it and no way to know what else broke.