We designed an admin screen off the product README once. Clean mockup, shipped it to engineering, and it assumed a team.roster.manage permission the codebase never had and an endpoint that returned a flat list where the real one returned a paginated envelope. The rework was quiet and it cost us about a day.
So now, before designing a screen or a service on top of an existing system, we grep first. Read the thing that's actually running before proposing what sits on top of it.
The failure mode has a name
We call it ABOUT-only authoring. You read the product's description — the README, the design doc, the model in your head — and design against that.
It feels efficient and it produces a clean mockup fast. It's also wrong in ways you can't see, because the description is a summary and the summary is stale. In that admin-screen case the role model had moved from role-strings to capabilities two sprints earlier and the README still described the old one. The mockup looked plausible, so nobody flagged it, so it got built, so it broke on contact with the code.
The tell that we were ABOUT-only authoring, in hindsight, was that not one sentence in the design doc pointed at a file. It described the roster, the permissions, the endpoint shape, all in prose, all plausible, none checkable. A reviewer reading it had nothing to verify against except their own mental model of the system, which was the same stale model that produced the doc. Two people can agree on a design and both be wrong about the code, because agreement is between the two mental models, not between either model and the repo.
What we actually read, and in what order
Before designing a UI, an IA, an RBAC surface, or a data shape on a product with existing code, we open these.
The real role model:
grep -rn "capability\|role" lib/auth lib/rbac lib/roles
We're checking whether it's role-strings or capabilities, one role per user or many, before drawing a single permission-gated screen. This is the exact check we skipped on the admin screen.
The real shell and routes:
rg -l "Shell|Nav|Layout" components/
rg "createRoute|path:" app/ pages/
The router is the source of truth for what exists. The doc's IA is aspirational; the routes are real. We found three "planned" pages in a design doc that had no route and one live route the doc never mentioned.
The real signatures — base class versus concrete implementations:
rg "class .*Reader" --type py # the abstract contract
rg "def normalize|def transform" # what implementations actually override
When you're designing the ninth of something, the existing eight tell you the exact shape to match. The base class is the contract. Read it, don't guess it.
And the real customization seams — per-tenant settings tables, feature flags — because that's where "just make it configurable" either already exists or doesn't. We designed a settings screen once assuming the deduction rates were per-tenant, then greped and found them as a hardcoded constant in lib/payroll/rates.ts:12. That's a schema and a migration before the screen makes any sense, and we'd have found it three weeks late if we'd built to the mockup.
Here's a second one that cost us more than the admin screen. We designed a bulk-import feature and assumed, from the API doc, that the import endpoint accepted a list and returned per-row results. The doc said "accepts multiple records." We greped the actual handler:
rg -n "def import_|@route.*import" services/
It accepted one record per call and had no batch path at all. The "accepts multiple" in the doc meant you could call it multiple times. We'd designed a results table showing per-row success and failure for a 500-row upload against an endpoint that did one row per request with no partial-failure semantics. That's not a UI change, it's a backend feature — batching, partial-failure reporting, a job model for a long-running import — and none of it existed. Twenty minutes of grep before the mockup would have turned "build this table" into "we need to build the batch endpoint first, here's the real scope." We found it after the frontend was half-built.
A concrete pass looks like this. Twenty minutes, in order:
# 1. how does auth actually work here?
grep -rn "capability\|role\|can(" lib/auth lib/rbac | head
# 2. what routes exist, and what's only in the doc?
rg "createRoute|path:" app/ pages/
# 3. what's the shape of the thing I'm adding a sibling to?
rg "class .*Reader|def normalize" --type py
# 4. is the knob I'm assuming a real setting or a constant?
rg -n "tenant_settings|feature_flag|const .*RATE"
Cite file:line or it didn't happen
The tell for a grounded design versus a guess is whether it points at code.
"The admin dashboard shows the team roster" is ABOUT-only. It might be true.
"The roster is served by app/api/team/route.ts:34, gated on people.roster.view (lib/rbac/capabilities.ts:88), and components/AppShell.tsx:120 already renders it" is grounded. Every claim has a coordinate anyone can check in ten seconds. The discipline is: every load-bearing statement in the design cites a file and line. If you can't cite it, you're guessing, and you go read the code until you can.
The citation does one more thing: it dates the design. capabilities.ts:88 is a claim about a specific state of the repo, and if that line moves, the reviewer can diff it and see whether the design's assumption still holds. A prose claim like "gated on the roster permission" has no such anchor — it stays plausible-sounding forever, even after the code underneath it changes. We had a design sit in review for two weeks, and the file:line citations were what let us re-check it against main before building, because we could go straight to the coordinates and confirm nothing had shifted. Three had. The uncited version would have looked equally valid on day 14 as day 1, and been wrong.
When we skip it
Grep-first is for building on top of live code, not everywhere.
Greenfield with no code yet — nothing to grep, design freely. Marketing copy — no implementation to drift from. Product-strategy docs that deliberately sit above the code — the abstraction is the point there.
Everywhere else the rule holds. If there's code and you're designing against a description of it instead of the thing itself, you're authoring drift.
One caveat on the greenfield exemption: it expires fast. The moment the first service ships and someone writes the second design against the README instead of the shipped code, you're back in drift territory. We treat "is there running code for the thing I'm designing on top of?" as the test, not "is this a new project?" A three-month-old project has plenty of running code, and the README is already behind it.
Why it's worth twenty minutes
Reading the actual role model, routes, and base classes before designing costs maybe twenty minutes. It feels like a detour when you already "know" how the system works.
You don't, quite — the admin screen taught us that. The system knows how it works and it's sitting in the repo. Twenty minutes of grep turned a plausible-looking guess into a design that survived contact with the code, which is the only kind worth building from.