whatileaked 0.3.1 → 0.5.0
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 +30 -11
- package/dist/cli.js +220 -48
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -105,6 +105,17 @@ same credential always produces the same code, so two rows sharing a code are
|
|
|
105
105
|
one key you reused in two places, and two different codes are two different
|
|
106
106
|
keys.
|
|
107
107
|
|
|
108
|
+
### Sharing it
|
|
109
|
+
|
|
110
|
+
The last line of every scan is made only of counts — no project, path, rule or
|
|
111
|
+
fingerprint — so it is the part you can paste or screenshot anywhere:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
whatileaked: 3 credentials sent to a model provider, across 561 transcripts.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A clean scan ends the same way, with `no credentials`.
|
|
118
|
+
|
|
108
119
|
---
|
|
109
120
|
|
|
110
121
|
## Is it a real key, or just a test fixture?
|
|
@@ -194,18 +205,26 @@ npx whatileaked scan || echo "found something"
|
|
|
194
205
|
|
|
195
206
|
| Agent | Folder |
|
|
196
207
|
| --- | --- |
|
|
197
|
-
| Claude Code | `~/.claude/projects/` · `~/.claude/CLAUDE.md` |
|
|
198
|
-
| Codex | `~/.codex/sessions/` · `~/.codex/archived_sessions/` · `~/.codex/AGENTS.md` |
|
|
208
|
+
| Claude Code | `~/.claude/projects/` · `~/.claude/history.jsonl` · `~/.claude/paste-cache/` · `~/.claude/CLAUDE.md` |
|
|
209
|
+
| Codex | `~/.codex/sessions/` · `~/.codex/archived_sessions/` · `~/.codex/history.jsonl` · `~/.codex/AGENTS.md` |
|
|
199
210
|
| Cursor | `~/.cursor/projects/*/agent-transcripts/` |
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
211
|
+
| opencode | `~/.local/share/opencode/opencode.db` |
|
|
212
|
+
|
|
213
|
+
opencode keeps its sessions in a SQLite database. Reading it needs Node's
|
|
214
|
+
built-in `node:sqlite`, which loads without a flag from Node 22.13. On an older
|
|
215
|
+
Node the scan still finds the file and says, in one line, that it was not
|
|
216
|
+
scanned, so a skipped database never looks clean. Only the messages table is
|
|
217
|
+
read, and `wipe` does not rewrite the database: delete those sessions in
|
|
218
|
+
opencode, and rotate.
|
|
219
|
+
|
|
220
|
+
Cursor keeps its older Ask-mode chats in SQLite as well, and those are not
|
|
221
|
+
scanned yet. Agent transcripts are covered; Ask history is not.
|
|
222
|
+
|
|
223
|
+
Transcripts record credentials that were already sent. So does prompt history,
|
|
224
|
+
which keeps every prompt you typed and every long paste after the session itself
|
|
225
|
+
has been cleaned up. Instruction and memory files are worse: they are read again
|
|
226
|
+
at the start of the next session, so a credential in one keeps leaking until the
|
|
227
|
+
file is edited. All of them are scanned, and `wipe` rewrites all of them.
|
|
209
228
|
|
|
210
229
|
Nowhere else. It does not read your repos, your shell history, or your
|
|
211
230
|
environment. Adding support for another agent is one small file — pull requests
|
package/dist/cli.js
CHANGED
|
@@ -161,7 +161,10 @@ var TerminalReporter = class {
|
|
|
161
161
|
if (totals.length === 0) {
|
|
162
162
|
this.#write(green("No credentials found."));
|
|
163
163
|
this.#write("");
|
|
164
|
+
this.#unread(stats);
|
|
165
|
+
if (stats.unread.length > 0) this.#write("");
|
|
164
166
|
this.#write(dim(DISCLOSURE));
|
|
167
|
+
this.#share(sent, stats);
|
|
165
168
|
return;
|
|
166
169
|
}
|
|
167
170
|
for (const total of totals) {
|
|
@@ -190,12 +193,36 @@ var TerminalReporter = class {
|
|
|
190
193
|
yellow(`${plural(stats.unscannable, "message")} could not be scanned and are not counted.`)
|
|
191
194
|
);
|
|
192
195
|
}
|
|
196
|
+
this.#unread(stats);
|
|
193
197
|
this.#write("");
|
|
194
198
|
this.#write(dim("The 8-character code is a fingerprint, never the secret: a truncated"));
|
|
195
199
|
this.#write(dim("SHA-256, so one credential shows the same code everywhere it leaked."));
|
|
196
200
|
this.#write(dim("Some findings will be test fixtures \u2014 open the file to tell."));
|
|
197
201
|
this.#write("");
|
|
198
202
|
this.#write(dim(DISCLOSURE));
|
|
203
|
+
this.#share(sent, stats);
|
|
204
|
+
}
|
|
205
|
+
/** One plain line per file found and not read. Printed under a clean result
|
|
206
|
+
* too, which is exactly where leaving it out would mislead. */
|
|
207
|
+
#unread(stats) {
|
|
208
|
+
for (const file of stats.unread) {
|
|
209
|
+
this.#write(yellow(`Found ${shorten(file.path)} but did not scan it: ${file.reason}.`));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The one line meant to be pasted somewhere public. Everything above it names
|
|
214
|
+
* projects, paths and fingerprints, so nobody shares it; this is counts only,
|
|
215
|
+
* and stays that way. It is last so a screenshot can crop to it, unstyled so
|
|
216
|
+
* a copy carries nothing but the words, and it repeats the headline's
|
|
217
|
+
* transmitted-only count for the same reason the headline keeps memory files
|
|
218
|
+
* out of it.
|
|
219
|
+
*/
|
|
220
|
+
#share(sent, stats) {
|
|
221
|
+
const credentials = sent === 0 ? "no credentials" : plural(sent, "credential");
|
|
222
|
+
this.#write("");
|
|
223
|
+
this.#write(
|
|
224
|
+
`whatileaked: ${credentials} sent to a model provider, across ${plural(stats.transcripts, "transcript")}.`
|
|
225
|
+
);
|
|
199
226
|
}
|
|
200
227
|
};
|
|
201
228
|
function count(value) {
|
|
@@ -2381,6 +2408,50 @@ async function* readLines(path) {
|
|
|
2381
2408
|
}
|
|
2382
2409
|
}
|
|
2383
2410
|
|
|
2411
|
+
// src/sources/listing.ts
|
|
2412
|
+
import { readdir, realpath, stat } from "fs/promises";
|
|
2413
|
+
import { join } from "path";
|
|
2414
|
+
async function listDirectories(root) {
|
|
2415
|
+
try {
|
|
2416
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
2417
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2418
|
+
} catch {
|
|
2419
|
+
return [];
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
var FileExtension = {
|
|
2423
|
+
jsonl: ".jsonl",
|
|
2424
|
+
markdown: ".md",
|
|
2425
|
+
text: ".txt",
|
|
2426
|
+
database: ".db"
|
|
2427
|
+
};
|
|
2428
|
+
function isJsonl(path) {
|
|
2429
|
+
return path.endsWith(FileExtension.jsonl);
|
|
2430
|
+
}
|
|
2431
|
+
async function listFiles(dir, extension) {
|
|
2432
|
+
try {
|
|
2433
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
2434
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(extension)).map((entry) => entry.name);
|
|
2435
|
+
} catch {
|
|
2436
|
+
return [];
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
async function resolveFile(path) {
|
|
2440
|
+
try {
|
|
2441
|
+
const real = await realpath(path);
|
|
2442
|
+
return (await stat(real)).isFile() ? real : null;
|
|
2443
|
+
} catch {
|
|
2444
|
+
return null;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
async function walkDirectories(root) {
|
|
2448
|
+
const found = [root];
|
|
2449
|
+
for (const name of await listDirectories(root)) {
|
|
2450
|
+
found.push(...await walkDirectories(join(root, name)));
|
|
2451
|
+
}
|
|
2452
|
+
return found;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2384
2455
|
// src/transcript/reader.ts
|
|
2385
2456
|
import { createReadStream as createReadStream2 } from "fs";
|
|
2386
2457
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -2413,6 +2484,7 @@ var TranscriptReader = class {
|
|
|
2413
2484
|
};
|
|
2414
2485
|
|
|
2415
2486
|
// src/scan-run.ts
|
|
2487
|
+
var NEEDS_SQLITE = "reading it needs Node 22.13 or newer";
|
|
2416
2488
|
var ScanRun = class {
|
|
2417
2489
|
#sources;
|
|
2418
2490
|
#scanner;
|
|
@@ -2433,12 +2505,19 @@ var ScanRun = class {
|
|
|
2433
2505
|
memoryFiles: 0,
|
|
2434
2506
|
entries: 0,
|
|
2435
2507
|
skipped: 0,
|
|
2436
|
-
unscannable: 0
|
|
2508
|
+
unscannable: 0,
|
|
2509
|
+
unread: []
|
|
2437
2510
|
};
|
|
2438
2511
|
for (const source of this.#sources) {
|
|
2439
2512
|
for await (const file of source.discover()) {
|
|
2440
|
-
if (file.
|
|
2441
|
-
stats
|
|
2513
|
+
if (file.messages !== void 0) {
|
|
2514
|
+
await this.#readDatabase(file, file.messages, findings, stats);
|
|
2515
|
+
this.#progress.advance(stats.transcripts + stats.memoryFiles, file.project);
|
|
2516
|
+
continue;
|
|
2517
|
+
}
|
|
2518
|
+
if (file.kind === FileKind.transcript) stats.transcripts++;
|
|
2519
|
+
else stats.memoryFiles++;
|
|
2520
|
+
if (isJsonl(file.path)) {
|
|
2442
2521
|
const reader = new TranscriptReader();
|
|
2443
2522
|
for await (const entry of reader.read(file.path)) {
|
|
2444
2523
|
this.#collect(file, entry.line, entry.index, findings, stats);
|
|
@@ -2446,7 +2525,7 @@ var ScanRun = class {
|
|
|
2446
2525
|
stats.entries += reader.stats.entries;
|
|
2447
2526
|
stats.skipped += reader.stats.skipped;
|
|
2448
2527
|
} else {
|
|
2449
|
-
stats.
|
|
2528
|
+
if (file.kind === FileKind.transcript) stats.entries++;
|
|
2450
2529
|
for await (const line of readLines(file.path)) {
|
|
2451
2530
|
this.#collect(file, line.text, line.number, findings, stats);
|
|
2452
2531
|
}
|
|
@@ -2458,6 +2537,24 @@ var ScanRun = class {
|
|
|
2458
2537
|
this.#reporter.report(findings, stats);
|
|
2459
2538
|
return findings.length;
|
|
2460
2539
|
}
|
|
2540
|
+
/** A database counts as one transcript once it has been read. One it could
|
|
2541
|
+
* not open is listed instead, so a scan that skipped it never reads as clean. */
|
|
2542
|
+
async #readDatabase(file, messages, findings, stats) {
|
|
2543
|
+
if (messages === null) {
|
|
2544
|
+
stats.unread.push({ path: file.path, reason: NEEDS_SQLITE });
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
let index = 0;
|
|
2548
|
+
try {
|
|
2549
|
+
for await (const message of messages()) {
|
|
2550
|
+
stats.entries++;
|
|
2551
|
+
this.#collect({ ...file, project: message.project }, message.text, index++, findings, stats);
|
|
2552
|
+
}
|
|
2553
|
+
stats.transcripts++;
|
|
2554
|
+
} catch {
|
|
2555
|
+
stats.unread.push({ path: file.path, reason: "it could not be opened" });
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2461
2558
|
/** One entry the engine chokes on must not cost the other 700,000. */
|
|
2462
2559
|
#collect(file, text, at, findings, stats) {
|
|
2463
2560
|
let matches = [];
|
|
@@ -2485,47 +2582,12 @@ var ScanRun = class {
|
|
|
2485
2582
|
// src/sources/claude-code-source.ts
|
|
2486
2583
|
import { basename, join as join2 } from "path";
|
|
2487
2584
|
|
|
2488
|
-
// src/sources/listing.ts
|
|
2489
|
-
import { readdir, realpath, stat } from "fs/promises";
|
|
2490
|
-
import { join } from "path";
|
|
2491
|
-
async function listDirectories(root) {
|
|
2492
|
-
try {
|
|
2493
|
-
const entries = await readdir(root, { withFileTypes: true });
|
|
2494
|
-
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2495
|
-
} catch {
|
|
2496
|
-
return [];
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
var FileExtension = { jsonl: ".jsonl", markdown: ".md" };
|
|
2500
|
-
async function listFiles(dir, extension) {
|
|
2501
|
-
try {
|
|
2502
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
2503
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(extension)).map((entry) => entry.name);
|
|
2504
|
-
} catch {
|
|
2505
|
-
return [];
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
async function resolveFile(path) {
|
|
2509
|
-
try {
|
|
2510
|
-
const real = await realpath(path);
|
|
2511
|
-
return (await stat(real)).isFile() ? real : null;
|
|
2512
|
-
} catch {
|
|
2513
|
-
return null;
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
|
-
async function walkDirectories(root) {
|
|
2517
|
-
const found = [root];
|
|
2518
|
-
for (const name of await listDirectories(root)) {
|
|
2519
|
-
found.push(...await walkDirectories(join(root, name)));
|
|
2520
|
-
}
|
|
2521
|
-
return found;
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
2585
|
// src/transcript/entry.ts
|
|
2525
2586
|
var SourceName = {
|
|
2526
2587
|
"claude-code": "claude-code",
|
|
2527
2588
|
codex: "codex",
|
|
2528
|
-
cursor: "cursor"
|
|
2589
|
+
cursor: "cursor",
|
|
2590
|
+
opencode: "opencode"
|
|
2529
2591
|
};
|
|
2530
2592
|
|
|
2531
2593
|
// src/transcript/shape.ts
|
|
@@ -2553,6 +2615,19 @@ var ClaudeCodeSource = class {
|
|
|
2553
2615
|
if (instructions !== null) {
|
|
2554
2616
|
yield { source: this.name, kind: FileKind.memory, path: instructions, project: this.name };
|
|
2555
2617
|
}
|
|
2618
|
+
const history = await resolveFile(join2(this.#home, ".claude", "history.jsonl"));
|
|
2619
|
+
if (history !== null) {
|
|
2620
|
+
yield { source: this.name, kind: FileKind.transcript, path: history, project: this.name };
|
|
2621
|
+
}
|
|
2622
|
+
const pastes = join2(this.#home, ".claude", "paste-cache");
|
|
2623
|
+
for (const file of await listFiles(pastes, FileExtension.text)) {
|
|
2624
|
+
yield {
|
|
2625
|
+
source: this.name,
|
|
2626
|
+
kind: FileKind.transcript,
|
|
2627
|
+
path: join2(pastes, file),
|
|
2628
|
+
project: this.name
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2556
2631
|
const root = join2(this.#home, ".claude", "projects");
|
|
2557
2632
|
for (const slug of await listDirectories(root)) {
|
|
2558
2633
|
const dir = join2(root, slug);
|
|
@@ -2606,6 +2681,10 @@ var CodexSource = class {
|
|
|
2606
2681
|
if (instructions !== null) {
|
|
2607
2682
|
yield { source: this.name, kind: FileKind.memory, path: instructions, project: this.name };
|
|
2608
2683
|
}
|
|
2684
|
+
const history = await resolveFile(join3(this.#home, ".codex", "history.jsonl"));
|
|
2685
|
+
if (history !== null) {
|
|
2686
|
+
yield { source: this.name, kind: FileKind.transcript, path: history, project: this.name };
|
|
2687
|
+
}
|
|
2609
2688
|
for (const root of ["sessions", "archived_sessions"]) {
|
|
2610
2689
|
for (const dir of await walkDirectories(join3(this.#home, ".codex", root))) {
|
|
2611
2690
|
for (const file of await listFiles(dir, FileExtension.jsonl)) {
|
|
@@ -2689,6 +2768,73 @@ async function isDirectory(path) {
|
|
|
2689
2768
|
}
|
|
2690
2769
|
}
|
|
2691
2770
|
|
|
2771
|
+
// src/sources/opencode-source.ts
|
|
2772
|
+
import { basename as basename4, join as join5 } from "path";
|
|
2773
|
+
var OpencodeSource = class {
|
|
2774
|
+
name = SourceName.opencode;
|
|
2775
|
+
#home;
|
|
2776
|
+
#load;
|
|
2777
|
+
constructor(home2, load = loadSqlite) {
|
|
2778
|
+
this.#home = home2;
|
|
2779
|
+
this.#load = load;
|
|
2780
|
+
}
|
|
2781
|
+
async *discover() {
|
|
2782
|
+
const dir = join5(this.#home, ".local", "share", "opencode");
|
|
2783
|
+
for (const file of await listFiles(dir, FileExtension.database)) {
|
|
2784
|
+
const path = join5(dir, file);
|
|
2785
|
+
const sqlite = await this.#load();
|
|
2786
|
+
yield {
|
|
2787
|
+
source: this.name,
|
|
2788
|
+
kind: FileKind.transcript,
|
|
2789
|
+
path,
|
|
2790
|
+
project: this.name,
|
|
2791
|
+
messages: sqlite === null ? null : () => readMessages(sqlite, path, this.name)
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
};
|
|
2796
|
+
async function* readMessages(sqlite, path, fallback) {
|
|
2797
|
+
const db = new sqlite.DatabaseSync(path, { readOnly: true });
|
|
2798
|
+
try {
|
|
2799
|
+
const directories = /* @__PURE__ */ new Map();
|
|
2800
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (?, ?)").all("session", "session_v2");
|
|
2801
|
+
for (const { name } of tables) {
|
|
2802
|
+
for (const row of db.prepare(`SELECT id, directory FROM "${String(name)}"`).iterate()) {
|
|
2803
|
+
if (typeof row.id === "string" && typeof row.directory === "string") {
|
|
2804
|
+
directories.set(row.id, row.directory);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
for (const row of db.prepare("SELECT session_id, data FROM session_message").iterate()) {
|
|
2809
|
+
if (typeof row.data !== "string") continue;
|
|
2810
|
+
const directory = typeof row.session_id === "string" ? directories.get(row.session_id) : null;
|
|
2811
|
+
yield { project: directory == null ? fallback : basename4(directory), text: row.data };
|
|
2812
|
+
}
|
|
2813
|
+
} finally {
|
|
2814
|
+
db.close();
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
var loading = null;
|
|
2818
|
+
function loadSqlite() {
|
|
2819
|
+
loading ??= importSqlite();
|
|
2820
|
+
return loading;
|
|
2821
|
+
}
|
|
2822
|
+
async function importSqlite() {
|
|
2823
|
+
const emitWarning = process.emitWarning;
|
|
2824
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
2825
|
+
if (String(warning).includes("SQLite")) return;
|
|
2826
|
+
Reflect.apply(emitWarning, process, [warning, ...rest]);
|
|
2827
|
+
});
|
|
2828
|
+
try {
|
|
2829
|
+
const specifier = "node:sqlite";
|
|
2830
|
+
return await import(specifier);
|
|
2831
|
+
} catch {
|
|
2832
|
+
return null;
|
|
2833
|
+
} finally {
|
|
2834
|
+
process.emitWarning = emitWarning;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2692
2838
|
// src/wipe/confirm.ts
|
|
2693
2839
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
2694
2840
|
var CONFIRMATION = "wipe";
|
|
@@ -2732,16 +2878,16 @@ var WipeRun = class {
|
|
|
2732
2878
|
this.#write = write;
|
|
2733
2879
|
}
|
|
2734
2880
|
async run() {
|
|
2735
|
-
const plans = await this.#plan();
|
|
2881
|
+
const { plans, untouched } = await this.#plan();
|
|
2736
2882
|
if (plans.length === 0) {
|
|
2737
|
-
return { planned: [], filesRewritten: 0, secretsRedacted: 0, confirmed: false };
|
|
2883
|
+
return { planned: [], untouched, filesRewritten: 0, secretsRedacted: 0, confirmed: false };
|
|
2738
2884
|
}
|
|
2739
2885
|
this.#preview(plans);
|
|
2740
2886
|
const answer = await this.#prompt.ask(
|
|
2741
2887
|
` Type ${bold(red(CONFIRMATION))} to rewrite these files, or anything else to stop: `
|
|
2742
2888
|
);
|
|
2743
2889
|
if (answer !== CONFIRMATION) {
|
|
2744
|
-
return { planned: plans, filesRewritten: 0, secretsRedacted: 0, confirmed: false };
|
|
2890
|
+
return { planned: plans, untouched, filesRewritten: 0, secretsRedacted: 0, confirmed: false };
|
|
2745
2891
|
}
|
|
2746
2892
|
let filesRewritten = 0;
|
|
2747
2893
|
let secretsRedacted = 0;
|
|
@@ -2749,12 +2895,18 @@ var WipeRun = class {
|
|
|
2749
2895
|
secretsRedacted += await this.#rewrite(plan.file);
|
|
2750
2896
|
filesRewritten++;
|
|
2751
2897
|
}
|
|
2752
|
-
return { planned: plans, filesRewritten, secretsRedacted, confirmed: true };
|
|
2898
|
+
return { planned: plans, untouched, filesRewritten, secretsRedacted, confirmed: true };
|
|
2753
2899
|
}
|
|
2754
2900
|
async #plan() {
|
|
2755
2901
|
const plans = [];
|
|
2902
|
+
const untouched = [];
|
|
2756
2903
|
for (const source of this.#sources) {
|
|
2757
2904
|
for await (const file of source.discover()) {
|
|
2905
|
+
if (file.messages !== void 0) {
|
|
2906
|
+
if (!await this.#holdsSecret(file.messages)) continue;
|
|
2907
|
+
untouched.push(file.path);
|
|
2908
|
+
continue;
|
|
2909
|
+
}
|
|
2758
2910
|
let lines = 0;
|
|
2759
2911
|
let secrets = 0;
|
|
2760
2912
|
for await (const text of this.#textOf(file)) {
|
|
@@ -2768,12 +2920,25 @@ var WipeRun = class {
|
|
|
2768
2920
|
}
|
|
2769
2921
|
}
|
|
2770
2922
|
}
|
|
2771
|
-
return plans;
|
|
2923
|
+
return { plans, untouched };
|
|
2924
|
+
}
|
|
2925
|
+
/** True for a database that cannot be read: it may hold one, and saying
|
|
2926
|
+
* nothing about it would let a wipe pass for complete. */
|
|
2927
|
+
async #holdsSecret(messages) {
|
|
2928
|
+
if (messages == null) return true;
|
|
2929
|
+
try {
|
|
2930
|
+
for await (const message of messages()) {
|
|
2931
|
+
if (this.#scanner.secretsOf(message.text).size > 0) return true;
|
|
2932
|
+
}
|
|
2933
|
+
return false;
|
|
2934
|
+
} catch {
|
|
2935
|
+
return true;
|
|
2936
|
+
}
|
|
2772
2937
|
}
|
|
2773
2938
|
/** The rewrite below is textual and kind-blind; only counting needs to know
|
|
2774
2939
|
* which reader gets the file. */
|
|
2775
2940
|
async *#textOf(file) {
|
|
2776
|
-
if (file.
|
|
2941
|
+
if (!isJsonl(file.path)) {
|
|
2777
2942
|
for await (const line of readLines(file.path)) yield line.text;
|
|
2778
2943
|
return;
|
|
2779
2944
|
}
|
|
@@ -2836,7 +3001,8 @@ async function main(argv) {
|
|
|
2836
3001
|
const sources = [
|
|
2837
3002
|
new ClaudeCodeSource(homedir()),
|
|
2838
3003
|
new CodexSource(homedir()),
|
|
2839
|
-
new CursorSource(homedir())
|
|
3004
|
+
new CursorSource(homedir()),
|
|
3005
|
+
new OpencodeSource(homedir())
|
|
2840
3006
|
];
|
|
2841
3007
|
const scanner = new Scanner(new KeywordIndex(compileRules(SECRET_RULE_SOURCES)));
|
|
2842
3008
|
return command === Command.wipe ? await wipe(sources, scanner, write) : await scan(sources, scanner, write);
|
|
@@ -2855,7 +3021,13 @@ async function wipe(sources, scanner, write) {
|
|
|
2855
3021
|
write(dim(" replacing credentials in your local transcripts with a placeholder"));
|
|
2856
3022
|
write("");
|
|
2857
3023
|
const outcome = await new WipeRun(sources, scanner, new TerminalPrompt(), write).run();
|
|
3024
|
+
for (const path of outcome.untouched) {
|
|
3025
|
+
write(yellow(` Not rewritten: ${shorten(path)} is a database, and wipe only rewrites text.`));
|
|
3026
|
+
write(dim(" Delete those sessions in the agent itself, and rotate what they held."));
|
|
3027
|
+
write("");
|
|
3028
|
+
}
|
|
2858
3029
|
if (outcome.planned.length === 0) {
|
|
3030
|
+
if (outcome.untouched.length > 0) return 1;
|
|
2859
3031
|
write(green(" No credentials found. Nothing to wipe."));
|
|
2860
3032
|
return 0;
|
|
2861
3033
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whatileaked",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scan your local Claude Code, Codex and
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Scan your local Claude Code, Codex, Cursor and opencode transcripts for credentials you already sent to a model provider.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"imports": {
|
|
7
7
|
"#scripts/*": "./scripts/*.ts",
|