codebase-snapshot

12 min read · by

Snapshot output

A run writes one directory (default ./snapshot) that is meant to be handed to a model as context. It is self-describing: manifest.json indexes everything the parts contain and explains everything they omit.

Layout

snapshot/
├── .codebase-snapshot          ownership marker (see Safety)
├── README.md                   human summary of this snapshot
├── manifest.json               machine index: files, parts, config, hashes, skips
├── parts/
│   ├── part-001.md             the code, grouped and size-budgeted
│   ├── part-002.md
│   └── …
└── reports/
    ├── project-tree.md         directory tree of included files
    ├── navigation.md           links between layers, kinds and parts
    └── audit.md                secrets, large files, parse diagnostics, git state

With --include-monolith you also get full-codebase.json and full-codebase.md — the whole repository in one file. They are off by default because they grow without bound; --max-monolith-bytes skips them when they would exceed a size you set.

full-codebase.json embeds its own files[] array, and its includedBytes / includedLines describe the monolith's raw content — the file as it was read — so they differ from the same fields in manifest.json for the same file. manifest.json reports what the part emitted (CRLF normalised, the trailing newline not counted); the monolith reports the content it embedded. Compare a file's fields within one artifact, not across the two.

The manifest embedded in full-codebase.json is otherwise identical to manifest.json with one impossible field: a file cannot record its own sha256, so artifacts.fullJson is null in the embedded copy and a selfReference sentence points at manifest.json, which carries the real byteSize and sha256 for that file.

Every artifact can be turned off. --skip-artifacts parts omits the parts, --only-artifacts manifest writes nothing but the manifest, and config.artifacts does the same persistently. The ownership marker is always written.

Parts

Each part-NNN.md holds whole files under the byte budget (maxPartBytes, default 300 KB). A file larger than the budget is not split mid-file: it is truncated and carries an omission marker naming the budget that cut it (... [347 lines omitted: part budget 1887 B] ...). A file above largeFileBytes is emitted as a head/tail excerpt instead, with the same marker. Each file section is tagged with its layer, kind, language and a structural summary (imports, exports, classes, functions) so a reader can navigate without opening every file.

Parts are packed in a stable order, so the same input produces the same content — which is what makes manifest.fingerprint meaningful.

Two runs over unchanged input are not byte-identical everywhere, and the difference is deliberate bookkeeping rather than content. manifest.json, the snapshot's README.md, reports/audit.md and .codebase-snapshot differ on every run — they carry a timestamp, and the marker also carries a fresh run id and pid. parts/*.md, reports/navigation.md and reports/project-tree.md are byte-identical across runs, and manifest.fingerprint is stable. So: diff the parts, the navigation index or the tree to see whether the content changed, and diff the manifest only when you need to know that a run happened.

Manifest

manifest.json is the index and the contract between runs. Top-level keys:

Key Contents
schemaVersion Manifest format version (3.0.0).
generatedAt ISO timestamp of the run.
generator { name, version } of the tool that wrote it.
git { branch, commit, isDirty }. branch falls back to the configured branch on a detached HEAD and outside a repository; commit is "unknown" and isDirty is false when git cannot report. A false there means "not known to be dirty", not "clean tree" — check commit to tell the two apart.
budgets The effective byte limits for the run: { maxPartBytes, largeFileBytes }. The per-layer overrides live at config.budgets, which is { default, byLayer }.
config The resolved configuration actually used (preset, artifacts, sizing, scoping).
counts { totalFiles, totalSizeBytes, totalLines, partsCount, skippedCount, largeFileCount, truncatedPreviewCount }.
totals Bookkeeping: { candidateFiles, includedFiles, fullFiles, previewFiles, skippedFiles, totalSourceBytes, totalContentBytes, partCount, warningCount, redactedLines }. redactedLines counts the lines whose values were masked — one per line, not one per matching rule — and is what the summary's Redacted: line prints.
fingerprint Stable hash of the snapshot content. Two runs over unchanged input produce the same value.
validation { status: "passed" | "failed", errors: [] } for the run's own integrity check.
artifacts The optional monolith files: { fullJson, fullMarkdown }. Each value is null when that file was not written, otherwise { byteSize, sha256 }. The object itself is always present.
layers, kinds File counts per layer and per kind.
indexes Cross-references: routes, components, handlers, features, imports, directives.
parts One entry per part (below).
files One entry per included file (below).
skipped One entry per excluded file (below).
warnings Non-fatal observations that may change how you read the snapshot — a disabled artifact, a file too large to parse, a transcoded file, a throwing hooks.afterGenerate. Masked values are not warnings: they are counted in totals.redactedLines and listed per line in reports/audit.md.

parts[]

{
  "index": 1,
  "filename": "part-001.md",
  "byteSize": 1432,
  "sha256": "3472b9edcd504403f2416606e4222f326ec207d7916bf928ade4ec7de1406d2d",
  "lines": 91,
  "fileCount": 2,
  "layersCovered": ["root", "src"],
  "includedFiles": [
    { "relativePath": "package.json", "sourceBytes": 35, "includedBytes": 35, "lines": 1, "layer": "root", "kind": "data" },
    { "relativePath": "src/math.ts", "sourceBytes": 63, "includedBytes": 63, "lines": 3, "layer": "src", "kind": "other" }
  ]
}

byteSize and sha256 are what validate checks against the file on disk. Both are full values — the hash is the whole 64-character digest, never elided.

files[]

{
  "relativePath": "src/math.ts",
  "sizeBytes": 63,
  "lines": 3,
  "hashSha256": "aa229260647049a42a0446f48f1b34f19657921b7a0a27a41c0b2a294a25c919",
  "part": "part-001.md",
  "layer": "src",
  "kind": "other",
  "language": "typescript",
  "inclusionMode": "full",
  "isLarge": false,
  "isPreviewTruncated": false,
  "originalLines": 3,
  "includedLines": 3,
  "includedBytes": 63,
  "summary": {
    "directives": [], "imports": [], "exports": ["add"], "reExports": [],
    "functions": ["add"], "classes": [], "interfaces": [], "types": [],
    "enums": [], "variables": [], "reactComponents": [], "hooks": [],
    "routeHandlers": []
  },
  "summaryQuality": "ast"
}

Querying this array is usually faster and cheaper than re-reading the parts.

skipped[]

{ "path": ".env", "reason": "sensitive-file", "detail": "credential-bearing file; enable includeSensitiveFiles to emit it" }

Reasons, and what each one means:

reason The file was dropped because
excluded It matched your own excludePatterns, or fell outside a non-empty include allow-list.
default-ignore A built-in rule caught it, the preset's ecosystem ignores did (node_modules, __pycache__, target, …), or it sat inside an output directory.
sensitive-file Its name is credential-bearing (.env, *.pem, .npmrc, …).
sensitive-path The path itself matched a secret rule. The file is skipped and the path is masked in every artifact.
binary It is not text: it holds a NUL byte outside a UTF-16 stream, or more than 1% control characters (a 4 KB blob of 0x07 is valid UTF-8 but is not text). detail carries the size and, when there is one to give, the reason: the control-byte count (Size: 13 B; 5 control byte(s)), or the offset of a NUL found past the 8 MB head scan (Size: 9000010 B; NUL byte at offset 9000000). A NUL inside the head scan is caught by the cheap extension-and-prefix check, so its detail is the size alone.
symlink It is a link, and links are not followed. A directory link names its target in detail.
outside-root It is a link whose target resolves outside the scanned root.
gitignore A .gitignore excluded it, in a scan that is not reading git's index.
read-error It could not be read.
not-file It is a directory, socket or FIFO. A directory that belongs to another repository is reported as nested-repository instead.
nested-repository It is a nested checkout or a submodule: a directory whose contents are tracked by a different repository, so they are not part of this codebase.

Every path the scanner discovers is recorded — nothing is dropped silently, in either discovery mode. Inside a git repository the candidate list comes from git, which applies .gitignore before the scanner runs; the ignored paths are then listed separately and recorded here too, so a .gitignored file or directory appears in skipped[] with reason gitignore whether or not git is present.

explain reports two further reasons that never appear here, because they describe a path rather than a decision about a candidate file: missing (nothing exists at that path) and not-discovered (the path exists but was never a candidate — inside node_modules, for example). See Troubleshooting.

Ignored trees are recorded at two granularities, which is why explain can answer not-discovered for a directory you can see in skipped[]. A built-in ignore (node_modules, dist, .next, …) is recorded once, as the directory itself, and its contents are never enumerated — explain node_modules/pkg/i.js inherits that record and says default-ignore. A preset ecosystem ignore (__pycache__, target, vendor, …) is recorded per file, so explain app/__pycache__ — the directory — finds no record of its own and answers not-discovered, while explain app/__pycache__/mod.pyc says default-ignore. Neither is an omission: the directory and its files are all accounted for, just not at the same level.

Safety

The marker also makes hook-written artifacts safe: the entries a hooks.afterGenerate adds to the output are recorded in the marker, so they do not make the next run refuse the directory.

Committing the snapshot

Usually you do not. Add the output directory to .gitignore — codebase-snapshot init does it for you. If you do commit snapshots (for example, to diff context across releases), manifest.json is the file that matters: it is compact and diff can compare two of them.