@pretense/cli 0.6.9 → 0.6.11
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 +538 -60
- package/dist/chunk-5GO2CY5W.js +24 -0
- package/dist/index.js +3106 -1029
- package/dist/postinstall.js +4 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,10 +1,60 @@
|
|
|
1
1
|
# pretense
|
|
2
2
|
|
|
3
|
-
Local-first AI firewall.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
```
|
|
22
176
|
|
|
23
|
-
|
|
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
|
-
|
|
182
|
+
Note what did **not** change: `DB_URL`, `AWS_KEY`, `CONTACT`. Pretense mutates
|
|
183
|
+
secret and PII *values*, not identifiers.
|
|
26
184
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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 |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
256
|
+
## The proxy
|
|
56
257
|
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
pretense
|
|
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
|
-
|
|
267
|
+
Then point your tool at it:
|
|
65
268
|
|
|
66
269
|
```bash
|
|
67
|
-
|
|
270
|
+
export ANTHROPIC_BASE_URL=http://localhost:9339
|
|
271
|
+
claude
|
|
68
272
|
```
|
|
69
273
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
376
|
+
## Command reference
|
|
377
|
+
|
|
378
|
+
Every line below was executed; the exit code is the one observed.
|
|
84
379
|
|
|
85
380
|
```bash
|
|
86
|
-
pretense
|
|
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
|
-
|
|
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
|
-
##
|
|
493
|
+
## Detection levels (L1 and L2)
|
|
96
494
|
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
525
|
+
Pretense Scan — s.ts
|
|
113
526
|
|
|
114
|
-
|
|
527
|
+
Patterns: 98 | Levels: L1 (L2 unavailable) | Duration: 16.25ms
|
|
115
528
|
|
|
116
|
-
|
|
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
|
-
|
|
532
|
+
🔴 L1 stripe-secret-key block L1:17 len=32
|
|
119
533
|
|
|
120
|
-
|
|
534
|
+
Total: 1 findings | 1 blocked, 0 redacted
|
|
535
|
+
```
|
|
121
536
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|