residoo 0.4.9 → 0.4.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 +241 -449
- package/package.json +1 -1
- package/src/cli.js +12 -10
- package/src/color.js +36 -0
- package/src/patterns.js +39 -0
- package/src/report.js +26 -24
- package/src/rotation.js +50 -0
- package/src/scan.js +96 -21
- package/src/verify.js +77 -1
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
|
|
22
|
-
browses a page
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
118
|
-
patterns: cloud provider keys, private key blocks, OAuth/API
|
|
119
|
-
database connection strings, and more
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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.9 · 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.9 · 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
|
|
242
|
-
*configured with*, and
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
`.claude/settings.local.json`
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
280
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
│
|
|
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.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
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
|
|
337
|
-
streamed
|
|
338
|
-
manifest
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
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.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
-
|
|
383
|
-
|
|
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
|
|
421
|
-
|
|
422
|
-
|
|
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
|
|
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.
|
|
268
|
+
- uses: dandovdub/residoo@v0.4.11
|
|
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.
|
|
276
|
+
rev: v0.4.11
|
|
441
277
|
hooks:
|
|
442
278
|
- id: residoo
|
|
443
279
|
```
|
|
444
280
|
|
|
445
|
-
Or with no integration at all:
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
494
|
-
|
|
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
|
|
498
|
-
|
|
499
|
-
|
|
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
|
|
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
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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,
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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()`,
|
|
594
|
-
is the reference
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
- **`files()`**
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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.
|
|
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
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
|