osdlc

explore-codebase

Systematic method for understanding an unfamiliar codebase: getting oriented in a new repo, locating where something is implemented, tracing how a feature or request flows end to end, and building an accurate mental model before answering questions or making changes. Use whenever you're dropped into a repo or the user asks things like "how does X work here", "where is Y handled", "how is this project structured", "help me understand this codebase", "onboard me to this code", "find where Z is implemented", or "explore this repo and write up notes" — or before editing code you haven't read yet. Can optionally save the resulting map as a markdown file, a linked Obsidian-style vault, or a self-contained HTML page carrying diagrams of the structure it found.

osdlc 3 Updated 5d ago

Resources

2
GitHub

Install

npx skillscat add osdlc/osdlc/claude-plugin-packs-craft-skills-explore-codebase

Install via the SkillsCat registry.

SKILL.md

/craft:explore-codebase

Build an accurate, verifiable mental model of a codebase as fast as possible, spending the least
context to get there.

The rule that governs everything

Context is your budget, and performance degrades as it fills. Never read the whole repo.
Search first, read narrowly, confirm against the source — never describe behavior you haven't
actually seen in a file.

Work top-down and stop as soon as you can answer the question at hand. Depth is on demand, not by
default.

1. Orient — cheap, high signal, do this first

Skim the highest-information-per-token files before any deep reading:

  • README / docs/ — what the project is and does.
  • Manifest (package.json, pyproject.toml, go.mod, Cargo.toml, pom.xml, Gemfile, …) —
    language, key frameworks, and the scripts (build/test/run) that show how the project is
    operated.
  • Agent/convention files (CLAUDE.md, AGENTS.md, .cursorrules) — gotchas the maintainers
    already wrote down.
  • Top 2–3 levels of the directory tree — names usually encode the architecture.
  • Entry/config (Dockerfile, CI yaml, main.*, index.*, app.*).

Then state a one-paragraph hypothesis of the architecture before reading further.

2. Find the entry points

Locate where execution actually begins: CLI main, server bootstrap, route/handler registration,
framework lifecycle hooks, or the public API surface. These anchor every later trace.

3. Search before you read

Use grep/glob on symbol names, definitions, call sites, and error strings to jump straight to the
files that matter, and open only those. Reading a file end to end is a last resort — prefer the
relevant function plus its immediate callers and callees.

4. Trace one real path end to end

Understanding comes from following a representative flow, not from cataloguing files — e.g. request
→ handler → service → data layer, or input → transform → output. Note where state lives and where
boundaries are crossed (I/O, network, database).

5. Use git history when "why" matters

For surprising or "weird" code, git log -p -- <file> and git blame -- <file> explain how it got
that way far faster than guessing, and point to the commit or PR that introduced it. Keep the --:
without it git rejects a path that no longer exists at HEAD, which is exactly the deleted or
renamed file whose history you wanted.

6. Delegate deep dives when subagents are available

Reading many files pollutes your main context. Push broad investigations into a read-only subagent,
where the harness offers one, and keep only the summary. Scope each one narrowly — open-ended
"investigate the codebase" burns context for little return. Tell each one to read the project's
CLAUDE.md and AGENTS.md first.

Answering codebase questions

Treat them as questions for a senior engineer and answer from the code, citing path:line:

  • "How does <subsystem> work?" → trace it (step 4), don't speculate.
  • "Where do I add a new <X>?" → find an existing X and name the files/pattern to copy.
  • "Why does this do A instead of B?" → check git history (step 5) and the call sites.

Deliver a map, not a file dump

When reporting what you've learned, give a structured model the user can act on:

  • What it is — one or two sentences.
  • Stack & shape — language, frameworks, architectural style (monolith, layered, microservices,
    …).
  • Layout — the few directories that matter and what each owns.
  • API — libraries only. The public surface: the entry points and exported types a caller
    actually uses, and what is deliberately withheld. Read the project's own export declaration
    first — exports in package.json, pub use in lib.rs, __all__, the barrel index.ts —
    because it is the authoritative list and it often disagrees with the README. Say which of the
    surface is stable and which is internal but reachable.
  • Key flows — the main path(s) traced, as A → B → C, with the entry file:line.
  • Where to look next — the right file for the user's likely next task.
  • Gotchas — non-obvious conventions, coupling, or risks you noticed.

Include the API section when other code imports this project: it publishes a package, exposes
a plugin or extension interface, or is consumed as a library elsewhere in a monorepo. An
application that is only run, never imported, does not need one — its entry points are already
covered by Key flows. When you are unsure which it is, the manifest settles it: a package with
declared exports is a library.

Keep it tight and link to specifics with path:line rather than pasting large blocks.

Optionally persist the map to disk

When the user asks for a file, or the codebase is large enough that the map won't sit comfortably in
chat, write the findings to a fresh directory in the harness's temp or scratchpad location (on
POSIX, /tmp/codebase-map-<repo>/) and tell the user the path. Three formats:

Single markdown file (<repo>-map.md) — the default. The map structure above as one document
with a short table of contents. Keep path:line references as inline code so they stay greppable.

Obsidian-style linked vault — for larger or multi-subsystem codebases. Write one short note per
major area/flow plus an index, connected with [[wikilinks]]:

  • index.md — a map-of-content note: the one-paragraph "what it is", the stack, and [[links]] to
    every other note.
  • One note per subsystem or traced flow (e.g. auth-flow.md, data-layer.md), each linking to
    related notes with [[...]] and back to [[index]].
  • Use [[wikilinks]] for concepts that have their own note; use plain path:line for source
    references.
  • Add optional YAML frontmatter (tags, aliases) so the vault is searchable in Obsidian.

One idea per note is what makes the graph useful — keep each note focused and link generously rather
than writing long notes.

HTML page with diagrams (<repo>-map.html) — when the structure is the finding: a layered
architecture, a pipeline with stages, a boundary that cuts across modules. Ask for it with
--html, or offer it when you notice you are describing a shape in prose that a picture would
settle.

Never hand-write the HTML. Emit a JSON payload and render it:

node <skill-dir>/render_map.mjs map.json <repo>-map.html

The contract is map-page.schema.json beside this skill — read it before writing the payload. You
supply content and diagram data; the renderer owns every pixel. That split is the point: a
payload cannot produce an SVG that overflows its box, overlaps its own labels, or vanishes in dark
mode, because those are not things a payload can say. The renderer validates before it writes and
fails with the path of each bad field.

Four diagram kinds, chosen by what the shape actually is:

Kind Use it for
layers Rows stacked top to bottom, optionally cut by a labelled boundary — layered architectures, anything separated by a seam.
pipeline Ordered stages left to right, optionally fanning out to terminal outputs — traced flows.
inventory A labelled container holding a grid of names — module rosters, anything whose point is membership.
containment Boxes inside boxes, up to three deep — which package owns which modules, and which of those are the seams. Use when the nesting itself is the finding.

Section headings become a fixed contents rail down the left of the page, which scrolls on its own
and tracks the section you are reading. It appears once a page has three sections. Write headings
that read as a table of contents — the rail is the only navigation a long map gets.

A fifth, raw, takes hand-authored SVG. It exists so an unusual shape is possible, not so it is
easy: raw markup skips every guarantee above. Prefer a kind, and prefer proposing a new kind over
reaching for raw twice.

What does not change from the other formats:

  • Draw the mechanism, not the directory tree. A picture of the folder layout tells the reader
    what ls already told them. Draw where control flows, which boundary separates what, what is
    translated crossing a stage. If a diagram would only restate a list, write the list.
  • Draw containment when the modules are significant, and group them by job. A containment
    diagram earns its place when the codebase has more than a handful of modules and which unit owns
    what is part of the answer. Group by what the modules do — content, build, output, seams — not
    by the folders they happen to sit in. If your groups end up named after directories and hold
    exactly their contents, you have drawn the tree; drop the figure.
  • Cite everything. Every figure takes a cite of path:line. A diagram is an assertion about
    the code and is held to the same standard as a sentence.
  • Two diagrams that show real mechanism beat six that decorate. Sections are optional — omit a
    heading rather than filling it.

Anti-patterns

  • Reading directories in full or alphabetically instead of following flow.
  • Describing what code "probably" does — verify, or say you're unsure.
  • Endless exploration with no question to answer — define the goal, then stop when it's met.
  • Pasting whole files into the reply; cite locations instead.