whatileaked 0.4.0 → 0.5.1
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 +21 -13
- package/dist/cli.js +213 -53
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ is all — no network connection is made at any point.
|
|
|
58
58
|
### When it finds nothing
|
|
59
59
|
|
|
60
60
|
```
|
|
61
|
-
scanned 561 transcripts · 197,016 messages
|
|
61
|
+
scanned 561 transcripts · 197,016 messages · 3 memory files
|
|
62
62
|
|
|
63
63
|
No credentials found.
|
|
64
64
|
```
|
|
@@ -68,7 +68,7 @@ Nothing to do. This is the good outcome.
|
|
|
68
68
|
### When it finds something
|
|
69
69
|
|
|
70
70
|
```
|
|
71
|
-
scanned 561 transcripts · 196,986 messages
|
|
71
|
+
scanned 561 transcripts · 196,986 messages · 3 memory files
|
|
72
72
|
|
|
73
73
|
* aws-access-token 2 secrets
|
|
74
74
|
f0b93847 billing-api sent 25 times
|
|
@@ -205,18 +205,26 @@ npx whatileaked scan || echo "found something"
|
|
|
205
205
|
|
|
206
206
|
| Agent | Folder |
|
|
207
207
|
| --- | --- |
|
|
208
|
-
| Claude Code | `~/.claude/projects/` · `~/.claude/CLAUDE.md` |
|
|
209
|
-
| 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` |
|
|
210
210
|
| Cursor | `~/.cursor/projects/*/agent-transcripts/` |
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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.
|
|
220
228
|
|
|
221
229
|
Nowhere else. It does not read your repos, your shell history, or your
|
|
222
230
|
environment. Adding support for another agent is one small file — pull requests
|
package/dist/cli.js
CHANGED
|
@@ -161,6 +161,7 @@ var TerminalReporter = class {
|
|
|
161
161
|
if (totals.length === 0) {
|
|
162
162
|
this.#write(green("No credentials found."));
|
|
163
163
|
this.#write("");
|
|
164
|
+
if (this.#notScanned(stats)) this.#write("");
|
|
164
165
|
this.#write(dim(DISCLOSURE));
|
|
165
166
|
this.#share(sent, stats);
|
|
166
167
|
return;
|
|
@@ -186,11 +187,7 @@ var TerminalReporter = class {
|
|
|
186
187
|
red("Remove them there as well as rotating, or the next session sends them again.")
|
|
187
188
|
);
|
|
188
189
|
}
|
|
189
|
-
|
|
190
|
-
this.#write(
|
|
191
|
-
yellow(`${plural(stats.unscannable, "message")} could not be scanned and are not counted.`)
|
|
192
|
-
);
|
|
193
|
-
}
|
|
190
|
+
this.#notScanned(stats);
|
|
194
191
|
this.#write("");
|
|
195
192
|
this.#write(dim("The 8-character code is a fingerprint, never the secret: a truncated"));
|
|
196
193
|
this.#write(dim("SHA-256, so one credential shows the same code everywhere it leaked."));
|
|
@@ -199,6 +196,24 @@ var TerminalReporter = class {
|
|
|
199
196
|
this.#write(dim(DISCLOSURE));
|
|
200
197
|
this.#share(sent, stats);
|
|
201
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Everything that was found and not looked at: refused messages, then one
|
|
201
|
+
* plain line per file not read. Printed under a clean result too, which is
|
|
202
|
+
* exactly where leaving it out would mislead — a clean scan once returned
|
|
203
|
+
* before the refused count, so a partial scan read as a clean one. Answers
|
|
204
|
+
* whether it wrote anything.
|
|
205
|
+
*/
|
|
206
|
+
#notScanned(stats) {
|
|
207
|
+
if (stats.unscannable > 0) {
|
|
208
|
+
this.#write(
|
|
209
|
+
yellow(`${plural(stats.unscannable, "message")} could not be scanned and are not counted.`)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
for (const file of stats.unread) {
|
|
213
|
+
this.#write(yellow(`Found ${shorten(file.path)} but did not scan it: ${file.reason}.`));
|
|
214
|
+
}
|
|
215
|
+
return stats.unscannable > 0 || stats.unread.length > 0;
|
|
216
|
+
}
|
|
202
217
|
/**
|
|
203
218
|
* The one line meant to be pasted somewhere public. Everything above it names
|
|
204
219
|
* projects, paths and fingerprints, so nobody shares it; this is counts only,
|
|
@@ -2398,6 +2413,50 @@ async function* readLines(path) {
|
|
|
2398
2413
|
}
|
|
2399
2414
|
}
|
|
2400
2415
|
|
|
2416
|
+
// src/sources/listing.ts
|
|
2417
|
+
import { readdir, realpath, stat } from "fs/promises";
|
|
2418
|
+
import { join } from "path";
|
|
2419
|
+
async function listDirectories(root) {
|
|
2420
|
+
try {
|
|
2421
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
2422
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2423
|
+
} catch {
|
|
2424
|
+
return [];
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
var FileExtension = {
|
|
2428
|
+
jsonl: ".jsonl",
|
|
2429
|
+
markdown: ".md",
|
|
2430
|
+
text: ".txt",
|
|
2431
|
+
database: ".db"
|
|
2432
|
+
};
|
|
2433
|
+
function isJsonl(path) {
|
|
2434
|
+
return path.endsWith(FileExtension.jsonl);
|
|
2435
|
+
}
|
|
2436
|
+
async function listFiles(dir, extension) {
|
|
2437
|
+
try {
|
|
2438
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
2439
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(extension)).map((entry) => entry.name);
|
|
2440
|
+
} catch {
|
|
2441
|
+
return [];
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
async function resolveFile(path) {
|
|
2445
|
+
try {
|
|
2446
|
+
const real = await realpath(path);
|
|
2447
|
+
return (await stat(real)).isFile() ? real : null;
|
|
2448
|
+
} catch {
|
|
2449
|
+
return null;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
async function walkDirectories(root) {
|
|
2453
|
+
const found = [root];
|
|
2454
|
+
for (const name of await listDirectories(root)) {
|
|
2455
|
+
found.push(...await walkDirectories(join(root, name)));
|
|
2456
|
+
}
|
|
2457
|
+
return found;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2401
2460
|
// src/transcript/reader.ts
|
|
2402
2461
|
import { createReadStream as createReadStream2 } from "fs";
|
|
2403
2462
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -2430,6 +2489,7 @@ var TranscriptReader = class {
|
|
|
2430
2489
|
};
|
|
2431
2490
|
|
|
2432
2491
|
// src/scan-run.ts
|
|
2492
|
+
var NEEDS_SQLITE = "reading it needs Node 22.13 or newer";
|
|
2433
2493
|
var ScanRun = class {
|
|
2434
2494
|
#sources;
|
|
2435
2495
|
#scanner;
|
|
@@ -2450,12 +2510,19 @@ var ScanRun = class {
|
|
|
2450
2510
|
memoryFiles: 0,
|
|
2451
2511
|
entries: 0,
|
|
2452
2512
|
skipped: 0,
|
|
2453
|
-
unscannable: 0
|
|
2513
|
+
unscannable: 0,
|
|
2514
|
+
unread: []
|
|
2454
2515
|
};
|
|
2455
2516
|
for (const source of this.#sources) {
|
|
2456
2517
|
for await (const file of source.discover()) {
|
|
2457
|
-
if (file.
|
|
2458
|
-
stats
|
|
2518
|
+
if (file.messages !== void 0) {
|
|
2519
|
+
await this.#readDatabase(file, file.messages, findings, stats);
|
|
2520
|
+
this.#progress.advance(stats.transcripts + stats.memoryFiles, file.project);
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
if (file.kind === FileKind.transcript) stats.transcripts++;
|
|
2524
|
+
else stats.memoryFiles++;
|
|
2525
|
+
if (isJsonl(file.path)) {
|
|
2459
2526
|
const reader = new TranscriptReader();
|
|
2460
2527
|
for await (const entry of reader.read(file.path)) {
|
|
2461
2528
|
this.#collect(file, entry.line, entry.index, findings, stats);
|
|
@@ -2463,7 +2530,7 @@ var ScanRun = class {
|
|
|
2463
2530
|
stats.entries += reader.stats.entries;
|
|
2464
2531
|
stats.skipped += reader.stats.skipped;
|
|
2465
2532
|
} else {
|
|
2466
|
-
stats.
|
|
2533
|
+
if (file.kind === FileKind.transcript) stats.entries++;
|
|
2467
2534
|
for await (const line of readLines(file.path)) {
|
|
2468
2535
|
this.#collect(file, line.text, line.number, findings, stats);
|
|
2469
2536
|
}
|
|
@@ -2475,6 +2542,24 @@ var ScanRun = class {
|
|
|
2475
2542
|
this.#reporter.report(findings, stats);
|
|
2476
2543
|
return findings.length;
|
|
2477
2544
|
}
|
|
2545
|
+
/** A database counts as one transcript once it has been read. One it could
|
|
2546
|
+
* not open is listed instead, so a scan that skipped it never reads as clean. */
|
|
2547
|
+
async #readDatabase(file, messages, findings, stats) {
|
|
2548
|
+
if (messages === null) {
|
|
2549
|
+
stats.unread.push({ path: file.path, reason: NEEDS_SQLITE });
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
let index = 0;
|
|
2553
|
+
try {
|
|
2554
|
+
for await (const message of messages()) {
|
|
2555
|
+
stats.entries++;
|
|
2556
|
+
this.#collect({ ...file, project: message.project }, message.text, index++, findings, stats);
|
|
2557
|
+
}
|
|
2558
|
+
stats.transcripts++;
|
|
2559
|
+
} catch {
|
|
2560
|
+
stats.unread.push({ path: file.path, reason: "it could not be opened" });
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2478
2563
|
/** One entry the engine chokes on must not cost the other 700,000. */
|
|
2479
2564
|
#collect(file, text, at, findings, stats) {
|
|
2480
2565
|
let matches = [];
|
|
@@ -2502,47 +2587,12 @@ var ScanRun = class {
|
|
|
2502
2587
|
// src/sources/claude-code-source.ts
|
|
2503
2588
|
import { basename, join as join2 } from "path";
|
|
2504
2589
|
|
|
2505
|
-
// src/sources/listing.ts
|
|
2506
|
-
import { readdir, realpath, stat } from "fs/promises";
|
|
2507
|
-
import { join } from "path";
|
|
2508
|
-
async function listDirectories(root) {
|
|
2509
|
-
try {
|
|
2510
|
-
const entries = await readdir(root, { withFileTypes: true });
|
|
2511
|
-
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2512
|
-
} catch {
|
|
2513
|
-
return [];
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
|
-
var FileExtension = { jsonl: ".jsonl", markdown: ".md" };
|
|
2517
|
-
async function listFiles(dir, extension) {
|
|
2518
|
-
try {
|
|
2519
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
2520
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(extension)).map((entry) => entry.name);
|
|
2521
|
-
} catch {
|
|
2522
|
-
return [];
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
async function resolveFile(path) {
|
|
2526
|
-
try {
|
|
2527
|
-
const real = await realpath(path);
|
|
2528
|
-
return (await stat(real)).isFile() ? real : null;
|
|
2529
|
-
} catch {
|
|
2530
|
-
return null;
|
|
2531
|
-
}
|
|
2532
|
-
}
|
|
2533
|
-
async function walkDirectories(root) {
|
|
2534
|
-
const found = [root];
|
|
2535
|
-
for (const name of await listDirectories(root)) {
|
|
2536
|
-
found.push(...await walkDirectories(join(root, name)));
|
|
2537
|
-
}
|
|
2538
|
-
return found;
|
|
2539
|
-
}
|
|
2540
|
-
|
|
2541
2590
|
// src/transcript/entry.ts
|
|
2542
2591
|
var SourceName = {
|
|
2543
2592
|
"claude-code": "claude-code",
|
|
2544
2593
|
codex: "codex",
|
|
2545
|
-
cursor: "cursor"
|
|
2594
|
+
cursor: "cursor",
|
|
2595
|
+
opencode: "opencode"
|
|
2546
2596
|
};
|
|
2547
2597
|
|
|
2548
2598
|
// src/transcript/shape.ts
|
|
@@ -2570,6 +2620,19 @@ var ClaudeCodeSource = class {
|
|
|
2570
2620
|
if (instructions !== null) {
|
|
2571
2621
|
yield { source: this.name, kind: FileKind.memory, path: instructions, project: this.name };
|
|
2572
2622
|
}
|
|
2623
|
+
const history = await resolveFile(join2(this.#home, ".claude", "history.jsonl"));
|
|
2624
|
+
if (history !== null) {
|
|
2625
|
+
yield { source: this.name, kind: FileKind.transcript, path: history, project: this.name };
|
|
2626
|
+
}
|
|
2627
|
+
const pastes = join2(this.#home, ".claude", "paste-cache");
|
|
2628
|
+
for (const file of await listFiles(pastes, FileExtension.text)) {
|
|
2629
|
+
yield {
|
|
2630
|
+
source: this.name,
|
|
2631
|
+
kind: FileKind.transcript,
|
|
2632
|
+
path: join2(pastes, file),
|
|
2633
|
+
project: this.name
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2573
2636
|
const root = join2(this.#home, ".claude", "projects");
|
|
2574
2637
|
for (const slug of await listDirectories(root)) {
|
|
2575
2638
|
const dir = join2(root, slug);
|
|
@@ -2623,6 +2686,10 @@ var CodexSource = class {
|
|
|
2623
2686
|
if (instructions !== null) {
|
|
2624
2687
|
yield { source: this.name, kind: FileKind.memory, path: instructions, project: this.name };
|
|
2625
2688
|
}
|
|
2689
|
+
const history = await resolveFile(join3(this.#home, ".codex", "history.jsonl"));
|
|
2690
|
+
if (history !== null) {
|
|
2691
|
+
yield { source: this.name, kind: FileKind.transcript, path: history, project: this.name };
|
|
2692
|
+
}
|
|
2626
2693
|
for (const root of ["sessions", "archived_sessions"]) {
|
|
2627
2694
|
for (const dir of await walkDirectories(join3(this.#home, ".codex", root))) {
|
|
2628
2695
|
for (const file of await listFiles(dir, FileExtension.jsonl)) {
|
|
@@ -2706,6 +2773,73 @@ async function isDirectory(path) {
|
|
|
2706
2773
|
}
|
|
2707
2774
|
}
|
|
2708
2775
|
|
|
2776
|
+
// src/sources/opencode-source.ts
|
|
2777
|
+
import { basename as basename4, join as join5 } from "path";
|
|
2778
|
+
var OpencodeSource = class {
|
|
2779
|
+
name = SourceName.opencode;
|
|
2780
|
+
#home;
|
|
2781
|
+
#load;
|
|
2782
|
+
constructor(home2, load = loadSqlite) {
|
|
2783
|
+
this.#home = home2;
|
|
2784
|
+
this.#load = load;
|
|
2785
|
+
}
|
|
2786
|
+
async *discover() {
|
|
2787
|
+
const dir = join5(this.#home, ".local", "share", "opencode");
|
|
2788
|
+
for (const file of await listFiles(dir, FileExtension.database)) {
|
|
2789
|
+
const path = join5(dir, file);
|
|
2790
|
+
const sqlite = await this.#load();
|
|
2791
|
+
yield {
|
|
2792
|
+
source: this.name,
|
|
2793
|
+
kind: FileKind.transcript,
|
|
2794
|
+
path,
|
|
2795
|
+
project: this.name,
|
|
2796
|
+
messages: sqlite === null ? null : () => readMessages(sqlite, path, this.name)
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
};
|
|
2801
|
+
async function* readMessages(sqlite, path, fallback) {
|
|
2802
|
+
const db = new sqlite.DatabaseSync(path, { readOnly: true });
|
|
2803
|
+
try {
|
|
2804
|
+
const directories = /* @__PURE__ */ new Map();
|
|
2805
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (?, ?)").all("session", "session_v2");
|
|
2806
|
+
for (const { name } of tables) {
|
|
2807
|
+
for (const row of db.prepare(`SELECT id, directory FROM "${String(name)}"`).iterate()) {
|
|
2808
|
+
if (typeof row.id === "string" && typeof row.directory === "string") {
|
|
2809
|
+
directories.set(row.id, row.directory);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
for (const row of db.prepare("SELECT session_id, data FROM session_message").iterate()) {
|
|
2814
|
+
if (typeof row.data !== "string") continue;
|
|
2815
|
+
const directory = typeof row.session_id === "string" ? directories.get(row.session_id) : null;
|
|
2816
|
+
yield { project: directory == null ? fallback : basename4(directory), text: row.data };
|
|
2817
|
+
}
|
|
2818
|
+
} finally {
|
|
2819
|
+
db.close();
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
var loading = null;
|
|
2823
|
+
function loadSqlite() {
|
|
2824
|
+
loading ??= importSqlite();
|
|
2825
|
+
return loading;
|
|
2826
|
+
}
|
|
2827
|
+
async function importSqlite() {
|
|
2828
|
+
const emitWarning = process.emitWarning;
|
|
2829
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
2830
|
+
if (String(warning).includes("SQLite")) return;
|
|
2831
|
+
Reflect.apply(emitWarning, process, [warning, ...rest]);
|
|
2832
|
+
});
|
|
2833
|
+
try {
|
|
2834
|
+
const specifier = "node:sqlite";
|
|
2835
|
+
return await import(specifier);
|
|
2836
|
+
} catch {
|
|
2837
|
+
return null;
|
|
2838
|
+
} finally {
|
|
2839
|
+
process.emitWarning = emitWarning;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2709
2843
|
// src/wipe/confirm.ts
|
|
2710
2844
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
2711
2845
|
var CONFIRMATION = "wipe";
|
|
@@ -2749,16 +2883,16 @@ var WipeRun = class {
|
|
|
2749
2883
|
this.#write = write;
|
|
2750
2884
|
}
|
|
2751
2885
|
async run() {
|
|
2752
|
-
const plans = await this.#plan();
|
|
2886
|
+
const { plans, untouched } = await this.#plan();
|
|
2753
2887
|
if (plans.length === 0) {
|
|
2754
|
-
return { planned: [], filesRewritten: 0, secretsRedacted: 0, confirmed: false };
|
|
2888
|
+
return { planned: [], untouched, filesRewritten: 0, secretsRedacted: 0, confirmed: false };
|
|
2755
2889
|
}
|
|
2756
2890
|
this.#preview(plans);
|
|
2757
2891
|
const answer = await this.#prompt.ask(
|
|
2758
2892
|
` Type ${bold(red(CONFIRMATION))} to rewrite these files, or anything else to stop: `
|
|
2759
2893
|
);
|
|
2760
2894
|
if (answer !== CONFIRMATION) {
|
|
2761
|
-
return { planned: plans, filesRewritten: 0, secretsRedacted: 0, confirmed: false };
|
|
2895
|
+
return { planned: plans, untouched, filesRewritten: 0, secretsRedacted: 0, confirmed: false };
|
|
2762
2896
|
}
|
|
2763
2897
|
let filesRewritten = 0;
|
|
2764
2898
|
let secretsRedacted = 0;
|
|
@@ -2766,12 +2900,18 @@ var WipeRun = class {
|
|
|
2766
2900
|
secretsRedacted += await this.#rewrite(plan.file);
|
|
2767
2901
|
filesRewritten++;
|
|
2768
2902
|
}
|
|
2769
|
-
return { planned: plans, filesRewritten, secretsRedacted, confirmed: true };
|
|
2903
|
+
return { planned: plans, untouched, filesRewritten, secretsRedacted, confirmed: true };
|
|
2770
2904
|
}
|
|
2771
2905
|
async #plan() {
|
|
2772
2906
|
const plans = [];
|
|
2907
|
+
const untouched = [];
|
|
2773
2908
|
for (const source of this.#sources) {
|
|
2774
2909
|
for await (const file of source.discover()) {
|
|
2910
|
+
if (file.messages !== void 0) {
|
|
2911
|
+
if (!await this.#holdsSecret(file.messages)) continue;
|
|
2912
|
+
untouched.push(file.path);
|
|
2913
|
+
continue;
|
|
2914
|
+
}
|
|
2775
2915
|
let lines = 0;
|
|
2776
2916
|
let secrets = 0;
|
|
2777
2917
|
for await (const text of this.#textOf(file)) {
|
|
@@ -2785,12 +2925,25 @@ var WipeRun = class {
|
|
|
2785
2925
|
}
|
|
2786
2926
|
}
|
|
2787
2927
|
}
|
|
2788
|
-
return plans;
|
|
2928
|
+
return { plans, untouched };
|
|
2929
|
+
}
|
|
2930
|
+
/** True for a database that cannot be read: it may hold one, and saying
|
|
2931
|
+
* nothing about it would let a wipe pass for complete. */
|
|
2932
|
+
async #holdsSecret(messages) {
|
|
2933
|
+
if (messages == null) return true;
|
|
2934
|
+
try {
|
|
2935
|
+
for await (const message of messages()) {
|
|
2936
|
+
if (this.#scanner.secretsOf(message.text).size > 0) return true;
|
|
2937
|
+
}
|
|
2938
|
+
return false;
|
|
2939
|
+
} catch {
|
|
2940
|
+
return true;
|
|
2941
|
+
}
|
|
2789
2942
|
}
|
|
2790
2943
|
/** The rewrite below is textual and kind-blind; only counting needs to know
|
|
2791
2944
|
* which reader gets the file. */
|
|
2792
2945
|
async *#textOf(file) {
|
|
2793
|
-
if (file.
|
|
2946
|
+
if (!isJsonl(file.path)) {
|
|
2794
2947
|
for await (const line of readLines(file.path)) yield line.text;
|
|
2795
2948
|
return;
|
|
2796
2949
|
}
|
|
@@ -2853,7 +3006,8 @@ async function main(argv) {
|
|
|
2853
3006
|
const sources = [
|
|
2854
3007
|
new ClaudeCodeSource(homedir()),
|
|
2855
3008
|
new CodexSource(homedir()),
|
|
2856
|
-
new CursorSource(homedir())
|
|
3009
|
+
new CursorSource(homedir()),
|
|
3010
|
+
new OpencodeSource(homedir())
|
|
2857
3011
|
];
|
|
2858
3012
|
const scanner = new Scanner(new KeywordIndex(compileRules(SECRET_RULE_SOURCES)));
|
|
2859
3013
|
return command === Command.wipe ? await wipe(sources, scanner, write) : await scan(sources, scanner, write);
|
|
@@ -2872,7 +3026,13 @@ async function wipe(sources, scanner, write) {
|
|
|
2872
3026
|
write(dim(" replacing credentials in your local transcripts with a placeholder"));
|
|
2873
3027
|
write("");
|
|
2874
3028
|
const outcome = await new WipeRun(sources, scanner, new TerminalPrompt(), write).run();
|
|
3029
|
+
for (const path of outcome.untouched) {
|
|
3030
|
+
write(yellow(` Not rewritten: ${shorten(path)} is a database, and wipe only rewrites text.`));
|
|
3031
|
+
write(dim(" Delete those sessions in the agent itself, and rotate what they held."));
|
|
3032
|
+
write("");
|
|
3033
|
+
}
|
|
2875
3034
|
if (outcome.planned.length === 0) {
|
|
3035
|
+
if (outcome.untouched.length > 0) return 1;
|
|
2876
3036
|
write(green(" No credentials found. Nothing to wipe."));
|
|
2877
3037
|
return 0;
|
|
2878
3038
|
}
|
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.1",
|
|
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",
|