@pretense/cli 0.6.9 → 0.6.10

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.
package/README.md CHANGED
@@ -1,10 +1,60 @@
1
1
  # pretense
2
2
 
3
- Local-first AI firewall. Mutates proprietary code before it leaves your laptop.
4
-
5
- Your `getUserPaymentMethod` becomes `_fn8a3c` on the wire to Claude, Codex, Cursor, or any OpenAI-compatible API. The reply gets reversed back to your real names. The LLM never sees your real names. Comments and strings stay byte-exact.
6
-
7
- Think locked garage. Your car never leaves.
3
+ Local-first AI firewall. It finds the secrets and PII in a file, swaps them for
4
+ deterministic synthetic values before anything leaves your laptop, and swaps the
5
+ real values back afterwards.
6
+
7
+ Nothing in this README is written from the spec. Every command below was run
8
+ against the published `@pretense/cli` on 2026-07-21 and its real exit code is
9
+ recorded. Where something does not work yet, this file says so instead of
10
+ pretending.
11
+
12
+ <!-- ═══════════════════════════════════════════════════════════════════════════
13
+ EXECUTABLE-DOCS REGISTRY — read by cli/e2e/docs-commands-execute.test.ts.
14
+
15
+ That test extracts every command in every fenced block below, runs it
16
+ against the built CLI in a temp dir with an isolated HOME, and asserts the
17
+ exit code. Default expectation is 0; a `console` block that already prints
18
+ `$ echo $?` needs nothing here, because the doc's own number is lifted out
19
+ and asserted.
20
+
21
+ Two markers, and every one of them requires a reason. (The two lines below
22
+ use "->" where a real marker uses an em dash, so that these examples are
23
+ not themselves parsed as markers.)
24
+ docs-exec:skip "<command>" -> why it cannot run in CI
25
+ docs-exec:expect "<command>" N -> the exit code it really returns
26
+
27
+ A marker that matches no command in this file FAILS the test, so this list
28
+ cannot rot: delete a command from the docs and its marker starts shouting.
29
+ Add a command and you must run it — there is no third option.
30
+
31
+ ── Cannot run here ─────────────────────────────────────────────────────
32
+ docs-exec:skip "npm i -g @pretense/cli" — mutates the developer's global npm prefix
33
+ docs-exec:skip "npm i -g @pretense/cli@0.6.9" — mutates the developer's global npm prefix
34
+ docs-exec:skip "npm i -g @pretense/cli --version 0.6.9" — documented anti-example; npm prints its own version and installs nothing
35
+ docs-exec:skip "curl -fsSL https://www.pretense.ai/install.sh | sh" — downloads and installs a binary from the network
36
+ docs-exec:skip "pretense start" — long-running server; it would outlive the suite holding port 9339
37
+ docs-exec:skip "pretense start --port 9500" — long-running server; it would outlive the suite holding port 9500
38
+ docs-exec:skip "curl -s http://localhost:9339/health" — needs the proxy from the preceding line, which cannot be started here
39
+ docs-exec:skip "claude" — third-party CLI that is not a dependency of this repo
40
+ docs-exec:skip `curl -fsSLO "${BASE}/SHA256SUMS"` — network fetch from the release host
41
+ docs-exec:skip `curl -fsSLO "${BASE}/SHA256SUMS.cosign.bundle"` — network fetch from the release host
42
+ docs-exec:skip `curl -fsSLO "${BASE}/${TARGET}"` — network fetch of a large release binary
43
+ docs-exec:skip `cosign verify-blob \` — needs the cosign binary and the artifacts the skipped curls would fetch
44
+ docs-exec:skip `grep " ${TARGET}$" SHA256SUMS | shasum -a 256 -c -` — needs SHA256SUMS from the skipped network fetch
45
+ docs-exec:skip "pretense scan ci" — needs a git working tree with an untracked file; the session dir is deliberately not a git repo
46
+ docs-exec:skip "git add untracked.ts && pretense scan ci" — stages a file in a git repo; the session dir is deliberately not a git repo
47
+
48
+ ── Runs, and does not exit 0 ───────────────────────────────────────────
49
+ docs-exec:expect "pretense scan <file|dir>" 1 — findings present in the fixture
50
+ docs-exec:expect "pretense scan <file> --policy hipaa" 1 — findings present in the fixture
51
+ docs-exec:expect "pretense scan <file> --policy <preset>" 1 — findings present in the fixture
52
+ docs-exec:expect "pretense scan <file> --severity high" 1 — the fixture holds a critical finding
53
+ docs-exec:expect "pretense scan s.ts" 1 — findings present in the fixture
54
+ docs-exec:expect "pretense scan s.ts --level 1" 1 — L1 finds the fixture secrets
55
+ docs-exec:expect "pretense scan s.ts --level 9" 3 — invalid level is a usage error (D-02: plain scan usage errors exit 3, distinct from findings=1)
56
+ docs-exec:expect "pretense scan ci src/" 2 — the session fixture dir holds secrets; 2 is the documented findings code
57
+ ═══════════════════════════════════════════════════════════════════════ -->
8
58
 
9
59
  ## Install
10
60
 
@@ -12,24 +62,169 @@ Think locked garage. Your car never leaves.
12
62
  npm i -g @pretense/cli
13
63
  ```
14
64
 
15
- Or with pnpm:
65
+ Verify the install with the only version string in the product that is read from
66
+ the package itself:
67
+
68
+ ```bash
69
+ pretense --version
70
+ ```
71
+
72
+ `pnpm add -g @pretense/cli` also installs and runs `scan`, `mutate` and
73
+ `reverse`. It does **not** currently give you a working `pretense start` — pnpm
74
+ blocks the `better-sqlite3` build script by default and the proxy's audit store
75
+ cannot load its native addon. Use npm if you want the proxy.
76
+
77
+ There is also a standalone binary that needs no Node at all:
78
+
79
+ ```bash
80
+ curl -fsSL https://www.pretense.ai/install.sh | sh
81
+ ```
82
+
83
+ Use `-fsSL` and the `www` host exactly as written. The apex domain answers with
84
+ a redirect; without `-L` curl prints the redirect page and your shell tries to
85
+ execute the word `Redirecting...`.
86
+
87
+ The binary runs `scan`, `mutate` and `reverse`. `pretense start` does **not**
88
+ work on it today — see [The proxy](#the-proxy).
89
+
90
+ To pin a version, attach it to the package name:
16
91
 
17
92
  ```bash
18
- pnpm add -g @pretense/cli
93
+ npm i -g @pretense/cli@0.6.9 # correct
94
+ npm i -g @pretense/cli --version 0.6.9 # WRONG — npm reads --version as its own flag
95
+ ```
96
+
97
+ The second form exits 0, prints npm's own version, and installs nothing.
98
+
99
+ Requires Node.js 20 or newer (`engines.node` is `>=20.0.0`). The standalone
100
+ binary has no Node requirement.
101
+
102
+ ## Three verbs and one flag
103
+
104
+ ```bash
105
+ pretense scan <file|dir> # what would leak
106
+ pretense mutate <file> --map m.json -i # swap it before the AI sees it
107
+ pretense reverse <file> --map m.json -i # put the real values back
108
+ pretense start # proxy mode (npm install only, see below)
109
+ pretense scan <file> --policy hipaa # the flag most people type
110
+ ```
111
+
112
+ Two honest caveats about the verbs:
113
+
114
+ - `mutate` and `reverse` take **one file at a time**. `pretense mutate .` and
115
+ `pretense mutate src` both fail with `Cannot read file: .` and exit 1.
116
+ Directory mutation is not available yet. `scan` does accept a directory.
117
+ - `mutate` **previews by default** — it prints to stdout and leaves the file
118
+ untouched. `-i` writes in place. `-i` without `--map` is irreversible; there
119
+ is no global map store to recover from.
120
+
121
+ ## The 30-second walkthrough
122
+
123
+ This is a complete, copy-pasteable session with synthetic values. The output
124
+ below is verbatim from a real run; only the mutated values will differ on your
125
+ machine (they are salted per install).
126
+
127
+ ```bash
128
+ mkdir demo && cd demo && mkdir src
129
+ cat > src/config.ts <<'EOF'
130
+ export const DB_URL = "postgres://placeholder_user:placeholder_pw@db.example.invalid:5432/appdb";
131
+ export const AWS_KEY = "AKIAEXAMPLEEXAMPLE00";
132
+ export const CONTACT = "nobody@example.invalid";
133
+ EOF
134
+ ```
135
+
136
+ **1. Record the original bytes.**
137
+
138
+ ```console
139
+ $ shasum -a 256 src/config.ts
140
+ 8cbe81bb0608b0e8ed23fa9dea1f4d0e89921abb857612d7182e11db0c448258 src/config.ts
141
+ ```
142
+
143
+ **2. See what would leak.**
144
+
145
+ ```console
146
+ $ pretense scan src/config.ts
147
+
148
+ Pretense Scan — src/config.ts
149
+
150
+ Patterns: 98 | Duration: 46.19ms
151
+
152
+ 🔴 database-url block post...ppdb
153
+ 🔴 aws-access-key block AKIAEX...LE00
154
+ 🟡 email warn n***@example.invalid
155
+
156
+ Total: 3 findings | 2 blocked, 0 redacted
157
+
158
+ $ echo $?
159
+ 1
19
160
  ```
20
161
 
21
- Both install the `pretense` command the package name is scoped, the binary is not.
162
+ Exit 1 is expectedsee [Exit codes](#exit-codes).
163
+
164
+ **3. Swap the secrets, keeping a reversal map.**
165
+
166
+ ```console
167
+ $ pretense mutate src/config.ts --map map.json -i
168
+ ✓ Wrote encrypted reversal map → map.json (AES-256-GCM, mode 0600)
169
+ ✓ Mutated 3 secrets in src/config.ts (in-place)
170
+
171
+ $ cat src/config.ts
172
+ export const DB_URL = "postgres://pretense_mut_u2ovo8wiyqa:pretense_mut_pvck8pnmq1g@db.example.invalid:5432/appdb";
173
+ export const AWS_KEY = "AKIA3ff0p1g3d9vdf2n0";
174
+ export const CONTACT = "1llwxqz4wi36bdpgffjt6j";
175
+ ```
22
176
 
23
- Requires Node.js 20 or newer.
177
+ Structured credentials keep their shape — the Postgres URL is still a parseable
178
+ Postgres URL, the AWS key still carries its `AKIA` prefix — so the file stays
179
+ useful as context for a model. Shape preservation is not universal: the email
180
+ address above came back as a bare token, not a synthetic email.
24
181
 
25
- ## Verifying a release
182
+ Note what did **not** change: `DB_URL`, `AWS_KEY`, `CONTACT`. Pretense mutates
183
+ secret and PII *values*, not identifiers.
26
184
 
27
- The released binaries are signed with Sigstore cosign (keyless no key to
28
- fetch or leak). Each release ships a `SHA256SUMS` file plus a
29
- `SHA256SUMS.cosign.bundle` signature covering it. To verify a download:
185
+ **4. Put the real values back and prove it was exact.**
186
+
187
+ ```console
188
+ $ pretense reverse src/config.ts --map map.json -i
189
+ ✓ Restored .../src/config.ts (3 tokens, in-place)
190
+
191
+ $ shasum -a 256 src/config.ts
192
+ 8cbe81bb0608b0e8ed23fa9dea1f4d0e89921abb857612d7182e11db0c448258 src/config.ts
193
+ ```
194
+
195
+ Byte-identical to step 1.
196
+
197
+ ## Verify it yourself
198
+
199
+ Four checks a skeptical reviewer can run in a couple of minutes. Each one can
200
+ fail, which is the point.
201
+
202
+ **1. The round trip is byte-exact, not approximately exact.**
30
203
 
31
204
  ```bash
32
- VERSION=0.6.8
205
+ B=$(shasum -a 256 src/config.ts | cut -d' ' -f1)
206
+ pretense mutate src/config.ts --map map.json -i
207
+ pretense reverse src/config.ts --map map.json -i
208
+ A=$(shasum -a 256 src/config.ts | cut -d' ' -f1)
209
+ [ "$A" = "$B" ] && echo IDENTICAL || echo DIFFER
210
+ ```
211
+
212
+ **2. The reversal map holds no plaintext.** Grep it for a secret you put in.
213
+ An exit code of 1 from `grep` means not found, which is what you want.
214
+
215
+ ```console
216
+ $ grep -c placeholder_pw map.json
217
+ 0
218
+ $ echo $?
219
+ 1
220
+ $ ls -l map.json
221
+ -rw------- 1 you staff 1215 map.json
222
+ ```
223
+
224
+ **3. The release binaries are signed, and the signature can be broken.**
225
+
226
+ ```bash
227
+ VERSION="$(pretense --version)"
33
228
  TARGET=pretense-darwin-arm64
34
229
  BASE="https://www.pretense.ai/releases/${VERSION}"
35
230
 
@@ -42,91 +237,374 @@ cosign verify-blob \
42
237
  --certificate-identity "https://github.com/pretense-a1/pretense/.github/workflows/release.yml@refs/tags/v${VERSION}" \
43
238
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
44
239
  SHA256SUMS
240
+ # Verified OK
45
241
 
46
- grep " ${TARGET}$" SHA256SUMS | sha256sum -c - # shasum -a 256 -c - on macOS
242
+ grep " ${TARGET}$" SHA256SUMS | shasum -a 256 -c - # sha256sum -c - on Linux
243
+ # pretense-darwin-arm64: OK
47
244
  ```
48
245
 
49
- Verify the signature over `SHA256SUMS` *before* trusting any hash inside it
50
- checking a binary against an unverified checksum file proves nothing.
246
+ Verify the signature over `SHA256SUMS` *before* trusting any hash inside it.
247
+ Then confirm the check is real: append a byte to `SHA256SUMS` and re-run
248
+ `cosign verify-blob` — it must fail with `invalid signature`.
249
+
250
+ Take `VERSION` from `pretense --version`, not from this file. Signature bundles
251
+ are not published for every historical version.
51
252
 
52
- Full details, including the regexp form for automation and why npm provenance
53
- is not used, are in [docs/verifying-releases.md](../docs/verifying-releases.md).
253
+ Full details, including the regexp form for automation and why npm provenance is
254
+ not used, are in [docs/verifying-releases.md](../docs/verifying-releases.md).
54
255
 
55
- ## First 5 minutes
256
+ ## The proxy
56
257
 
57
- 1. Initialize Pretense in your repo.
258
+ `pretense start` runs a local HTTP proxy that applies the same mutation engine to
259
+ LLM API traffic.
58
260
 
59
261
  ```bash
60
- cd your-project
61
- pretense init
262
+ pretense start # listens on port 9339
263
+ pretense start --port 9500 # any other port
264
+ curl -s http://localhost:9339/health
62
265
  ```
63
266
 
64
- 2. Scan for secrets and identifiers.
267
+ Then point your tool at it:
65
268
 
66
269
  ```bash
67
- pretense scan src
270
+ export ANTHROPIC_BASE_URL=http://localhost:9339
271
+ claude
68
272
  ```
69
273
 
70
- 3. Start the proxy on port 9339.
71
-
72
- ```bash
73
- pretense start
274
+ `OPENAI_BASE_URL` works the same way, as does any client that accepts a base URL.
275
+
276
+ Read this before you rely on the proxy:
277
+
278
+ - **npm install only.** `start` crashes on the `curl | sh` binary
279
+ (`Could not find module root ...`, a native-addon resolution failure inside the
280
+ compiled runtime) and on a `pnpm add -g` install (build scripts blocked). A fix
281
+ for the binary is in progress. `npm i -g @pretense/cli` is the only path where
282
+ `start` runs today.
283
+ - **Upgrading from an older CLI can crash it.** If `~/.pretense/audit.db` was
284
+ created by an earlier version, `start` exits with
285
+ `SqliteError: table audit_log has no column named detector_kinds`. There is no
286
+ schema migration yet. This is a bug, not a configuration problem.
287
+ - **The default is to mutate.** The generated `pretense.yaml` sets
288
+ `blockMode: mutate`: a request containing a secret is forwarded with the
289
+ secret replaced by a reversible token, so the upstream never sees plaintext.
290
+ Set `blockMode: reject` if you want the request refused with HTTP 400
291
+ instead — that is strict mode, for orgs where a secret must never leave the
292
+ perimeter even tokenized.
293
+ - **`proxy.port` in `pretense.yaml` is not honoured.** Only `--port` changes the
294
+ port. `pretense status` also probes 9339 unconditionally, so it reports
295
+ "Proxy not running" whenever you started on another port.
296
+ - **It listens on every interface, not just loopback.** `lsof` shows the
297
+ listener bound to `*:9339`, so anything that can reach your machine can reach
298
+ the proxy. Do not run it on an untrusted network.
299
+ - **There is no dashboard.** Up to 0.6.9 the start banner advertised
300
+ `Dashboard: http://localhost:9340` and `pretense init` wrote a `dashboardPort`
301
+ key into `pretense.yaml`. There is no dashboard process at all and
302
+ nothing listens on 9340, so the banner line, the config key and the
303
+ `--dashboard-port` flag were all removed. An existing `pretense.yaml` that
304
+ still sets `dashboardPort` keeps loading; the key is ignored.
305
+ - **The banner's version used to be wrong; it is not any more.** It printed a
306
+ hardcoded `v0.2.0` for eight releases. Every version this CLI emits —
307
+ `--version`, the `version` command, the start banner, `/health`, the
308
+ `x-pretense-version` header, SARIF `tool.driver.version` and the npm
309
+ postinstall notice — is now injected at build time from `cli/package.json`
310
+ and asserted equal by the test suite, in the compiled binary as well as the
311
+ npm bundle.
312
+
313
+ ## Exit codes
314
+
315
+ Measured, not aspirational.
316
+
317
+ | Command | Exit | Meaning |
318
+ | --- | --- | --- |
319
+ | `pretense scan <path>` | 0 | no findings |
320
+ | `pretense scan <path>` | 1 | findings present |
321
+ | `pretense scan <path>` | 3 | usage error — unknown flag, invalid `--level`, missing option value. Kept distinct from findings (`1`) so a CI gate can tell a typo from a leak |
322
+ | `pretense scan ci` | 0 | no findings *(see caveat)* |
323
+ | `pretense scan ci` | 2 | findings present |
324
+ | `pretense scan ci` | 1 | the scan could not run — not a git repository, git unavailable, or the given path does not exist |
325
+ | `pretense scan ci` | 1 | usage error — its findings code is `2`, so a bad flag stays on `1` and is already distinct from a leak |
326
+ | `pretense --version`, `init`, `status`, `audit`, `policy`, `credits` | 0 | success |
327
+ | `pretense mutate`, `pretense reverse` | 0 | success |
328
+ | anything else — unknown command, unreadable file, integrity failure, or a bad flag on any command other than plain `scan` | 1 | failure |
329
+
330
+ In one sentence, because the tests cite this line:
331
+ `pretense scan ci` returns `0` when clean and `2` when findings are present.
332
+ <!-- VERIFIED 2026-07-21 against the built binary: in a git repo with a
333
+ committed Stripe key, `pretense scan ci` exited 2; after removing and
334
+ committing, it exited 0. cli/e2e/exit-code-contract.test.ts asserts this
335
+ exact sentence is present — do not reword it without updating that test. -->
336
+
337
+ Because a clean `scan` is 0 and a dirty `scan` is 1, `pretense scan . && echo ok`
338
+ never prints `ok` when there is anything to find. That is intentional; use it as
339
+ your gate.
340
+
341
+ **Caveat on `pretense scan ci`:** with no path argument it emits SARIF over your
342
+ **git-tracked** content (`git ls-files`). Files you have `git add`ed — **staged,
343
+ even if not yet committed** — ARE scanned; the hole is files git has never been
344
+ told about. An **untracked** file (say `untracked.ts`, never `git add`ed) is
345
+ invisible to `git ls-files`, so `scan ci` reports zero results and exits 0 while
346
+ its secret sits on disk:
347
+
348
+ ```console
349
+ $ pretense scan ci # untracked.ts is not git-tracked, so it is not seen
350
+ $ echo $?
351
+ 0 # 0 results in the SARIF — and the secret is still there
352
+
353
+ $ git add untracked.ts && pretense scan ci
354
+ $ echo $?
355
+ 2 # now tracked (staging is enough) — the secret is caught
74
356
  ```
75
357
 
76
- 4. Point your AI tool at the proxy. Example for Claude Code:
358
+ Staged files are not the hole; untracked files are. To gate the whole working
359
+ tree regardless of git state, scan a path directly — `pretense scan .` walks
360
+ everything on disk, and `pretense scan ci <path>` needs no git at all.
77
361
 
78
- ```bash
79
- export ANTHROPIC_BASE_URL=http://localhost:9339
80
- claude
362
+ **`scan ci` fails CLOSED.** It used to swallow every git failure and report an
363
+ empty SARIF with exit 0, so a run in a directory that was not a git repository —
364
+ a shallow CI checkout, a missing `.git`, git not on `PATH` — marked the gate
365
+ green over a tree it had never read. It now exits `1` with a named message and
366
+ scans nothing silently. Exit `0` from `scan ci` means git answered and the
367
+ answer was clean; it never means "the scan did not happen".
368
+
369
+ `scan ci` also accepts an explicit path, which needs no git at all and is the
370
+ way to gate a non-repository build:
371
+
372
+ ```console
373
+ $ pretense scan ci src/
81
374
  ```
82
375
 
83
- 5. Check what got mutated.
376
+ ## Command reference
377
+
378
+ Every line below was executed; the exit code is the one observed.
84
379
 
85
380
  ```bash
86
- pretense audit --limit 20
381
+ pretense --version # 0 — the authoritative version
382
+ pretense init # 0 — writes pretense.yaml + .pretense/
383
+ pretense scan <file|dir> # 0 clean / 1 findings
384
+ pretense scan <file> --policy <preset> # 0 clean / 1 findings
385
+ pretense scan <file> --severity high # 0 clean / 1 findings
386
+ pretense mutate <file> # 0 — preview to stdout, file untouched
387
+ pretense mutate <file> --map m.json -i # 0 — in place, reversible
388
+ pretense reverse <file> --map m.json -i # 0 — byte-exact restore
389
+ pretense status # 0
390
+ pretense audit --limit 20 # 0
391
+ pretense audit --json # 0 — JSONL; empty state emits 0 bytes
392
+ pretense audit --list-frameworks # 0
393
+ pretense policy list # 0
394
+ pretense policy show <id> # 0
395
+ pretense credits # 0
396
+ pretense start # runs until Ctrl-C; see The proxy
87
397
  ```
88
398
 
89
- That is it. Your code is mutated outbound and reversed inbound.
399
+ Run `pretense <command> --help` for the full flag list.
400
+
401
+ One known rough edge: `pretense init` prints `✓ Added .pretense/ to .gitignore`
402
+ even when there is no `.gitignore` to add it to, in which case it creates
403
+ nothing. Check for the entry yourself, or use `pretense quickstart`, which
404
+ creates the file correctly.
405
+
406
+ `--level` is a real flag again — see [Detection levels](#detection-levels-l1-and-l2).
407
+ It is a debugging filter, not something most people need to type.
408
+
409
+ Not documented here because it does not work: `pretense mutate <directory>`, the
410
+ port-9340 dashboard, and `pretense-mcp` (no such binary ships).
411
+
412
+ ### `pretense login` verifies the key
413
+
414
+ `pretense login --key <key>` (and `pretense auth login`) checks the key against
415
+ `POST /api/cli/auth/validate` before saying anything about it. It reports one of
416
+ three outcomes, and never conflates them:
417
+
418
+ | Outcome | Message | Exit | Stored? |
419
+ | --- | --- | --- | --- |
420
+ | API accepted the key | `Authenticated. The key was verified by the Pretense API.` | 0 | yes, with `verified: true` |
421
+ | API rejected the key | `The Pretense API REJECTED this key. It will not work.` | 1 | yes, marked rejected — unless a verified key already exists, which is kept |
422
+ | API unreachable, or `--offline` | `Key saved — NOT verified.` + the reason | 0 | yes, with `verified: false` |
423
+
424
+ Up to 0.6.9 it printed `Authenticated successfully.` for *any* correctly
425
+ prefixed string, with no server call at all.
426
+
427
+ The key is written in **plaintext** to `~/.pretense/credentials.json` at mode
428
+ `0600`, inside a `~/.pretense` directory at mode `0700`. It is not encrypted:
429
+ anything running as your user can read it. `pretense auth status` says whether
430
+ the stored key was ever verified, and `pretense logout` deletes the file.
431
+
432
+ Point it at a different backend with `PRETENSE_API_URL`; raise the 5s
433
+ verification timeout with `PRETENSE_API_TIMEOUT_MS`.
434
+
435
+ ## What actually gets mutated
436
+
437
+ Secret and PII **values**: API keys, database URLs, tokens, email addresses, and
438
+ the rest of the 98 shipped detectors. Substitutions are deterministic for a given
439
+ install: the same input produces the same synthetic value every time, which is
440
+ what makes `reverse` possible. The salt is per-install, so two machines produce
441
+ different synthetics for the same secret.
442
+
443
+ Identifiers — function, class and variable names — are **not** mutated. Older
444
+ docs claimed `getUserPaymentMethod` would appear as `_fn8a3c` on the wire. It
445
+ does not; it goes over the wire unchanged. Identifier-level mutation is on the
446
+ roadmap and is not in this release.
447
+
448
+ `reverse` is a strict, byte-exact restore. It verifies a SHA-256 of the whole
449
+ file and refuses to write if anything changed after mutation — so a file an LLM
450
+ has edited will not reverse today. Token-substitution reversal is on the roadmap.
451
+
452
+ ## Where the reversal map lives
453
+
454
+ In the CLI path, a map exists only where you ask for one, via
455
+ `pretense mutate --map <path>`. It is AES-256-GCM encrypted and written mode
456
+ `0600`.
457
+
458
+ In the proxy path the reverse map is held in memory for the life of the request
459
+ and is never written to disk — deliberately, so Pretense never becomes an
460
+ at-rest store of your secrets.
461
+
462
+ Note on the encryption: the AES key sits next to the map at `~/.pretense/keys/`,
463
+ same user, no passphrase or OS keychain. That defeats a stolen, backed-up, or
464
+ accidentally committed map file. It does **not** defeat another process running
465
+ as you. We do not claim "encrypted at rest" to security reviewers, and neither
466
+ should you.
467
+
468
+ ## Compliance presets
469
+
470
+ `pretense policy list` and `pretense policy show <id>` cover **5** built-in
471
+ presets: `hipaa`, `gdpr`, `soc2`, `nist`, `pci`.
472
+
473
+ `scan --policy <preset>` re-weights detector severity for that framework. It does
474
+ **not** narrow the scan — you still see every finding, including ones outside
475
+ that framework's scope. Measured on the walkthrough fixture: `scan` reports the
476
+ email finding as `warn`, `scan --policy hipaa` reports the same finding as
477
+ `redact`. Same 3 findings, same 98 patterns, different actions.
478
+ <!-- Measured 2026-07-21 against @pretense/cli@0.6.9. A preset only produces
479
+ actionOverrides (cli/src/compliance-preset.ts). `mutate --policy` does not
480
+ exist. Separately, `audit --framework` is backed by a DIFFERENT 36-name
481
+ registry — the two lists are disjoint, so never quote 36 here. Per the
482
+ compliance baseline, never quote CMMC_L2 anywhere. -->
483
+
484
+ `pretense audit --framework <name>` maps findings against a separate, larger set
485
+ of **36** compliance frameworks vendored from the `pretense-compliance-standards`
486
+ dataset (`pretense audit --list-frameworks` prints them). The two lists are not
487
+ the same thing and should never be conflated.
90
488
 
91
489
  ## Supported languages
92
490
 
93
491
  TypeScript, JavaScript, Python, Go, Java, C#, Ruby, Rust.
94
492
 
95
- ## Supported AI tools
493
+ ## Detection levels (L1 and L2)
96
494
 
97
- Anything that talks to an OpenAI-compatible API. Claude Code, Codex CLI, Cursor, Copilot CLI, Continue, Aider, custom scripts. Set the base URL to `http://localhost:9339` and you are done.
495
+ <!-- OBSERVED, not specified. Every output block below was produced on
496
+ 2026-07-21 by installing the packed tarball (npm pack -> npm install into
497
+ an empty dir with an isolated HOME) and running the commands shown.
498
+ The "installed user" claim is proved by: cli/package.json#files is
499
+ ["dist/","README.md","LICENSE"] (no *.node), @pretense/scanner-rs is a
500
+ devDependency and is `"private": true`, and `tar -tzf` on the packed
501
+ tarball returns ZERO .node files. -->
98
502
 
99
- ## Common commands
503
+ Pretense has two detection passes:
504
+
505
+ | Level | How it works | What it finds |
506
+ | --- | --- | --- |
507
+ | **L1** | regex over raw bytes | flat secrets and PII — 98 patterns |
508
+ | **L2** | tree-sitter AST + data-flow | credentials regex cannot see, e.g. an opaque literal passed to `client.setApiKey("...")` |
509
+
510
+ Both run by default and every row is labelled with the level that found it.
511
+ L2 is strictly **additive**: L1 runs first and locks its spans, so a value that
512
+ is both a secret and a code identifier is reported once, by L1, with L1's
513
+ severity. A merged scan can never report fewer findings than L1 alone.
514
+
515
+ ### L2 is not available if you installed from npm
516
+
517
+ This is the part that matters, and it is the honest answer: **L2 needs a native
518
+ Rust addon that is not published.** It is not in the tarball, so on a normal
519
+ `npm i -g @pretense/cli` it never loads. You get L1 in full, and the CLI tells
520
+ you so rather than quietly reporting less:
100
521
 
101
- ```bash
102
- pretense init Initialize .pretense/ in current dir
103
- pretense scan <file|dir> Scan for secrets and identifiers
104
- pretense mutate <file> Preview a mutation
105
- pretense reverse <file> Reverse a mutation using the saved map
106
- pretense start Start the proxy on port 9339
107
- pretense status Show plan, quota, seat usage
108
- pretense audit View the audit log
109
- pretense --version Print the CLI version
110
522
  ```
523
+ $ pretense scan s.ts
111
524
 
112
- Run `pretense <command> --help` for command-specific help.
525
+ Pretense Scan s.ts
113
526
 
114
- ## How it works
527
+ Patterns: 98 | Levels: L1 (L2 unavailable) | Duration: 16.25ms
115
528
 
116
- Pretense runs a tiny proxy on `localhost:9339`. Outbound requests to LLM APIs get parsed. Identifiers (variables, functions, classes) get hashed to deterministic stand-ins like `_v8a3c`, `_fn8a3c`, `_cls8a3c`. Comments and string literals are left alone. The LLM response gets passed through a reverse map so your editor sees real names again.
529
+ ! L2 AST core unavailable (native @pretense/scanner-rs addon not installed for this platform)
530
+ L1 (regex) ran in full; no finding was dropped.
117
531
 
118
- No code touches our servers. Your mutation map lives in `.pretense/` in your repo. Same as `.git/`.
532
+ 🔴 L1 stripe-secret-key block L1:17 len=32
119
533
 
120
- ## Plan limits
534
+ Total: 1 findings | 1 blocked, 0 redacted
535
+ ```
121
536
 
122
- Free: 1,000 mutations per 7-day rolling window.
123
- Pro: 100,000 mutations per 7-day rolling window. $23 per seat per month billed
124
- annually ($276/seat/year), or $29 per seat billed monthly.
125
- Enterprise: unlimited. $79 per seat per month billed annually ($948/seat/year),
126
- or $99 per seat billed monthly. Contact sales for SSO, audit export, custom rules.
537
+ Exit code 1, as with any scan that finds something. `npm install` never needs a
538
+ Rust toolchain, and a missing addon is never an error it degrades to L1 and
539
+ says the word "unavailable" out loud. The same is true of the `curl | sh`
540
+ binary and of any language for which no grammar is compiled in: L1 always runs
541
+ in full, and a parse failure is never reported as "clean".
542
+
543
+ Shipping L2 to installed users needs prebuilt binaries for darwin-arm64,
544
+ darwin-x64, linux-arm64, linux-x64 and win32-x64, or a WASM build. That work is
545
+ not done, so no released version of this CLI performs L2 detection.
546
+
547
+ ### `--level`, for debugging
548
+
549
+ If you have built the addon from source, `--level` filters which pass reports.
550
+ It is a debugging aid — the default (both levels) is what you want:
551
+
552
+ ```bash
553
+ pretense scan s.ts --level 1 # L1 rows only
554
+ pretense scan s.ts --level 2 # L2 rows only
555
+ ```
556
+
557
+ Anything other than `1` or `2` is a usage error and exits `3` (see [Exit codes](#exit-codes) — plain `scan` usage errors are `3`, kept distinct from findings `1`):
558
+
559
+ ```
560
+ $ pretense scan s.ts --level 9
561
+ error: option '--level <n>' argument '9' is invalid. Detection level must be 1 (regex) or 2 (AST).
562
+ $ echo $?
563
+ 3
564
+ ```
565
+
566
+ One rough edge, recorded rather than hidden: on a build with no addon,
567
+ `--level 2` prints the "L2 unavailable" notice and then `✓ No secrets or PII
568
+ detected` with exit 0 — it filtered to a pass that could not run. The notice is
569
+ printed, so it is not silent, but the checkmark is misleading. Use the default
570
+ or `--level 1`.
571
+
572
+ ## Plan limits
127
573
 
128
- Check what you have left with `pretense credits` (alias: `pretense tokens`), see the
129
- full usage table with `pretense usage`, and compare plans with `pretense upgrade`.
574
+ <!-- NUMBERS AND THEIR SOURCE do not edit without re-reading these.
575
+ Seats: packages/billing/src/plan-limits.ts PLAN_CONFIGS (free.maxSeats 3,
576
+ pro.maxSeats 100) and the backend PLAN_SEAT_LIMITS
577
+ (FREE 1-3, PRO 1-100, ENTERPRISE 25-100, hard cap MAX_ORG_SEATS
578
+ 100, enforced by assertSeatsValidForPlan — it throws above 100).
579
+ Counts: PLAN_CONFIGS.mutationsPerPeriod — free 1000, pro 100_000,
580
+ enterprise null (unlimited), over a ROLLING 7-DAY period.
581
+ Prices: PLAN_CONFIGS.pricePerSeatCents — free 0, pro 2900,
582
+ enterprise null (no self-serve price; contact sales).
583
+ pricePerSeatCents is the MONTHLY rate; annual is 20% off and
584
+ pricing-consistency.test.ts is the spec.
585
+ RESET WINDOW: a rolling 7 days on every plan (founder-confirmed
586
+ 2026-07-21). apps/web (api/cli/log/route.ts PERIOD_DAYS = 7), the CLI
587
+ (cli/src/plan-usage.ts QUOTA_PERIOD_DAYS = 7) and the backend all meter
588
+ the same window. Do not describe it as a calendar month. -->
589
+
590
+ Free: $0. 1,000 code protections per 7 days, up to 3 seats.
591
+ Pro: $29 per seat per month, or $23 per seat per month billed annually
592
+ ($276/seat/year). 100,000 code protections per 7 days, 1 to 100 seats.
593
+ Enterprise: custom pricing — contact sales. Unlimited code protections,
594
+ 25 to 100 seats. SSO/SAML, SIEM and on-prem are gated to this tier and are
595
+ marked coming soon.
596
+ <!-- Enterprise IS custom-priced (founder-confirmed 2026-07-21). Do not print an
597
+ Enterprise rate. Do not write "5 to 100,000 seats" for either paid tier;
598
+ the enforced cap is 100 and assertSeatsValidForPlan throws above it.
599
+ Do not write "SOC 2 exports" — no such export command exists; the only
600
+ export the CLI has is JSONL via `pretense audit --json`.
601
+ Log retention is deliberately not listed: five mutually contradictory
602
+ values exist across the codebase and NONE of them are enforced (no prune
603
+ job, no clamp on the query window). Add it back when C9 ships. -->
604
+
605
+ Check what you have left with `pretense credits` (alias: `pretense tokens`), see
606
+ the full usage table with `pretense usage`, and compare plans with
607
+ `pretense upgrade`.
130
608
 
131
609
  ## License
132
610