Reading and Owning Code You Didn't Write
Open in the interactive app: practice, quiz, mock interview →In one minute
- More and more of the code you are responsible for was written by someone else: a colleague who left, a vendor, an acquired company, or an agent that wrote it in ten minutes last month. Ownership means you can explain it, change it safely and debug it at 3 a.m.
- Read with a goal. Start from entry points and one real request, not from file one; build a map of the critical path before reading details.
- Pin current behaviour with characterization tests before changing anything. They record what the code does today, right or wrong, so you notice when that changes.
- Use history as evidence:
git log,git blame, pull request discussions and decision records explain why code looks the way it does. Agent-written code often has no "why", so write it down when you learn it. - Track knowledge risk: files no current human understands are an incident waiting to happen. Make sure at least two people can explain every critical path.
How it works
The new legacy code
Michael Feathers defined legacy code as code without tests. A useful update for the AI era: legacy code is code nobody on the team understands, however new it is. Agents can produce a working service in an afternoon; if no human read it carefully, it becomes legacy the moment it merges. Inherited code, acquired code and agent code are the same problem: behaviour you depend on, with intent you cannot see.
Read with a goal
Reading a codebase file by file is like reading a dictionary to learn a language. Instead:
- Pick one real flow, such as "a user places an order", and follow it from the entry point (an HTTP handler, a queue consumer, a cron job) to the database and back.
- Run it locally or in staging with logging or a debugger, and watch the path rather than imagining it.
- Draw the map: components, data stores and calls on the critical path, in a C4-style context or container diagram. A page of boxes beats a hundred pages of notes.
- Find the edges: what external services it calls, what it writes, what it assumes about time, retries and failure.
- Then read details where the map shows risk: money, permissions, concurrency, data deletion.
Agents are useful readers here. Ask one to trace a flow and cite file paths and line numbers, then check the citations. An explanation without references is a guess.
Characterization tests
Before changing code you do not understand, write tests that capture what it does now. Call the function with realistic inputs, record the outputs, and assert them, even outputs that look wrong. Feathers' recipe: break dependencies so the code runs in a harness, write a test with a placeholder expected value, run it, read the real output from the failure, paste it into the assertion, rename the test to describe the behaviour, and repeat with new inputs. If a result surprises you, keep the test and investigate before calling it a bug (Feathers). These tests do not say the behaviour is correct; they say it has not changed. When you later fix a "bug", one test will fail, and you will decide on purpose whether that change is intended. Golden-file or snapshot tests (record a whole response or report and compare future runs against it) do the same job at the level of a service.
History as evidence
Code shows what; history shows why. git log -p --follow path shows how a file evolved; git blame -w -C attributes each line while ignoring whitespace and code moved between files (one -C looks at other files changed in the same commit, -C -C also at the commit that created the file, and three look at any commit) (git docs); the linked pull request often has the discussion that explains an odd if. A bulk reformatting commit hides real history, so list it in a .git-blame-ignore-revs file and set blame.ignoreRevsFile (git docs). Architecture decision records, where they exist, are the best source of all. Nygard's format is deliberately short, one or two pages with title, context, decision, status and consequences, because large documents are never kept up to date (Nygard). Agent-written code is often missing this layer: the reasoning lived in a chat session that is gone. When you work out why something is the way it is, record it in a comment, a decision record or the spec.
Knowledge risk
The bus factor is the number of people who would have to leave before nobody understands a part of the system. For critical paths it should be at least two. A simple proxy from version control: for each file, which current team members have committed to it? Files touched only by former members or only by agents have a bus factor of zero for practical purposes.
Down the stack
Layer 1The contract
Owning a component promises that a named team can explain its critical paths (at Google, ownership is a stewardship duty and owners are expected to understand the code they own, Software Engineering at Google, Ch 9), has tests that pin its current behaviour, can deploy and roll it back, and knows its dependencies and failure modes. A good test of ownership: could the on-call engineer debug it at night without the original author, human or agent?
Layer 2The mechanism
Version control stores what you need to measure ownership. Every commit records an author, a timestamp and the files changed; git log --format='%an' -- path | sort | uniq -c counts commits per author for a file. git blame walks history backwards to find the last commit that touched each line, and with -C detects lines moved or copied from other files. Agent commits can be marked by a trailer such as Co-Authored-By: or by a bot author name, so tooling can separate them. CODEOWNERS maps paths to owning teams for review; ownership dashboards join that map with commit history to show where owners have never touched the code they own.
Layer 3The machine
Git's history is a chain of immutable snapshots. Each commit object points to a tree object (a directory listing of file names and content hashes) and to its parent commits, and every object is named by the hash of its content (SHA-1 or SHA-256). Diffs are not stored; they are computed by comparing trees, which is why git blame on a large file with long history can take seconds: it repeatedly diffs a file's versions back through time. Because the hashes chain, history cannot be silently edited without changing every later commit ID, which is what makes it trustworthy evidence about who changed what and when.
Why it's built this way
- Teams depend on code whose authors are gone, whether people or agent sessions.
- Behaviour can be observed and pinned, even when intent is lost, through characterization tests.
- Intent can often be recovered from history and discussion, if it was recorded.
- Understanding concentrated in one head, or in no head, is fragile.
- So: read by flows, pin behaviour, mine history, record the why, and measure knowledge risk.
Diagram
flowchart LR E["Entry point"] --> F["Follow one real flow"] F --> MAP["Draw the critical path map"] MAP --> CT["Characterization tests pin behaviour"] MAP --> H["git log, blame, PRs: recover the why"] CT --> CH["Change safely"] H --> DOC["Record decisions"] CH --> OWN["Two people can explain it"] DOC --> OWN
Worked example
A platform team inherits a billing reconciliation service after its author leaves. It is 14,000 lines; git shows 80 percent of lines were last touched by agent commits during a three-week sprint, and none of the five current team members has ever committed to it. It runs nightly and compares 2 million ledger rows with the payment provider's report.
The team follows one flow: the nightly job's entry point, the provider download, the matching step and the mismatch report. Running it against a copy of last night's data takes 11 minutes and finds 37 mismatches, which matches production. They draw the map: downloader, parser, matcher, report writer, and a little-known retry loop that re-fetches the provider file if it is under 1 MB.
They write characterization tests: a golden test over a 10,000-row sample that records the exact mismatch report, plus tests around the retry loop. git blame on the retry loop leads to a pull request where the agent's summary says "handle partial downloads", which explains the 1 MB rule: the provider once served truncated files. They write that into a decision record.
Two weeks later, a change to the parser breaks the golden test: amounts with three decimal places, used for one currency, were now rounded. Without the characterization test this would have shown up as a month-end discrepancy in finance.
In the wild
- Knight Capital, 2012: dead code nobody owned. The "Power Peg" function had been discontinued around 2003 but remained present and callable in the order router. New code repurposed the flag that used to activate it, and one of eight servers missed the deployment. Orders sent with the flag to that server triggered the old code, producing over 4 million executions and a $460 million loss in about 45 minutes. Knight had no written deployment procedures requiring a second review (SEC order).
- Air Canada, 2024: owning what an agent said. The airline argued its chatbot was a separate legal entity responsible for its own statements. The tribunal rejected this because the chatbot was part of the airline's website, and held the airline liable for negligent misrepresentation (Wikipedia summary). Whoever deploys the system owns its output, whoever wrote it.
Trade-offs
| Approach | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Follow one flow end to end | Fast understanding of what matters | Misses rarely used paths | First days with any codebase |
| Read everything | Complete | Slow, low retention | Small codebases only |
| Characterization tests first | Safe changes even without understanding | Pins bugs as well as features | Before any change to unknown code |
| Ask an agent to explain | Fast orientation | Can be confidently wrong | Always with file and line citations, checked |
| Rewrite from scratch | Clean result | Loses undocumented behaviour, high risk | Rarely; prefer gradual replacement |
Pitfalls in production
- Rewriting code you do not understand because it looks messy, and losing hidden behaviour. Fix: characterization tests, then gradual replacement (strangler fig). Fowler notes that existing behaviour is hard to pin down, so big-bang replacements "go down in flames" most of the time, and that each step teaches the team about the legacy system (Fowler).
- Accepting an agent's explanation without checking references. Fix: require file and line citations and verify a sample.
- Ownership by
CODEOWNERSfile only, with owners who never read the code, or reviewers who skip comprehension questions. Google treats "I don't understand this" as a valid review comment that deserves a clearer explanation (SWE at Google, Ch 9). Fix: pair the ownership map with commit and review history. - The "why" disappearing with the chat session that produced the code. Fix: record decisions in the repository as you rediscover them.
- One person who understands everything. Fix: rotate on-call and reviews deliberately across critical components.
Leadership lens
- Run a knowledge-risk report each quarter: critical files with no current human committer or reviewer.
- When a team inherits a system, budget time for the map and characterization tests before any feature work.
- Require agent pull requests to include a short "why" in the description, and keep it with the code.
- Use the on-call test: if the owning team could not debug it at night, the ownership is not real yet.
Check yourself
Why are characterization tests useful even when they pin behaviour that may be wrong?
Show answer
They make any change in behaviour visible, so fixes are deliberate decisions rather than accidents, and unknown dependencies on current behaviour surface before production.
Why start reading from one real flow rather than from the top of the repository?
Show answer
A flow shows the code that actually runs and matters, in the order it runs, and gives you a map to hang details on; reading files in order shows lots of code with no context.
What makes agent-written code a particular knowledge risk?
Show answer
It can be produced quickly and in large amounts with no human ever reading it closely, and the reasoning behind it often lived only in a chat session that is gone.
What does
git blame -Cadd?Show answer
It detects lines moved or copied from other files, so attribution points to where the code was really written rather than to the commit that moved it.
Sources and further study
Primary
- SEC order against Knight Capital Americas (2013): how retired code and a repurposed flag became a $460 million loss.
- Nygard, Documenting Architecture Decisions: the original short-ADR format.
Books
- Software Engineering at Google (2020), Ch 9: Code Review (read free): ownership, comprehension and OWNERS files.
- Building Secure and Reliable Systems (2020), Ch 6: Design for Understandability: why understandable systems are easier to keep secure and reliable.
- Working Effectively with Legacy Code (Michael Feathers, 2004): the source of the characterization-test technique (no chapter verified here).
Other curricula
- Moffatt v. Air Canada (Wikipedia): a legal view of who owns what an automated system says.
Free resources
- Characterization Testing (Michael Feathers): the author of Working Effectively with Legacy Code on pinning behaviour.
- Strangler Fig (Martin Fowler): replacing a system gradually instead of rewriting.
- git-blame documentation: options such as
-wand-Cfor accurate attribution. - Architectural Decision Records: recording the why so it outlives its authors.
- Bus factor (Wikipedia): the knowledge-concentration risk and how it is measured.
- C4 model: drawing maps of a system at useful zoom levels.