# codebase-snapshot 0.9.6 — complete documentation > Pack a codebase for an LLM into budgeted Markdown parts, with a manifest of every file and skip, and validate to prove the snapshot is current. Source: https://github.com/ankurhalder/codebase-snapshot Canonical site: https://ankurhalder.github.io/codebase-snapshot/ ---
codebase-snapshot

codebase-snapshot

A snapshot you can prove is current.
Plain-text parts under a hard byte budget, an index of every file and every skipped
file with the reason, and a validate command that fails when the code moves.

npm version weekly downloads license: MIT node: 18 or newer documentation

```bash snapshot # detects the project, sets it up, generates npx codebase-snapshot generate # or no install at all, straight to the snapshot ``` That writes `./snapshot/` — a directory you can drop into a chat, a prompt, a RAG pipeline or a repository review. TypeScript and JavaScript first, but also Python, Go, Rust, Java/Kotlin, C#/.NET, PHP, Ruby, C/C++, Vue, Svelte and Astro, without configuration. ![Two ways to give a repository to a model: a dump that was silently truncated, and a snapshot with a manifest and a validator](https://raw.githubusercontent.com/ankurhalder/codebase-snapshot/main/docs/assets/how-it-works.svg)

Install · Quick start · Trust · AI agents · Docs · Site

--- ## Why Feeding a real repository to a model is awkward: files are too big, context windows are too small, and choosing what matters by hand neither scales nor repeats. This does it deterministically. - **A hard ceiling, not a target.** `maxPartBytes` is a limit. A file bigger than the budget is cut with an explicit omission marker naming the budget, and above `largeFileBytes` it becomes a head/tail excerpt instead — nothing silently overflows the window you sized. - **An index, not just text.** `manifest.json` lists every included file with its layer, kind, language, part, hash and a structural summary, and every *excluded* file with the reason. - **Provably current.** `validate` re-hashes the repository against the snapshot and exits non-zero on any drift, so a pipeline can gate on the context being fresh. - **Repeatable.** Two runs over unchanged input give byte-identical parts and the same `manifest.fingerprint`; only the artifacts that carry a timestamp, run id or pid differ. ## Install ```bash npm install -g codebase-snapshot # puts `snapshot` on your PATH npx codebase-snapshot generate # or no install at all, one-off npm install -D codebase-snapshot # or add it to a project ``` `codebase-snapshot` is the canonical name to script against; `snapshot` is the short command a global install provides, and from a project dependency the same binary is `npx snapshot` or the `npm run snapshot` script `init` writes. Requires **Node.js 18+**. Any npm client works — npm, yarn (classic and Berry with strict PnP), pnpm and bun. Published from GitHub Actions with [build provenance](https://docs.npmjs.com/generating-provenance-statements), so `npm audit signatures` verifies the tarball came from this repository at a tagged commit. This is a **0.x** release: the CLI surface may still change in a minor release until 1.0.0, so pin an exact version if you need a fixed surface today. → [Installation guide](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/install.md) ## Quick start Type it. That is the whole first run. ```bash snapshot ``` It recognises the project and says what it keyed on — `Detected nextjs` / `Because next.config.ts` — then offers the recommended setup in one keystroke, or a walk through every setting with each question starting on its default. Either way it writes the config, adds the npm scripts and the devDependency, installs with your package manager, and generates. A project that already has a config just generates. Nothing to press — CI, a container, an agent: ```bash npx codebase-snapshot --autopilot ``` **A first run never guesses.** With no terminal and no `--autopilot` it exits `2` and names the three ways forward, because a snapshot built from settings nobody chose is a snapshot nobody can check. ```bash npx codebase-snapshot init --yes # config + scripts + devDependency + .gitignore npm install # the package, into node_modules npm run snapshot # generate npm run snapshot:validate # exit 0 = current with disk ``` The fingerprint is printed in full — all 64 hex characters, never elided — and it is the same value as `manifest.fingerprint`. ## What you get ``` snapshot/ ├── README.md human summary of this snapshot ├── manifest.json machine index: files, parts, hashes, config, skips ├── parts/part-001.md … the code, grouped and size-budgeted └── 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 ``` Every artifact can be turned off (`--skip-artifacts`, `--only-artifacts`, or the config). → [Snapshot output and manifest schema](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/output.md) ## Built to be trusted with a whole repository A snapshot is only useful if you can believe it, so the tool is explicit about what it does *not* contain. - **Nothing is dropped silently.** Every excluded path is recorded in `manifest.skipped[]` with a reason — `binary`, `gitignore`, `sensitive-file`, `symlink`, `read-error`, `excluded` and the rest — and `explain ` answers the same question for any single file. - **Counts describe what was emitted.** `includedBytes` and `includedLines` are measured from the block in the part, not from the file on disk. A file that is not valid UTF-8 is kept and labelled `utf-8-lossy` rather than vanishing as "binary". - **Drift is checked, not assumed.** `validate` compares every file's hash *and* a digest of the settings the snapshot was generated with, so a changed budget or secret pattern is drift rather than silence. - **The output directory is the tool's.** Each run is written to a scratch directory and swapped in atomically, and it refuses to replace a directory it did not create — `--out-dir src` cannot destroy source code. - **Credential files are excluded by default** — `.env`, `*.pem`, `.npmrc`, `id_rsa`, `*.tfstate` and more — and secret-shaped strings inside ordinary files are redacted by masking only the matched span, never the whole line. ## Supported projects 25 presets, detected from the repository — each contributing layers, kinds, ecosystem ignore patterns and a recommended part budget. | | | | | | :--- | :--- | :--- | :--- | | Next.js | Nuxt | SvelteKit | Remix | | Astro | Angular | Solid | Vue + Vite | | React + Vite | NestJS | Express / Fastify / Koa | Node library | | Monorepo | Django | FastAPI / Flask | Python library | | Go | Rust | Java / Spring | .NET | | Laravel (PHP) | Rails (Ruby) | Terraform / Docker infra | Docs-only | | generic fallback | | | | Non-TypeScript languages get import/export/class/function summaries rather than being treated as opaque text, and every summary is labelled `ast`, `structural` or `heuristic` so you know how much to trust it. → [Preset table and detection rules](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/configuration.md#project-presets) ## Commands | Command | Does | | :--- | :--- | | *(none)* | The guided first run. Asks once when the project has no config; generates otherwise. | | `generate` *(default)* | Write the snapshot. `--autopilot` sets the project up first, unasked. | | `setup` | Config, npm scripts, devDependency, install and generate, in one command. | | `validate` / `check` | Compare the snapshot with the repository; exit `1` on drift. | | `report` | Snapshot health, layer metrics, secret audit. | | `init` | Interactive (or `--yes`) setup that writes a self-documenting config. | | `explain ` | Why one path is included, which part it lands in, or why it was skipped. | | `diff ` | Compare two snapshots. | | `describe` | The machine-readable contract (`--format json`). | | `skill` | Print or install the agent skill. | | `mcp` | Serve the repository to an MCP client over stdio. | Exit codes: `0` success · `1` drift, refused generation, or nothing to snapshot · `2` usage error, or a first run with no terminal and no `--autopilot`. Once a day it checks the npm registry for a newer release and mentions it on stderr — skipped in CI, in any `--format json` run, and in `mcp`. Turn it off with `--no-update-check`. → [Full CLI reference](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/cli.md) ## For AI agents Everything an agent needs is available as data, and the tool can install its own skill. ```bash npx codebase-snapshot describe --format json # contract: workflow, options, exit codes npx codebase-snapshot skill --install # into .claude/skills + .cursor/rules npx -y codebase-snapshot@0.9.6 mcp # or serve it over MCP ``` An agent told only "use `npx codebase-snapshot`" can read the contract, follow its ordered workflow, and produce a validated snapshot unaided — that path is covered by a test. The MCP server exposes seven tools — status, plan, generate, validate, locate, read, explain — plus `snapshot://` resources and two prompts. Its reads come from the snapshot rather than the working tree, so an excluded file stays excluded and a redacted value stays redacted, and `snapshot_validate` answers the question an agent otherwise cannot ask: is the context I was given still true? → [Using this with an AI agent](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/agent.md) · [MCP server](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/mcp.md) · [AGENTS.md](https://github.com/ankurhalder/codebase-snapshot/blob/main/AGENTS.md) · [llms.txt](https://github.com/ankurhalder/codebase-snapshot/blob/main/llms.txt) ## Configuration `snapshot.config.ts` (or `.mjs`) is written by `init` with **every** setting at its current value, commented and grouped, so it doubles as the reference. Deleting any line is safe: the defaults are real. ```js const config = { maxPartBytes: "120k", // hard budget per part excludePatterns: ["vendor/**", "**/*.min.js"], artifacts: { monolith: false }, // single-file output stays off budgets: { default: "120k", byLayer: { docs: 40_000 } }, includeSensitiveFiles: false, // .env, *.pem … stay out }; export default config; ``` → [Configuration reference](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/configuration.md) ## Programmatic API ```js import { generateSnapshot, resolveConfig, validateSnapshot, loadConfigFile } from "codebase-snapshot"; // Pass the repository explicitly: loadConfigFile returns only the config it found, so a // directory with no config file would leave resolveConfig scanning process.cwd() instead. const dir = "/path/to/repo"; const config = resolveConfig(await loadConfigFile(dir), { cwd: dir }); const manifest = generateSnapshot(config); const validation = validateSnapshot(config); ``` CommonJS works too (`require("codebase-snapshot")`), with type definitions for both. The CLI, the API and the MCP server share one implementation, so anything one of them does is available in the others. ## Documentation | | | | :--- | :--- | | [Installation](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/install.md) | Every client, version pinning, agent skill install | | [CLI reference](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/cli.md) | Commands, options, exit codes, examples | | [Configuration](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/configuration.md) | Every setting, all 25 presets, budgets, hooks, secrets | | [Snapshot output](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/output.md) | Directory layout and the full manifest schema | | [AI agents](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/agent.md) | The contract, the skill, what an agent should avoid | | [MCP server](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/mcp.md) | Seven tools, resources, both protocol eras | | [Troubleshooting](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/troubleshooting.md) | Every message and what to do about it | | [Changelog](https://github.com/ankurhalder/codebase-snapshot/blob/main/CHANGELOG.md) | Release history and breaking changes | Guides: [feeding a codebase to ChatGPT, Claude or Cursor](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/how-to-feed-a-repo-to-claude.md) · [stale LLM context is a bug](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/stale-context-is-your-bug.md) · [how many tokens a codebase is](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/codebase-token-budget.md) · [budgeting a context window](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/how-to-context-budget.md) · [vs repomix](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/vs-repomix.md) · [vs gitingest](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/vs-gitingest.md) · [vs code2prompt](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/vs-code2prompt.md) ## Contributing Issues and pull requests are welcome — see [CONTRIBUTING.md](https://github.com/ankurhalder/codebase-snapshot/blob/main/CONTRIBUTING.md) and the [code of conduct](https://github.com/ankurhalder/codebase-snapshot/blob/main/CODE_OF_CONDUCT.md). Security reports: [SECURITY.md](https://github.com/ankurhalder/codebase-snapshot/blob/main/SECURITY.md). ## License [MIT](https://github.com/ankurhalder/codebase-snapshot/blob/main/LICENSE) © Ankur Halder

Built by · documentation · npm

--- # Using this with an AI agent The package ships two things that make it usable by an agent that has never heard of it: a **machine-readable contract** and a **bundled skill**. Nothing needs to be installed globally and no prior knowledge of the tool is required. ## The three commands an agent needs ```bash # 1. Read the contract. This is the machine-readable replacement for --help. npx codebase-snapshot describe --format json # 2. Write a commented config that lists every setting (non-interactive). npx codebase-snapshot init --yes # 3. Generate and verify. npx codebase-snapshot generate npx codebase-snapshot validate ``` The bare package name works; pin an exact version (`npx codebase-snapshot --version` prints the current one) when the same version must run on every machine. ### One command instead of three ```bash npx codebase-snapshot --autopilot ``` Sets the project up — config, npm scripts, devDependency, `.gitignore`, install — and generates, with no prompts. It keeps an existing config, and `--no-install` avoids touching the lockfile. `setup` is the same operation as a command word. ### Do not rely on the bare command ```bash npx codebase-snapshot # ❌ in an unconfigured project this exits 2 ``` An agent has no terminal, so the guided first run cannot ask its question and refuses instead of generating with settings nobody chose. That exit `2` is the tool being honest, not broken: say which setup you want — `--autopilot`, `init --yes`, or `generate` on the documented defaults. ## The contract `describe --format json` returns the whole tool as data on stdout: | Field | Contents | | :--- | :--- | | `invocation` | The supported one-shot and installed commands, and the bin names. | | `workflow` | An ordered six-step procedure: verify → understand → configure → generate → verify output → consume. | | `commands` | Every command with what it does, its positionals and its exit codes. | | `options` | Every option with its type, default, which commands it applies to, and a summary. | | `exitCodes` | `0` success, `1` drift or refused generation, `2` usage error. | | `presets` | All 25, with priority, recommended budget and ecosystem ignores. | | `artifacts` | Each artifact with its on/off default and what it contains. | | `config` | Every config filename and every setting with its default. | | `output` | The directory layout and how to read `manifest.json`. | | `safety` | What is excluded or redacted, and which directories are refused. | | `troubleshooting` | Symptom → cause → fix. | It is generated from the same code the CLI runs — presets, artifact names and versions are read directly — and a test compares its documented defaults against a real `generate --dry-run` manifest. If a default changes, the test fails rather than the contract silently misleading an agent. `describe` with no `--format` renders the same content for a human. ## The skill ```bash npx codebase-snapshot skill # print it (stdout) npx codebase-snapshot skill --install # write it into the repository ``` Printing goes to stdout deliberately, so it can be redirected: ```bash npx codebase-snapshot skill > SKILL.md ``` `--install` writes the same skill into the registries agent tools read by name: | Target | Path | Read by | | :--- | :--- | :--- | | `claude` | `.claude/skills/codebase-snapshot/SKILL.md` | Claude Code auto-discovery | | `cursor` | `.cursor/rules/codebase-snapshot.mdc` | Cursor rules | ```bash npx codebase-snapshot skill --install --target claude # one registry npx codebase-snapshot skill --install # both (default) ``` Both are **registries keyed by skill name**: the path carries `codebase-snapshot`, so the file cannot be mistaken for your project's own documentation, and two skills cannot collide. **It never writes `AGENTS.md`.** That file is the repository's own agent contract — build commands, conventions, house rules — and an install that creates or edits it puts a tool's marketing where the project's rules belong. If you want the skill in there, print it and paste it, so the diff and the commit are yours: ```bash npx codebase-snapshot skill >> AGENTS.md ``` The install is **idempotent**, and whatever it *creates* is appended to `.gitignore` in the same run — these are a tool's files written into your repository, and `git status` should not turn that into a decision about what to commit. A file that already existed is never ignored. `--format json` reports what was created, updated or unchanged, and which ignore entries were added. `codebase-snapshot init` does none of this: it writes the config, the npm scripts, the devDependency and the `.gitignore` entries for the snapshot directory, and nothing else. ## When the client speaks MCP `codebase-snapshot mcp` serves the repository to a Model Context Protocol client over stdio: seven tools, `snapshot://` resources and two prompts. Prefer it over shelling out when the agent already has an MCP connection, for two reasons that are properties of the design rather than of the wiring: - **Reads come from the snapshot, not the working tree.** `snapshot_read` extracts a file's section from the emitted part, so a file the snapshot excluded cannot be read back and a redacted value stays redacted. - **Staleness is a first-class question.** `snapshot_validate` answers "has the code moved since this was built", which an agent with only a filesystem tool has no way to ask. The tools are listed in [the MCP guide](mcp.md) and in the `mcp` block of `describe --format json`. A `root` argument may name a sub-directory of the directory the server was started for, and nothing outside it. ## Why both a contract and a skill The contract is for **doing** — the data an agent needs to pick correct commands and options. The skill is for **knowing when to act** — it carries the trigger conditions, the ordered workflow, and the surprises an agent cannot infer from a schema: sensitive files excluded by default, secrets redacted, the output directory being tool-owned. ## What an agent should avoid - **Do not point `--out-dir` at source code.** It is refused, and the refusal is deliberate. - **Do not add `--include-sensitive-files`** unless the user explicitly asked for credential files; by default they are excluded and recorded as `sensitive-file` in `manifest.skipped[]`. - **Do not work around redaction.** If a value is masked, ask the user rather than reading the original file. - **Do not assume exit 2 means the repository is broken.** It means the command was wrong. Re-read the contract. - **Do not treat a bare `npx` that prints nothing as a repository problem.** A stale global install shadows `npx`; see [Troubleshooting](troubleshooting.md#the-command-does-nothing). ## Verifying an agent can drive it `tests/agent-simulation.test.js` runs the published instructions as an agent would: read `--help`, discover `describe`, then execute the six workflow steps in order against an 8-file Django fixture. It asserts what the contract promises at each step: `--version` prints a version, every published option states where it applies, `init --yes` writes a config naming **every** setting the contract lists, `generate --format json` exits `0` with `validation.status: "passed"` and the preset `python-django`, every fixture file is either in `manifest.files[]` or explained in `manifest.skipped[]`, each `files[]` entry carries its classification and hash, and `validate` then exits `0`. If an agent following the published words would fail, that test fails. When you parse the manifest, remember what is conditional: `manifest.files[]` carries `relativePath`, `layer`, `kind`, `language`, `part`, `inclusionMode` and `hashSha256` for every included file, but `summary` and `summaryQuality` only for a language the tool can analyse. A `.gitignore` or plain-text entry has neither key, and a file whose summary was too large for its part carries `summaryOmitted: true` instead of a `summary`. `encoding` appears only when a file was not plain UTF-8: `utf-16le`/`utf-16be` were transcoded, and `utf-8-lossy` means the body you are reading contains `U+FFFD` where undecodable bytes were. --- # CLI reference ```bash npx codebase-snapshot [options] ``` The package installs one executable under two names: **`codebase-snapshot`** (canonical, and the one to script against) and **`snapshot`** (the short command a global install puts on your `PATH`). Both are the same file. From a project dependency use `npx codebase-snapshot` or `npx snapshot` — both resolve the local copy first. Running with no command is the guided first run: in a project with no configuration it asks once, sets the project up and generates; in a configured project it is the same as `generate`. Naming the command (`codebase-snapshot generate`) always means "do exactly this", on the documented defaults. `--autopilot` sets the project up first without asking. ```bash snapshot # global install: guided first run, then generate npx codebase-snapshot # the same, from a project dependency npx codebase-snapshot --autopilot # the same with no questions ``` ## The first run ```bash snapshot # global install npx codebase-snapshot # from a project dependency ``` In a project with no configuration the tool reports what it recognised **and what it keyed on**, then asks one question: ``` codebase-snapshot Detected nextjs Because next.config.ts Directory /Users/you/app ◆ How should I set this up? ❯ Use the recommended setup nextjs · config · npm scripts · install · generate Go through each setting every question starts on its default Pick a different preset everything else stays at the defaults ``` The first entry is the whole setup in one keystroke. The second walks through the settings, and every prompt starts on its answer, so pressing `enter` through the lot is the recommended setup spelled out. When nothing matched, the tool says so, names the manifests it looked for, and asks which preset is closest — the one case where it needs an answer it cannot find itself. A project that already has a config, or one this tool generated before, skips all of it and just generates. `--autopilot` does the same work with no questions at all, and reports each decision as it makes it. ## Contents - [Commands](#commands) - [`generate`](#generate) - [`validate` / `check`](#validate--check) - [`report`](#report) - [`setup`](#setup) - [`init`](#init) - [`explain `](#explain-path) - [`diff `](#diff-before-after) - [`describe`](#describe) - [`skill`](#skill) - [Options](#options) - [Exit codes](#exit-codes) - [Output streams](#output-streams) - [Examples](#examples) ## Commands ### `generate` ```bash codebase-snapshot generate [options] ``` Scans the repository and writes the snapshot to `outDir` (default `./snapshot`). Naming the command is the deterministic form: it runs on the documented defaults and writes nothing but the snapshot. Without a command word an unconfigured project gets the [guided first run](#the-first-run) first; `--autopilot` asks for that setup from an explicit `generate` too, and never prompts. A first run neither form can resolve — no config, no previous snapshot, no `--autopilot`, no terminal — exits `2` and names the alternatives. It does not quietly generate on defaults. Detection is automatic: a preset is chosen from the 25 in the [preset table](configuration.md#project-presets), which supplies layers, kinds and ecosystem ignores. The preset's *recommended* part budget is not applied by `generate` — that runs on `--max-part-bytes` (default 307200) or your config. `init` is what writes the recommended budget into a fresh config, so a project set up with `init` gets the preset's figure. Exit `0` when the snapshot passes its own integrity check, `1` when it does not. ``` 📸 Generating snapshot for: /path/to/repo Branch: main | Part budget: 300 KB ✅ Snapshot generated in 0.14s: - Tracked files: 128 - Total lines: 9,431 - Total size: 0.41 MB - Parts written: 3 under snapshot/parts/ - Reports: snapshot/reports/ (project-tree, navigation, audit) - Manifest: snapshot/manifest.json - Fingerprint: e4607b570268c8def0911babbace97e4a0196d9eaf2cac22c8847500883d70aa - Integrity: passed - Redacted: 2 line(s) — values masked; see snapshot/reports/audit.md ``` The fingerprint is printed in full — all 64 hex characters, never elided — and it matches `manifest.fingerprint`. The summary only lists artifacts that were actually written, so a run with `--only-artifacts parts` does not claim a manifest exists. **`Redacted` and `Warnings` are different lines, and that is deliberate.** A redaction means the tool masked a secret-shaped value: it is the tool doing its job, and it is counted **once per line** even when two rules match that line. The per-line list — file, line, every rule that matched — is `reports/audit.md`, and `manifest.totals.redactedLines` carries the count. `Warnings` is for things you may want to act on: a disabled artifact, a file too large to parse for a summary, a non-UTF-8 file that was transcoded. The summary shows the first ten and then says `… and N more in manifest.warnings[]`; nothing is dropped silently. ### `validate` / `check` ```bash codebase-snapshot validate [options] ``` Compares the snapshot on disk with the repository. Reports five classes of drift — modified files, deleted files, files added since the snapshot, damaged or missing parts, and config changes — plus bookkeeping mismatches in the manifest. Exit `0` when the snapshot is current, `1` on any drift. It never writes anything. ### `report` ```bash codebase-snapshot report [options] ``` Prints what the snapshot says about itself: counts, layer breakdown, warnings, fingerprint. It reads `manifest.json` and nothing else, so it is fast on any repository size — a 20 000-file snapshot reports in about half a second. Requires an existing `manifest.json` (exit `1` if missing); exits `0` otherwise. It does **not** check whether the snapshot is still true — that is `validate`, which re-reads the repository and exits `1` on drift. ### `setup` ```bash codebase-snapshot setup ``` The whole first run in one command, with defaults and no prompts. This is the same implementation `--autopilot` uses, spelled as a command word: 1. Detects the project preset. 2. Writes `snapshot.config.ts` (or `.mjs`) — **only when the repository has none**. An existing config is kept byte for byte and the output says `Keeping …`. 3. Adds `scripts.snapshot`, `scripts.snapshot:validate`, the `devDependencies` entry and the `.gitignore` entries, exactly as `init` does. 4. Installs dependencies with the package manager the repository already uses — a lockfile decides (`bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, otherwise npm). A failed install is a warning, not a dead end: the snapshot is still generated. 5. Generates through the normal `generate` command, so the summary, the output-directory guard and the `--format json` document are the ones a plain run produces. ``` 🚀 Setup for: /path/to/repo ✅ Created snapshot.config.ts (preset: nextjs) Added npm scripts: snapshot, snapshot:validate Added devDependency: codebase-snapshot ^0.9.6 Added to .gitignore: snapshot/, snapshot.tmp/ ⏳ Installing dependencies… ✅ Installed dependencies (npm install, 2.1s) ✅ Snapshot generated in 0.16s: - Tracked files: 128 … Next: npm run snapshot ``` - `--no-install` skips step 4 — CI, an offline machine, or a lockfile you would rather no package manager touched. - `--dry-run` prints the plan and the config it would write, and writes nothing. - `--force` rewrites an existing config; `--preset`, `--include`, `--exclude`, `--out-dir`, `--max-part-bytes`, `--include-sensitive-files` and `--skip-artifacts` behave as they do for `generate` and `init`. - `--format json` emits **one** document: `{ "setup": { configPath, configReused, preset, scripts, dependency, gitignoreEntries, install, notes }, "manifest": { … } }`, where `manifest` is the same object `generate --format json` writes. - If `outDir` holds files this tool did not write, `setup` still refuses to replace it (exit 1, naming the entries) after writing the config — the guard is not weakened by the convenience. ### `init` ```bash codebase-snapshot init [options] ``` Detects the project and writes `snapshot.config.ts` (or `.mjs`, see below) containing **every** setting at its current value, commented and grouped. It is a reference you can keep, not just a diff — there is no shorter variant, because the file that documents the settings is the file you edit. It also has three side effects on files you own, and it says so when it does them: ``` ✅ Created snapshot.config.ts (preset: node-lib) Added npm scripts: snapshot, snapshot:validate Added devDependency: codebase-snapshot ^0.9.6 Added to .gitignore: snapshot/, snapshot.tmp/ ``` - `scripts.snapshot` and `scripts.snapshot:validate` are added to `package.json` if those keys are absent. Existing values are left alone, and the rewrite keeps the file's own indentation and line endings. Without a `package.json` it notes that scripts were not added. - `codebase-snapshot` is added to `devDependencies` when no dependency field declares it yet. That is what puts a real copy in `node_modules` and stops `npm run snapshot` from resolving `@latest` out of the registry on every run. A range you already declared — including a pin to an older version — is never overwritten. - `snapshot/` and `snapshot.tmp/` are appended to `.gitignore` if they are not already listed; the file is created if missing, and entries already present are not duplicated. ### Going back At a terminal, `init` asks five questions and every one of them can be revisited: **`←`** goes back on a list or a text field, **`b`** on a list or a confirmation. `text` takes `←` alone because `b` is a character you may be typing, and `confirm` takes `b` alone because `←`/`→` already mean yes/no. Back steps to the previous question with your earlier answer still selected; back from the first one returns to *"How should I set this up?"*. Answering **No** at the review steps back rather than abandoning the run — Ctrl+C is how you abandon. Nothing else in the repository is touched: no `AGENTS.md`, no `.claude/`, no `.cursor/`. In a repository with a `package.json` and no config, `git status` shows the config as the only new file. Interactive by default; with `--yes` it accepts every default and never prompts, which is what agents and release checks should use. The filename adapts to the project: `.ts` only when the running Node can strip types (≥ 22.6) **and** the project is an ES module; otherwise `.mjs`, which loads everywhere. The generated file is a plain object with no imports, so it works even when the package is only run through `npx` and never installed. Related options: `--preset`, `--include`, `--exclude`, `--yes` and `--force` apply to `init` only. `init` also accepts `--dry-run`, `--max-part-bytes`, `--out-dir`, `--include-sensitive-files`, `--skip-artifacts` and `--only-artifacts`, which it shares with the commands that re-read a snapshot, and records their effect in the generated file. The `Applies to` column is in the [options table](#options). ### `explain ` ```bash codebase-snapshot explain src/server.ts ``` Answers "what would happen to this file?" — layer, kind, language, size, which part it lands in, and the exact reason if it is skipped (`sensitive-file`, `binary`, `default-ignore`, `excluded`, and the other reasons in [Troubleshooting](troubleshooting.md#a-file-is-missing-from-the-snapshot)). Exit `0` whenever the path could be inspected, whether or not it would be included — the answer is in the output. Exit `1` only when the command itself could not run. This is the fastest way to debug a file that is unexpectedly missing from a snapshot. With `--format json` the same answer is one object on stdout, with only the keys that apply: ```json { "path": "src/math.ts", "exists": true, "included": true, "layer": "src", "kind": "other", "language": "typescript", "layerMatch": "function (custom predicate)", "bytes": 63, "lines": 3, "secretCount": 0, "secrets": [], "part": "part-001.md", "inclusionMode": "full" } ``` | Key | Meaning | | :--- | :--- | | `path` | The path you asked about, relative to `--cwd` and with POSIX separators. | | `exists` | Whether anything is on disk at that path. | | `included` | Whether the scanner would include it in the next snapshot. | | `reason`, `detail` | Why not, when `included` is `false`. The same strings `manifest.skipped[]` uses, plus `missing` and `not-discovered`. A path with no record of its own inherits the record of the nearest skipped ancestor directory, and `detail` names that ancestor — for example ``ignored by .gitignore — inherited from the skipped directory `ignored` `` or ``built-in ignore: directory "node_modules" (not descended) — inherited from the skipped directory `node_modules` ``. | | `layer`, `kind`, `language` | The classification, present only when `included` is `true`. | | `layerMatch`, `kindMatch` | Which rule chose the layer or kind. A glob is printed as written (`src/api/**`); a `RegExp` as its literal form (`/\.tsx$/i`); a function matcher, which is what the presets use, as `function (custom predicate)`. Absent when no rule matched and the `other` fallback was used. | | `bytes`, `lines` | Size of the file on disk. | | `encoding` | Present only when the file is not plain UTF-8 — `utf-16le`, `utf-16be` or `utf-8-lossy`. | | `secretCount`, `secrets` | How many secret rules fire, and the first 20 as `{ rule, line }` — never the matched text. | | `part` | The part filename from the last written manifest, or `null` when it is not in that snapshot. | | `inclusionMode` | `full` or `preview`, when a part entry exists. | When `included` is `false` the object stops after `path`, `exists`, `included`, `reason` and `detail`. ### `diff ` ```bash codebase-snapshot diff ./snapshot-old ./snapshot codebase-snapshot diff ./a/manifest.json ./b/manifest.json ``` Compares two snapshots and lists added, removed and changed files. Either argument may be a snapshot directory or a `manifest.json`. Useful in review: "what changed in the context we hand to the model?" Comparison is per file (`relativePath` + `sha256`), so on its own it cannot see a run that repacked the *same* sources differently. `diff` therefore also compares the two manifests' `generator.version`, `config.digest`, content fingerprint, part count and layer totals, and prints a `payload/configuration differs:` section when the file set is identical but the emitted payload — or the configuration that produced it — is not. That covers `--large-file-bytes` (a preview becomes a full body), the preview head/tail counts, redaction rules, artifact selection and per-layer budgets, none of which move a source hash. The per-file lists and the JSON fields `added` / `removed` / `changed` / `unchanged` are unchanged; the extra data is under `payload` in `--format json`. Manifests written before 4.0.5 have no `config.digest`. In that case `diff` compares the content fingerprint and the part count only, and prints `configuration not compared: a manifest has no config digest (written before 4.0.5).` rather than implying the settings match. ### `describe` ```bash codebase-snapshot describe # human-readable codebase-snapshot describe --format json # machine-readable contract ``` Prints the tool's complete contract: the supported invocation, an ordered workflow, every command, every option with its default, the exit-code meanings, all presets, the artifact list, config filenames, output layout, safety rules and a troubleshooting table. The JSON form is what an agent should read instead of `--help`. See [Using this with an AI agent](agent.md). ### `skill` ```bash codebase-snapshot skill # print on stdout codebase-snapshot skill --install # write both registries codebase-snapshot skill --install --target claude ``` Prints or installs the bundled agent skill. Installation writes `.claude/skills/codebase-snapshot/SKILL.md` and `.cursor/rules/codebase-snapshot.mdc`; `--target claude` or `--target cursor` limits it to one. Both are **registries keyed by skill name**, so the file cannot be mistaken for your own documentation. It does not write `AGENTS.md`. That file is your project's agent contract — build commands, conventions, house rules — and a tool has no business editing it. If you want the skill there, print it and paste it: ```bash codebase-snapshot skill >> AGENTS.md ``` Whatever the install *creates* is appended to `.gitignore` in the same run, so a one-line command does not leave three untracked files behind. Re-running is idempotent, and a file that already existed is never ignored. `--format json` reports what was created, updated or unchanged, plus the ignore entries it added. Printing goes to **stdout** so it can be redirected: `codebase-snapshot skill > SKILL.md`. ### `mcp` ```bash codebase-snapshot mcp # serve the current directory over stdio codebase-snapshot mcp --cwd # serve another repository ``` Serves the repository to a Model Context Protocol client over stdio: seven tools, `snapshot://` resources and two prompts. Stdout carries JSON-RPC messages and nothing else; every note goes to stderr. It speaks both protocol eras — the legacy `initialize` handshake and the stateless `2026-07-28` revision — and answers whichever the client starts with. `--cwd` is the only directory it will read. A tool's `root` argument can name a sub-directory of it and nothing outside, symlinks included. It never installs packages and never writes a config file. There is no `--format` here, because stdout *is* the protocol stream. The full tool list, resource URIs and protocol versions appear in [the MCP server guide](mcp.md) and in `describe --format json` under `mcp`. ## Options An option a command does not accept is **rejected**, not ignored: exit `2` and a message naming the commands that do accept it. Options marked *init only* or *skill only* are the narrowest cases of the same rule. | Option | Type | Default | Applies to | Meaning | | :--- | :--- | :--- | :--- | :--- | | `--cwd ` | path | current directory | generate, validate, report, explain, init, skill | Repository to operate on. Must exist and be a directory. `diff` and `describe` take no `--cwd`. | | `--out-dir ` | path | `snapshot` | generate, validate, report, explain, init | Where the snapshot is written or read. May be absolute. | | `--max-part-bytes ` | size | `307200` (300 KB) | generate, validate, report, explain, init | Hard ceiling per markdown part. Accepts `120k`, `1.5mb`. Minimum 4096. | | `--large-file-bytes ` | size | `204800` (200 KB) | generate, validate, report, explain | Above this, a file becomes a head/tail preview. | | `--max-monolith-bytes ` | size | unset | generate, validate, report, explain | Skip the single-file monolith when it would exceed this. | | `--include-monolith` | flag | off | generate, validate, report, explain | Also write `full-codebase.json` and `full-codebase.md`. | | `--include-sensitive-files` | flag | off | generate, validate, report, explain, init | Emit `.env`, `*.pem`, `.npmrc` and similar. Off by default; they are reported as skipped. | | `--skip-artifacts ` | list | unset | generate, validate, report, explain, init | Comma-separated artifacts to omit. | | `--only-artifacts ` | list | unset | generate, validate, report, explain, init | Write only these. Cannot be combined with `--skip-artifacts`. | | `--format ` | enum | `text` | generate, validate, report, explain, init, setup, diff, describe, skill | `json` writes a pure JSON payload to stdout. Every command honours it, and `describe` reports the same nine-command list. | | `--dry-run` | flag | off | generate, init, setup | Compute everything, print it, write nothing. | | `--watch` | flag | off | generate | Regenerate on every change until interrupted. | | `--autopilot` | flag | off | generate | Set the project up without asking — config, `.gitignore`, npm scripts, devDependency, install — then generate. Never overwrites an existing config; refuses an empty directory (exit `1`); contradicts `--no-config` (exit `2`). | | `--no-install` | flag | off | setup, generate `--autopilot` | Skip the dependency install. Anywhere else it is a usage error, because it would change nothing. | | `--no-config` | flag | off | generate, validate, report, explain, diff | Ignore every `snapshot.config.*` file and run on the built-in defaults. `diff` never loads a config anyway and accepts the flag for consistency. | | `--discovery ` | enum | `auto` | generate, validate, explain | How files are found: `auto` lists through git when git works and refuses when a `.git` directory cannot be listed; `git` requires git; `walk` scans the tree and reads `.gitignore` files directly. The escape hatch for a broken `.git`. | | `--preset ` | name | detected | *init only* | Force a preset instead of detecting one. | | `--include ` | list | unset | *init only* | Allow-list recorded in the generated config. | | `--exclude ` | list | unset | *init only* | Exclude globs recorded in the generated config. | | `--yes` | flag | off | *init only* | Never prompt; accept every default. | | `--force` | flag | off | *init only* | Replace an existing `.ts`/`.js`/`.mjs` config in place. A JSON-family config is refused, naming the file to remove. | | `--install` | flag | off | *skill only* | Write the skill into the repository instead of printing it. | | `--target ` | enum | `all` | *skill only* | `claude`, `cursor` or `all`. | | `-h`, `--help` | flag | — | every command | Print usage. | | `-v`, `--version` | flag | — | every command | Print the version on stdout: a bare `v`-prefixed semver (for example `v1.2.3`), with no package name. | The artifact and size options apply to `validate`, `report` and `explain` as well as `generate` because those commands re-read the snapshot: pass the selection you generated with and the comparison is like for like. `validate --skip-artifacts parts` on a snapshot built without parts therefore agrees, while the same flag on a full snapshot reports a config mismatch. Every option also accepts the `--option=value` form. That form is required for negative numbers, which otherwise look like flags. ### `--mode` `--mode ` names the command instead of passing it positionally, which is handy when a wrapper builds the argument list. `--mode validate` and `codebase-snapshot validate` are the same run. It accepts the nine command names, and it is deliberately strict: asking for two different commands is an error rather than a silent winner. ``` ❌ --mode validate contradicts the command "generate". Pass one or the other. ❌ Unknown --mode "bogus". Expected one of: generate, validate, check, report, init, explain, diff, describe, skill. ``` `--mode` is not listed in `describe`'s option table or `--help`, on purpose: an agent has no reason to use it, and the positional form is the documented one. ## Exit codes | Code | Meaning | | :--- | :--- | | `0` | Success. For `validate`/`check`, no drift. | | `1` | Drift detected, generation refused or failed (nothing was written), or the state the command needed was not there. | | `2` | Usage error: unknown flag or command, an option the command does not accept, bad value, wrong argument count. Also a first run that needs an answer and has no terminal — run `--autopilot`, `init --yes`, or name the command. | `1` covers the cases where the tool did the right thing but the state is not what you wanted — drift, a refused output directory, an unparseable config. `2` means the command itself was wrong and retrying it unchanged will fail the same way. ## Output streams - **stdout** carries the payload. With `--format json` it is pure JSON, safe to pipe into `jq` or `JSON.parse`. - **stderr** carries progress, the human summary, warnings and errors. - **In text mode the human result is on stderr, and stdout is empty.** That holds for every command: `report`, `explain`, `diff` and `validate` print their answer — tables, verdicts, drift lines — to stderr, and `generate` prints its summary there too. Redirecting `command > file.txt` therefore writes an empty file in text mode; use `--format json` when you want the output on stdout. A failing invocation still writes one JSON document to stdout, so an agent never has to parse an empty string: ```json { "error": { "message": "Manifest not found at /repo/snapshot/manifest.json. Run generate first.", "exitCode": 1 } } ``` The human line stays on stderr and the exit code is unchanged — `{ error: … }` is an addition, not a replacement. A successful run never carries an `error` key. So `codebase-snapshot describe --format json > contract.json`, `codebase-snapshot validate --format json | jq .drift` and `codebase-snapshot report --format json || jq -r .error.message` all work. ## Update notice Once a day, after a command finishes, `snapshot` asks the npm registry whether a newer version is published and prints two lines on stderr if there is one: ``` ↑ codebase-snapshot 0.9.6 is available — you have 0.9.5. npm install -g codebase-snapshot ``` The request goes to `registry.npmjs.org/codebase-snapshot/latest`, carries no identifier of any kind, and its answer is remembered for 24 hours — one request per machine per day at most. It is skipped when stdout is not a terminal, in CI, in any `--format json` run, and in `mcp`, whose server makes no outbound connections at all. `--no-update-check` or `CODEBASE_SNAPSHOT_NO_UPDATE_CHECK=1` turns it off entirely. ## Examples ```bash # The common path npx codebase-snapshot generate # A tighter budget for a small model context, in a separate directory npx codebase-snapshot generate --out-dir .context --max-part-bytes 120k # Just the code parts, no reports npx codebase-snapshot generate --only-artifacts parts,manifest # One file per part, with everything else left alone npx codebase-snapshot generate --only-artifacts parts # Why is this file missing? npx codebase-snapshot explain src/generated/schema.ts # What changed between two snapshots? npx codebase-snapshot diff old-snapshot snapshot # Non-interactive setup, then a JSON dry run to inspect the result npx codebase-snapshot init --yes npx codebase-snapshot generate --dry-run --format json | jq .config # Keep the snapshot fresh while editing npx codebase-snapshot generate --watch # Give an agent everything it needs npx codebase-snapshot skill --install ``` --- # How many tokens is a codebase? Before choosing a part budget, know the order of magnitude. A token is roughly 3.5–4 bytes of English prose; source code is denser, usually **3.3–3.7 bytes per token** because identifiers, punctuation and indentation tokenise less efficiently than words. The rule that survives contact with real repositories: ```text tokens ≈ bytes / 3.5 ≈ lines × 11 ``` ## A measured example This repository, measured rather than estimated: | | | | :--- | ---: | | TypeScript files under `src/` | 57 | | Lines | 14,626 | | Bytes | 603,420 | | **Estimated tokens** | **≈ 165,000** | That single number decides the whole conversation. 165k tokens does not fit in an 8k window, it almost exactly fills a 200k window leaving nothing for the question or the answer, and it fits in a 1M window without effort. ## What the default part budget means `maxPartBytes` defaults to **307,200 bytes (300 KB)** per part, which is roughly **84,000 tokens**. If your model's window is 128k, one part plus a prompt and a reply fits, and the second part has to go in a separate conversation — which is exactly what the parts are for. | `maxPartBytes` | ≈ tokens per part | Fits comfortably in | | :--- | ---: | :--- | | `60k` | ≈ 16k | 32k windows, small chat prompts | | `120k` | ≈ 33k | 128k windows with room for a long answer | | `300k` (default) | ≈ 84k | 200k windows, one part per session | | `1mb` | ≈ 280k | 1M windows, or a summarising pass first | Set it from the destination, not from taste: ```bash codebase-snapshot generate --max-part-bytes 120k ``` ## Sizing the window, not just the code A window has to hold three things, and only the middle one is your snapshot: ```text [ system prompt + instructions ] + [ snapshot parts ] + [ the answer ] ``` A rough allocation that works: **never spend more than 70% of the window on the snapshot**, and leave at least 8k tokens for the reply. On a 128k window that caps you at ~82k tokens of context — about one 300k-byte part. If you are re-reading several parts in one conversation, the sum is what matters, not the budget of any single part. ## When the repository does not fit It usually does not. 165k tokens is a small library; a real application is five to twenty times that. In order of preference: 1. **Scope the run, not the budget.** `include`/`exclude` patterns, or the wizard's "only specific folders" option, cut more than any budget change. Packing the thing you are asking about beats packing the repository. 2. **Let the layers do the work.** The detected preset groups files into layers, and `reports/navigation.md` maps every file to the part it landed in — read the index, open one part, instead of feeding everything. 3. **Raise the budget and lower the count.** One 300k part is easier to reason about than six 60k parts, and cheaper in prompt overhead. 4. **Summarise once, in a separate session.** Ask the model for a map of the parts, then use that map to choose which parts to read in detail. What not to do: silently truncate. A snapshot never exceeds its budget — a file too large for the remaining space is cut with an explicit omission marker that names the budget, and above `largeFileBytes` it becomes a head/tail excerpt rather than a full body. ## Checking your own number The manifest records what was actually written, so you never have to guess: ```bash codebase-snapshot report --format json \ | jq '{files: .manifest.counts.totalFiles, bytes: .manifest.counts.totalSizeBytes, parts: .manifest.counts.partsCount}' ``` `totalSizeBytes / 3.5` is your token estimate for the run. If it is far from the window you intended to fill, the budget was not the problem — the selection was. ## Related - [Budgeting LLM context](how-to-context-budget.md) — choosing ceilings per layer - [Feeding a repo to an LLM](how-to-feed-a-repo-to-claude.md) — the end-to-end recipe - [Configuration](configuration.md) — `maxPartBytes`, `largeFileBytes`, `maxMonolithBytes` --- # Configuration reference Every setting, every default, and what each one changes. For commands and options see the [CLI reference](cli.md). Nothing here is required: with no config at all the tool runs on the defaults below. ## Contents - [Config files](#config-files) - [Scoping: which files get in](#scoping-which-files-get-in) - [Project presets](#project-presets) - [Classification: layers and kinds](#classification-layers-and-kinds) - [Sizing: byte budgets](#sizing-byte-budgets) - [Artifacts: what gets written](#artifacts-what-gets-written) - [Secrets: redaction](#secrets-redaction) - [Output directories](#output-directories) - [Hooks](#hooks) - [Complete example](#complete-example) - [Interactive setup](#interactive-setup) ## Config files The first match wins: ``` snapshot.config.ts snapshot.config.js snapshot.config.mjs snapshot.config.cjs snapshot.config.json .snapshotrc / .snapshotrc.json package.json → "snapshot" ``` `snapshot.config.json` accepts comments. `package.json`'s `"snapshot"` key is read only when no dedicated config file exists, and it must be an object (a wrong type is a warning, since other tools use that key too). The extension decides how the file is loaded. `.cjs` is always CommonJS and must use `module.exports`; `.mjs` and `.ts` are always ESM and must use `export default`. `.js` follows the package's `type` field — `module` means ESM, `commonjs` means CommonJS; with no `type` Node decides by detecting the syntax, so `export default` works on current Node but is not portable to versions without detection. Getting it wrong is the `Could not load …` error above, not a silent fallback. A config file that exists but cannot be parsed is a **hard error** (exit `1`, nothing written). Silently ignoring it would produce a snapshot with none of your settings. A `.ts` config needs Node ≥ 22.6 **and** an ES-module project, because Node applies the package's `type` field to a `.ts` file. `codebase-snapshot init` picks the right filename for the project — `.ts` when both conditions hold, `.mjs` otherwise — and writes a plain object with no imports so it loads even when the package is only used through `npx`. The snippets below show an `export default { … }` object for brevity. `init` writes it as `const config = { … }; export default config;` because the anonymous form is reported by `import/no-anonymous-default-export`, which `eslint-config-next` enables by default — both load identically, so use whichever keeps your linter quiet. CLI flags override the config for a single run (`--max-part-bytes`, `--out-dir`, …). `--no-config` goes further and ignores every config file, so the run uses the built-in defaults and the project's detected preset. A code config is executed when it is loaded, so `--no-config` is also the way to snapshot a repository whose config you do not want to run — see [the trust boundary](../SECURITY.md#trust-boundary). ## Scoping: which files get in ```js export default { include: [], // allow-list; empty means "everything" excludePatterns: [], // extra excludes, applied on top of the preset's discovery: "auto", // "auto" | "git" | "walk" — how files are found }; ``` - `include` is an allow-list of globs. When non-empty, only matching files are considered. - `exclude` is accepted as an alias of `excludePatterns` and means the same thing; the resolved config always records it under `excludePatterns`. - `excludePatterns` removes files everywhere. Presets contribute their own ecosystem ignores on top; those are not removable, only addable to. - `discovery` chooses how the file list is produced, and is recorded in `manifest.config`, so changing it shows up as config drift: - `"auto"` (default) — list through `git ls-files` when git can do it; if a `.git` directory exists but git cannot list the files, refuse rather than silently switch methods. - `"git"` — require git; a directory without a usable repository is an error. - `"walk"` — scan the tree and read `.gitignore` files directly (nested ones included), still skipping credential files. Use it when a `.git` directory cannot be listed. It cannot reproduce git's own ignore sources (`.git/info/exclude`, your global `core.excludesFile`), git's inclusion of files that are tracked although a `.gitignore` now matches them, or its treatment of a submodule as a single entry. The same setting is available per run as [`--discovery`](cli.md#options). Built-in ignores always apply: `.git`, `node_modules`, `.next`, `dist`, `build`, lockfiles, minified assets and binaries. Sensitive files are handled separately (see [Secrets](#secrets-redaction)). Every path the scanner discovers is recorded in `manifest.skipped[]` with the reason and the rule that matched — nothing it sees is dropped silently, including a `.gitignore`d path in a repository where git does the filtering. See the [`skipped[]` reasons](output.md#skipped). To debug one path: ```bash npx codebase-snapshot explain src/some/file.ts ``` ## Project presets Detection reads the repository **once** and picks the highest-priority match, so a Next.js app inside a pnpm workspace is still `nextjs`, not `monorepo`. `peerDependencies` and `optionalDependencies` count as dependencies. A bare `package.json` ranks below any definitive language manifest, so a Go or Rust repository with a `package.json` for tooling is not mistaken for a Node library. | Preset | Priority | Detected from | Budget | Ecosystem ignores | | :--- | ---: | :--- | :--- | :--- | | `nextjs` | 100 | `next.config.*`, or `next` | 300k | `.svelte-kit`, `.astro`, `.vite`, `.output` | | `nuxt` | 98 | `nuxt.config.*`, or `nuxt` | 200k | `.nuxt`, `.output` | | `sveltekit` | 96 | `svelte.config.*`, `@sveltejs/kit` | 200k | `.svelte-kit`, `.vercel` | | `remix` | 94 | `@remix-run/*` | 200k | `.cache`, `public/build` | | `astro` | 92 | `astro.config.*`, `astro` | 150k | `.astro`, `dist` | | `angular` | 90 | `angular.json`, `@angular/core` | 200k | `.angular`, `dist` | | `solid` | 88 | `solid-js`, `solid-start` | 120k | `.solid`, `dist` | | `vue-vite` | 86 | `vue` + `vite`/`vite.config.*` | 120k | `.vite`, `dist` | | `react-vite` | 84 | `react` + `vite`/`vite.config.*` | 120k | `.vite`, `dist` | | `nest` | 80 | `@nestjs/core` | 200k | `dist`, `node_modules` | | `node-express` | 78 | `express`, `fastify`, `koa`, `@hapi/hapi` | 200k | `dist` | | `monorepo` | 70 | `pnpm-workspace.yaml`, `nx.json`, `turbo.json`, `lerna.json`, `"workspaces"` | 200k | `.turbo`, `.nx` | | `python-django` | 56 | `manage.py`, django in `pyproject.toml`/`requirements.txt` | 150k | `__pycache__`, `.venv`, `venv`, `.mypy_cache`, `.pytest_cache`, `.tox`, `.eggs`, `staticfiles`, `media` | | `python-fastapi` | 54 | fastapi or flask in `pyproject.toml`/`requirements.txt` | 150k | `__pycache__`, `.venv`, `venv`, `.mypy_cache`, `.pytest_cache`, `.tox`, `.eggs` | | `go` | 50 | `go.mod` | 200k | `vendor`, `testdata` | | `rust` | 48 | `Cargo.toml` | 200k | `target` | | `java-spring` | 46 | `pom.xml`, `build.gradle`, `build.gradle.kts` | 200k | `.gradle`, `out`, `target` | | `node-lib` | 45 | any `package.json` | 120k | `dist` | | `dotnet` | 44 | `*.csproj`, `*.sln`, `global.json` | 200k | `bin`, `obj`, `.vs` | | `php-laravel` | 42 | `artisan`, laravel in `composer.json`, `composer.json` | 150k | `vendor`, `storage`, `bootstrap/cache`, `public/build` | | `ruby-rails` | 40 | `Gemfile`, `config.ru`, `Rakefile` | 150k | `.bundle`, `log`, `tmp`, `vendor/bundle`, `public/assets` | | `python-lib` | 30 | `pyproject.toml`, `setup.py`, `requirements.txt`, `.py` files | 120k | `__pycache__`, `.venv`, `venv`, `.mypy_cache`, `.pytest_cache`, `.tox`, `.eggs` | | `infra` | 24 | `*.tf`, `Chart.yaml`, `kustomization.yaml`, `docker-compose.yml` | 100k | `.terraform`, `.terragrunt-cache` | | `docs` | 22 | ≥ 3 markdown files and no `package.json` | 100k | `.docusaurus`, `.vitepress/dist`, `site` | | `generic` | 0 | anything else | 120k | — | Each detector reports **the signal that fired**, not just a verdict: `next.config.ts`, `depends on express`, `django in pyproject.toml`. The first run prints it — `Detected nextjs` / `Because next.config.ts` — so a detection can be checked rather than taken on faith. `generic` is the fallback, reached only when no detector matched, and the first run says so instead of pretending it recognised the project. A preset contributes layers, kinds, ignores and a recommended budget — what `init` writes as `maxPartBytes`. Force one when detection is wrong: ```js export default { preset: "react-vite" }; ``` An unknown name is an error listing every valid preset, never a silent fallback. The resolved preset is recorded in `manifest.config.preset`, so changing it shows up as config drift. ## Classification: layers and kinds Layers group files (roughly directories), kinds label their role. Both are first-match-wins with `other` as the fallback, and both accept a glob string, a `RegExp`, or a function. ```js export default { layersMode: "merge", // "merge" (default) or "replace" layers: [ { name: "api", match: "src/api/**" }, { name: "ui", match: /\.(tsx|vue|svelte)$/ }, ], kinds: [ { kind: "route", match: /(^|\/)route\.tsx?$/ }, { kind: "hook", match: "src/hooks/**" }, ], }; ``` A layer rule is `{ name, match }` and a kind rule is `{ kind, match }`. The label key is different in each, and a row that gets it wrong is rejected rather than silently ignored, so copy the shapes above. - `layersMode: "merge"` keeps the preset's rules and lets yours win by being evaluated first. - `layersMode: "replace"` discards the preset's rules entirely. Classification drives per-layer budgets and the `Layers:` breakdown in `report`. ## Sizing: byte budgets ```js export default { maxPartBytes: "300k", // hard ceiling per part; default 307200, minimum 4096 largeFileBytes: "200k", // above this a file becomes a head/tail preview previewHeadLines: 80, previewTailLines: 40, budgets: { // optional per-layer overrides default: "120k", byLayer: { docs: 40_000, core: 300_000 }, }, }; ``` Sizes accept bytes (`307200`), `"120k"`, `"1.5mb"`, and spaces (`"120 k"`). Zero and negative values are errors; so is anything below 4096 — a part budget that small cannot hold a header, let alone code. The budget is a hard ceiling: a part never exceeds it. A single file larger than the budget is not split mid-file — it becomes a head/tail preview, marked `isPreviewTruncated`. `budgets.byLayer` lets expensive layers get more room than generated docs, for example. ## Artifacts: what gets written ```js export default { artifacts: { readme: true, // snapshot/README.md manifest: true, // snapshot/manifest.json parts: true, // snapshot/parts/*.md navigation: true, // snapshot/reports/navigation.md projectTree: true, // snapshot/reports/project-tree.md audit: true, // snapshot/reports/audit.md monolith: false, // full-codebase.json + full-codebase.md }, }; ``` Everything except `monolith` is on by default; `monolith` follows `includeMonolith`. Turn artifacts off with the config, `--skip-artifacts a,b`, or `--only-artifacts a,b` (the two cannot be combined). Two combinations warn because they produce a snapshot that contradicts itself: `manifest` off means `validate` and `report` cannot inspect the snapshot, and `parts` off with `navigation` on leaves links pointing at files that were never written. Both are allowed. The `.codebase-snapshot` ownership marker is always written, whatever the selection. See [Snapshot output](output.md) for the layout and the manifest schema. ## Secrets: redaction ```js export default { secrets: { customPatterns: [ { rule: "github-pat", pattern: /ghp_[A-Za-z0-9]{36}/ }, { rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }, ], disableDefaultPatterns: false, }, includeSensitiveFiles: false, // .env, *.pem, .npmrc, .ssh/, .tfstate … }; ``` Each entry is `{ rule, pattern }`, where `rule` is the name that appears in the audit and `pattern` is a `RegExp`, or a string in a `.json` config. A bare `RegExp` in the array works too — it comes out as `custom-1`, `custom-2`, … in the findings — but naming the rule is what makes an audit entry mean something. - Credential-bearing **files** are skipped by default and listed in `skipped[]` with reason `sensitive-file`. `includeSensitiveFiles: true` emits them. - Secret-shaped **strings** inside ordinary files are redacted in the emitted parts. Only the matched span is masked, never the whole line, and no excerpt of a matched line reaches any artifact. Multi-line PEM keys are masked as a block. - `customPatterns` add rules; `disableDefaultPatterns` removes the built-in set — it does not disable redaction itself. Redaction is **fail-closed**, so a placeholder is indistinguishable from a secret: a `.env.example` whose line reads `JWT_SECRET=replace-with-base64url-48-bytes` is masked like a live key would be, and a test fixture literal such as `process.env.X_SECRET = "test-value"` is masked too. The same holds for an environment reference written as a quoted value — `password: "${DB_PASSWORD}"` is masked exactly like a literal password, because the rule sees a credential-shaped key followed by a string and a `${…}` reference cannot be told apart from a secret by shape alone. That is the intended trade-off — the alternative is guessing which values are throwaway — and it is worth knowing before you hand the snapshot to a model and find the template less readable than the original. To narrow it, remove a rule by name with `disableDefaultPatterns: true` plus your own `customPatterns`. Findings are summarised in `reports/audit.md` (one row per line, naming every rule that matched it), counted in `manifest.totals.redactedLines`, and shown in the `generate` summary as a `Redacted:` line separate from `Warnings:`. ## Output directories ```js export default { outDir: "snapshot", tmpDir: "snapshot.tmp", }; ``` `outDir` may not be the working directory, an ancestor, or the filesystem root, and `outDir`/`tmpDir` may not contain one another — the message prints the resolved paths. Writes are atomic: the previous snapshot survives until the new one is complete. Both directories are also **refused if they already exist and this generator did not write them**, so `outDir: "src"` cannot replace your source tree. Ownership is proved by the `.codebase-snapshot` marker and nothing else: a JSON object naming this generator, with a schema version this build understands, a per-snapshot `id`, and the list of entries it wrote. A file with that name but no valid marker, or a directory holding a manifest from another tool, is refused. The proof is never a filename, so an existing `docs/README.md` is safe. Files your hook writes into the output directory are recorded in the marker's entry list, so they do not block later runs; a file you add by hand afterwards does, because it is not in the snapshot the marker describes. ## Hooks ```js export default { hooks: { afterGenerate: (manifest) => { // runs after a real generate; a dry run does not call it }, }, }; ``` A throwing hook is downgraded to a warning on the manifest — it never loses a snapshot that was already written. A hook may return a promise: it is not awaited (this API is synchronous), and a rejection is recorded as a warning on the re-shipped manifest and marker. Anything the hook writes to stdout is redirected to stderr for the duration of the call, so `--format json` stays parseable. "Not awaited" does not mean "free": Node keeps the process alive while the hook's own timers, sockets or pending I/O are outstanding, so a hook that schedules a long job delays the command by that long even though the snapshot is already written. Keep `afterGenerate` short — hand long work to a background process — or use the programmatic `generateSnapshot`, whose promise you can `await` alongside your own work. A hook may write its own files into the output directory; that is safe across reruns because ownership is established by the marker rather than by the filenames present. A hook runs in the **process** working directory, so with `--cwd elsewhere` a relative path resolves against your shell, not the target repository. Use the absolute paths on `manifest` (or resolve against `outDir` yourself) when passing `--cwd`. ## Complete example Every setting at its built-in default. Nothing here has to be written — the tool runs on these values with no config at all. ```js const config = { // ── Project ────────────────────────────────────────────── preset: "detected", // "detected" auto-detects; or name one of the 25 branch: "main", // recorded when git cannot report one // ── Output ─────────────────────────────────────────────── outDir: "snapshot", tmpDir: "snapshot.tmp", // ── Size ───────────────────────────────────────────────── maxPartBytes: "300k", largeFileBytes: "200k", previewHeadLines: 80, previewTailLines: 40, budgets: { default: "300k", byLayer: {} }, // ── Scope ──────────────────────────────────────────────── include: [], excludePatterns: [], discovery: "auto", // "auto" | "git" | "walk" // ── Artifacts ──────────────────────────────────────────── artifacts: { readme: true, manifest: true, parts: true, navigation: true, projectTree: true, audit: true, monolith: false, }, // ── Safety ─────────────────────────────────────────────── includeSensitiveFiles: false, secrets: { customPatterns: [], disableDefaultPatterns: false }, // ── Classification ─────────────────────────────────────── layersMode: "merge", layers: [], kinds: [], // ── Advanced ───────────────────────────────────────────── includeMonolith: false, maxMonolithBytes: null, hooks: {}, }; export default config; ``` Deleting any line is safe — the defaults are real, and each one is what the comment says. The object is exported through a name because `export default { … }` is reported by `import/no-anonymous-default-export`, which `eslint-config-next` enables by default: the inline form made the generated file a lint error on first save in a Next.js project. A named export is also what lets you reference the object (from a test, or from `--dry-run`) without a wrapper. This is **not** byte-identical to a generated file. `codebase-snapshot init` starts from these values and substitutes two things: `preset` becomes the detected name (`node-lib` for a plain npm package), and `maxPartBytes` with `budgets.default` become that preset's `recommendedBudget` (`"120k"` in the same project). `generate` on its own applies no recommended budget — without a config it uses `--max-part-bytes`, default 307200 — so the preset's figure only takes effect once `init` has written it into a config. ## Interactive setup ```bash npx codebase-snapshot init ``` Walks every tunable parameter, showing the detected value as the default — press `enter` through the lot and you get the recommended configuration. The bare `snapshot` command starts the same wizard when the project has no config, but asks one question first: take the recommended setup in a single keystroke, walk through each setting, or pick a different preset. Every prompt has a flag, so the same setup works non-interactively: ```bash npx codebase-snapshot init --yes npx codebase-snapshot init --preset go --max-part-bytes 200k \ --include "cmd/**,internal/**" --exclude "**/*_test.go" --yes ``` `--dry-run` prints the config and writes nothing. `--force` replaces an existing ES-module config (`.ts`, `.js`, `.mjs`) in place; without it, an existing config is left alone and the command exits `1`. It will not replace a JSON-family config — `snapshot.config.json`, `.snapshotrc`, `.snapshotrc.json`, or the `"snapshot"` key in `package.json` — because this wizard writes an ES-module file and a new file beside the old one would silently shadow it. Those cases exit `1` with a message naming the file to delete or rename. Note that this means `--force` writing `snapshot.config.ts` does shadow a `"snapshot"` key in `package.json`, since a dedicated config file always wins. --- # How to budget LLM context files for a codebase Context is a budget, not a bin. Every token spent on a lockfile is a token not spent on the function being asked about, and the failure is silent — the model simply gets vaguer. This page is about choosing the numbers and knowing what they cost. ## Start from the destination, not the repository Work backwards. The constraint is whatever consumes the output: | Consumer | What actually limits you | | :--- | :--- | | A chat window | The window, minus room for the conversation and the answer | | A RAG pipeline | Embedding chunk size, and how many chunks get retrieved per query | | An agent loop | The window, minus tool schemas and minus the reasoning it has left to do | | A review artifact | Human attention — usually far smaller than any window | An agent needs room to *think*, so it wants the smallest context that answers the question. A chat window can take the largest. ## The numbers that matter - **`maxPartBytes`** — the hard ceiling for one part. This is a promise, not an average: no part exceeds it. Set it to the destination's limit and stop worrying about the largest file. - **`largeFileBytes`** — above this, a file becomes a head/tail excerpt rather than its full body. This is what stops one generated 40 MB file from consuming an entire snapshot. - **`budgets.byLayer`** — different ceilings per layer. Documentation is usually worth less per byte than source; `{ docs: 40_000 }` is a common shape. - **`maxMonolithBytes`** — the single-file artifact roughly doubles output, so it is off by default and capped when on. ```js const config = { maxPartBytes: "120k", largeFileBytes: "200k", budgets: { default: "120k", byLayer: { docs: 40_000, config: 20_000 } }, artifacts: { monolith: false }, }; export default config; ``` ## What each byte buys Measured on this repository's own snapshot, as a rough guide: | Layer | Share of bytes | Worth it? | | :--- | :--- | :--- | | Source | largest | yes — this is the question | | Docs and comments | large | partly; often the best value per byte, but not when duplicated | | Config | small | yes — it explains how the source runs | | Tests | often 30–40% | rarely, for a code question | | Generated and vendored | varies | almost never | The cheapest large win in most repositories is excluding tests and build output. The second cheapest is excluding duplicated documentation (the same content in a wiki, a `docs/` tree and a README). ## Do not trust a size estimate you have not measured A snapshot is auditable: `manifest.json` records `includedBytes` and `includedLines` **measured from the block actually emitted**, not from the file on disk. That distinction matters — a truncated file does not report its on-disk size. ```bash npx codebase-snapshot report ``` `report` prints totals, per-layer metrics and the secret audit, so you can see where the budget went before you spend it on a model call. ## Tuning loop 1. Generate with a ceiling that matches the destination. 2. Run `report`; look at the per-layer breakdown. 3. Cut the largest layer that does not answer your question. 4. Re-generate and compare `manifest.totals.events` and the part count. 5. Keep the settings that produced a snapshot you actually used — the config is the durable artifact, not the output. ## The failure this prevents Without a ceiling, the boundary is wherever the tool stopped. Two runs can cut differently, so a worse answer is indistinguishable from a different cut. With `maxPartBytes` fixed and output deterministic, a changed answer means changed code — which is the only interpretation that lets you debug it. ## Next - [Configuration](configuration.md) — every setting, all 25 presets - [Snapshot output](output.md) — the full `manifest.json` schema - [How to feed a codebase to an LLM](how-to-feed-a-repo-to-claude.md) --- # How to feed a whole codebase to ChatGPT, Claude or Cursor A model cannot clone your repository. Everything it knows about your code has to arrive as text, and the text has to fit. This is the practical, repeated recipe — including the parts most guides skip, which are the ones that decide whether the answer is any good. ## The problem, stated properly There are three separate failures, and they need different fixes: 1. **It does not fit.** A real repository is far larger than any context window once you include node_modules, lockfiles and build output. Naively concatenating files produces something that is rejected, truncated by an unknown amount, or billed at a size you did not expect. 2. **It does not fit *predictably*.** If the boundary moves between runs, you cannot tell whether a changed answer came from changed code or a different cut of the same code. 3. **It is not current.** The document you pasted was generated yesterday. Nobody notices until the model confidently describes a function you deleted. Most "paste your repo into an LLM" tooling solves only the first. ## The recipe ### 1. Generate from the checkout, not from a website If the code is private, it should not be uploaded to a third-party service to be turned into text. Everything below runs locally. ```bash cd /path/to/your/project snapshot # installed globally: one word, and it asks once npx codebase-snapshot setup # or from a checkout with no global install ``` `setup` detects the project type from the repository, writes a commented `snapshot.config.mjs`, adds the npm scripts and the devDependency, installs it, and generates. It does not overwrite a config you already have. Use `--no-install` if you would rather not touch the lockfile. `npx codebase-snapshot --autopilot` does the same thing with no questions at all, which is what you want in CI. If you prefer to see each stage: ```bash npx codebase-snapshot init --yes # config + scripts + dependency + .gitignore npm install npm run snapshot # generate npm run snapshot:validate # exit 0 = still matches the disk ``` ### 2. Choose the budget, not the file list The setting that matters is `maxPartBytes` — the hard ceiling for one part. Pick it from the destination, not from taste: | Destination | Practical budget | | :--- | :--- | | A chat window | 40–120 KB per part; paste one or two | | A RAG index | 120–300 KB per part, chunked by heading | | An agent's context | 60–150 KB; smaller parts leave room to reason | ```js // snapshot.config.mjs const config = { maxPartBytes: "120k", // Per-layer ceilings, when docs and code deserve different treatment. budgets: { default: "120k", byLayer: { docs: 40_000 } }, }; export default config; ``` A file larger than the budget is not silently dropped: it is truncated with an explicit marker naming the budget, and files above `largeFileBytes` become a head/tail excerpt instead of a body. ### 3. Tell the model what it is looking at The snapshot is self-describing, and saying so up front measurably improves answers: > The context below is a generated snapshot of a repository. `manifest.json` lists every included > file with its language, part and hash, and every excluded file with the reason. Reads start with > `parts/part-001.md`. If something you need is missing, ask me to check `explain ` rather than > assuming it does not exist. That last sentence matters more than it looks: models fill gaps by invention, and telling them the gap is documented turns a hallucination into a question. ### 4. Include the index, always Paste `manifest.json` alongside the parts, or at least `reports/navigation.md`. Without the index the model sees text with no map and guesses at structure. With it, it can say "this file is in part 3, which I have not been given" instead of inventing the file. ### 5. Re-validate before every session that matters ```bash npm run snapshot:validate || npm run snapshot ``` `validate` compares each file's hash *and* a digest of the settings used to generate the snapshot. A changed budget, layer rule or secret pattern is drift, not silence — so the check catches both code changes and configuration changes. ## What to exclude The defaults already skip binaries, credential files (`.env`, `*.pem`, `.ssh/`, `.npmrc`) and anything gitignored. Beyond that, the highest-value exclusions are usually: ```js excludePatterns: ["**/*.test.ts", "**/__snapshots__/**", "public/**", "*.min.js", "vendor/**"], ``` Tests and fixtures are often 40% of a repository and rarely part of the question. Removing them buys context for the code that is being asked about. ## What this does not solve - **The model still has a fixed window.** If the code genuinely exceeds it, you are choosing what to omit — this tool makes that choice explicit and recorded rather than accidental. - **Understanding beats volume.** A smaller snapshot with the right 20 files outperforms a large one containing everything. Use `include` when you know the area. - **Drift is only detected when you run the check.** It is not a watcher, unless you use `--watch`. ## Next - [Configuration](configuration.md) — every setting and its default - [Snapshot output](output.md) — what each artifact contains - [Comparing with other tools](vs-repomix.md) --- # Installation `codebase-snapshot` is published to the public npm registry, so it installs with **any** npm-compatible client. There is no second registry to add and nothing to configure. Requires **Node.js 18 or newer**. ## One command ```bash npx codebase-snapshot --autopilot ``` Detects the project, writes the config (an existing one is kept), adds the npm scripts and the devDependency, installs them with the package manager your lockfile names, and generates the first snapshot. `--no-install` skips the install; `--dry-run` writes nothing. `setup` does the same as a command word, and the bare `snapshot` (or `npx codebase-snapshot` with no arguments) does the same after one question — see [the first run](#the-first-run). The sections below are the same steps taken one at a time. ## The first run With the package installed globally, the whole thing is one word: ```bash snapshot ``` In a project with no configuration it names what it recognised and what it keyed on (`Detected nextjs` / `Because next.config.ts`), then offers the recommended setup in one keystroke or a walk through each setting with the defaults pre-filled. It writes the config, adds the npm scripts and devDependency, installs, and generates. In a project that already has a config — or one it has generated before — the same command just generates, so nobody is walked through setup twice. **It never guesses.** With no terminal (CI, a container, an agent) a first run exits `2` and names the three ways forward instead of generating with settings nobody chose. `--autopilot` is the answer for unattended use; naming the command (`npx codebase-snapshot generate`) is the other, and it runs on the documented defaults exactly as before. ## One-off use ```bash npx codebase-snapshot generate ``` This is the shortest path and needs no install. Pin the version when you need the same build on every machine. `--version` prints the exact string to pin: ```bash npx codebase-snapshot@latest generate npx codebase-snapshot --version ``` **Why the bare name works.** npm resolves which binary to run from the package's `bin` entries. This package declares `codebase-snapshot` and `snapshot` pointing at the *same file*, so npm treats them as aliases and can always resolve one. Keep them aliases: two *different* executables under a scoped package name would make the bare form unresolvable. If a command exits with no output at all, a stale global install is the usual cause — see [Troubleshooting](#troubleshooting). ## Add to a project ```bash npm install -D codebase-snapshot yarn add -D codebase-snapshot # yarn classic and Berry pnpm add -D codebase-snapshot bun add -d codebase-snapshot ``` Then run it through your client so the binary resolves from the project: | Client | Run | | :--- | :--- | | npm | `npx codebase-snapshot generate` | | yarn (classic and Berry) | `yarn codebase-snapshot generate` | | pnpm | `pnpm exec codebase-snapshot generate` | | bun | `bun run codebase-snapshot generate` | `snapshot` works everywhere `codebase-snapshot` does — `npx snapshot` from a project dependency, `pnpm exec snapshot`, `yarn snapshot`, and the bare word after a global install. Adding the script to `package.json` is the most reproducible option: ```json { "scripts": { "snapshot": "npx codebase-snapshot generate", "snapshot:validate": "npx codebase-snapshot validate" } } ``` `codebase-snapshot init` writes these entries for you, in exactly this `npx` form: the bare `codebase-snapshot` command only exists on `PATH` inside a client runner or a script that has already resolved the package, and `init` does not assume the install has happened yet. ## One-off runners for other clients ```bash yarn dlx -p codebase-snapshot codebase-snapshot generate # yarn Berry pnpm dlx codebase-snapshot generate bunx codebase-snapshot generate ``` ## Agent skill To make a repository discoverable by an AI agent — so it finds this tool without being told it exists — run: ```bash npx codebase-snapshot skill --install ``` That writes `.claude/skills/codebase-snapshot/SKILL.md` and `.cursor/rules/codebase-snapshot.mdc`, and adds both to `.gitignore` so the install leaves nothing untracked. It does not touch `AGENTS.md`: that file is your project's own agent contract. To put the skill there, print it and paste it — `npx codebase-snapshot skill >> AGENTS.md`. See [Using this with an AI agent](agent.md). ## Verified clients Every client below installs the published package and runs it successfully; the release check enforces this on every pull request, so a change that breaks one fails the build rather than a user's terminal. | Client | Run installed binary with | | :--- | :--- | | npm 10–11 | `npx codebase-snapshot` | | yarn classic 1.22 | `yarn codebase-snapshot` | | yarn Berry 4.6 (`nodeLinker: pnp`) | `yarn codebase-snapshot` | | pnpm 10 | `pnpm exec codebase-snapshot` | | bun 1.4 | `bun run codebase-snapshot` | Yarn Berry's default **strict PnP** works: there is no `node_modules` directory, and the runtime `typescript` dependency still resolves through yarn's `.pnp.cjs` loader. ## Before scanning a repository you do not own A `snapshot.config.ts`, `.js`, `.mjs` or `.cjs` in the target repository is **imported and executed** when the tool runs there — that is what lets a config express a `RegExp` or a `match` function. Pointing `--cwd` at a checkout you do not trust therefore runs its code. `--no-config` ignores every config file and uses the built-in defaults plus the detected preset, and JSON configs are parsed rather than executed. See [the trust boundary](../SECURITY.md#trust-boundary). ## Global install ```bash npm install -g codebase-snapshot ``` This is what puts the short `snapshot` command on your `PATH`. Convenient, but prefer pinning per project: snapshots are only reproducible if the generator version is. A global install also shadows `npx` for this package — npm will run your global copy instead of the published one, which is confusing when the two differ. That is exactly the situation described under [Troubleshooting](#troubleshooting). ## Workspaces The binary resolves from the workspace it is installed in. In a monorepo, install it in the package that runs the snapshot (usually the root), or use a one-off runner (`npx` / `pnpm dlx` / `yarn dlx` / `bunx`) which works from any subdirectory. ## Upgrading ```bash npm install -D codebase-snapshot@latest npx codebase-snapshot generate # rewrite the snapshot with the new version ``` `manifest.generator.version` records which version produced a snapshot. Re-run `generate` after an upgrade so the snapshot, and the version it records, match the build you are now running. A snapshot written by **3.0.0 or earlier** is refused rather than replaced: its `.codebase-snapshot` marker has no per-snapshot id, so the current build cannot prove it wrote the directory. Delete the old snapshot directory and generate again. See [CHANGELOG](../CHANGELOG.md) for breaking changes — 3.0.0 removed nothing, but it briefly did in error; 4.0.0 restored the `snap` alias, and 0.9.2 replaced that alias with `snapshot` when the version line was restarted. ## Uninstalling Standard for your client (`npm uninstall`, `yarn remove`, `pnpm remove`, `bun remove`). Snapshots already written to the output directory are ordinary files and are left alone. If you installed globally, `npm rm -g codebase-snapshot` also removes the shim that shadows `npx`. ## Troubleshooting **`codebase-snapshot: command not found`** — when installed locally (not globally) the binary lives in `node_modules/.bin` and is only on `PATH` inside a package script or a client runner. Use `npx codebase-snapshot`, `pnpm exec codebase-snapshot`, `yarn codebase-snapshot` or `bun run codebase-snapshot`. **A command exits 0 and prints nothing** — almost always a stale **global** install shadowing `npx`. Check and remove it: ```bash npm ls -g --depth=0 | grep codebase-snapshot npm rm -g codebase-snapshot ``` Then re-run. If the package is not installed globally, clear the npx cache (`npm cache clean --force`) and retry. **`Cannot find module 'typescript'`** — the package declares `typescript` as a runtime dependency for AST analysis. If your client prunes it (some `--ignore-scripts` CI setups do), rerun a clean install or pin `typescript@^5` yourself. **`yarn codebase-snapshot` prints extra lines / breaks a script** — yarn classic wraps everything: it echoes `yarn run 1.22.x`, the resolved command, your output, then `Done in 0.37s.`. For anything that parses the output, call the binary directly (`./node_modules/.bin/codebase-snapshot` or `npx codebase-snapshot`). More symptoms — refusals, config errors, drift, missing files — are in [Troubleshooting](troubleshooting.md). --- # Run codebase-snapshot as an MCP server `codebase-snapshot mcp` serves a repository to any [Model Context Protocol](https://modelcontextprotocol.io) client over stdio. It is one command, no configuration, and it is the snapshot — not a directory walk — that the client sees. > Available from **0.9.4**. Run `npx codebase-snapshot --version` before wiring a client; anything > older does not have the `mcp` command. ## Start it ```bash npx -y codebase-snapshot@0.9.6 mcp ``` The server serves the directory it was started in. `--cwd ` points it elsewhere, and that directory is the only one it will read. A client needs two things: the command `npx` and the arguments above. The surrounding configuration shape belongs to the client — the example below is the `mcpServers` form used by Claude Desktop and Cursor: ```json { "mcpServers": { "codebase-snapshot": { "command": "npx", "args": ["-y", "codebase-snapshot@0.9.6", "mcp", "--cwd", "/absolute/path/to/repo"] } } } ``` Claude Code registers the same thing from a shell: ```bash claude mcp add codebase-snapshot -- npx -y codebase-snapshot@0.9.6 mcp --cwd /absolute/path/to/repo ``` On Windows, wrap the command as `cmd /c npx` with the same arguments — `npx` is a `.cmd` shim there, and clients that spawn without a shell cannot execute it directly. ## The seven tools | Tool | Answers | Writes | | :-- | :-- | :-- | | `snapshot_status` | Is there a snapshot, when was it built, what does it cover? | no | | `snapshot_plan` | What would a generate at this budget include, and what would it leave out? | no | | `snapshot_generate` | Build it. | yes | | `snapshot_validate` | Is the context the agent was given still current? | no | | `snapshot_locate` | Which part holds this symbol, route or file? | no | | `snapshot_read` | A window of one file, as the snapshot contains it. | no | | `snapshot_explain` | Why is this path not in the snapshot? | no | Six are declared `readOnlyHint`, which most clients use to run them without asking. Only `snapshot_generate` prompts. `snapshot_locate` is the one that makes the others usable: a twelve-part snapshot is hundreds of kilobytes, and asking "which part holds authentication?" is cheaper than reading all twelve. It searches the manifest's index — file paths, exported symbols, component names, routes, layers and kinds — and returns the part each match lives in. `snapshot_read` is windowed on purpose. It defaults to 200 lines and reports `nextFromLine` when more remains, so an agent continues deliberately instead of pulling a whole part into a context window it has not budgeted. ## Resources Resources are the browse surface; tools are the act surface. | URI | Contents | | :-- | :-- | | `snapshot://manifest` | Every included file with layer, kind, part and hash, plus every skip reason | | `snapshot://parts/{index}` | One budgeted part, by 1-based index | | `snapshot://reports/audit` | Secret findings, large files, parse diagnostics | | `snapshot://reports/tree` | The directory tree of included files | | `snapshot://reports/navigation` | File-to-part lookup | | `snapshot://contract` | The machine-readable CLI contract | | `snapshot://skill` | The agent skill, as one read | Part resources carry their size in the description, so a client choosing between them can see that one is 300 kB before attaching it. ## Prompts Two, both about order of operations rather than features: `orient` maps the repository before anything is read, and `keep-current` validates before trusting earlier context and again after making changes. ## What it will not do - **Read outside its root.** `--cwd` is the only directory that is readable. A `root` argument can name a sub-directory of it and nothing else; symlinks are resolved before the check. - **Emit a sensitive file.** `.env`, `*.pem` and the rest stay out, and `includeSensitiveFiles` is not a tool argument — a prompt cannot ask for it. Reads come from the emitted snapshot, so redaction applies to everything the client sees. - **Install or configure.** `snapshot_generate` writes the snapshot directory and nothing else. It never runs a package manager and never writes a config file. - **Touch the network.** The server makes no outbound connections, and there is no HTTP transport. ## Both protocol eras Revision `2026-07-28` removed the handshake that every client in use today still performs. The server speaks both and answers whichever arrives first. | Era | Revisions | Opening move | | :-- | :-- | :-- | | Legacy | `2025-11-25`, `2025-06-18` | `initialize`, then `notifications/initialized` | | Modern | `2026-07-28` | `server/discover`, or `_meta` on every request | The two disagree about an unsupported version, and the difference is observable: a legacy client is negotiated *down* to a revision the server speaks, while a modern client receives `-32022` with the list of supported revisions so it can retry. ## Why there is no SDK `@modelcontextprotocol/sdk` is 4.5 MB unpacked and brings Express, Hono, Ajv, JOSE and Zod with it. The transport here is newline-delimited JSON-RPC, which is roughly 400 lines of readable code, and the package's only runtime dependency remains `typescript`. The parity test — a live `tools/list` compared against `describe --format json` — is what keeps the protocol implementation honest, not the presence of a library. ## Performance Generation is synchronous, so a tool call blocks the server for its duration. Both numbers are measured, not estimated: | Repository | Files | `snapshot_generate` | | :-- | --: | --: | | This project | 127 | 1.1 s | | A synthetic tree | 3,000 | 7.5 s | Both are under any client's request timeout. If a repository large enough to exceed yours shows up, `snapshot_plan` gives the same numbers without writing, and the fix — moving generation to a worker thread — is tracked rather than assumed. ## Troubleshooting **The client lists no tools.** The server writes JSON-RPC to stdout and nothing else; every note goes to stderr. Check the client's server log — the first line says which directory is being served. **`Unknown tool` or a missing command.** The published version is older than 0.9.4. Pin the exact version in the client arguments. **`Unknown resource` for a part.** Resources are computed from the manifest, so nothing but the static set exists before the first generate. Call `snapshot_status`, then `snapshot_generate`. **A file is missing from `snapshot_locate`.** It is excluded, and `snapshot_explain` will name the reason — an ignore rule, the preset, a sensitive-file pattern, or your own config. --- # 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[]` ```json { "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[]` ```json { "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" } ``` - `isLarge` is a fact about the file: it is bigger than `largeFileBytes` (default 200 KB). A large file is emitted as a head/tail excerpt rather than inlined whole. - `isPreviewTruncated` is a fact about what was written: the body in this snapshot is **not** the whole file, because lines were dropped. - `includedLines` counts content lines after CRLF normalisation and without a trailing empty element, which is what the fenced block holds. An empty file therefore records `0` while the fence shows one empty line, and a file ending in a blank line records one fewer than a reader counts — the number describes content, not rendering. - `inclusionMode` is `preview` exactly when `isPreviewTruncated` is `true`, and there are two routes to it: - **Over `largeFileBytes`** — the head/tail excerpt drops the middle, so `isLarge: true`, `isPreviewTruncated: true`. If the head and tail happen to cover every line, nothing is dropped and the entry stays `isLarge: true`, `inclusionMode: "full"`. - **Under `largeFileBytes` but too big for its part** — the partitioner byte-truncates the section to keep the part inside its budget, so `isLarge: false`, `isPreviewTruncated: true`. This is the one that surprises people: the file is not "large" by the configured threshold, yet its body is incomplete. - Only `isPreviewTruncated` tells you content is missing; `isLarge` alone does not. - `hashSha256` is the hash of the source file on disk — `validate` recomputes it to detect drift. It is the full 64-character digest. - `encoding` is present **only** when the file is not plain UTF-8; its absence means clean UTF-8. `utf-16le` and `utf-16be` mean the file was transcoded to UTF-8 before it was emitted, so a UTF-16 source file is readable in the parts instead of vanishing as "binary". `utf-8-lossy` means the file is not valid UTF-8 at all: its undecodable byte spans became `U+FFFD`, the count is reported in `warnings`, and the file is kept rather than dropped. The hash still covers the raw bytes, so a lossy file validates as current — `encoding` is how you learn that the body you are reading is not byte-exact. - `summary` and `summaryQuality` are **conditional**. Both keys are present for a language with an analyser and both are absent for a shapeless one — a `.gitignore`, a `.txt`. Treat a missing `summary` as "no structural information", not as an empty one. A file larger than 2 MB gets no summary at all: parsing it costs more memory than the file itself, and its body is a head/tail preview at that size anyway. `manifest.warnings` names those files. - `summary` has one of three shapes. Code files get the thirteen keys above (`directives`, `imports`, `exports`, `reExports`, `functions`, `classes`, `interfaces`, `types`, `enums`, `variables`, `reactComponents`, `hooks`, `routeHandlers`), plus `parseErrors` when the parser reported syntax errors. JSON gets `{ shape, itemCount, keys, sampleKeys }` and Markdown gets `{ headings, frontmatterKeys }` — a shape description rather than a code summary. - `summaryOmitted: true` means the file's summary was dropped because the summary alone would not fit the part budget. The entry then has `summaryQuality` but no `summary`, and the file's body is still emitted in full. It is only ever present when `true`. - `summaryQuality` is `ast` (parsed by the TypeScript compiler), `structural` (JSON or Markdown shape) or `heuristic` (pattern-matched). It tells you how much to trust the summary: `heuristic` means a line scan, not a parse. - The summary block is written **as-is** into the part, so a control or bidi character that appears in a source file can appear inside the fenced JSON too — the same reason the file body is emitted raw. Names are escaped where a path is *displayed* (part headers, the tree, navigation, audit, `explain`), but the two raw regions — the summary fence and the body — reproduce the repository's bytes. Treat the whole part as untrusted data, not just the body. Querying this array is usually faster and cheaper than re-reading the parts. ### `skipped[]` ```json { "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 `.gitignore`d 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](troubleshooting.md#a-file-is-missing-from-the-snapshot). **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 - **Sensitive files are excluded by default.** `.env`, `*.pem`, `*.key`, `.npmrc`, `.netrc`, `.git-credentials`, `id_rsa`, `.kube/config`, `*.tfstate` and similar are skipped and listed in `skipped[]`. `--include-sensitive-files` opts in. - **Secret-shaped strings are redacted** inside ordinary source files. Only the matched span is masked, never the whole line, and no excerpt of a matched line is written to any artifact. - **The output directory is owned by the tool.** Each run writes `.codebase-snapshot` into it first, then swaps the new snapshot in atomically. A previous snapshot survives until the replacement is complete. - **The tool refuses to replace a directory it did not write.** Pointing `--out-dir` at `src/` is refused with exit `1` and the directory is left untouched — a mistyped output path cannot delete source code. Empty directories, directories that hold nothing but `.DS_Store`, and directories holding a complete snapshot this build wrote are accepted. A `.gitkeep` or `.gitignore` inside the directory is not accepted, because the generator never writes either into an output directory: a directory that holds one is somebody's data, so the run refuses instead of deleting it. - **Ownership is the marker, not a filename.** `.codebase-snapshot` is a JSON record carrying this generator's name and `version`, the marker's `schemaVersion`, a `createdAt` timestamp, a `pid`, a per-snapshot `id`, the run `state` (`staging` while it is being written, `complete` once it is whole), the `manifestSha256` digest of the manifest it shipped, and `entries` — the inventory of everything it wrote. `pid` is what lets a later run tell a crashed run's staging directory and lock from a live one. A zero-byte marker, a marker that is not JSON, a marker written by 3.0.0 (no `id`), a bare `manifest.json`, and a directory with files you added after the run are all refused. See [Troubleshooting](troubleshooting.md#refusing-to-replace-outdir-). 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. --- # Stale LLM context is a bug A snapshot is a copy. The code keeps moving. Nothing in the copy says so, and nothing in the model can notice. That is the whole failure mode: you hand a model a faithful description of your repository on Monday, change four files on Wednesday, and on Thursday ask about a function you have already deleted. The answer is confident, plausible and about a repository that no longer exists. ```text you ──▶ snapshot (Monday) ──▶ model ──▶ answer about Monday's code code moves (Wednesday) ────┘ ``` There is no error message, because nothing failed. The model did exactly what it was given. ## Why "just regenerate it" is not a control Regenerating is cheap — it takes a second. The problem is that you have to *know* to do it, and nobody notices a stale snapshot until an answer is wrong. A habit is not a guard, and a README line that says "re-run this before asking" is a habit. What you need is not more discipline. It is a comparison that can fail. ## Three things that make drift visible ### 1. An identity for the copy Every run writes `manifest.fingerprint`, a hash of what was included, derived from the content of every file. Two repositories that differ by one byte have different fingerprints; two runs over the same tree on different machines have the same one. ```bash codebase-snapshot report --format json | jq -r .manifest.fingerprint ``` Quote that value wherever the snapshot is used — in the prompt, in the PR description, in the ticket. It turns "the snapshot we sent" into a value that can be compared later. ### 2. A comparison you can run `validate` re-scans the repository, re-hashes each file the snapshot claims to contain, and compares. It exits `1` when the code has moved, and `0` when the snapshot still describes what is on disk. ```bash codebase-snapshot validate ``` ```text ✖ Snapshot does not match the repository: - modified: src/parser/partition.ts - added: src/presets/evidence.ts - removed: src/legacy/old-partitioner.ts ``` That is the difference between a copy and an artefact. A copy is trusted; an artefact is checked. ### 3. A record of what was left out `manifest.skipped[]` names every path the snapshot did not include, with a reason for each: | Reason | Meaning | | :--- | :--- | | `gitignore` | the repository's own rules excluded it | | `default-ignore` | `node_modules`, build output, VCS metadata | | `sensitive-file` | `.env`, `*.pem`, `.npmrc` — excluded unless explicitly requested | | `binary` | not text, so not useful as context | | `outside-root` | a symlink pointing out of the repository | | `nested-repository` | a submodule or nested checkout | | `read-error` | it could not be read, and the run says so instead of dropping it quietly | When an answer is wrong because the model never saw a file, this is the list that tells you whether the file was excluded on purpose or by accident. ## Gate it The comparison is only a guard if something runs it. In CI, after whatever changes the tree: ```yaml - name: Snapshot is still current run: npx codebase-snapshot validate ``` A failure means the context you are about to rely on describes a different repository. Whether that blocks the pipeline or just warns is a policy question; having it at all is not. The same gate works for a long-running agent: run `validate` before a session that will read the snapshot, and regenerate when it fails. ## What this does not tell you Worth being precise about the limits, because "validate" sounds like more than it is: - **It compares files, not meaning.** A refactor that preserves every byte proves nothing is wrong — correctly, because nothing is. - **It cannot see what the model did with the text.** A current snapshot can still be misunderstood, summarised badly, or ignored. - **It says nothing about whether the snapshot was a good selection.** Use `explain ` to see why one path was included or skipped, and `report` for the shape of the whole run. - **It does not follow history.** If you keep three snapshots from three branches, each one validates against the branch it was generated from. ## When this is overkill If you generate a snapshot, paste it into a chat, and throw it away in the same hour, you do not need any of this — `snapshot` alone is the right amount of ceremony. The controls start to pay for themselves the moment a snapshot outlives the session that made it: a checked-in context directory, a scheduled job, a review bot, an agent that runs for days, or a team where the person who generated the snapshot is not the person who reads it. ## Related - [Budgeting LLM context](how-to-context-budget.md) — choosing a part size that fits the window - [Snapshot output](output.md) — the manifest fields this page refers to - [CLI reference](cli.md#validate--check) — `validate` in full --- # Troubleshooting Every message below is the real text the tool prints. Find your symptom, then the cause and the fix. ## The command does nothing **Symptom** — `npx codebase-snapshot generate` exits `0` and prints nothing. No `snapshot/` directory appears. **Cause** — a **stale global install** is shadowing `npx`. When a global copy exists, npm runs it instead of the published package, and a leftover `npm link` (pointing at a development checkout) resolves to a binary that is not there. **Fix** ```bash npm ls -g --depth=0 | grep codebase-snapshot npm rm -g codebase-snapshot npx codebase-snapshot generate ``` If nothing is installed globally, clear the npx cache (`npm cache clean --force`) and retry. This is a local environment problem, not a package problem: on a machine without a global copy the same command works. ## `command not found` **Cause** — when the package is installed as a project dependency, its binary lives in `node_modules/.bin` and is only on `PATH` inside a client runner or a package script. **Fix** — use `npx codebase-snapshot`, `pnpm exec codebase-snapshot`, `yarn codebase-snapshot`, `bun run codebase-snapshot`, or `./node_modules/.bin/codebase-snapshot` in scripts. ## Exit code 2 — the command itself was wrong Exit `2` always means "retrying this unchanged will fail the same way". The message names the problem, and for an option the command does not take it also names the commands that do: ``` ❌ --watch does not apply to `report` and was not applied. It is accepted by: `generate`. ❌ --cwd does not apply to `describe` and was not applied. It is accepted by: `generate`, `validate`, `report`, `init`, `explain`, `skill`. ``` Nothing is silently ignored — that is the point of the message. The per-option `Applies to` column is in the [CLI reference](cli.md#options). | Message | Fix | | :--- | :--- | | `Unknown command "frobnicate". Expected one of: …` | Use one of the commands in the [CLI reference](cli.md). | | `No configuration found in … and this session is not interactive, so nothing was generated.` | A first run and no terminal to ask its question. Nothing is wrong with the repository: say which setup you want — `--autopilot` for the full setup, `init --yes` then `generate` for the steps, or `generate` for the documented defaults. See [the first run](cli.md#the-first-run). | | `--autopilot sets a project up and writes a config; --no-config ignores config files. Pass one or the other.` | The two flags contradict each other. Drop one. | | `--no-install only applies to \`setup\`, or to \`generate --autopilot\`.` | The install only happens during a setup. Drop the flag, or add `--autopilot`. | | `--preset does not apply to \`generate\` and was not applied. It is accepted by: \`init\`. To scope a run, set include/excludePatterns/preset in snapshot.config.ts (or run \`codebase-snapshot init\` to generate one).` | `--preset`, `--include`, `--exclude`, `--yes` and `--force` configure `init`, which writes them into the config. Set them at generation time in `snapshot.config.ts` instead. | | `--install does not apply to \`generate\` and was not applied. It is accepted by: \`skill\`.` | `--install` and `--target` configure `skill` only. | | `--cwd directory does not exist: …` / `--cwd is not a directory: …` | Point `--cwd` at an existing repository directory. | | `--out-dir requires a value (received an empty string).` | An unset shell variable expanded to nothing. Quote it or drop the flag. Every string option rejects `""` rather than silently using the default. | | `--max-part-bytes received a value that looks like another flag (for example a negative number). Use the "--option=value" form, and note that sizes must be positive.` | A negative number reads as a flag. Use `--max-part-bytes=-5`, and note that sizes must be positive. | | `Unknown preset "nope". Expected one of: …` | The message lists all 25 valid names. | | `--target must be one of: claude, cursor, all (received "bogus").` | Fix the value. | | `diff expects exactly two arguments …` / `explain expects exactly one path.` | Fix the arity. | The `agents` target was removed in 4.1.1: `skill --install` writes only into the two registries agent tools read by name (`.claude/skills/` and `.cursor/rules/`) and never writes `AGENTS.md`, which is your repository's own agent contract. To put the skill there, print it and paste it yourself: `codebase-snapshot skill >> AGENTS.md`. ## Exit code 1 — the state was wrong ### `Refusing to replace outDir …` ``` ❌ Failed to generate snapshot: Refusing to replace outDir /repo/src: it holds no .codebase-snapshot marker (found app.ts). Remove the directory yourself, or point outDir at a new or empty directory. ``` **Cause** — the output directory contains files this tool did not write, and it cannot prove it wrote them. This guard exists because a mistyped `outDir` used to replace the directory: `outDir: "src"` deleted source code while reporting success. Ownership is proved by a `.codebase-snapshot` marker that parses as ours — a JSON object naming this generator, carrying a schema version this build understands, a per-snapshot `id` and the digest of the manifest it shipped. A file with the right name is not enough: | The directory | What the tool says | | :--- | :--- | | has files, no marker | `it holds no .codebase-snapshot marker (found app.ts)` | | has a zero-byte marker | `its .codebase-snapshot marker is empty (found .codebase-snapshot, app.ts)` | | has a marker that is not JSON | `its .codebase-snapshot marker is not valid JSON (found .codebase-snapshot, app.ts)` | | was written by 3.0.0 | `its .codebase-snapshot marker carries no snapshot id (found .codebase-snapshot, manifest.json)` | | carries our marker but you added files | `it carries this generator's ownership marker, but these entries are not part of the snapshot it recorded: custom.txt. Remove them yourself, or point outDir at a new or empty directory.` | **Fix** — point `--out-dir` somewhere new, or delete the directory yourself. Do not delete your source. Accepted without complaint: a missing directory, an empty directory, one holding nothing but `.DS_Store`, and a complete snapshot this build wrote. `.gitkeep` and `.gitignore` are **not** accepted, even in a directory that holds nothing else. Both are files you create — the generator never writes either into an output directory — so a directory holding one is somebody's data and is refused rather than replaced. Move the file out, or point `--out-dir` somewhere new. A snapshot written by 3.0.0 or earlier is **not** upgraded in place: its marker has three keys and no `id`, so the new build cannot prove it wrote the directory and refuses. Delete the old snapshot directory and generate again. ### `Refusing to write into outDir …: it exists but is not a directory.` ``` ❌ Failed to generate snapshot: Refusing to write into outDir /repo/notes.txt: it exists but is not a directory. ``` A regular file has the same name as the output directory. Rename or remove it. ### `The config file could not be read` ``` ⚠ Config: Could not parse /repo/snapshot.config.json: Expected property name or '}' … ❌ The config file could not be read. Fix it, or delete it to use the defaults. ``` **Cause** — a config file exists but does not parse, or a `.ts` config cannot be loaded. This is fatal on purpose: skipping a broken config would generate a snapshot with none of your settings and still report success. **Fix** — fix the syntax, or delete the file to run on defaults. Check that the config is loadable on your Node version: a `.ts` config needs Node ≥ 22.6 **and** an ES-module project. `codebase-snapshot init` picks the right filename for the project automatically. ### `This directory is a git repository, but git could not list its files.` ``` ❌ Failed to generate snapshot: This directory is a git repository, but git could not list its files. A plain walk honours .gitignore files, but git also applies .git/info/exclude and your global excludes file, includes files that are tracked although a .gitignore now matches them, and treats a submodule as a single entry. Fix the repository (git status should work), run outside git, or pass --discovery walk to accept those differences. ``` `git` is present and a `.git` directory exists, but it cannot list the files — an empty or half-initialised `.git`, a permissions problem, or a damaged index. The tool refuses rather than silently choosing between two discovery methods: which files are in the snapshot would otherwise depend on whether git happened to work. **Fix** — make `git status` work in that directory (repair or re-clone the repository), generate from a copy that has no `.git` directory, or pass `--discovery walk` to scan the tree anyway. The walk reads every `.gitignore` it meets, nested ones included, and still skips credential files; what it cannot reproduce is git's own ignore sources (`.git/info/exclude` and your global `core.excludesFile`), git's inclusion of files that are tracked although a `.gitignore` now matches them, and its treatment of a submodule as one entry instead of a tree. `--discovery git` says the opposite — require git — so it refuses in a directory that has no usable repository: ``` ❌ Failed to generate snapshot: Discovery mode is `git`, but git could not list this directory's files. Run inside a working repository, or pass --discovery walk (or auto) to scan the tree directly. ``` ### `Another generate run is already using the staging directory …` ``` ❌ Failed to generate snapshot: Another generate run is already using the staging directory /repo/snapshot.tmp. Wait for it to finish, or delete /repo/codebase-snapshot-1a2b3c4d5e6f7890.lock if no other run is active. ``` A second `generate` against the same repository started while the first still held the staging directory — two terminals, or a CI job and a developer at once. The lock is advisory and on disk, so a run that was killed cannot release it. **Fix** — wait for the other run, then retry. If no other run is active, the named lock file is stale: delete it, and the next run proceeds. ### `Refusing to generate: …` ``` ❌ Failed to generate snapshot: Refusing to generate: outDir is the working directory or one of its ancestors. outDir=/repo tmpDir=/repo/snapshot.tmp cwd=/repo ``` The output paths are unsafe: `outDir` or `tmpDir` is the working directory, an ancestor, the filesystem root, or they contain one another. The message prints the resolved paths so you can see why. Use a subdirectory. ### `Nothing to snapshot: … has no files.` ``` ❌ Nothing to snapshot: this directory has no files. Run this inside the repository you want to snapshot, or pass --cwd. ``` The directory is empty apart from `.git` (or `.hg`, `.svn`, `.DS_Store`, `Thumbs.db`). Setting the project up would install a devDependency and add scripts for a repository with nothing in it, so `--autopilot` refuses too. Run the command from the repository, or point `--cwd` at it. ### Budget errors A budget set in a **config file** exits `1`, and names the setting as you wrote it: | Message | Fix | | :--- | :--- | | `maxPartBytes must be a positive byte count (received 0).` | Zero and negative values are errors, not "use the default". | | `maxPartBytes must be at least 4096 bytes (received 1000).` | Raise it to 4096 or more (`"120k"` is 122880). | | `maxPartBytes must be a byte count such as 120000, "120k" or "1.5mb" (received "abc").` | The value is not a number or a supported size string. | The same mistake on the **command line** is a usage error, exits `2`, names the flag, and is followed by the usage text: | Message | Fix | | :--- | :--- | | `--max-part-bytes must be at least 4096, received "1000".` | Raise it to 4096 or more. `--max-part-bytes 0` and `--max-part-bytes 1000` both produce this form. | | `--max-part-bytes expects a byte count such as 120000 or "120k", received "abc".` | The value is not a number or a supported size string. `--max-part-bytes=-5` produces this form too. | `--large-file-bytes` and `--max-monolith-bytes` share the second message, with their own flag name, and their minimum is 1 rather than 4096: `--large-file-bytes must be at least 1, received "0".` ### `Cannot write the snapshot: permission denied under …` The output directory is not writable (or a parent is not readable). The previous snapshot is left intact. Fix the permissions, or choose another `--out-dir`. ### `Snapshot validation failed` (from `validate`) Exit `1` with one line per class of drift: | Line | Meaning | | :--- | :--- | | `- modified: path` | The file changed since the snapshot. | | `- deleted: path` | The file is in the snapshot but no longer on disk. | | `- new file: path` | The file exists on disk and is not in the snapshot. | | `- part: part-001.md (missing from snapshot/parts)` | The part file was deleted or renamed. | | `- part: … (size mismatch …)` | The part was edited by hand. | | `- config: …` | A setting differs from the one recorded in the manifest. | | `- counts: …` | The manifest's own bookkeeping disagrees with its contents. | **Fix** — re-run `generate`. If it persists, the snapshot directory is being modified by something else. ### `report` exits 1 (only when it cannot read the snapshot) - `Manifest not found at … Run generate first.` — nothing has been generated yet. - `… is not a snapshot manifest: missing \`counts\`` — the file parsed as JSON but is not a manifest this tool wrote. Wrong `--out-dir`, or a foreign file named `manifest.json`. ### `diff` exits 1 - `No snapshot found at X: the path does not exist.` - `Could not read a manifest at X.` - `… is not a snapshot manifest (no "counts" object).` Point it at a snapshot directory or a `manifest.json` written by this tool. ## A file is missing from the snapshot Run `explain` on it — it names the exact reason and the rule responsible: ```bash npx codebase-snapshot explain src/generated/schema.ts ``` `manifest.json` records the same for every file in `skipped[]`: | `reason` | Meaning | Fix | | :--- | :--- | :--- | | `sensitive-file` | `.env`, `*.pem`, `.npmrc`, `.ssh/`, `.tfstate`, … | `--include-sensitive-files` if you really want it. | | `sensitive-path` | The path itself matched a secret rule, so the file is skipped and its path is masked in every artifact. | Rename the file; a path that looks like a key is treated as one. | | `binary` | Not text: it contains a NUL byte outside a UTF-16 stream, or is more than 1% control characters. The `detail` names the file size and the reason — including the offset of the first NUL when that byte sits past the 8 MB head scan. | Nothing to do. A UTF-16 file is *not* binary — it is transcoded and reported with `encoding`, not skipped. | | `default-ignore` | A built-in rule, the preset's ecosystem ignores (`node_modules`, `__pycache__`, `target`, `vendor`, …), or the output directory. | A preset's ignores are not configurable; a custom `excludePatterns` can only remove more. | | `excluded` | Your own `excludePatterns`, or a path outside a non-empty `include` allow-list. | Remove the pattern, or widen `include`. | | `symlink` | Links are not followed (a directory link names its target in `detail`). | Snapshot the target. | | `outside-root` | A link whose target resolves outside the scanned root. | Keep the target inside the repository, or snapshot it directly. | | `gitignore` | A `.gitignore` excluded it, in a scan that is not reading git's index. | Remove the entry, or add the path to `include`. | | `read-error` | Unreadable file. | Check permissions. | | `not-file` | A directory, socket, FIFO. | Expected. | | `nested-repository` | A nested checkout or submodule — a directory tracked by a different repository. | Expected; snapshot that repository separately if you need its code. | Inside a git repository the candidate list comes from git, which applies `.gitignore` before the scanner runs; those ignored paths are listed separately and recorded too, so a `.gitignore`d file or directory appears here with reason `gitignore` in either mode. A skipped directory is recorded, but the files inside it are never descended to and so have no record of their own. For those, `explain` reports the **nearest skipped ancestor** and names it in `detail`, whatever the reason — including built-in ignores: ``` node_modules/dep/index.js included no reason default-ignore detail built-in ignore: directory "node_modules" (not descended) — inherited from the skipped directory `node_modules` ``` The same applies to a `.gitignore`d tree, where `detail` reads ``ignored by .gitignore — inherited from the skipped directory `ignored` ``. `explain` can report two more, which never appear in `skipped[]` because they are about the path rather than about a candidate file: | `reason` | Meaning | Fix | | :--- | :--- | :--- | | `missing` | `no such file or directory, relative to the configured cwd` — nothing is at that path. | Check the path and `--cwd`. | | `not-discovered` | `exists but is not tracked or is otherwise not a candidate` — the path exists but nothing accounts for it: no rule, glob or skipped ancestor explains it. The tool's own output directory is pruned this way, so a file under `snapshot/` reports it. | If it sits in a skipped directory you will normally get that directory's reason instead; otherwise copy it into the repository, since `include` cannot rescue a pruned directory. | Also check `include`: if it is non-empty it acts as an allow-list, and everything else is excluded. ## The snapshot is too big - Lower the budget: `--max-part-bytes 120k`. - Exclude whole trees: `excludePatterns: ["vendor/**", "**/*.min.js"]` in the config. - Per-layer budgets: `budgets: { default: "120k", byLayer: { docs: 40000 } }`. - Skip artifacts you do not read: `--skip-artifacts navigation,projectTree,audit`. - Large files are already previewed rather than inlined; tune with `--large-file-bytes`. ## Warnings that are working as intended These come from `generate`. They are recorded in `manifest.warnings` and repeated on stderr under a `- Warnings: N` line in the summary, and none of them stops a run: - `manifest.json is disabled: \`validate\` and \`report\` cannot inspect this snapshot.` — you used `--skip-artifacts manifest`. - `parts are disabled but navigation.md is enabled: its links point at part files that were not written.` - `every artifact is disabled: only the .codebase-snapshot ownership marker will be written.` - A `hooks.afterGenerate` error is downgraded to a warning: the snapshot it was called for is already written and is not lost. - `N file(s) exceed 2 MB and were not parsed for a structural summary: …` — the named files are emitted as head/tail previews without an `imports`/`exports` summary. Parsing them costs several times their own size in memory, which is how a single 50 MB source file used to reach 3.3 GB of resident memory. Nothing to fix; raise the part budget if the preview is too small. - `N file(s) are UTF-16LE and were transcoded to UTF-8: …` — those files were UTF-16 and are now ordinary text in the snapshot. They are listed in `manifest.files[]` with `encoding: "utf-16le"`, or `"utf-16be"`. Nothing to fix; the warning exists because the bytes in the artifact are not the bytes on disk. - `N file(s) are not valid UTF-8; M undecodable byte(s) became U+FFFD: …` — the named files are not UTF-8 at all (Latin-1 is the usual cause). They are kept, with `encoding: "utf-8-lossy"`, but the undecodable spans are substituted, so **do not hand the emitted body to a model as if it were the source**. Convert the file to UTF-8 (`iconv -f WINDOWS-1252 -t UTF-8 file > file.tmp`) and regenerate. The snapshot still validates as current, because the hash covers the raw bytes. ## Redactions in the summary are not warnings If your run printed something like ``` - Warnings: 0 - Redacted: 78 line(s) — values masked; see snapshot/reports/audit.md ``` then nothing is wrong. That line counts **lines whose values were masked**, once per line even when two rules matched the same line, and the per-line detail — file, line, every rule that fired — is the table in `reports/audit.md` (section 1). The same number is `manifest.totals.redactedLines`. Three things surprise people about it, and all three are the intended trade-off: - **`.env.example` and docs templates are masked too.** A placeholder such as `JWT_SECRET=replace-with-base64url-48-bytes` is masked exactly like a live key, because the rule cannot tell them apart — guessing which values are throwaway is how a real key escapes. The template is therefore less readable in the snapshot than on disk. Narrow it with `customPatterns` and `disableDefaultPatterns` (see [Configuration](configuration.md#secrets-redaction)). - **Test fixtures count.** `process.env.DB_SECRET = "test-value"` is a credential assignment by shape, so it is masked and counted. - **A long list is not a truncated list.** The `Warnings:` preview stops at ten rows and prints `… and N more in manifest.warnings[]`; redactions are never previewed in the summary at all — the count is stated and the table is the list. If `reports/audit.md` was disabled with `--skip-artifacts audit`, the manifest keeps the count and warns that the per-line detail was not written, rather than dropping it silently. `validate` produces its own warnings, printed with a `⚠ ` prefix and never written to the manifest, because they describe the state of the repository rather than the snapshot: - `⚠ Working tree has uncommitted changes.` - `⚠ Snapshot was generated on branch "zeta" but the configured branch is "main".` — the branch moved since the snapshot was written. ## Detached HEAD On a detached HEAD (normal in CI) `manifest.git.branch` records the configured branch — `main` by default — because a detached HEAD cannot name one. Set `branch` in the config if your CI convention differs. This is not a warning condition. ## Not a git repository Outside a repository the tool does not fail; it records what it can: ```json { "branch": "main", "commit": "unknown", "isDirty": false } ``` `branch` is the configured branch, `commit` is `"unknown"`, and `isDirty` is `false` because there is no tree to compare — not because the tree is clean. Read `commit` when you need to tell "no repository" from "clean repository", and do not treat `isDirty: false` alone as evidence that the snapshot was taken from committed state. ## Still stuck ```bash npx codebase-snapshot describe --format json > contract.json npx codebase-snapshot generate --dry-run --format json > dry-run.json ``` The first is the machine-readable contract, including a troubleshooting table. The second computes a full snapshot without writing anything, so you can inspect `config`, `counts`, `skipped` and `warnings` before committing to a run. --- # codebase-snapshot vs code2prompt `code2prompt` is a Rust CLI that renders a codebase through a Handlebars template into a prompt. The templating is the feature — it decides the shape of what you get. This tool decides the *size and the freshness* of what you get, and leaves the shape to a manifest. They are close to opposites, which makes choosing between them straightforward. > Written by the author of `codebase-snapshot`. code2prompt is a separate project; the summary > reflects its commonly documented behaviour at the time of writing rather than a benchmark. ## The short version **code2prompt** — you write a template, it renders your repository into it. Output shape is entirely yours. **codebase-snapshot** — output shape is fixed and predictable: budgeted Markdown parts, plus a JSON manifest with hashes. You tune what goes in, not what the page looks like. If the prompt format is the thing you care about: **use code2prompt.** ## Side by side | | codebase-snapshot | code2prompt | | :--- | :--- | :--- | | Output shape | Fixed: parts + `manifest.json` + reports | **Yours** — Handlebars template | | Custom prompt scaffolding | No — that is out of scope | **Yes**, the core feature | | Hard size ceiling | **Yes** — `maxPartBytes` | Not to this project's knowledge | | Staleness check | `validate` exits non-zero on drift | Not a feature | | Reproducible output | Byte-identical parts, with a fingerprint | Template-driven; no determinism guarantee | | Distribution | Node.js, npm | Single Rust binary, no runtime | | Machine index | `manifest.json` with hashes and skip reasons | Template output only | | Language awareness | 25 presets, AST/structural/heuristic summaries | Template access to file contents | | Secret handling | 30 defaults, value-span masking, audit table | Ignore rules | ## Where code2prompt is the better choice - **You need a specific prompt structure** — a system preamble, per-file headers, a task framing. Templates are the right abstraction and this tool has no answer to them. - **You want a single binary with no runtime.** Rust means no Node install on the target machine. - **You are generating prompts, not context artifacts.** If the output is a prompt, you want a templating engine. ## Where this is the better choice - **You need the output to fit a budget.** `maxPartBytes` is a ceiling: an oversized file is truncated with an omission marker naming the budget rather than silently blowing the limit. - **You need to detect staleness.** `validate` compares every file hash plus a digest of the generation settings, so a changed budget counts as drift. Prompt generators generally have no concept of a snapshot that can expire. - **You need reproducibility.** Frozen output means the artifact can be diffed in review or cached by hash — not just regenerated and hoped over. - **You need to explain exclusions.** `manifest.skipped[]` names a reason for every path left out; `explain ` answers for one. - **You need an agent to run it unaided.** `describe --format json` is the whole contract as data. ## Both at once Legitimate and common: generate the parts with this tool, then reference those parts from a template you render yourself. The two are not competing for the same slot — one produces a payload, the other produces a prompt that might contain it. ## Honest summary If your problem is *"my prompt is not structured the way I want"*, code2prompt solves it and this tool does not. If your problem is *"my context does not fit, and I cannot tell whether it is still accurate"*, that is this tool's entire reason to exist. --- # codebase-snapshot vs gitingest `gitingest` is the fastest path from *a GitHub URL* to *text in your clipboard*: paste a link on a web page and get a digest back, with no install at all. This page says where that is better than this tool, and where it is not. > Written by the author of `codebase-snapshot`. gitingest is a separate project; the summary below > reflects how it is commonly used at the time of writing rather than a benchmark of it. ## The short version **gitingest** is a web service plus a Python package that turns a repository into a single digest you can paste into a prompt. Its great strength is that it needs nothing installed and no thought. **codebase-snapshot** runs locally, writes partitioned output under a hard byte budget, and can tell you later whether the snapshot still matches the code. If you want to paste a repo into a chat right now: **use gitingest.** ## Side by side | | codebase-snapshot | gitingest | | :--- | :--- | :--- | | Getting started | `npx codebase-snapshot setup` (Node.js ≥ 18) | Open a website and paste a URL | | Install required | Yes | **No**, for the hosted version | | Works offline | **Yes** — local filesystem only | The hosted flow needs the network | | Output shape | Parts under a hard byte budget, plus a JSON manifest | One digest | | Hard size ceiling | **Yes** — `maxPartBytes` | Not to this project's knowledge | | Staleness check | `validate` exits non-zero on drift | Not a feature | | Reproducible output | Byte-identical parts across runs, with a fingerprint | Not a published guarantee | | Excluded-file accounting | Every skipped path recorded with a reason | Ignore rules applied | | Secret redaction | 30 default rules, value-span masking, audit table | Relies on ignore rules | | Runtime | Node.js | Python | | Repo URL input | No — needs a checkout | **Yes** | ## Where gitingest is the better choice - **You have not installed anything and do not want to.** Nothing here competes with a URL box. - **You want one digest, not parts.** If the destination is a chat window, one blob is the right shape. - **You are in Python.** `pip install gitingest` fits an existing pipeline without adding a Node runtime. - **You are exploring.** Pointing at a repository you have never seen is what it is for. ## Where this is the better choice - **The repository must not leave your machine.** This tool never makes a network request; it reads the local tree. If the code is private enough that a hosted digest is not allowed, that settles it. - **You need a size guarantee.** A part either fits the budget or carries an explicit truncation marker naming the budget. A digest that grows with the repository eventually stops fitting, and discovering that at ingestion time is too late. - **You need to prove the context is current.** `validate` re-hashes the tree and compares a digest of the generation settings, so a stale snapshot fails instead of quietly misleading a model. - **You need the exclusions explained.** Every skipped path carries a reason — `binary`, `gitignore`, `sensitive-file`, `nested-repository`, `read-error` and so on — and `explain ` answers for any single file. - **You need automation.** Exit codes, a machine-readable contract and a manifest make this gate-able; a copy-paste digest is not. ## Both at once They compose naturally: use gitingest to look at an unfamiliar repository quickly, and this tool once the repository is one you maintain and want context generated reproducibly. ## Honest summary For a one-off look at someone else's repository, gitingest is better and always will be — it wins on friction, and friction is what most people are short of. This tool is for codebases you own and pipelines you maintain, where a ceiling, a drift check and an audit trail are the point. --- # codebase-snapshot vs repomix Both tools answer the same question — *how do I hand a whole repository to a model that cannot clone it?* They disagree about almost everything else. This page states where each one is the better choice, including where repomix clearly is. > Written by the author of `codebase-snapshot`. Numbers are the maintained project's own claims at > the time of writing, and the comparison is deliberately unkind to this project where it deserves > it. ## The short version **repomix** packs a repository into one AI-friendly file. It is the established tool in this space by a wide margin — tens of thousands of stars, an MCP server, remote repository URLs, and an ecosystem of integrations. **codebase-snapshot** produces *size-budgeted parts* rather than one file, and treats the snapshot as something that can go stale and be proven current. Its distinguishing features are a hard byte ceiling per part, deterministic output, and `validate`. If you want the most popular, most integrated tool: **use repomix.** ## Side by side | | codebase-snapshot | repomix | | :--- | :--- | :--- | | Output shape | Multiple parts, each under a hard byte budget, plus a JSON manifest | A single packed file (XML, Markdown or JSON) | | Hard size ceiling | **Yes** — `maxPartBytes` is a ceiling, not a target | Token counting and warnings; no per-artifact ceiling | | Reproducible output | Parts are byte-identical across runs; a fingerprint covers them | Content is stable; no published determinism guarantee | | Staleness check | `validate` re-hashes the repository and exits non-zero on drift | Not a feature | | Remote repositories | No — operates on a local checkout | **Yes** — packs a GitHub URL directly | | MCP server | **Yes** — seven tools over stdio, and it serves the validated snapshot rather than the working tree | **Yes** — packs a repository, including by URL | | Secret handling | 30 default rules, value-span masking, per-line audit table | Secretlint integration | | Language awareness | 25 presets, AST or structural summaries labelled by confidence | Tree-sitter parsing with a compression mode | | Adoption | New | **Mature** — large user base, many integrations | | Runtime | Node.js ≥ 18, one dependency | Node.js, npm package | ## Where repomix is the better choice - **You want one file.** Pasting a single artifact into a chat is simpler than assembling parts, and repomix is built around that workflow. - **You need a remote URL.** `repomix --remote https://github.com/owner/repo` works without a clone. This project has no equivalent. - **You want MCP against a remote URL.** repomix's server can pack a repository by URL. This one serves a local checkout, and refuses to read outside it. Both ship a server; ours is narrower on purpose, and it can tell the agent whether the snapshot it is reading is still current. - **You want the ecosystem.** More stars, more tutorials, more people who have already hit your problem. - **You are packing something enormous and prefer compression to budgets.** repomix's tree-sitter compression mode is a different bet — shrink the content — and on very large repositories it may suit you better than partitioning. ## Where this is the better choice - **You need a guarantee, not a best effort.** `maxPartBytes` promises no part exceeds the ceiling; a file too large is truncated with an explicit marker naming the budget. If a downstream system rejects payloads above a size, a warning is not enough. - **You need to know the snapshot still matches the code.** `validate` compares every file hash *and* a digest of the settings the snapshot was generated with, so a changed budget or secret pattern counts as drift. That makes "is my context current?" a CI-gateable question. - **You need byte-identical output.** Two runs over unchanged input produce identical `parts/`, so a snapshot can live in review, be diffed, or be cached by hash. - **You need a machine index.** `manifest.json` records every included file *and every excluded file with a reason*, which is what makes `explain ` able to answer "why is this not in here?". - **You want the tool to be drivable by an agent without documentation.** `describe --format json` emits the whole contract, and `skill --install` writes a skill into the registries agent tools read. ## Both at once There is no conflict. A common arrangement is to use this tool for the *budgeted, validated* case (CI, RAG ingestion, anything automated) and repomix when a human wants one file to paste into a chat. They read the same repository and write to different directories. ```bash npx codebase-snapshot setup # config, scripts, dependency, install, generate npx codebase-snapshot validate # exit 0 means the snapshot matches the disk ``` ## Honest summary repomix is the safer recommendation for most people today, because maturity and integrations matter more than any single feature. This tool exists for the narrower case where a size ceiling, a drift check and reproducibility are requirements rather than nice-to-haves — and for the case where an agent has to operate it without a human reading the docs first.