DEV Community

yureki_lab
yureki_lab

Posted on

How I Onboarded Onto a 400,000-Line Legacy Codebase in 5 Days With Claude Code

TL;DR

I inherited a 9-year-old, ~400,000-line codebase with no surviving original authors and a ticket due in a week. Instead of asking Claude Code to "explain the code," I gave it five days of structured jobs — build a map, trace one request end to end, cite every claim with file:line — and shipped my first real PR on day 5. Here's the exact workflow, including the three times the agent confidently lied to me.

The Problem

The handover was one paragraph long. That's not an exaggeration — the person who knew this system left, and what I got was a README that referenced a deploy script that no longer exists.

The shape of the thing:

  • ~400,000 lines, ~1,900 source files
  • 9 years old, three languages in the same repo (a Python 3.13 service layer, a Node.js 22.x API, and a pile of TypeScript on the front end)
  • Zero architecture docs that were written after 2023
  • A ticket assigned to me on day 1, due at the end of the week

The classic advice is "read the code." At 400k lines, reading the code is not a plan, it's a coping mechanism. You can read 50 files in a week and still not know which of them matter.

The temptation with an AI coding agent is to paste in a file and ask "what does this do?" I did that for about two hours on the first morning and got exactly what you'd expect: fluent, plausible summaries of code I could already read myself. The bottleneck was never reading — it was not knowing where to look.

So I changed the question I was asking.

How I Solved It

The reframe that made everything work:

Don't ask the agent to explain code. Ask it to produce artifacts you can check.

An explanation is unfalsifiable prose. A map, a list, a trace, a table — those have shapes. You can spot-check a shape. You cannot spot-check a vibe.

Here's how the five days actually went.

Day 1 — Ask for a map, not a tour

My first real prompt wasn't about code at all. It was about build configuration, because build config is the one part of a legacy repo that can't lie — if it's in the build, it ships.

Do not read source files yet.

Read every build/dependency manifest in this repo (package.json,
pyproject.toml, Dockerfile, docker-compose*.yml, CI workflow files).

Output a markdown table of deployable units with these columns:
  unit | language | entrypoint file | what it talks to | last touched (git log -1)

Rules:
- Every row must cite the manifest path it came from.
- If you cannot determine a column from a manifest, write UNKNOWN.
  Do not infer it.
Enter fullscreen mode Exit fullscreen mode

That last rule is doing most of the work. Without an explicit UNKNOWN escape hatch, a language model will fill the gap with something reasonable-sounding, because a table with every cell filled in looks more like a good answer than one with holes.

The result was 11 deployable units. Four of them had a last-commit date older than three years. That single table told me something no amount of file-reading would have: most of this repo is not alive. The ticket I'd been handed touched two units. I could ignore the other nine.

Eight hours of the week, saved on day one.

Day 2 — Trace exactly one request, end to end

Now I picked the single most boring, most central request in the system — a customer-facing read endpoint — and had the agent walk it.

Trace GET /v2/accounts/:id from HTTP entry to database and back.

For each hop output:
  file:line -> what happens -> what it calls next

Constraints:
- Only include hops you have actually read. No summarizing "and then
  validation happens" without a file:line.
- Stop and say STUCK if you can't find the next hop.
Enter fullscreen mode Exit fullscreen mode

The STUCK instruction matters as much as the tracing instruction. It gives the model a legal way to fail, so failure shows up as a word instead of as an invention.

It got stuck twice, both times at a dynamic dispatch — a handler registry keyed by strings built at runtime. Which was itself the most useful finding of the day: the two places the agent got lost were the two places a new human gets lost too. Those became the first two entries in my notes.

The trace collapsed into this:

flowchart LR
    A[HTTP layer<br/>Node 22] --> B[handler registry<br/>runtime string keys]
    B --> C[service layer<br/>Python 3.13]
    C --> D[(primary DB)]
    C --> E[legacy cache<br/>3 yrs untouched]
    E -.stale reads.-> C

That dotted line is a real bug I filed in week two. I found it because tracing one path end to end shows you the edges between systems, and edges are where the bodies are buried.

Day 3 — Make it prove things (the citation rule)

By day 3 I trusted the agent enough to be dangerous. So I added one standing rule to the project's CLAUDE.md:

## Answering questions about this codebase

Every factual claim about behavior must be followed by `path/to/file.py:LINE`.
Claims without a citation are drafts, not answers.
If asked about something you cannot cite, say "not found in repo" and stop.
Enter fullscreen mode Exit fullscreen mode

Then I actually checked the citations. Not all of them — I sampled, roughly one in five, using a dumb loop:

# paste the agent's claimed citations into refs.txt as "path:line"
while IFS=: read -r file line; do
  printf '\n=== %s:%s ===\n' "$file" "$line"
  sed -n "$((line-3)),$((line+3))p" "$file" 2>/dev/null || echo "MISSING FILE"
done < refs.txt
Enter fullscreen mode Exit fullscreen mode

Out of roughly 60 sampled citations across the week, three were wrong, and they were wrong in an instructive way:

  1. A file that didn't exist. The path looked exactly like the repo's naming convention — right directory, right suffix, plausible name. It had simply never been created. This is the failure mode that scares me most: it's wrong in a way that pattern-matches to correct.
  2. A real file, wrong line. Off by ~40 lines, pointing at a different function in the same module. Harmless if you check, poisonous if you quote it in a design doc.
  3. A retry policy that was described as "exponential backoff with jitter" and was, at the cited line, a bare time.sleep(1) in a for loop. The library it imported supported backoff. The code didn't use it.

That third one is the whole argument for the citation rule. The agent wasn't hallucinating from nothing — it was pattern-completing from what code like this usually does. In a 9-year-old codebase, "what code like this usually does" is precisely the assumption that will page you at 3am.

Day 4 — The doc is the deliverable

I stopped treating my notes as scratch and started treating them as the output of the week. One file, committed to the repo:

docs/orientation.md
├── Deployable units (the day-1 table, hand-corrected)
├── One traced request (the day-2 mermaid diagram)
├── Danger zones (the 2 places the trace got STUCK)
├── Dead-ish code (4 units, no commits in 3+ years)
└── Open questions (things nobody alive can answer)
Enter fullscreen mode Exit fullscreen mode

The agent drafted the connective prose; I owned every fact in it. Total: about 300 lines.

This is the part I'd push hardest on. Onboarding knowledge normally evaporates — you learn a system, the confusion fades, and six months later you can't remember what was confusing. The window where you know what's confusing is about five days wide. Write it down inside that window or it's gone.

Day 5 — Ship something small

The actual ticket was a 40-line change to a validation rule. I shipped it Friday afternoon. It was reviewed by someone who'd been on the team four months and knew less about the deploy topology than my day-1 table did.

Lessons Learned

1. Ask for artifacts, not explanations. Tables, traces, file lists, diagrams. Anything with a shape can be checked. "Explain this module" produces text that is 90% right and 0% verifiable, which is the worst ratio in engineering.

2. Always give the model a legal way to say "I don't know." UNKNOWN, STUCK, not found in repo — an explicit escape hatch converts silent invention into a visible gap. Every prompt I wrote that week had one, and the ones that didn't are exactly where I got burned.

3. Citations, and then actually check them. A citation rule you don't spot-check is theater. My hit rate was ~95%, which sounds great until you notice that the 5% clustered around the most load-bearing questions — retry logic, error handling, the stuff with no obvious right answer to pattern-match against.

4. The agent's confusion is a map of the codebase's confusion. Where it got stuck (runtime string dispatch, implicit registries) is where every new hire gets stuck. Treat STUCK as a finding, not a failure. I now log them deliberately.

5. Build config over source code, on day one. Manifests, Dockerfiles, and CI workflows are declarative, small, and can't drift from reality the way a comment can. They told me nine-elevenths of the repo was irrelevant to my task before I'd read a single function.

The meta-lesson: the agent did not make me understand the codebase faster. It made me understand the shape of the codebase faster, and then I understood the code at normal human speed on a much smaller surface area. Those are different claims, and only one of them is true.

What's Next

Two things I'm testing now:

  • Onboarding docs as a standing artifact. Regenerate the day-1 unit table monthly and diff it. A new row appearing that nobody discussed is a governance signal, not just a docs update.
  • A "danger zones" section in CLAUDE.md. Feeding the places the agent got stuck back into its own instructions, so the next session starts where the last one gave up. Early results are promising for the dynamic-dispatch case; it now flags the registry instead of guessing through it.

If you're staring down a codebase you didn't write: don't ask it to explain anything. Make it draw you a map, and then go check the map.

Wrap-up

If this was useful, follow me here on Dev.to — I write up what actually happens when I point AI coding agents at real, ugly, load-bearing systems, including the parts that don't work.

And if you've got a legacy monster of your own, try the day-1 prompt above. Build manifests only, UNKNOWN allowed, no source files. Then drop a comment with how many of your deployable units turned out to be dead. I'm collecting data points, and I suspect four-out-of-eleven is not unusual.

Top comments (0)