Onboarding an AI Coding Agent to an Unfamiliar Codebase

Photo by Роман Рябенко via Wikimedia Commons (CC0)
Start with the perimeter rather than the architecture: entry points, the build and test commands that actually run, where configuration comes from, and which external systems the code talks to. Run every command the project claims to have on a clean clone and record the failures verbatim, because those failures are the notes worth keeping. Then write the findings into a committed instruction file such as CLAUDE.md or AGENTS.md so the next session does not re-derive them.
Because it reads authoritative and therefore gets trusted. A tidy generated ARCHITECTURE.md is committed, treated as ground truth by everyone who joins after you, and survives review because checking it properly means redoing the original exploration. If you cannot afford a verification pass, leaving the file empty is the more honest option.
Ask for the summary as a list of claims, one per line, each carrying the file path and line number that proves it. Open each cited line yourself, or hand the list to a second session that never saw the first one. Delete any claim whose citation does not say what the claim says, rather than hedging it into appears to, because a hedged wrong sentence still survives review.
Four failures recur: dead code described as live, a deprecated path described as the current one, a configuration default reported as the running value, and the module everyone quietly routes around. They share one cause, which is that the agent read the source and the source is not the deployment. Git history and the running environment settle all four.
Either works, and the choice comes down to how many tools need to read the file. CLAUDE.md is what Claude Code loads at startup and what its /init command generates from an exploration of the codebase. AGENTS.md is plain Markdown with no required fields that many coding agents read, and its own site says over 60,000 open-source projects carry one. Write instructions rather than descriptions in whichever you pick.

Photo by Роман Рябенко via Wikimedia Commons (CC0)
Key Takeaway
Onboarding an AI coding agent to an unfamiliar codebase works when you map the perimeter first — entry points, the build and test commands that actually run, configuration, external boundaries — write the findings to a committed instruction file, and delete every claim you cannot trace to a file and a line.
The repository reached me as a Bitbucket URL and a message saying it was mine now. It ran in production and it invoiced real customers. The README had a clone command, a screenshot of a UI that no longer existed, and a setup section naming a database engine the service had stopped using. Everyone who wrote it had left.
This is the procedure I use for the first hour with a codebase nobody on my team wrote, run with a coding agent instead of by reading files in alphabetical order. It is deliberately unglamorous. The examples are Claude Code because that is what I use, and the documented behaviour is cited, but nothing in the method depends on the tool. The load-bearing part is the verification pass at the end, and it is the part almost nobody does.
Ask an agent to understand the codebase and it hands you an architecture summary. That is the most expensive thing it can produce and the hardest thing you own to check. Spend the first hour on the perimeter instead: where the process starts, which commands the project claims to have, where configuration comes from, and which other systems it talks to. All four are answerable from manifests, imports and file names, so they are cheap to produce and, more importantly, cheap to falsify.
# The perimeter, in the order I ask for it. Every answer is a path or a
# command, never a paragraph.
# 1. Is this repository alive, and where is the work actually happening?
git log -1 --format='%as %an %s'
git log --since='6 months ago' --name-only --pretty=format: \
| grep -v '^$' | sort | uniq -c | sort -rn | head -20
# 2. What commands does the project CLAIM to have? Claims, not facts.
jq '.scripts' package.json
ls Makefile Dockerfile docker-compose.yml .github/workflows/ 2>/dev/null
# 3. Where does configuration come from?
rg -o 'process\.env\.[A-Z0-9_]+' --no-heading | sort -u | head -40
ls .env.example .env.sample config/ 2>/dev/null
# 4. Which other systems does it touch?
rg -o 'https?://[a-z0-9.-]+' --glob '!*.lock' --no-heading \
| sort | uniq -c | sort -rn | head -20Perimeter work has a property architecture work does not: it fails out loud. A build command either runs or prints an error you can paste straight into the notes. A paragraph about layering is never wrong out loud — it is wrong quietly, six weeks later, inside somebody else's decision. Start where the feedback is immediate and the hour goes on accumulating facts rather than sentences.
The scripts block in package.json is a historical document. On the service I inherited, some of the scripts named a test runner that was no longer in the dependencies at all, and the dev script bound to a port the Compose file had stopped publishing. An agent reading that file will report every one of those scripts as the project's commands, because that is what the file says, and it has no way to know the file lost an argument with reality two refactors ago.
# Run every claimed command once, on a clean clone. The failure text is the
# note worth keeping — it is what the next person would otherwise rediscover.
$ npm ci
npm error notarget No matching version found for @internal/billing-orm
# -> private registry. Needs .npmrc with the internal token. Not in the README.
$ npm run test
> jest --runInBand
sh: jest: command not found
# -> the script is stale. Tests actually run under vitest, see vitest.config.ts
$ npm run dev
Server listening on 4000
# -> works, but only after: docker compose up db redis
# Without them it exits 0 and logs nothing, which reads like success.So the second step is execution, not reading. Have the agent propose the command list, then run each one on a clean clone in a sandbox you are willing to lose, and keep the output. What you write down is not npm test. It is npm test followed by the exact failure it prints on a clean clone, because that failure is the onboarding note the rest of the team would otherwise rediscover one person at a time.
Claude Code's /init writes a starting CLAUDE.md by exploring the codebase for build commands, test instructions and conventions, and with CLAUDE_CODE_NEW_INIT=1 it runs an interactive flow that explores with a subagent, asks follow-up questions and presents a reviewable proposal. Both are drafts. That flow also pulls in rules other tools left behind, in .cursor/rules, AGENTS.md and .windsurf/rules, and those can be exactly as stale as the README you have just stopped trusting.
Onboarding produces an artefact or it produces nothing. A chat window closes at the end of the day, the next session starts from zero, and you pay the exploration cost again for slightly different answers. Put the findings where the agent loads them at startup: CLAUDE.md for Claude Code, or AGENTS.md if you want one file that several tools read — it is plain Markdown with no required fields, and its own site says over 60,000 open-source projects now carry one.
# CLAUDE.md — written after the first hour, not before it.
# Instructions, not description. Every line names what proves it.
## Commands that actually run
- Install: npm ci (needs .npmrc with the internal registry token)
- Test: npx vitest run (the "test" script still calls jest; it is stale)
- Dev: docker compose up db redis, then npm run dev
## Entry points
- HTTP: src/server.ts -> src/routes/index.ts (one router per domain)
- Scheduled: src/jobs/cron.ts, registered at src/server.ts:41
- CLI: bin/reconcile.ts, run by hand at month end
## Boundaries
- Postgres via Knex. Migrations in db/migrations — never edit a merged one.
- Outbound: partner billing API, base URL from BILLING_BASE_URL. No retries.
## Unverified — do not trust
- src/legacy-invoicing/ appears unreachable from any entry point.
Last commit 2024-11. Confirm against the running service before deleting.Write instructions, not description. That the API package uses Knex is a fact the agent can rediscover in ten seconds. That you must never edit a migration after it has merged, and must add a new one instead, is a rule it cannot derive from any file — and that is the sentence worth committing. Claude Code's own large-codebase guide uses exactly that pair as its example of a per-package instruction file, which is a useful calibration for what belongs in one.
This is the step that decides whether the hour was worth anything. In the 2025 Stack Overflow Developer Survey, 66% of respondents named answers that are almost right but not quite as their biggest frustration with AI tools, and 45% said debugging AI-generated code takes longer than they expect. An architecture summary is the purest form of that category: nothing in it is checkable at a glance, and its errors surface as a wrong decision weeks later rather than as a failing test today.
The discipline works because of an asymmetry: a file path and a line number can be falsified in seconds, and a paragraph cannot be falsified at all. It also changes what gets written. Asked for claims with citations, the agent produces noticeably fewer of them, and the ones it stops producing are the ones it had least support for — which is the filtering you wanted, happening at generation time instead of review time.
The worst outcome of an onboarding session is a tidy, confident ARCHITECTURE.md that nobody checked. It gets committed, the next three joiners read it as ground truth, and it survives every review because reviewing it properly means redoing the original work. An unverified architecture summary is worse than no summary at all. If you cannot afford the verification pass, do not generate the summary — an empty file is honest.

An agent's summary tends to describe a codebase's aspiration rather than what runs in it. The abstractions get named, the layering gets described, and the description would be accurate if every module were reachable and every branch were taken. These are the five questions I ask to find out whether the map matches the territory, and every one of them has to be answered with paths rather than prose.
The first question alone usually settles it. A summary that is confident about middleware, controllers and services in the abstract will, when pushed to name files in order, either produce a chain you can follow or paper over a gap with a phrase like the request is then handled by the service layer. The gap is the finding, and it is always in the part nobody documented.
Four failures recur often enough that I now check for them by name before believing anything else in a summary. They share one cause: the agent read the code, and the code is not the same object as the deployment.
| What the summary says | Why the agent believes it | What settles it |
|---|---|---|
| Dead code described as live | The module exists, exports cleanly and is decently written, so it reads as current | Search for importers, then check the last commit date on the file |
| A deprecated path described as the current one | Both paths exist, and the old one usually has more code, more comments and more tests | Follow the entry point inwards instead of reading directories outwards |
| A config default reported as the running value | The default is in the source; the override lives in the deployment, which is not in the repository | Read the value out of the running environment, not out of the file that declares it |
| The module everyone quietly routes around | Nothing in the source marks a module as avoided, and its imports still look central | Rank files by how often they changed in the last six months |
The fourth is the failure no amount of reading will surface. Most old codebases contain a module everyone has silently agreed to work around: new code calls the thing it wraps, or reimplements the one slice of it that is needed, and nobody deleted it because deleting it is a project rather than a task. The source says it is central. The history says it is abandoned — and abandonment is a fact about people, which is precisely the layer a static read cannot see.
# History answers the question reading the source cannot: what do people
# actually touch? The live surface of a codebase is rarely its directory tree.
# Most-changed files in the last six months.
git log --since='6 months ago' --name-only --pretty=format: \
| grep -v '^$' | sort | uniq -c | sort -rn | head -25
# Is anything still importing the module that reads as central?
rg -l 'legacy-invoicing' --glob '!node_modules' --glob '!*.lock'
# When was it last touched at all, and by whom?
git log -1 --format='%as %an' -- src/legacy-invoicing/index.ts
# And the one that ends the argument: is the config default the running value?
kubectl exec deploy/billing-svc -- printenv BILLING_TIMEOUT_MS
Everything above produces a map: where things are, which commands work, what talks to what. It does not tell you which of those things is load-bearing. The module to be most careful with is not identifiable from its imports — it is the one where a small, obviously safe change produces a support ticket three days later, and no reading of any kind finds it.
METR's 2025 study is the caution I keep in mind here. Sixteen experienced open-source maintainers working 246 issues on repositories they already knew were 19% slower with AI tools, while forecasting that they would be 24% faster and still believing afterwards that they had been 20% faster. That is the opposite situation to this one — they knew their code — but the gap between felt speed and measured speed is exactly why I write the artefact down and verify it rather than trusting an impression that the hour went well.
So the honest bound on all of this: the map is worth having on day one, and it is not understanding. Understanding arrives one way, by changing something small, shipping it, and watching what breaks. What the onboarding artefact buys you is a cheaper and safer version of that experiment, because you know how to run the tests and where the boundaries are before you touch anything.
The rule I carry now is short. The first hour with an unfamiliar repository produces a committed file rather than a conversation, every line in that file names the code that proves it, and whatever cannot be proved sits under a heading that says so. Anyone describing an agent that understands a codebase it has never deployed is describing a reading, not an understanding.
Sources and further reading