residoo 0.4.10 → 0.4.12

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
@@ -18,12 +18,11 @@
18
18
 
19
19
  </div>
20
20
 
21
- Every time Claude Code, Cursor, or a similar tool reads a file, runs a command, or
22
- browses a page on your behalf, it writes a transcript of the whole session to
23
- disk, including the contents of whatever it touched. If that ever included a
24
- `.env` file, a config with a real key in it, or a login token captured during
25
- testing, that credential is now sitting in plaintext, indefinitely, in a place
26
- almost nobody thinks to check.
21
+ Every time Claude Code, Cursor, or a similar tool reads a file, runs a
22
+ command, or browses a page for you, it writes the whole session to disk,
23
+ including whatever it touched. A `.env` file, a config with a real key, a
24
+ login token captured during testing: that credential is now sitting in
25
+ plaintext, indefinitely, somewhere almost nobody thinks to check.
27
26
 
28
27
  residoo scans those transcripts and tells you what's in them.
29
28
 
@@ -41,251 +40,89 @@ Values are redacted in this report (first/last 4 characters only). Nothing
41
40
  scanned here left your machine; residoo makes no network calls.
42
41
  ```
43
42
 
44
- ## Why this, and not a git secret scanner
45
-
46
- Tools like `gitleaks` and `trufflehog` are excellent at what they do, and what
47
- they do is scan **commits**. That's a different, well-covered space. Nobody was
48
- looking at the **conversation transcripts** these agents leave behind, which
49
- contain a superset of everything a commit does: not just code, but file
50
- contents, terminal output, and whatever got pasted into a prompt.
51
-
52
- Two newer categories are adjacent but solve a different problem, worth being
53
- precise about rather than lumping together:
54
-
55
- - **Real-time hooks** (e.g. GitGuardian's `ggshield` AI hook, GitHub's secret
56
- scanning via its MCP server) intercept a prompt or a code change *as it
57
- happens*, going forward, in the session that has the hook installed. They
58
- do nothing for the months of transcripts already sitting on disk, or for
59
- any session run without the hook active. residoo scans **retroactively, at
60
- rest**: every file already there, from every past session.
61
- - **agentsweep** is a genuine, welcome peer covering similar ground. Broader
62
- on detection rules (209 to residoo's smaller, deliberately high-confidence
63
- set) and it does in-place redaction, where residoo's `--seal` makes an
64
- encrypted copy instead. residoo has more agent sources (43 to 31), and
65
- both now ship SARIF output and a pre-commit hook. The tradeoffs are worth
66
- naming precisely rather than either dismissing it or copying it blindly.
67
- It needs Python 3.11+ and three pip packages (all clean ones, on
68
- inspection; no known CVEs), where residoo needs nothing beyond Node. Its
69
- own README documents that its in-place redaction leaves the pre-redaction
70
- original sitting in a **plaintext** `.bak` file, and its issue tracker shows
71
- the real cost of that design: a merged fix
72
- ([PR #13](https://github.com/Ishannaik/agent-sweep/pull/13)) for a case
73
- where redacting a WAL-mode SQLite database left the secret recoverable from
74
- a leftover journal file. residoo's `--seal` takes a different tradeoff
75
- (encrypt a copy, touch nothing, never claim a file is "cleaned") precisely
76
- to avoid that failure class. Its tracker also shows several real,
77
- since-fixed false-*clean* reports: schema drift and malformed lines
78
- silently skipped, `--root` pointed at a file scanning nothing and exiting
79
- 0. That is the exact failure mode residoo's `broken`/`partial` status
80
- contract (see `CONTRIBUTING.md`) exists to make structurally hard to
81
- reproduce. None of this makes agentsweep bad; it makes for a legitimately
82
- different set of choices, and its README is honest about its own tradeoffs
83
- too. Worth a look if broader source coverage matters more to you than a
84
- minimal dependency footprint.
85
- - **On verifying a found value is still live**, not just shaped like one:
86
- the field splits into two real postures, and residoo picked a side.
87
- [trufflehog](https://github.com/trufflesecurity/trufflehog) verifies
88
- **by default**, an opt-out (`--no-verification`) rather than an opt-in,
89
- across 700+ vendor-specific checks; that means a plain, unconfigured
90
- trufflehog run makes network calls. gitleaks never added verification at
91
- all and is now feature-complete (security patches only); its declared
92
- successor, [betterleaks](https://github.com/betterleaks/betterleaks),
93
- added it the other way: **off by default**, one global `--validation`
94
- flag, each rule's own validate expression deciding whether and how it
95
- calls out, with repeated occurrences of the same value deduped to one
96
- request. agentsweep has none either, and is explicit about it: its own
97
- README scores trufflehog's verification with a checkmark and its own
98
- with an X, and states it is "fully offline, with zero network calls
99
- during scanning or redacting" apart from an optional PyPI version-check
100
- ping. residoo's `--verify` follows betterleaks' posture, not
101
- trufflehog's: off by default, an explicit flag, and 32 vendors today
102
- (still short of trufflehog's 700+) rather than every vendor its own
103
- detection can name, deduped the same way betterleaks dedupes, and gated
104
- the same way patterns.js's own detection rules are: only added where a
105
- real, cited endpoint exists, not assumed by analogy to a similar vendor.
106
- See [verify.js](src/verify.js) and
107
- [What it does not do](#what-it-does-not-do) for exactly what it touches
108
- and when.
109
-
110
- This isn't a gap Anthropic is planning to close upstream, either: a
111
- [request to scrub secrets from `~/.claude/projects` natively](https://github.com/anthropics/claude-code/issues/50014)
112
- was filed and closed as **not planned**. Whatever scans this directory, it
113
- won't be built into the tool that writes it.
43
+ > [!NOTE]
44
+ > gitleaks and trufflehog scan **commits**. residoo scans the **conversation
45
+ > transcripts** an AI agent leaves behind: a different, previously
46
+ > uncovered surface. Full comparison, including agentsweep and
47
+ > trufflehog/betterleaks' verification postures, in
48
+ > [docs/comparison.md](docs/comparison.md).
114
49
 
115
50
  ## What it does
116
51
 
117
- - Scans your local AI-agent session transcripts for high-confidence secret
118
- patterns: cloud provider keys, private key blocks, OAuth/API tokens,
119
- database connection strings, and more (see `src/patterns.js`).
120
- - Sees through two transcript-specific disguises. A credential present only
121
- base64-encoded on a line (an env dump piped through `base64`, wrap
122
- newlines included) is decoded and rescanned with the high-confidence
123
- vendor-prefixed rules; the report marks it `base64-wrapped` and redacts
124
- the decoded value. A credential split across two adjacent streaming
125
- records, contiguous on neither line, is rejoined at the content boundary
126
- and rescanned; the report marks it `split across lines` with the line
127
- pair. Both are general mechanisms with stated limits (one decode level,
128
- no base64 blocks spanning physical lines, two-way splits only; see
129
- `src/decode.js`).
130
- - Covers Stripe keys in both modes: live (`sk_live`/`rk_live`) and test
131
- (`sk_test`/`rk_test`), because a leaked test key still holds real
132
- permissions in its sandbox and reveals account structure.
133
- - Pairs an AWS secret access key (40 base64 characters, no vendor prefix, not
134
- a rule on its own) with a nearby confirmed access key id, and reports both
135
- at high confidence: the pairing is the vendor-specific signal, not the
136
- shape alone. Ambiguous pairings (more than one candidate nearby) are
137
- reported as nothing rather than a guess. See `src/pairing.js`. An access key
138
- id alone cannot authenticate anything; it takes the paired secret too. So
139
- when a scan turns up several access-key-id findings, the one with an actual
140
- secret sitting next to it in the transcript is called out in red as a
141
- demonstrated usable credential and sorted to the top of its group in the
142
- Rotation section, ahead of the ones that are, on their own, not yet proven
143
- exploitable.
144
- - Decodes a JWT-shaped token's own `exp` claim locally (no network call: the
145
- claim is inside the signed payload, so it cannot be altered without
146
- breaking the signature) and reports "valid until" or "expired" next to it
147
- in the Rotation section, instead of just "last seen." Only `exp` is ever
148
- read; every other claim in the payload is decoded transiently and
149
- discarded. See `src/jwtExpiry.js`.
150
- - **`--verify`** (opt-in, makes a real network call): asks a credential's own
151
- vendor whether it still authenticates, using the exact value found in your
152
- transcript. 32 vendors today. Two need a paired id+secret (see the
153
- Rotation section below for what that means): **AWS** (checked via
154
- `sts:get-caller-identity`, the same free, read-only, permission-less call
155
- the AWS CLI and tools like aws-vault use for exactly this; shells out to
156
- your own `aws` CLI rather than reimplementing AWS request signing, since
157
- residoo ships zero runtime dependencies and a subtly wrong signing
158
- implementation would silently report real keys as invalid, worse than not
159
- checking) and **PlanetScale** (a direct API call, like every other
160
- non-AWS vendor here, with the id/secret pairing found the same way AWS's
161
- is, see `src/pairing.js`). The other **30 are a single credential each,
162
- one direct API call, no CLI needed**: Slack, OpenAI, Anthropic, GitHub,
163
- Hugging Face, Replicate, DigitalOcean, Pinecone, SendGrid, Groq, xAI,
164
- OpenRouter, Stripe, npm, Notion, GitLab, Supabase (management tokens
165
- only, project-scoped anon/service_role keys need a project URL residoo
166
- doesn't have), ElevenLabs, CircleCI, Airtable, Cloudflare, Heroku,
167
- Netlify, Linear, Telegram, Discord webhooks, Vercel, Cerebras, Render,
168
- and Fly.io's `fo1_` tokens (Fly.io's other token family, `fm1a_`/`fm1r_`/
169
- `fm2_` "macaroons," is detected nowhere in residoo: caught on this
170
- project's own real-machine testing, that shape's short prefix plus a
171
- wide, unstructured base64 body produced over a dozen apparent matches
172
- inside one real, unrelated job-queue log file that simply contained a lot
173
- of embedded base64 data, a measured false-positive rate, not a
174
- hypothetical one). Every one of the 32 was researched against that
175
- vendor's own current docs or a
176
- named open-source scanner's real, running verifier code before being
177
- wired up, the same bar as the first five; a real, sourced reason (no free
178
- endpoint, needs external context the credential doesn't carry, or a
179
- credential format not confirmed specifically enough to detect safely in
180
- the first place) is why some detected credential types aren't wired to
181
- `--verify` at all, not an oversight. A
182
- verified-active credential is escalated to "rotate immediately"; a
183
- verified-invalid one is reported as already dead, no action needed, and
184
- sorted out of the way. Off by default; every environment variable the
185
- `aws` CLI reads is built from scratch, never inherited, so it can never
186
- fall back to your own real AWS profile. See `src/verify.js`.
187
- - With `--include-noisy`, filters the broad generic-secret rules by how
188
- machine-random the matched value actually looks (a lightweight, offline
189
- approximation of BPE-tokenization rarity checks): ordinary English, a
190
- placeholder, or a variable name is suppressed with its own stated reason
191
- instead of padding the count; a value that reads as random gets its
192
- confidence raised to `medium`. Never applied to the default high-confidence
193
- rules. See `src/rarity.js`.
194
- - Redacts everything in its own output. You get a shape and a first/last-4
195
- preview, never the real value, including in `--json` mode. A decoded or
196
- rejoined secret is redacted exactly like a plain one.
197
- - On an interactive terminal, prints who it is and where it lives before
198
- scanning starts (`residoo v0.4.10 · find secrets your AI coding agent left
199
- on disk` plus the repo URL), then a live spinner naming the current file
200
- as it scans. Every report also opens with the exact version and timestamp
201
- it was run with (`residoo v0.4.10 · scanned 2026-01-01 12:00`; `--json`
202
- carries the same as `residooVersion`/`scannedAt`), so a report pasted or
203
- screenshotted later never leaves you guessing which build produced it.
204
- When there are findings, the report closes with a "Next steps" pointer to
205
- `--json` and `--seal`. All of the interactive chatter (the intro, the
206
- spinner) goes to stderr only and is a complete no-op when stdout/stderr
207
- are piped, redirected, or run in CI, so none of it can ever interleave
208
- with `--json`/`--sarif` output.
209
- - `--sarif` emits SARIF 2.1.0 for GitHub code scanning's Security tab and
210
- inline pull-request annotations, the same format gitleaks/trufflehog/
211
- agentsweep already speak, so residoo's own Action and pre-commit hook plug
212
- straight into GitHub's native UI. `--json` remains the format for the full
213
- picture (findings, integrity, rotation) together.
214
- - `--seal --keychain` stores the vault key in the OS's own secure credential
215
- store (macOS today, Linux with `secret-tool` installed) instead of a typed
216
- passphrase: nothing to remember, and a truly random key instead of one
217
- whose strength depends on what you typed. Tradeoff stated plainly: a
218
- keychain-backed vault lives on that machine/account only, a passphrase
219
- travels, a keychain-backed key does not. See `src/keychain.js`.
220
- - Tells you how many **distinct** secrets it found versus how many times one
221
- got echoed back across tool calls, so the headline number reflects real
222
- exposure, not repetition.
223
- - Flags likely placeholder/example matches (an HTML form's
224
- `placeholder="AKIA..."` hint, a doc's example key) separately from real
225
- findings, rather than either hiding them or inflating the count with them.
226
- - Scans agent **config** files too (settings, MCP server configs, memory
227
- files), and checks the places the 2026 supply-chain campaigns planted
228
- persistence: hooks, dropper scripts, auto-run tasks, invisible Unicode.
229
- See the next section.
230
- - Attaches a **rotation runbook** to every finding: the vendor's real
231
- revocation path, verified against their own docs, plus a local
232
- acknowledgement ledger so "found it" can become "closed it". See
233
- [Rotation](#rotation-from-found-to-closed).
234
- - Scans a **repository checkout** instead of the machine with
235
- `--project <dir>`: committed transcripts, agent configs, and root `.env`
236
- files, built for CI and pre-commit. See
237
- [CI and pre-commit](#ci-and-pre-commit).
52
+ - Scans your local AI-agent session transcripts for 50 high-confidence
53
+ secret patterns: cloud provider keys, private key blocks, OAuth/API
54
+ tokens, database connection strings, and more. See
55
+ [`src/patterns.js`](src/patterns.js).
56
+ - Sees through two transcript-specific disguises: a credential dumped only
57
+ as base64 on a line, or split across two adjacent streaming records, is
58
+ decoded/rejoined and rescanned, then reported as `base64-wrapped` or
59
+ `split across lines` so you know it was hidden. See
60
+ [`src/decode.js`](src/decode.js).
61
+ - Pairs an AWS secret access key (no vendor prefix of its own) with a
62
+ nearby confirmed access key id and reports both at high confidence: the
63
+ *pairing* is the signal, not the shape alone. Ambiguous pairings are
64
+ reported as nothing rather than a guess. Same mechanism now also covers
65
+ PlanetScale and MongoDB Atlas Service Account credentials. See
66
+ [`src/pairing.js`](src/pairing.js).
67
+ - Decodes a JWT's own `exp` claim locally (no network call, since the
68
+ claim is inside the signed payload) and reports "valid until" or
69
+ "expired" instead of just "last seen." See
70
+ [`src/jwtExpiry.js`](src/jwtExpiry.js).
71
+ - **`--verify`** (opt-in, makes a real network call): asks a credential's
72
+ own vendor whether it still authenticates, using the exact value found in
73
+ your transcript. 35 vendors today, off by default. See
74
+ [Verifying credentials are still live](#verifying-credentials-are-still-live)
75
+ below.
76
+ - With `--include-noisy`, filters broad generic-secret rules by how
77
+ machine-random the matched value looks; never applied to the default
78
+ rules. See [`src/rarity.js`](src/rarity.js).
79
+ - Redacts everything in its own output, including `--json`: you get a
80
+ shape and a first/last-4 preview, never the real value.
81
+ - `--sarif` emits SARIF 2.1.0 for GitHub code scanning; `--json` carries
82
+ the full picture (findings, integrity, rotation) together.
83
+ - `--seal --keychain` stores the vault key in the OS's own credential store
84
+ instead of a typed passphrase. See [Sealing](#sealing-what-it-finds).
85
+ - Tells you how many **distinct** secrets it found versus how many times
86
+ one got echoed back across tool calls, so the headline number reflects
87
+ real exposure, not repetition.
88
+ - Flags likely placeholder/example matches separately from real findings,
89
+ rather than hiding them or inflating the count.
90
+ - Also scans agent **config files** and checks for **planted persistence**
91
+ (hooks, droppers, invisible Unicode); see the next section.
92
+ - Attaches a **rotation runbook** to every finding, plus a local
93
+ acknowledgement ledger. See [Rotation](#rotation-from-found-to-closed).
94
+ - `--project <dir>` scans a repository checkout instead of the machine, for
95
+ CI and pre-commit. See [CI and pre-commit](#ci-and-pre-commit).
238
96
 
239
97
  ## Beyond transcripts: configs and planted persistence
240
98
 
241
- Transcripts leak what your agent *saw*. Config files leak what your agent was
242
- *configured with*, and it turns out that is the better-measured problem.
243
- GitGuardian counted 24,008 secrets inside MCP config files on public GitHub
244
- (2,117 still valid when checked), and Lakera found live credentials inside
245
- `.claude/settings.local.json` files shipped in roughly 30 published npm
246
- packages, because Claude Code's approved-command cache quietly accumulates
247
- tokens and no packaging tool ignores `.claude/` by default. So as of v0.2.0,
248
- `residoo scan` includes an **agent config source** covering the home-level
249
- config files of Claude Code, Claude Desktop, Cursor, Gemini CLI, Codex, and
250
- Kiro. As of v0.3.1 it also reaches project-level Claude Code configs
251
- (`.mcp.json`, `.claude/settings.json`, `.claude/settings.local.json`) by
252
- resolving the project roots the agent itself recorded at home level
253
- (`~/.claude.json` and transcript `cwd` fields) rather than by walking or
254
- guessing directories; only those vendor-fixed per-project filenames are read. Every path is verified against a real install or published sources (one
255
- disclosed exception, a stealer-target path backed by a single published
256
- list, argued openly in the source header), with the full verification trail
257
- written into `src/sources/agent-configs.js`.
258
-
259
- The same files are also where the year's supply-chain campaigns planted
260
- their persistence. Mini Shai-Hulud wrote a `SessionStart` hook into
261
- `.claude/settings.json` and a `"runOn": "folderOpen"` task into
262
- `.vscode/tasks.json`. Miasma reused both plants and added
263
- `.gemini/settings.json` hooks and `.cursor/rules/setup.mdc` prompt-injection
264
- files. The keyv/ChainDrop wave dropped a script literally named `setup.mjs`
265
- into `.claude/` and `.vscode/`. And TrapDoor hid instructions in
266
- `CLAUDE.md`/`.cursorrules` as zero-width Unicode: invisible in your editor,
267
- fully visible to the agent. So every scan now also runs **integrity checks**
268
- over those exact locations:
269
-
270
- - Every auto-executing hook found in the checked locations is listed (hooks
271
- run without asking; you should be able to vouch for each one). Only
272
- commands matching a published campaign IOC (`setup.mjs`) or a
273
- campaign-shaped behavior escalate to warnings: piping a download straight
274
- into a shell, decoding base64 before executing, running repo-local scripts
275
- out of dot-directories.
276
- - Loose scripts in `.claude/`, and the exact planted filenames from the
277
- published IOC lists, are flagged by name.
99
+ Transcripts leak what your agent *saw*. Config files leak what it was
100
+ *configured with*, and that's the better-measured problem: GitGuardian
101
+ counted 24,008 secrets inside MCP config files on public GitHub (2,117
102
+ still valid), and Lakera found live credentials inside
103
+ `.claude/settings.local.json` shipped in ~30 published npm packages. So
104
+ `residoo scan` also covers the home-level config files of Claude Code,
105
+ Claude Desktop, Cursor, Gemini CLI, Codex, and Kiro, plus project-level
106
+ Claude Code configs (`.mcp.json`, `.claude/settings*.json`) resolved from
107
+ project roots the agent itself recorded, never by guessing directories.
108
+
109
+ Those same files are where 2026's supply-chain campaigns (Mini Shai-Hulud,
110
+ Miasma, the keyv/ChainDrop wave, TrapDoor) planted hooks, dropper scripts,
111
+ and zero-width-Unicode prompt injection. Every scan now also runs
112
+ **integrity checks** over those exact locations:
113
+
114
+ - Every auto-executing hook is listed; only a published campaign IOC or
115
+ campaign-shaped behavior (piping a download into a shell, base64-decode-
116
+ then-execute) escalates to a warning.
117
+ - Loose scripts in `.claude/` and known planted filenames are flagged by
118
+ name.
278
119
  - `CLAUDE.md`, `.cursorrules`, and `.cursor/rules/*` are checked for
279
- zero-width Unicode, with legitimate emoji/script joiners kept to an
280
- informational tier so the warning count stays meaningful.
281
- - `.vscode/tasks.json` is parsed (as JSONC, comments and all) for tasks that
282
- execute on folder open.
120
+ zero-width Unicode.
121
+ - `.vscode/tasks.json` is parsed for folder-open auto-run tasks.
283
122
 
284
- The checks are read-only like everything else, warnings (not review items)
285
- count toward `--fail-on-find`, project-level checks cover the directory you
286
- run from, and `--no-integrity` skips the whole thing. A config that exists
287
- but can't be read or parsed is reported as unverified, never silently
288
- counted as clean.
123
+ Read-only like everything else. `--no-integrity` skips it entirely. A
124
+ config that can't be read is reported as unverified, never silently
125
+ counted clean.
289
126
 
290
127
  ## How it works
291
128
 
@@ -300,7 +137,7 @@ counted as clean.
300
137
  │ ├──────────────┬───────────────┤ │
301
138
  │ ▼ │ ▼ │
302
139
  │ stream + match │ integrity checks │
303
- 36 verified rules │ hooks · droppers · │
140
+ 50 verified rules │ hooks · droppers · │
304
141
  │ │ │ zero-width unicode │
305
142
  │ ▼ ▼ │ │
306
143
  │ redacted report (first/last 4 chars only) ◀────────────┤
@@ -318,118 +155,117 @@ counted as clean.
318
155
  ```
319
156
 
320
157
  The `--seal` and `--upload-cloudroam` legs never run unless you pass their
321
- flag. Everything above the vault happens on every scan; nothing in the
322
- diagram ever modifies or deletes an existing file. The one exception, stated
323
- in the open: `residoo ack` writes residoo's own rotation ledger at
324
- `~/.residoo/rotations.json` (atomic, redacted, never a user file).
158
+ flag. Nothing in the diagram ever modifies or deletes an existing file. The
159
+ one exception, stated in the open: `residoo ack` writes residoo's own
160
+ rotation ledger at `~/.residoo/rotations.json` (atomic, redacted, never a
161
+ user file).
325
162
 
326
163
  ## Sealing what it finds
327
164
 
328
- Finding a leaked key in a transcript raises the obvious next question: *now
329
- what?* `--seal` is the answer:
165
+ Finding a leaked key raises the obvious next question: *now what?*
330
166
 
331
167
  ```bash
332
168
  residoo scan --seal
333
169
  ```
334
170
 
335
171
  Every transcript that carried a finding is encrypted into a local vault
336
- directory. AES-256-GCM, key derived from your passphrase with scrypt,
337
- streamed, so an 800MB transcript never touches memory whole. The vault's
338
- manifest (the mapping from numbered blobs back to real paths) is itself
339
- encrypted, so the vault doesn't advertise what's inside it even by name.
340
- **Originals are never touched.** Once you've verified a restore works
341
- (`residoo unseal <vault> --restore 0001.sealed --out /tmp/check`, verified
172
+ directory: AES-256-GCM, key derived from your passphrase with scrypt,
173
+ streamed so an 800MB transcript never touches memory whole. The vault's own
174
+ manifest is encrypted too, so it doesn't advertise what's inside even by
175
+ name. **Originals are never touched.** Once you've verified a restore works
176
+ (`residoo unseal <vault> --restore 0001.sealed --out /tmp/check`, checked
342
177
  byte-identical via a recorded SHA-256), deleting the plaintext is your
343
- decision, made by you, not by this tool.
178
+ decision, made by you.
344
179
 
345
180
  Optionally, `--upload-cloudroam` (with `CLOUDROAM_API_KEY`, `--connector`,
346
181
  `--bucket`) copies the sealed vault to [CloudRoam](https://cloudroam.io) for
347
- durable, cross-cloud storage. **This is the only feature in residoo that
348
- touches the network, it never runs unless you pass the flag, and only
349
- ciphertext is transmitted.** The vault is sealed before upload code ever
350
- executes.
182
+ durable, cross-cloud storage.
183
+
184
+ > [!IMPORTANT]
185
+ > `--upload-cloudroam` is the *only* feature in residoo that touches the
186
+ > network to send your data anywhere. It never runs unless you pass the
187
+ > flag, and only ciphertext is transmitted: the vault is sealed before any
188
+ > upload code executes.
189
+
190
+ ## Verifying credentials are still live
191
+
192
+ `--verify` asks a credential's own vendor whether it still authenticates,
193
+ using the exact value found in your transcript. Off by default, one real
194
+ network call per distinct credential.
195
+
196
+ Three vendors need a paired id+secret: **AWS** (via `sts:get-caller-identity`,
197
+ shelling out to your own `aws` CLI rather than reimplementing request
198
+ signing), **PlanetScale**, and **MongoDB Atlas** (Service Account
199
+ credentials only, the legacy Public/Private Key pair has no distinguishing
200
+ prefix and isn't detected at all). The other 32 are a single credential
201
+ each, one direct API call:
202
+
203
+ Slack · OpenAI · Anthropic · GitHub · Hugging Face · Replicate ·
204
+ DigitalOcean · Pinecone · SendGrid · Groq · xAI · OpenRouter · Stripe · npm ·
205
+ Notion · GitLab · Supabase · ElevenLabs · CircleCI · Airtable · Cloudflare ·
206
+ Heroku · Netlify · Linear · Telegram · Discord webhooks · Vercel · Cerebras ·
207
+ Render · Neon · PostHog · Fly.io
208
+
209
+ Every vendor clears the same two-stage bar before being wired up:
210
+ independent research against that vendor's own current docs, then a
211
+ separate, adversarial pass that tries to refute the first before it's
212
+ trusted. A real, sourced reason (no free endpoint, needs context the
213
+ credential doesn't carry, or a format not confirmed specifically enough to
214
+ detect safely) is why some detected credential types aren't wired to
215
+ `--verify` at all, not an oversight (Fly.io's `fm1a_`/`fm1r_`/`fm2_`
216
+ "macaroon" tokens are the clearest example: real-machine testing produced a
217
+ measured false-positive rate, so that family is detected nowhere in
218
+ residoo). A verified-active credential is escalated to "rotate
219
+ immediately"; a verified-invalid one is reported already dead, no action
220
+ needed. See [`src/verify.js`](src/verify.js).
351
221
 
352
222
  ## Rotation: from found to closed
353
223
 
354
- Detection without rotation is theater, and the field's own numbers say so:
355
- 64% of secrets leaked publicly in 2022 were still valid years later, 88% of
356
- re-verified leaked AWS keys still authenticated, and the median time to
357
- remediate a GitHub-leaked secret is 94 days. A scanner that stops at "found
358
- it" leaves all of that untouched. So every finding in a residoo report comes
359
- with the way out:
360
-
361
- - **A rotation hint per finding**, from a per-rule guidance map covering all
362
- 36 detection rules (plus the opt-in noisy ones). Where a rotation URL is
363
- shown, that exact URL was fetched and confirmed to document rotating or
364
- revoking that credential type; where a vendor's docs are login-walled or
365
- unfetchable, the report gives the console path in words instead of a link
366
- it could not verify. Generic shapes (a JWT, a bearer header) get honest
367
- generic guidance that says how to identify the issuer, never a pretend
368
- vendor.
369
- - **`residoo explain <rule-id>`** prints the full runbook for one credential
370
- type: where to revoke, the steps, and what revocation actually does at
371
- that vendor. `residoo explain --list` shows the whole catalogue.
372
- - **`residoo ack <fingerprint>`** records that you rotated one finding.
373
- **`residoo dismiss <fingerprint>`** records that you determined it was
374
- never a real secret (a test fixture, a vendor example not already
375
- recognized), a separate resolution from ack, since nothing was rotated.
376
- Every finding carries a stable fingerprint (derived only from
377
- already-redacted material, so the ledger can never leak), shown in the
378
- report and in `--json`. Acked and dismissed findings are both reported as
379
- resolved on the next scan instead of re-alarming forever. Both live in the
380
- same ledger at `~/.residoo/rotations.json`: residoo's own file, written
381
- atomically, notes redacted through the same pipeline as previews.
382
- - **A "Recommended actions" summary leads the report**, before the detailed
383
- findings breakdown: how many *distinct* values still need a decision,
384
- versus how many are already resolved. A machine with a lot of history can
385
- report hundreds of raw findings that are really a handful of distinct
386
- values echoed repeatedly; the summary is built around what's actually left
387
- to triage, not the raw count. A value `--verify` confirmed dead, or a JWT
388
- whose own signed `exp` claim is already past, is subtracted from "needs
389
- review" the same way an acked or dismissed one is, since residoo already
390
- knows it needs no action, not just that nobody has said so yet. This is a
391
- strictly per-VALUE fact: it is never rolled up into a whole rule's
392
- confidence tag in the breakdown below, since `--verify` only ever checks
393
- the specific values it can (a paired AWS credential, a bearer token), and
394
- a rule's other, unchecked findings say nothing either way.
395
- - **The rotation list is grouped by credential type**, so the rotation URL
396
- prints once per type instead of once per finding. Each distinct value's own
397
- line shows its redacted preview, which file it's in, and when it was last
398
- seen in a transcript, not just a bare fingerprint. "Last seen" is exactly
399
- that: the most recent transcript occurrence residoo found, not proof a
400
- credential is still live or that an older value was rotated. Most
401
- credential formats (AWS access keys, vendor API tokens) carry no shared
402
- identifier linking a rotated key to its predecessor, and residoo makes no
403
- network calls to ask the provider, so two distinct pending values of the
404
- same type are always shown as two separate lines, never collapsed on a
405
- guess.
406
- - **Order matters, and the report says so when it does.** The ChainDrop
407
- campaign (Aug 2026) shipped a token monitor that fires an attacker payload
408
- the moment the stolen GitHub token is revoked. When one scan finds both
409
- integrity warnings and leaked credentials, the report tells you to remove
410
- the planted persistence first and rotate second, because "rotate
411
- everything now" advice can itself trigger the damage.
412
-
413
- Acks and dismissals change what the report says, never what CI does:
414
- `--fail-on-find` fails on every finding, resolved or not, unless you
415
- explicitly pass `--allow-acked` (integrity warnings always fail either way).
224
+ Detection without rotation is theater: 64% of secrets leaked publicly in
225
+ 2022 were still valid years later, 88% of re-verified leaked AWS keys still
226
+ authenticated, and the median time to remediate a GitHub-leaked secret is
227
+ 94 days. Every finding in a residoo report comes with the way out:
228
+
229
+ - **A rotation hint per finding**, from a guidance map covering all 50
230
+ detection rules. Where shown, a rotation URL was fetched and confirmed to
231
+ document revoking that exact credential type.
232
+ - **`residoo explain <rule-id>`** prints the full runbook: where to revoke,
233
+ the steps, what revocation does. `residoo explain --list` shows the whole
234
+ catalogue.
235
+ - **`residoo ack <fingerprint>`** records that you rotated a finding.
236
+ **`residoo dismiss <fingerprint>`** records that it was never a real
237
+ secret. Both live in `~/.residoo/rotations.json`, residoo's own ledger,
238
+ written atomically, redacted through the same pipeline as previews.
239
+ - **"Recommended actions" leads the report**: how many *distinct* values
240
+ still need a decision, versus how many are already resolved (acked,
241
+ dismissed, or `--verify`-confirmed dead).
242
+ - **The rotation list groups by credential type**, so the URL prints once
243
+ per type. Each value's own line shows its redacted preview, file, and
244
+ when it was last seen.
245
+ - **Order matters, and the report says so.** The ChainDrop campaign (Aug
246
+ 2026) shipped a token monitor that fires an attacker payload the moment a
247
+ stolen GitHub token is revoked. When a scan finds both integrity warnings
248
+ and leaked credentials, the report tells you to remove the planted
249
+ persistence first, rotate second.
250
+
251
+ Acks and dismissals change what the report *says*, never what CI *does*:
252
+ `--fail-on-find` fails on every finding, resolved or not, unless you pass
253
+ `--allow-acked` (integrity warnings always fail either way).
416
254
 
417
255
  ## CI and pre-commit
418
256
 
419
257
  `residoo scan --project <dir>` scans a repository checkout instead of the
420
- machine it runs on: committed agent transcripts (Claude Code `.jsonl`
421
- trees, Codex `rollout-*.jsonl`, SpecStory histories), agent config and
422
- rules files at any depth, and root-level `.env` files, plus the integrity
423
- checks anchored at that directory. It deliberately does not touch the
424
- machine's home-level sources, so a clean CI run means the checkout is
258
+ machine it runs on: committed transcripts, agent configs, root `.env`
259
+ files, plus integrity checks anchored at that directory. It never touches
260
+ the machine's home-level sources, so a clean CI run means the checkout is
425
261
  clean and claims nothing about anyone's laptop.
426
262
 
427
- As a GitHub Action (this repository doubles as a composite action):
263
+ As a GitHub Action (this repo doubles as a composite action):
428
264
 
429
265
  ```yaml
430
266
  steps:
431
267
  - uses: actions/checkout@v4
432
- - uses: dandovdub/residoo@v0.4.10
268
+ - uses: dandovdub/residoo@v0.4.12
433
269
  ```
434
270
 
435
271
  As a pre-commit hook:
@@ -437,34 +273,29 @@ As a pre-commit hook:
437
273
  ```yaml
438
274
  repos:
439
275
  - repo: https://github.com/dandovdub/residoo
440
- rev: v0.4.10
276
+ rev: v0.4.12
441
277
  hooks:
442
278
  - id: residoo
443
279
  ```
444
280
 
445
- Or with no integration at all: `npm install -g residoo && residoo scan --project . --fail-on-find`
446
- (more reliable in CI than `npx --yes`, which failed consistently in real
447
- GitHub Actions runs while working fine locally; see the design note at the
448
- top of `action.yml`).
449
- Exit codes, inputs, and exactly what project mode does and does not see are
450
- documented in [docs/ci.md](docs/ci.md).
281
+ Or with no integration at all:
282
+ `npm install -g residoo && residoo scan --project . --fail-on-find`.
283
+ Exit codes and exactly what project mode does and doesn't see are in
284
+ [docs/ci.md](docs/ci.md).
451
285
 
452
286
  ## What it does not do
453
287
 
454
- - **No network calls in the default path, and none at all unless you
455
- explicitly pass `--upload-cloudroam` or `--verify`.** A secret scanner that
456
- phones home is not a tool you should trust with your secrets. Verify this
457
- yourself: every network-capable call in the codebase lives behind one of
458
- those two flags. `src/sealvault.js` holds the one `fetch` call reachable
459
- from `--upload-cloudroam`, and sends only encrypted bytes. `src/verify.js`
460
- holds everything reachable from `--verify`: a `fetch` call per vendor
461
- (Slack, OpenAI, Anthropic, GitHub), each sending nothing but the exact
462
- credential a scan found to that credential's own vendor, plus a subprocess
463
- call to your own `aws` CLI for AWS credentials, never a `fetch`. Neither
464
- file's code runs unless you pass the matching flag.
465
- - **Nothing destructive, ever.** Scanning is read-only. Sealing creates *new*
466
- files and modifies or deletes nothing, not even the plaintext it just
467
- encrypted a copy of. That last step is deliberately left to a human.
288
+ > [!IMPORTANT]
289
+ > **No network calls in the default path, and none at all unless you pass
290
+ > `--upload-cloudroam` or `--verify`.** A secret scanner that phones home
291
+ > is not a tool you should trust with your secrets. Every network-capable
292
+ > call in the codebase lives behind one of those two flags: verify it
293
+ > yourself in [`src/sealvault.js`](src/sealvault.js) and
294
+ > [`src/verify.js`](src/verify.js).
295
+
296
+ - **Nothing destructive, ever.** Scanning is read-only. Sealing creates
297
+ *new* files and modifies or deletes nothing, not even the plaintext it
298
+ just encrypted a copy of.
468
299
  - **No telemetry, no analytics, no update-check ping.**
469
300
 
470
301
  ## Install
@@ -480,24 +311,17 @@ npm install -g residoo
480
311
  residoo scan
481
312
  ```
482
313
 
483
- A Homebrew formula ships in this repo at `packaging/homebrew/`. It installs
484
- the exact tarball published to npm (same bits, sha256 verified), so Homebrew
485
- is a second door to the same release, not a second build. Once the tap
486
- repository (`dandovdub/homebrew-residoo`) is published, installation is:
487
-
488
314
  ```bash
489
315
  brew tap dandovdub/residoo
490
316
  brew install residoo
491
317
  ```
492
318
 
493
- Until the tap is up, npm above is the way in. The formula always points at
494
- the latest *published* npm release (its sha256 is computed from the real
495
- tarball), so it can lag a fresh tag by one publish cycle.
319
+ The Homebrew formula installs the exact tarball published to npm (sha256
320
+ verified): same bits, not a second build.
496
321
 
497
- Requires Node.js 18+. The SQLite-backed sources listed below additionally
498
- need 22.5+; residoo still runs and scans every line-delimited/JSON source,
499
- including Claude Code, fine without it. Zero runtime dependencies, and you
500
- can check `package.json` rather than take that on faith.
322
+ Requires Node.js 18+ (22.5+ for the SQLite-backed sources listed below;
323
+ residoo still runs fine without it). Zero runtime dependencies: check
324
+ `package.json` rather than take that on faith.
501
325
 
502
326
  ## Usage
503
327
 
@@ -515,6 +339,8 @@ residoo scan [options]
515
339
  fail the run (pending ones and warnings still do)
516
340
  --no-integrity skip the integrity checks
517
341
  --no-color disable ANSI colour
342
+ --verify ask each credential's own vendor if it still authenticates
343
+ (real network call; see "Verifying credentials are still live")
518
344
 
519
345
  --seal encrypt every transcript with findings into a local vault
520
346
  --vault-dir <dir> vault location (default ./residoo-vault-<stamp>)
@@ -530,107 +356,73 @@ residoo unseal <vault-dir> --restore <n> --out <p> restore one file, hash-verif
530
356
  ```
531
357
 
532
358
  The vault passphrase comes from `RESIDOO_PASSPHRASE` or a hidden interactive
533
- prompt. There is no recovery if you lose it. That is the point of the design,
534
- so pick one you keep.
359
+ prompt. There is no recovery if you lose it, so pick one you keep.
535
360
 
536
361
  ## Sources supported today
537
362
 
538
- 43 sources as of this writing (42 transcript stores plus the agent-config
539
- source described above), in two honestly-distinct tiers. Project mode
540
- (`--project`) adds one more, opt-in source (`src/sources/project-artifacts.js`)
541
- that scans a repository checkout rather than the machine and never
542
- participates in a default scan. See
543
- `src/sources/index.js` for the full list and grouping, and each source file's
544
- own header for exactly what was and wasn't checked.
545
-
546
- **Real-install-verified.** The adapter was run against an actual, populated
547
- installation and confirmed to find real content:
548
-
549
- - **Claude Code** (`~/.claude/projects/**/*.jsonl`)
550
- - **Agent config files**, for its Claude-family paths (`~/.claude.json` and
551
- its `.backup`, `~/.claude/settings*.json`, Claude Desktop's
552
- `claude_desktop_config.json`). Its Cursor/Gemini/Codex/Kiro paths are in
553
- the tier below; `src/sources/agent-configs.js` tracks verification per
554
- path, not per file.
555
-
556
- **Multi-source-corroborated-but-unverified.** The path/schema is backed by
557
- 2+ independent, credible sources (official docs, the tool's own shipped
558
- source code, a real community tool that reads the same files for a living,
559
- or a real user's own reported install) but was **not** checked against a real
560
- install of the tool on any machine this project was built on. Every adapter
561
- in this tier is still built to fail loudly (`broken: true`, `status:
562
- "failed"`) rather than silently report "all clear", but the path itself
563
- could still be stale or wrong in a way only a real install can catch. If you
564
- use one of these and can confirm `residoo scan`'s file counts look right for
565
- what's actually on your disk, that report is exactly what moves a source out
566
- of this tier:
363
+ 43 sources: 42 transcript stores plus the agent-config source above, in two
364
+ honestly-distinct tiers. `--project` adds one more, opt-in source that
365
+ scans a checkout instead of the machine.
366
+
367
+ **Real-install-verified**: run against an actual, populated installation
368
+ and confirmed to find real content: **Claude Code**
369
+ (`~/.claude/projects/**/*.jsonl`) and **agent config files** for its
370
+ Claude-family paths.
371
+
372
+ **Multi-source-corroborated-but-unverified**: backed by 2+ independent,
373
+ credible sources but not checked against a real install on any machine this
374
+ project was built on. Still built to fail loudly rather than silently
375
+ report "all clear":
567
376
 
568
377
  Cursor, Codex CLI, OpenCode, Aider, Cline, Roo Code, Kilo Code, Windsurf,
569
- PearAI, Trae, Void, Gemini CLI, Qwen Code, Continue, Open Interpreter, Goose,
570
- GitHub Copilot Chat, GitHub Copilot CLI, `llm` (Simon Willison's Datasette-
571
- adjacent CLI), Codebuff, Mentat, Hermes, OpenClaw, Warp, Crush, Grok Build,
572
- Kiro CLI, Kiro IDE, Zed, JetBrains Junie, JetBrains AI Assistant, Sourcegraph
573
- Cody, Amazon Q Developer, Qodo Gen, OpenHands, Factory Droid CLI, Devin CLI,
574
- Pi, Google Antigravity, Kimi Code, and `fx`.
575
-
576
- A few of these are SQLite-backed (Cursor, Crush, Cody, Devin CLI, Hermes,
577
- Kiro CLI, `llm`, Trae, Void, Warp, Zed) and need Node.js 22.5+ for the
578
- built-in `node:sqlite` module (not a dependency; see `package.json`). On an
579
- older Node, `residoo scan` reports each of those as detected-but-not-scanned
580
- rather than silently dropping it or crashing.
581
-
582
- **Investigated and deliberately not included**, rather than guessed at:
583
- Plandex (confirmed, from its own source, to be client-server with nothing
584
- local to scan), CodeGPT and Augment Code (both account/cloud-based, no
585
- evidence of a local transcript file), and Replit Agent (confirmed
586
- cloud-only). Tabby, Tabnine, Zencoder, Tongyi Lingma, and Berd were
587
- researched but didn't clear this project's 2-independent-source bar in the
588
- time available. A verified adapter for any of these is a welcome PR.
378
+ PearAI, Trae, Void, Gemini CLI, Qwen Code, Continue, Open Interpreter,
379
+ Goose, GitHub Copilot Chat/CLI, `llm`, Codebuff, Mentat, Hermes, OpenClaw,
380
+ Warp, Crush, Grok Build, Kiro CLI/IDE, Zed, JetBrains Junie/AI Assistant,
381
+ Sourcegraph Cody, Amazon Q Developer, Qodo Gen, OpenHands, Factory Droid
382
+ CLI, Devin CLI, Pi, Google Antigravity, Kimi Code, and `fx`.
383
+
384
+ A few are SQLite-backed (Cursor, Crush, Cody, Devin CLI, Hermes, Kiro CLI,
385
+ `llm`, Trae, Void, Warp, Zed) and need Node.js 22.5+ for the built-in
386
+ `node:sqlite` module. On an older Node, each reports as detected-but-not-
387
+ scanned rather than silently dropping.
388
+
389
+ **Investigated and deliberately not included:** Plandex (client-server,
390
+ nothing local to scan), CodeGPT and Augment Code (account/cloud-based, no
391
+ local transcript file), Replit Agent (cloud-only). Tabby, Tabnine,
392
+ Zencoder, Tongyi Lingma, and Berd didn't clear the 2-independent-source bar
393
+ in the time available. A verified adapter for any of these is a welcome PR.
394
+
395
+ See [`src/sources/index.js`](src/sources/index.js) for the full list, and
396
+ each source file's own header for exactly what was and wasn't checked.
589
397
 
590
398
  ## Adding a source
591
399
 
592
400
  A source is a small object with four methods: `id()`, `label()`,
593
- `available()`, `files()`, and `readLines(file)`. `src/sources/claude-code.js`
594
- is the reference implementation. Copy it, point it at the real local
595
- storage path for your tool, and open a PR. Two contracts scan.js actually
596
- depends on, worth getting right rather than guessing from a quick skim:
597
-
598
- - **`files()`** is a generator yielding `{ file, mtimeMs, sizeBytes, broken }`.
599
- Set `broken: true` (other fields can be omitted) for an entry that looked
600
- like it should be scannable but wasn't; a dangling symlink is the main
601
- case. Don't just `continue` past it inside the generator. An early version
602
- of the Claude Code source did exactly that, and a real, non-hypothetical
603
- case (a project directory relocated via a symlink whose target no longer
604
- exists) went completely invisible: not in the scan count, not in any
605
- warning, nothing. Surfacing it as `broken` is what lets scan.js report it
606
- instead.
607
- - **`readLines(file)`** is `async`, returning `{ lines, status, bytesRead }`.
608
- `status` is `"complete"`, `"partial"` (some real lines WERE read before a
609
- failure partway through; return them, don't discard real content because
610
- the rest of the file didn't finish cleanly), `"too-large"`, or `"failed"`.
611
- Whatever you return in `lines` for a non-"complete" status still gets
612
- scanned normally.
401
+ `available()`, `files()`, `readLines(file)`.
402
+ [`src/sources/claude-code.js`](src/sources/claude-code.js) is the reference
403
+ implementation: copy it, point it at your tool's real local storage path,
404
+ open a PR. Two contracts worth getting right:
405
+
406
+ - **`files()`** yields `{ file, mtimeMs, sizeBytes, broken }`. Set
407
+ `broken: true` for an entry that looked scannable but wasn't (a dangling
408
+ symlink); don't just skip past it silently.
409
+ - **`readLines(file)`** is `async`, returning `{ lines, status, bytesRead }`
410
+ with `status` one of `"complete"`, `"partial"`, `"too-large"`, `"failed"`.
411
+ Whatever's in `lines` for a non-`"complete"` status still gets scanned.
613
412
 
614
413
  Please verify the path actually exists and holds real content before
615
- submitting. See the note above on why that matters here specifically.
414
+ submitting.
616
415
 
617
416
  ## A known limitation, stated plainly
618
417
 
619
418
  Shape-based detection can't tell a real secret from a realistic-looking
620
- example in a fetched web page or a piece of documentation your agent read
621
- aloud back to you. Three suppression layers narrow the gap: known
622
- vendor-documented example values (AWS's `AKIAIOSFODNN7EXAMPLE` and its
623
- siblings, GitHub's docs tokens, jwt.io's demo token) are suppressed by
624
- exact match; a placeholder body built from one repeated character (no
625
- vendor issues zero-entropy key material) is suppressed by value; and
626
- placeholder-looking context around a match catches the common UI-hint
627
- case. The two value-based layers apply identically to base64-decoded and
628
- boundary-joined findings, since a decoded example is the same non-secret
629
- as a plain one. None of the three catches every case, and all are
630
- re-includable with `--include-suppressed`. Treat every finding as a lead
631
- to check, not a
632
- certainty. The same is true of every tool in this category, including the
633
- well-established ones.
419
+ example in a fetched web page or documentation your agent read back to
420
+ you. Three suppression layers narrow the gap (known vendor-documented
421
+ example values, placeholder bodies built from one repeated character, and
422
+ placeholder-looking surrounding context), but none catches every case, and
423
+ all are re-includable with `--include-suppressed`. Treat every finding as a
424
+ lead to check, not a certainty. The same is true of every tool in this
425
+ category, including the well-established ones.
634
426
 
635
427
  ## License
636
428