@pretense/cli 0.6.8 → 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,91 +62,549 @@ 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:
91
+
92
+ ```bash
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).
16
126
 
17
127
  ```bash
18
- pnpm add -g @pretense/cli
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
19
134
  ```
20
135
 
21
- Both install the `pretense` command — the package name is scoped, the binary is not.
136
+ **1. Record the original bytes.**
22
137
 
23
- Requires Node.js 20 or newer.
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
160
+ ```
161
+
162
+ Exit 1 is expected — see [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
+ ```
176
+
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.
181
+
182
+ Note what did **not** change: `DB_URL`, `AWS_KEY`, `CONTACT`. Pretense mutates
183
+ secret and PII *values*, not identifiers.
184
+
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
+ ```
24
194
 
25
- ## First 5 minutes
195
+ Byte-identical to step 1.
26
196
 
27
- 1. Initialize Pretense in your repo.
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.**
28
203
 
29
204
  ```bash
30
- cd your-project
31
- pretense init
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
32
222
  ```
33
223
 
34
- 2. Scan for secrets and identifiers.
224
+ **3. The release binaries are signed, and the signature can be broken.**
35
225
 
36
226
  ```bash
37
- pretense scan src
227
+ VERSION="$(pretense --version)"
228
+ TARGET=pretense-darwin-arm64
229
+ BASE="https://www.pretense.ai/releases/${VERSION}"
230
+
231
+ curl -fsSLO "${BASE}/SHA256SUMS"
232
+ curl -fsSLO "${BASE}/SHA256SUMS.cosign.bundle"
233
+ curl -fsSLO "${BASE}/${TARGET}"
234
+
235
+ cosign verify-blob \
236
+ --bundle SHA256SUMS.cosign.bundle \
237
+ --certificate-identity "https://github.com/pretense-a1/pretense/.github/workflows/release.yml@refs/tags/v${VERSION}" \
238
+ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
239
+ SHA256SUMS
240
+ # Verified OK
241
+
242
+ grep " ${TARGET}$" SHA256SUMS | shasum -a 256 -c - # sha256sum -c - on Linux
243
+ # pretense-darwin-arm64: OK
38
244
  ```
39
245
 
40
- 3. Start the proxy on port 9339.
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.
252
+
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).
255
+
256
+ ## The proxy
257
+
258
+ `pretense start` runs a local HTTP proxy that applies the same mutation engine to
259
+ LLM API traffic.
41
260
 
42
261
  ```bash
43
- pretense start
262
+ pretense start # listens on port 9339
263
+ pretense start --port 9500 # any other port
264
+ curl -s http://localhost:9339/health
44
265
  ```
45
266
 
46
- 4. Point your AI tool at the proxy. Example for Claude Code:
267
+ Then point your tool at it:
47
268
 
48
269
  ```bash
49
270
  export ANTHROPIC_BASE_URL=http://localhost:9339
50
271
  claude
51
272
  ```
52
273
 
53
- 5. Check what got mutated.
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
356
+ ```
357
+
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.
361
+
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/
374
+ ```
375
+
376
+ ## Command reference
377
+
378
+ Every line below was executed; the exit code is the one observed.
54
379
 
55
380
  ```bash
56
- 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
57
397
  ```
58
398
 
59
- 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.
60
488
 
61
489
  ## Supported languages
62
490
 
63
491
  TypeScript, JavaScript, Python, Go, Java, C#, Ruby, Rust.
64
492
 
65
- ## Supported AI tools
493
+ ## Detection levels (L1 and L2)
66
494
 
67
- 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. -->
68
502
 
69
- ## 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:
70
521
 
71
- ```bash
72
- pretense init Initialize .pretense/ in current dir
73
- pretense scan <file|dir> Scan for secrets and identifiers
74
- pretense mutate <file> Preview a mutation
75
- pretense reverse <file> Reverse a mutation using the saved map
76
- pretense start Start the proxy on port 9339
77
- pretense status Show plan, quota, seat usage
78
- pretense audit View the audit log
79
- pretense --version Print the CLI version
80
522
  ```
523
+ $ pretense scan s.ts
81
524
 
82
- Run `pretense <command> --help` for command-specific help.
525
+ Pretense Scan s.ts
83
526
 
84
- ## How it works
527
+ Patterns: 98 | Levels: L1 (L2 unavailable) | Duration: 16.25ms
85
528
 
86
- 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.
87
531
 
88
- 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
89
533
 
90
- ## Plan limits
534
+ Total: 1 findings | 1 blocked, 0 redacted
535
+ ```
91
536
 
92
- Free: 1,000 mutations per 7-day rolling window.
93
- Pro: 100,000 mutations per 7-day rolling window. $23 per seat per month billed
94
- annually ($276/seat/year), or $29 per seat billed monthly.
95
- Enterprise: unlimited. $79 per seat per month billed annually ($948/seat/year),
96
- 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
97
573
 
98
- Check what you have left with `pretense credits` (alias: `pretense tokens`), see the
99
- 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`.
100
608
 
101
609
  ## License
102
610
 
@@ -106,5 +614,4 @@ MIT. See [LICENSE](./LICENSE).
106
614
 
107
615
  - Homepage: https://pretense.ai
108
616
  - Docs: https://pretense.ai/docs
109
- - Source: https://github.com/jimmymalhan/pretense
110
- - Issues: https://github.com/jimmymalhan/pretense/issues
617
+ - Support: https://pretense.ai/contact