codebase-snapshot 0.0.0-stage → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,196 @@
1
+ # Installation
2
+
3
+ `codebase-snapshot` is published to the public npm registry, so it installs with
4
+ **any** npm-compatible client. There is no second registry to add and nothing to configure.
5
+
6
+ Requires **Node.js 18 or newer**.
7
+
8
+ ## One command
9
+
10
+ ```bash
11
+ npx codebase-snapshot setup
12
+ ```
13
+
14
+ Detects the project, writes the config (an existing one is kept), adds the npm scripts and the
15
+ devDependency, installs them with the package manager your lockfile names, and generates the first
16
+ snapshot. `--no-install` skips the install; `--dry-run` writes nothing. The sections below are the
17
+ same steps taken one at a time.
18
+ ## One-off use
19
+
20
+ ```bash
21
+ npx codebase-snapshot generate
22
+ ```
23
+
24
+ This is the shortest path and needs no install. The command is also available as
25
+ `codebase-snapshot` and, once installed, as the short alias `snap`.
26
+
27
+ Pin the version when you need the same build on every machine:
28
+
29
+ ```bash
30
+ npx codebase-snapshot@latest generate
31
+ npx codebase-snapshot@0.9.0 generate
32
+ ```
33
+
34
+ **Why the bare name works.** npm resolves which binary to run from the package's `bin`
35
+ entries. This package declares `codebase-snapshot` and `snap` pointing at the *same file*, so
36
+ npm treats them as aliases and can always resolve one. Keep them aliases: two *different*
37
+ executables under a scoped package name would make the bare form unresolvable.
38
+
39
+ If a command exits with no output at all, a stale global install is the usual cause — see
40
+ [Troubleshooting](#troubleshooting).
41
+
42
+ ## Add to a project
43
+
44
+ ```bash
45
+ npm install -D codebase-snapshot
46
+ yarn add -D codebase-snapshot # yarn classic and Berry
47
+ pnpm add -D codebase-snapshot
48
+ bun add -d codebase-snapshot
49
+ ```
50
+
51
+ Then run it through your client so the binary resolves from the project:
52
+
53
+ | Client | Run |
54
+ | :--- | :--- |
55
+ | npm | `npx codebase-snapshot generate` |
56
+ | yarn (classic and Berry) | `yarn codebase-snapshot generate` |
57
+ | pnpm | `pnpm exec codebase-snapshot generate` |
58
+ | bun | `bun run codebase-snapshot generate` |
59
+
60
+ `snap` works everywhere `codebase-snapshot` does. Adding it to `package.json` scripts is the
61
+ most reproducible option:
62
+
63
+ ```json
64
+ {
65
+ "scripts": {
66
+ "snapshot": "npx codebase-snapshot generate",
67
+ "snapshot:validate": "npx codebase-snapshot validate"
68
+ }
69
+ }
70
+ ```
71
+
72
+ `codebase-snapshot init` writes these entries for you, in exactly this `npx` form: the bare
73
+ `codebase-snapshot` command only exists on `PATH` inside a client runner or a script that has
74
+ already resolved the package, and `init` does not assume the install has happened yet.
75
+
76
+ ## One-off runners for other clients
77
+
78
+ ```bash
79
+ yarn dlx -p codebase-snapshot codebase-snapshot generate # yarn Berry
80
+ pnpm dlx codebase-snapshot generate
81
+ bunx codebase-snapshot generate
82
+ ```
83
+
84
+ ## Agent skill
85
+
86
+ To make a repository discoverable by an AI agent — so it finds this tool without being told
87
+ it exists — run:
88
+
89
+ ```bash
90
+ npx codebase-snapshot skill --install
91
+ ```
92
+
93
+ That writes `.claude/skills/codebase-snapshot/SKILL.md` and
94
+ `.cursor/rules/codebase-snapshot.mdc`, and adds both to `.gitignore` so the install leaves
95
+ nothing untracked. It does not touch `AGENTS.md`: that file is your project's own agent
96
+ contract. To put the skill there, print it and paste it —
97
+ `npx codebase-snapshot skill >> AGENTS.md`. See
98
+ [Using this with an AI agent](agent.md).
99
+
100
+ ## Verified clients
101
+
102
+ Every client below installs the published package and runs it successfully; the release check
103
+ enforces this on every pull request, so a change that breaks one fails the build rather than
104
+ a user's terminal.
105
+
106
+ | Client | Run installed binary with |
107
+ | :--- | :--- |
108
+ | npm 10–11 | `npx codebase-snapshot` |
109
+ | yarn classic 1.22 | `yarn codebase-snapshot` |
110
+ | yarn Berry 4.6 (`nodeLinker: pnp`) | `yarn codebase-snapshot` |
111
+ | pnpm 10 | `pnpm exec codebase-snapshot` |
112
+ | bun 1.4 | `bun run codebase-snapshot` |
113
+
114
+ Yarn Berry's default **strict PnP** works: there is no `node_modules` directory, and the
115
+ runtime `typescript` dependency still resolves through yarn's `.pnp.cjs` loader.
116
+
117
+ ## Before scanning a repository you do not own
118
+
119
+ A `snapshot.config.ts`, `.js`, `.mjs` or `.cjs` in the target repository is **imported and
120
+ executed** when the tool runs there — that is what lets a config express a `RegExp` or a
121
+ `match` function. Pointing `--cwd` at a checkout you do not trust therefore runs its code.
122
+ `--no-config` ignores every config file and uses the built-in defaults plus the detected
123
+ preset, and JSON configs are parsed rather than executed. See
124
+ [the trust boundary](../SECURITY.md#trust-boundary).
125
+
126
+ ## Global install
127
+
128
+ ```bash
129
+ npm install -g codebase-snapshot
130
+ ```
131
+
132
+ Convenient, but prefer pinning per project: snapshots are only reproducible if the generator
133
+ version is. A global install also shadows `npx` for this package — npm will run your global
134
+ copy instead of the published one, which is confusing when the two differ. That is exactly
135
+ the situation described under [Troubleshooting](#troubleshooting).
136
+
137
+ ## Workspaces
138
+
139
+ The binary resolves from the workspace it is installed in. In a monorepo, install it in the
140
+ package that runs the snapshot (usually the root), or use a one-off runner
141
+ (`npx` / `pnpm dlx` / `yarn dlx` / `bunx`) which works from any subdirectory.
142
+
143
+ ## Upgrading
144
+
145
+ ```bash
146
+ npm install -D codebase-snapshot@latest
147
+ npx codebase-snapshot generate # rewrite the snapshot with the new version
148
+ ```
149
+
150
+ `manifest.generator.version` records which version produced a snapshot. Re-run `generate`
151
+ after an upgrade so the snapshot, and the version it records, match the build you are now
152
+ running.
153
+
154
+ A snapshot written by **3.0.0 or earlier** is refused rather than replaced: its
155
+ `.codebase-snapshot` marker has no per-snapshot id, so the current build cannot prove it wrote
156
+ the directory. Delete the old snapshot directory and generate again.
157
+
158
+ See [CHANGELOG](../CHANGELOG.md) for breaking changes — 3.0.0 removed nothing, but it briefly
159
+ did in error; 4.0.0 restored the `snap` alias.
160
+
161
+ ## Uninstalling
162
+
163
+ Standard for your client (`npm uninstall`, `yarn remove`, `pnpm remove`, `bun remove`).
164
+ Snapshots already written to the output directory are ordinary files and are left alone.
165
+ If you installed globally, `npm rm -g codebase-snapshot` also removes the
166
+ shim that shadows `npx`.
167
+
168
+ ## Troubleshooting
169
+
170
+ **`codebase-snapshot: command not found`** — when installed locally (not globally) the binary
171
+ lives in `node_modules/.bin` and is only on `PATH` inside a package script or a client
172
+ runner. Use `npx codebase-snapshot`, `pnpm exec codebase-snapshot`,
173
+ `yarn codebase-snapshot` or `bun run codebase-snapshot`.
174
+
175
+ **A command exits 0 and prints nothing** — almost always a stale **global** install
176
+ shadowing `npx`. Check and remove it:
177
+
178
+ ```bash
179
+ npm ls -g --depth=0 | grep codebase-snapshot
180
+ npm rm -g codebase-snapshot
181
+ ```
182
+
183
+ Then re-run. If the package is not installed globally, clear the npx cache
184
+ (`npm cache clean --force`) and retry.
185
+
186
+ **`Cannot find module 'typescript'`** — the package declares `typescript` as a runtime
187
+ dependency for AST analysis. If your client prunes it (some `--ignore-scripts` CI setups do),
188
+ rerun a clean install or pin `typescript@^5` yourself.
189
+
190
+ **`yarn codebase-snapshot` prints extra lines / breaks a script** — yarn classic wraps
191
+ everything: it echoes `yarn run 1.22.x`, the resolved command, your output, then
192
+ `Done in 0.37s.`. For anything that parses the output, call the binary directly
193
+ (`./node_modules/.bin/codebase-snapshot` or `npx codebase-snapshot`).
194
+
195
+ More symptoms — refusals, config errors, drift, missing files — are in
196
+ [Troubleshooting](troubleshooting.md).
package/docs/output.md ADDED
@@ -0,0 +1,264 @@
1
+ # Snapshot output
2
+
3
+ A run writes one directory (default `./snapshot`) that is meant to be handed to a model as
4
+ context. It is self-describing: `manifest.json` indexes everything the parts contain and
5
+ explains everything they omit.
6
+
7
+ ## Layout
8
+
9
+ ```
10
+ snapshot/
11
+ ├── .codebase-snapshot ownership marker (see Safety)
12
+ ├── README.md human summary of this snapshot
13
+ ├── manifest.json machine index: files, parts, config, hashes, skips
14
+ ├── parts/
15
+ │ ├── part-001.md the code, grouped and size-budgeted
16
+ │ ├── part-002.md
17
+ │ └── …
18
+ └── reports/
19
+ ├── project-tree.md directory tree of included files
20
+ ├── navigation.md links between layers, kinds and parts
21
+ └── audit.md secrets, large files, parse diagnostics, git state
22
+ ```
23
+
24
+ With `--include-monolith` you also get `full-codebase.json` and `full-codebase.md` — the
25
+ whole repository in one file. They are off by default because they grow without bound;
26
+ `--max-monolith-bytes` skips them when they would exceed a size you set.
27
+
28
+ `full-codebase.json` embeds its own `files[]` array, and its `includedBytes` / `includedLines`
29
+ describe the monolith's raw content — the file as it was read — so they differ from the same
30
+ fields in `manifest.json` for the same file. `manifest.json` reports what the part emitted
31
+ (CRLF normalised, the trailing newline not counted); the monolith reports the content it
32
+ embedded. Compare a file's fields within one artifact, not across the two.
33
+
34
+ The manifest embedded in `full-codebase.json` is otherwise identical to `manifest.json` with one
35
+ impossible field: a file cannot record its own sha256, so `artifacts.fullJson` is `null` in the
36
+ embedded copy and a `selfReference` sentence points at `manifest.json`, which carries the real
37
+ `byteSize` and `sha256` for that file.
38
+
39
+ Every artifact can be turned off. `--skip-artifacts parts` omits the parts,
40
+ `--only-artifacts manifest` writes nothing but the manifest, and `config.artifacts` does the
41
+ same persistently. The ownership marker is always written.
42
+
43
+ ## Parts
44
+
45
+ Each `part-NNN.md` holds whole files under the byte budget (`maxPartBytes`, default 300 KB).
46
+ A file larger than the budget is not split mid-file: it is truncated and carries an omission
47
+ marker naming the budget that cut it (`... [347 lines omitted: part budget 1887 B] ...`). A file
48
+ above `largeFileBytes` is emitted as a head/tail excerpt instead, with the same marker. Each file
49
+ section is tagged with its layer, kind, language and a structural summary (imports, exports,
50
+ classes, functions) so a reader can navigate without opening every file.
51
+
52
+ Parts are packed in a stable order, so the same input produces the same content — which is
53
+ what makes `manifest.fingerprint` meaningful.
54
+
55
+ Two runs over unchanged input are **not** byte-identical everywhere, and the difference is
56
+ deliberate bookkeeping rather than content. `manifest.json`, the snapshot's `README.md`,
57
+ `reports/audit.md` and `.codebase-snapshot` differ on every run — they carry a timestamp, and the
58
+ marker also carries a fresh run id and pid. `parts/*.md`, `reports/navigation.md` and
59
+ `reports/project-tree.md` are **byte-identical** across runs, and `manifest.fingerprint` is stable.
60
+ So: diff the parts, the navigation index or the tree to see whether the *content* changed, and
61
+ diff the manifest only when you need to know that a run happened.
62
+
63
+ ## Manifest
64
+
65
+ `manifest.json` is the index and the contract between runs. Top-level keys:
66
+
67
+ | Key | Contents |
68
+ | :--- | :--- |
69
+ | `schemaVersion` | Manifest format version (`3.0.0`). |
70
+ | `generatedAt` | ISO timestamp of the run. |
71
+ | `generator` | `{ name, version }` of the tool that wrote it. |
72
+ | `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. |
73
+ | `budgets` | The effective byte limits for the run: `{ maxPartBytes, largeFileBytes }`. The per-layer overrides live at `config.budgets`, which is `{ default, byLayer }`. |
74
+ | `config` | The resolved configuration actually used (preset, artifacts, sizing, scoping). |
75
+ | `counts` | `{ totalFiles, totalSizeBytes, totalLines, partsCount, skippedCount, largeFileCount, truncatedPreviewCount }`. |
76
+ | `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. |
77
+ | `fingerprint` | Stable hash of the snapshot content. Two runs over unchanged input produce the same value. |
78
+ | `validation` | `{ status: "passed" \| "failed", errors: [] }` for the run's own integrity check. |
79
+ | `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. |
80
+ | `layers`, `kinds` | File counts per layer and per kind. |
81
+ | `indexes` | Cross-references: `routes`, `components`, `handlers`, `features`, `imports`, `directives`. |
82
+ | `parts` | One entry per part (below). |
83
+ | `files` | One entry per included file (below). |
84
+ | `skipped` | One entry per excluded file (below). |
85
+ | `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`. |
86
+
87
+ ### `parts[]`
88
+
89
+ ```json
90
+ {
91
+ "index": 1,
92
+ "filename": "part-001.md",
93
+ "byteSize": 1432,
94
+ "sha256": "3472b9edcd504403f2416606e4222f326ec207d7916bf928ade4ec7de1406d2d",
95
+ "lines": 91,
96
+ "fileCount": 2,
97
+ "layersCovered": ["root", "src"],
98
+ "includedFiles": [
99
+ { "relativePath": "package.json", "sourceBytes": 35, "includedBytes": 35, "lines": 1, "layer": "root", "kind": "data" },
100
+ { "relativePath": "src/math.ts", "sourceBytes": 63, "includedBytes": 63, "lines": 3, "layer": "src", "kind": "other" }
101
+ ]
102
+ }
103
+ ```
104
+
105
+ `byteSize` and `sha256` are what `validate` checks against the file on disk. Both are full
106
+ values — the hash is the whole 64-character digest, never elided.
107
+
108
+ ### `files[]`
109
+
110
+ ```json
111
+ {
112
+ "relativePath": "src/math.ts",
113
+ "sizeBytes": 63,
114
+ "lines": 3,
115
+ "hashSha256": "aa229260647049a42a0446f48f1b34f19657921b7a0a27a41c0b2a294a25c919",
116
+ "part": "part-001.md",
117
+ "layer": "src",
118
+ "kind": "other",
119
+ "language": "typescript",
120
+ "inclusionMode": "full",
121
+ "isLarge": false,
122
+ "isPreviewTruncated": false,
123
+ "originalLines": 3,
124
+ "includedLines": 3,
125
+ "includedBytes": 63,
126
+ "summary": {
127
+ "directives": [], "imports": [], "exports": ["add"], "reExports": [],
128
+ "functions": ["add"], "classes": [], "interfaces": [], "types": [],
129
+ "enums": [], "variables": [], "reactComponents": [], "hooks": [],
130
+ "routeHandlers": []
131
+ },
132
+ "summaryQuality": "ast"
133
+ }
134
+ ```
135
+
136
+ - `isLarge` is a fact about the file: it is bigger than `largeFileBytes` (default 200 KB).
137
+ A large file is emitted as a head/tail excerpt rather than inlined whole.
138
+ - `isPreviewTruncated` is a fact about what was written: the body in this snapshot is **not**
139
+ the whole file, because lines were dropped.
140
+ - `includedLines` counts content lines after CRLF normalisation and without a trailing empty
141
+ element, which is what the fenced block holds. An empty file therefore records `0` while the
142
+ fence shows one empty line, and a file ending in a blank line records one fewer than a reader
143
+ counts — the number describes content, not rendering.
144
+ - `inclusionMode` is `preview` exactly when `isPreviewTruncated` is `true`, and there are two
145
+ routes to it:
146
+ - **Over `largeFileBytes`** — the head/tail excerpt drops the middle, so
147
+ `isLarge: true`, `isPreviewTruncated: true`. If the head and tail happen to cover every
148
+ line, nothing is dropped and the entry stays `isLarge: true`, `inclusionMode: "full"`.
149
+ - **Under `largeFileBytes` but too big for its part** — the partitioner byte-truncates the
150
+ section to keep the part inside its budget, so `isLarge: false`,
151
+ `isPreviewTruncated: true`. This is the one that surprises people: the file is not "large"
152
+ by the configured threshold, yet its body is incomplete.
153
+ - Only `isPreviewTruncated` tells you content is missing; `isLarge` alone does not.
154
+ - `hashSha256` is the hash of the source file on disk — `validate` recomputes it to detect
155
+ drift. It is the full 64-character digest.
156
+ - `encoding` is present **only** when the file is not plain UTF-8; its absence means clean UTF-8.
157
+ `utf-16le` and `utf-16be` mean the file was transcoded to UTF-8 before it was emitted, so a
158
+ UTF-16 source file is readable in the parts instead of vanishing as "binary". `utf-8-lossy`
159
+ means the file is not valid UTF-8 at all: its undecodable byte spans became `U+FFFD`, the count
160
+ is reported in `warnings`, and the file is kept rather than dropped. The hash still covers the
161
+ raw bytes, so a lossy file validates as current — `encoding` is how you learn that the body you
162
+ are reading is not byte-exact.
163
+ - `summary` and `summaryQuality` are **conditional**. Both keys are present for a language
164
+ with an analyser and both are absent for a shapeless one — a `.gitignore`, a `.txt`. Treat a
165
+ missing `summary` as "no structural information", not as an empty one. A file larger than
166
+ 2 MB gets no summary at all: parsing it costs more memory than the file itself, and its body is
167
+ a head/tail preview at that size anyway. `manifest.warnings` names those files.
168
+ - `summary` has one of three shapes. Code files get the thirteen keys above (`directives`,
169
+ `imports`, `exports`, `reExports`, `functions`, `classes`, `interfaces`, `types`, `enums`,
170
+ `variables`, `reactComponents`, `hooks`, `routeHandlers`), plus `parseErrors` when the parser
171
+ reported syntax errors. JSON gets `{ shape, itemCount, keys, sampleKeys }` and Markdown gets
172
+ `{ headings, frontmatterKeys }` — a shape description rather than a code summary.
173
+ - `summaryOmitted: true` means the file's summary was dropped because the summary alone would
174
+ not fit the part budget. The entry then has `summaryQuality` but no `summary`, and the
175
+ file's body is still emitted in full. It is only ever present when `true`.
176
+ - `summaryQuality` is `ast` (parsed by the TypeScript compiler), `structural` (JSON or
177
+ Markdown shape) or `heuristic` (pattern-matched). It tells you how much to trust the
178
+ summary: `heuristic` means a line scan, not a parse.
179
+ - The summary block is written **as-is** into the part, so a control or bidi character that
180
+ appears in a source file can appear inside the fenced JSON too — the same reason the file
181
+ body is emitted raw. Names are escaped where a path is *displayed* (part headers, the tree,
182
+ navigation, audit, `explain`), but the two raw regions — the summary fence and the body —
183
+ reproduce the repository's bytes. Treat the whole part as untrusted data, not just the body.
184
+
185
+ Querying this array is usually faster and cheaper than re-reading the parts.
186
+
187
+ ### `skipped[]`
188
+
189
+ ```json
190
+ { "path": ".env", "reason": "sensitive-file", "detail": "credential-bearing file; enable includeSensitiveFiles to emit it" }
191
+ ```
192
+
193
+ Reasons, and what each one means:
194
+
195
+ | `reason` | The file was dropped because |
196
+ | :--- | :--- |
197
+ | `excluded` | It matched your own `excludePatterns`, or fell outside a non-empty `include` allow-list. |
198
+ | `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. |
199
+ | `sensitive-file` | Its name is credential-bearing (`.env`, `*.pem`, `.npmrc`, …). |
200
+ | `sensitive-path` | The path itself matched a secret rule. The file is skipped and the path is masked in every artifact. |
201
+ | `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. |
202
+ | `symlink` | It is a link, and links are not followed. A directory link names its target in `detail`. |
203
+ | `outside-root` | It is a link whose target resolves outside the scanned root. |
204
+ | `gitignore` | A `.gitignore` excluded it, in a scan that is not reading git's index. |
205
+ | `read-error` | It could not be read. |
206
+ | `not-file` | It is a directory, socket or FIFO. A directory that belongs to another repository is reported as `nested-repository` instead. |
207
+ | `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. |
208
+
209
+ Every path the scanner discovers is recorded — nothing is dropped silently, in either discovery
210
+ mode. Inside a git repository the candidate list comes from git, which applies `.gitignore`
211
+ before the scanner runs; the ignored paths are then listed separately and recorded here too, so
212
+ a `.gitignore`d file or directory appears in `skipped[]` with reason `gitignore` whether or not
213
+ git is present.
214
+
215
+ `explain` reports two further reasons that never appear here, because they describe a path
216
+ rather than a decision about a candidate file: `missing` (nothing exists at that path) and
217
+ `not-discovered` (the path exists but was never a candidate — inside `node_modules`, for
218
+ example). See [Troubleshooting](troubleshooting.md#a-file-is-missing-from-the-snapshot).
219
+
220
+ **Ignored trees are recorded at two granularities**, which is why `explain` can answer
221
+ `not-discovered` for a directory you can see in `skipped[]`. A *built-in* ignore
222
+ (`node_modules`, `dist`, `.next`, …) is recorded once, as the directory itself, and its contents
223
+ are never enumerated — `explain node_modules/pkg/i.js` inherits that record and says
224
+ `default-ignore`. A *preset ecosystem* ignore (`__pycache__`, `target`, `vendor`, …) is recorded
225
+ per file, so `explain app/__pycache__` — the directory — finds no record of its own and answers
226
+ `not-discovered`, while `explain app/__pycache__/mod.pyc` says `default-ignore`. Neither is an
227
+ omission: the directory and its files are all accounted for, just not at the same level.
228
+
229
+ ## Safety
230
+
231
+ - **Sensitive files are excluded by default.** `.env`, `*.pem`, `*.key`, `.npmrc`, `.netrc`,
232
+ `.git-credentials`, `id_rsa`, `.kube/config`, `*.tfstate` and similar are skipped and
233
+ listed in `skipped[]`. `--include-sensitive-files` opts in.
234
+ - **Secret-shaped strings are redacted** inside ordinary source files. Only the matched span
235
+ is masked, never the whole line, and no excerpt of a matched line is written to any
236
+ artifact.
237
+ - **The output directory is owned by the tool.** Each run writes `.codebase-snapshot` into it
238
+ first, then swaps the new snapshot in atomically. A previous snapshot survives until the
239
+ replacement is complete.
240
+ - **The tool refuses to replace a directory it did not write.** Pointing `--out-dir` at
241
+ `src/` is refused with exit `1` and the directory is left untouched — a mistyped output
242
+ path cannot delete source code. Empty directories, directories that hold nothing but
243
+ `.DS_Store`, and directories holding a complete snapshot this build wrote are accepted.
244
+ A `.gitkeep` or `.gitignore` inside the directory is not accepted, because the generator never
245
+ writes either into an output directory: a directory that holds one is somebody's data, so the
246
+ run refuses instead of deleting it.
247
+ - **Ownership is the marker, not a filename.** `.codebase-snapshot` is a JSON record carrying
248
+ this generator's name and `version`, the marker's `schemaVersion`, a `createdAt` timestamp,
249
+ a `pid`, a per-snapshot `id`, the run `state` (`staging` while it is being written, `complete`
250
+ once it is whole), the `manifestSha256` digest of the manifest it shipped, and `entries` — the
251
+ inventory of everything it wrote. `pid` is what lets a later run tell a crashed run's staging
252
+ directory and lock from a live one. A zero-byte marker, a marker that is not JSON, a marker
253
+ written by 3.0.0 (no `id`), a bare `manifest.json`, and a directory with files you added
254
+ after the run are all refused.
255
+ See [Troubleshooting](troubleshooting.md#refusing-to-replace-outdir-).
256
+
257
+ The marker also makes hook-written artifacts safe: the entries a `hooks.afterGenerate` adds to
258
+ the output are recorded in the marker, so they do not make the next run refuse the directory.
259
+
260
+ ## Committing the snapshot
261
+
262
+ Usually you do not. Add the output directory to `.gitignore` — `codebase-snapshot init` does
263
+ it for you. If you do commit snapshots (for example, to diff context across releases),
264
+ `manifest.json` is the file that matters: it is compact and `diff` can compare two of them.