Skip to content
Episode 165 · The file nobody wants to touch

Changing legacy code with AI agents

Tuesday, 10:15. Atlantica wants consolidated invoices instead of 420 separate ones, and the only invoicing code is a 6,400-line file from 2017 with 11 tests. Claude Code offers to rewrite it "in about a day". On the 1st of the month it must still send 10,000 hotel invoices, right to the cent.

Old code nobody understands? Let the agent map it, photograph what it does today, then replace it piece by piece. Never all at once.

30 min advancedMapping a legacy codebase with an agent (claims with evidence)Characterization tests: pin what the code does todayStrangler fig migration through a seamSmall, reversible steps in legacy codeAgent claims vs evidence ("done" is not proof)
  1. Problem

    StayHub for Chains needs consolidated invoices for Atlantica (one per legal entity and country), and the only invoicing code is src/billing/invoice_builder.js: 6,400 lines from 2017, 11 tests, its author long gone. It also produces 10,000 hotel commission invoices every month that must stay right to the cent.

  2. Why the simple system fails

    An agent can rewrite 6,400 lines in a day, and its rewrite will look cleaner. But nobody wrote down what the old code does, so nobody can check that the new code does the same. The undocumented behaviour (Swiss 0.05 rounding, yen without decimals, a 2019 tax exception) disappears silently.

  3. Engineering concept

    Understand before changing: map the code with the agent and verify its claims; pin today's behaviour with characterization tests built from real past outputs; find a seam; route new behaviour around the old code (strangler fig) in small steps that can each be undone.

  4. Technology

    Claude Code in plan mode or read-only for mapping, sub-agents for research, file:line citations, traces and logs of a real run, golden-master tests on anonymized production samples, feature flags, shadow comparison, a per-directory CLAUDE.md that marks the legacy area.

  5. Trade-offs

    Pinning and strangling cost weeks up front and keep two paths alive for a while. A rewrite feels faster and is cheaper to type with an agent, but the cost of a rewrite was never typing: it is discovering, by breaking it, what the old code quietly did.

01

The problem

2 min
Tuesday, 10:15, week 5. Atlantica wants consolidated invoices.
  1. You · Engineer at StayHub
    The only invoicing code is invoice_builder.js: 6,400 lines, 2017, 11 tests. The author left in 2020.
  2. Inès
    It sends about 10,000 hotel invoices on the 1st at 06:00. Please don't break it.
  3. You · Engineer at StayHub
    Claude Code says it can rewrite it as clean modules in about a day.
02

The big idea

4 min
A rewrite in a day?
The big idea

Agents make understanding old code cheap, not verifying it. Map, check the map, pin today's behaviour, then replace slice by slice.

A rewrite takes a day to type. Proving 10,000 invoices still come out identical takes exactly as long as before agents. That's where the order of the steps comes from.

03

How it works

5 min

Legacy code is code you're afraid to change because you can't tell what will break. You don't knock down an old house with the family inside: you survey it, photograph every room, add the new kitchen through one doorway, close the old one last. The surveyor is fast but sometimes confidently wrong about which walls hold the roof.

Changing the invoice builder
  1. 1
    Map, read-only

    A session that can't edit maps entry points, callers, what it writes and hidden rules (CHF rounded to 0.05, JPY without decimals, Canary Islands tax), each claim with file:line.

  2. 2
    Verify the map

    Spot-check claims with search, a staging trace of one real invoice, and the owner. The map is fluent and sometimes wrong.

  3. 3
    Pin behaviour

    A records what the code does today on real inputs and fails on any difference, bugs included. Here: 2,000 real past invoices as golden tests.

  4. 4
    Add a seam

    A is one place to switch which code runs without editing the old code: a router in front of buildInvoice, flag off.

  5. 5
    Strangle, slice by slice

    With the pattern, new code takes over slice by slice: chain accounts go to new code behind a flag; hotels stay on the old path.

04

Watch a decision being made

4 min
Worked example

You add chain invoices without risking hotel invoices

Ship consolidated invoices while 10,000 hotel invoices go out every month unchanged.

  1. 1Map, then verify

    14 cited claims and 3 unknowns. You spot-check 12; two are wrong: roundLegacy() is called dynamically as this['round' + mode] (line 1399), and credit notes reuse buildInvoice with negative lines.

    Why: The wrong claims are exactly the dangerous ones: deleting "dead" code, building credit notes separately.

  2. 2Pin today's behaviour

    2,000 anonymized invoices covering every currency, country, tax rule and credit-note type. Deliberate break on a throwaway branch: change one rounding rule, 41 tests go red. Reverted.

    Why: A test you've never seen fail proves nothing.

  3. 3A seam that changes nothing

    Router in front of buildInvoice, flag chain_invoices off. 3 CHF invoices off by 0.02. Claude offers to update the expected files. You refuse: the router passed a number instead of the currency object, skipping the 0.05 rounding. Fixed.

    Why: A step meant to change nothing must change nothing.

  4. 4Strangle, enforced

    src/billing/chains/ calls the old rounding and tax functions instead of copying them, behind the flag for the 20 pilot hotels. src/billing/CLAUDE.md marks the legacy file off-limits; a hook blocks agent edits.

    Why: Chain and hotel invoices share the exact same rules, and the rules agents follow are enforced.

The whole thing in one line: The agent made mapping fast; checking made it trustworthy. Real invoices turned "please don't break it" into a failing build.
05

In real life

3 min
Start over, or replace while running?

Netscape: rewrite from scratch

  • Restarted the browser codebase
  • About three years to the next major release
  • Joel Spolsky's "Things You Should Never Do"

Years of hidden fixes thrown away.

GitHub: incremental Rails upgrade

  • Ran the app on old and new versions side by side
  • Fixed differences in small steps
  • Feature work never stopped

Compared until the switch was boring.

06

What goes wrong

3 min
Trusting "unused" from a text search

Code can be called by a name built at runtime, a config or a scheduled job.

Instead

Runtime evidence over a full cycle before deleting anything.

Updating expected outputs to make tests pass

The alarm goes quiet exactly when it rang.

Instead

A difference is a question for the owner.

Mixing cleanups with behaviour changes

The line that matters hides among 400 lines of formatting.

Instead

Separate diffs, separately reviewed.

07

Trade-offs

3 min
ApproachFits whenYou pay
Rewrite with an agentSmall modules, fully known and well testedHidden rules vanish; users find differences
Pin + seam + strangleMoney, tax, availability with undocumented rulesWeeks of pinning; two paths a while
Copy and build besideNew work sharing nothing, or a dated temporary stepRules drift apart
Friday, 11:00. Planning the invoicing slice.
  1. Karim · CTO
    Two weeks of tests on old code? Let Claude rewrite it. We'll fix whatever breaks.
  2. Omar · On-call SRE
    Or copy the rounding and tax logic into a new service. Zero risk to hotel invoices.
  3. Karim · CTO
    And maintain two copies of tax rules forever?
08

Your job as tech lead

4 min

Claude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.

Decisions you own
  • Rewrite, strangle or leave alone, decided by how well behaviour is pinned and how reversible mistakes are.
  • Which real past outputs become characterization tests, and which cases they must cover.
  • Any intended change to legacy behaviour: only with the owner, as its own change.
Tell Claude Code
Don't edit any files. Map src/billing/invoice_builder.js: entry points, every caller in the repo (including dynamic calls and scheduled jobs), what it writes, and every rounding, tax or special-case rule. Cite file and line for each claim, mark how sure you are, list what you couldn't determine. Write it to docs/legacy/invoicing-map.md. Then propose how to select ~2,000 anonymized past invoices covering every case.
Check before you accept
  • Spot-check a sample of map claims yourself: search, a staging trace, the owner.
  • Golden samples cover CHF, JPY, Canary Islands tax and credit notes, with no personal data.
  • A seam step shows zero golden differences with the flag off.
Red flags
  • The agent "tidies" legacy code during a mapping session.
  • It proposes updating expected outputs or deleting samples to get green.
  • "This is dead code" based only on a text search.
09

Check yourself

2 min
Q1/3Scenario
Your project must change a 6,400-line file nobody understands. What's the first thing you ask Claude Code to do?
How confident are you?

Next episode: week 6. The invoicing slice is safe, but Karim has just heard from Atlantica's CTO that the pilot might slip, and it wasn't from you.

Finished?

Mark it complete to update your skill map.

Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Visual diagram, Analogy, Why it matters, Check your understanding, Practice ladder, Explain it yourself.
10

Technical deep dive

Checking the map
Worked example

Chain invoices without risking hotel invoices

Add Atlantica's consolidated invoices on top of src/billing/invoice_builder.js in six weeks, while 10,000 hotel invoices go out every month unchanged.

  1. 1Map, read-only

    New session in plan mode: "Don't edit anything. Map invoice_builder.js: entry points, callers anywhere in the repo, what it writes (tables, files, emails), rounding and tax rules, anything that looks like a special case. Cite file:line for every claim. List what you're unsure about." Result: 14 claims and 3 unknowns in docs/legacy/invoicing-map.md.

    Why: A session that can't edit can't "fix" things on the way. Citations turn a fluent essay into claims you can check.

  2. 2Verify the map

    You spot-check 12 of the 14 claims: grep for callers, one real hotel invoice traced in staging, and 20 minutes with Inès on the tax rules. Two claims are wrong: roundLegacy() is called dynamically as this['round' + mode] (line 1399), and credit notes reuse buildInvoice with negative lines.

    Why: The two wrong claims are exactly the ones that would have caused damage: deleting "dead" code and building chain credit notes separately.

  3. 3Pin today's behaviour

    Claude Code selects 2,000 anonymized invoices from the last 12 months, covering every currency, country, tax rule and credit-note type, and writes golden-master tests. You check coverage (CHF, JPY, Canary Islands present?) and ask for a deliberate break on a throwaway branch: change one rounding rule. 41 tests go red. Reverted.

    Why: Representative samples catch the hidden rules; a test you've never seen fail proves nothing.

  4. 4Add a seam, change nothing

    A router invoice_router.ts in front of buildInvoice, flag chain_invoices off. Expected: zero golden differences. Actual: 3 CHF invoices off by 0.02. Claude offers to update the 3 expected files. You refuse: the router passed a plain number instead of the currency object, skipping the 0.05 rounding. Fixed; 2,000 green.

    Why: A step meant to change nothing must change nothing. The golden tests just caught the same kind of bug as the spring incident.

  5. 5Strangle, slice by slice

    The new src/billing/chains/ module builds consolidated invoices per legal entity and country and calls the old rounding and tax functions instead of copying them. Behind the flag for Atlantica's 20 pilot hotels; shadow comparison of totals against the bookings ledger on every run. src/billing/CLAUDE.md says the legacy file is off-limits, and a hook blocks agent edits to it.

    Why: Hotels never touch the new path; chain invoices reuse the exact rules hotels' invoices use; the rules agents follow are enforced, not just requested.

The whole thing in one line: The agent made mapping fast; you made it trustworthy by checking it. Golden tests from real invoices turned "please don't break it" into a failing build, and the seam let chain invoices ship without editing the code that bills 10,000 hotels.
StepEvidence it's doneWhat it caught
Map14 claims with citations; 3 unknowns with ownersThe hidden rules: CHF, JPY, Canary Islands tax
Verify12 claims checked by grep, trace or InèsDynamic call to roundLegacy(); credit notes reuse buildInvoice
Pin2,000 golden tests; 41 red on a deliberate breakProof the tests can fail
SeamRouter merged, flag off, 2,000 greenRouter skipping CHF rounding
Stranglethe 20 pilot hotels; shadow totals match the ledgerNothing yet: that's the point
11

Visual diagram

Click a component

Strangling the invoice builder: chain invoices go to new code through one seam, while the old path keeps serving hotels. Click a component, or follow the walkthrough.

12

Analogy

  • Renovating a lived-in old houseSurveyor walks the houseChanging legacy codeAgent maps the code
  • Renovating a lived-in old houseFlip each switch to checkChanging legacy codeTrace one real run
  • Renovating a lived-in old housePhotograph every roomChanging legacy codeCharacterization tests
  • Renovating a lived-in old houseOne doorway to the extensionChanging legacy codeA seam in the code
  • Renovating a lived-in old houseClose old kitchen lastChanging legacy codeRetire old path last

Where it breaks: a surveyor rarely invents a wall. An agent can describe code that isn't there, or call a function unused when it isn't. Always check.

13

Why it matters

Rewrite or replace slowly?

Rewrite it

  • Clean code by tomorrow
  • Hidden rules silently vanish
  • Nothing to compare against
  • One giant switch-over

Fast to type, slow to trust.

Pin, then replace

  • Old path keeps working
  • Every difference shows up
  • Each step can be undone

Slower start, no surprises.

Old code holds forgotten decisions. StayHub's rounds Swiss totals to 5 centimes and drops decimals for yen. Nobody wrote that down, but hotel accountants rely on it.

And mistakes here are often permanent. A sent invoice can't be edited. Fixing the code doesn't fix the 1,800 wrong invoices already in hotels' inboxes.

14

Check your understanding

Q1/3Reasoning
Put the steps for changing legacy code in order.
  1. 1.Retire the old path once nothing uses it
  2. 2.Pin today's behaviour with characterization tests
  3. 3.Add a seam that still routes everything to the old code
  4. 4.Map the code read-only, with citations
  5. 5.Route one slice to the new code behind a flag; compare
  6. 6.Verify the map's claims (grep, trace, ask a person)
How confident are you?
15

Practice ladder

Step 1 · Trace

Which claims would you check?

Debugging Warm-up

Claude Code mapped src/inventory/calendar.ts (2016, 4,100 lines, 30 tests; used by search and every booking) in a read-only session. Its map includes:

  1. "releaseHold() is only called by the expiry job" (cites :2210)
  2. "Overbooking allowance is read from hotel settings" (cites :880)
  3. "syncChannel() is dead code; nothing calls it" (no citation)
  4. "Availability results are cached for 60 seconds" (cites :120)

For each claim, trace what could make it wrong and what evidence you'd ask for before relying on it.

Step 2 · Complete

Complete the pinning plan

Design Practice

Fill the blanks in the plan for pinning the invoice builder before the router is added.

  1. Samples: ___ anonymized invoices from the last ___ months, covering every ___, ___, ___ and credit-note type.
  2. Each test compares the ___ output (totals, every line, rounding, tax codes) with the saved ___ copy.
  3. Before trusting the tests: on a throwaway branch, ___ and confirm the tests go ___.
  4. Rule in src/billing/CLAUDE.md: never ___ to make a golden test pass; report the ___ instead.
  5. After adding the router with the flag ___, the expected number of golden differences is ___.
Step 3 · Solve

Chain availability through the old calendar

Design Stretch

Atlantica's central reservation system (CRS) will push availability (rooms left per night) through the new ARI API, and the batch applier must write it. Availability lives in src/inventory/calendar.ts (2016, 4,100 lines, 30 tests), used by search and every booking for ~1.3M guests a day. Today hotels update it by hand in the extranet.

Write the plan: the first instruction to Claude Code, how you verify its map, what you pin and from what data, where the seam is, how you roll out, and what makes each step reversible.

Step 4 · Decide

Rewrite this one?

Decision Stretch

A different module: format/currency.ts, 900 lines from 2019, formats prices for display (not for invoices). It has 140 tests covering every currency StayHub supports, and they run in 3 seconds. It's tangled and slows every change to the price display. Claude Code proposes rewriting it as a thin wrapper around the platform's built-in number formatting, about 120 lines.

Write your decision: "We chose X over Y because…, accepting the risk of…".

16

Explain it yourself

Karim says: "Claude can rewrite the invoice builder in a day. Why are we spending two weeks on tests for old code?" Answer him in a few sentences.

0 words