whatileaked 0.5.2 → 0.6.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 +13 -5
- package/dist/cli.js +88 -20
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
Your coding agent saves every conversation to your disk. If you ever pasted an
|
|
6
6
|
API key into a chat, or asked the agent to read a `.env` file, that key is now
|
|
7
|
-
sitting in a log file on your laptop — and it was uploaded to Anthropic
|
|
8
|
-
OpenAI the moment you sent the message.
|
|
7
|
+
sitting in a log file on your laptop — and it was uploaded to Anthropic,
|
|
8
|
+
OpenAI or Google the moment you sent the message.
|
|
9
9
|
|
|
10
10
|
Secret scanners point at repositories and CI. You can aim one at `~/.claude` by
|
|
11
11
|
hand, but nobody does — it is not a repo, it is not in CI, and it does not look
|
|
@@ -114,7 +114,9 @@ fingerprint — so it is the part you can paste or screenshot anywhere:
|
|
|
114
114
|
whatileaked: 3 credentials sent to a model provider, across 561 transcripts.
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
A clean scan ends the same way, with `no credentials`.
|
|
117
|
+
A clean scan ends the same way, with `no credentials`. If some of it could not
|
|
118
|
+
be scanned, the line says how much, still in counts only — `(7 messages and 1
|
|
119
|
+
file not scanned)` — so a pasted partial scan never reads as a clean one.
|
|
118
120
|
|
|
119
121
|
---
|
|
120
122
|
|
|
@@ -209,6 +211,7 @@ npx whatileaked scan || echo "found something"
|
|
|
209
211
|
| Codex | `~/.codex/sessions/` · `~/.codex/archived_sessions/` · `~/.codex/history.jsonl` · `~/.codex/AGENTS.md` · `~/.codex/AGENTS.override.md` |
|
|
210
212
|
| Cursor | `~/.cursor/projects/*/agent-transcripts/` |
|
|
211
213
|
| opencode | `~/.local/share/opencode/opencode.db` |
|
|
214
|
+
| Gemini CLI | `~/.gemini/tmp/*/chats/` · `~/.gemini/tmp/*/memory/` · `~/.gemini/GEMINI.md` |
|
|
212
215
|
|
|
213
216
|
opencode keeps its sessions in a SQLite database. Reading it needs Node's
|
|
214
217
|
built-in `node:sqlite`, which loads without a flag from Node 22.13. On an older
|
|
@@ -222,6 +225,11 @@ scanned yet. Agent transcripts are covered; Ask history is not.
|
|
|
222
225
|
|
|
223
226
|
Claude Code transcripts it set aside (`*.jsonl.superseded-*`) are scanned too.
|
|
224
227
|
|
|
228
|
+
If you moved Claude Code's folder with `CLAUDE_CONFIG_DIR`, Codex's with
|
|
229
|
+
`CODEX_HOME`, or Gemini CLI's with `GEMINI_CLI_HOME`, the folder it names is
|
|
230
|
+
scanned as well as the default one, which still holds every session from before
|
|
231
|
+
the move.
|
|
232
|
+
|
|
225
233
|
Transcripts record credentials that were already sent. So does prompt history,
|
|
226
234
|
which keeps every prompt you typed and every long paste after the session itself
|
|
227
235
|
has been cleaned up. Instruction and memory files are worse: they are read again
|
|
@@ -229,8 +237,8 @@ at the start of the next session, so a credential in one keeps leaking until the
|
|
|
229
237
|
file is edited. All of them are scanned, and `wipe` rewrites all of them.
|
|
230
238
|
|
|
231
239
|
Nowhere else. It does not read your repos, your shell history, or your
|
|
232
|
-
environment
|
|
233
|
-
welcome.
|
|
240
|
+
environment — beyond those three variables, to know where to look. Adding
|
|
241
|
+
support for another agent is one small file — pull requests welcome.
|
|
234
242
|
|
|
235
243
|
---
|
|
236
244
|
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
4
|
import { homedir } from "os";
|
|
5
|
+
import { join as join8 } from "path";
|
|
5
6
|
|
|
6
7
|
// src/commands.ts
|
|
7
8
|
var Command = {
|
|
@@ -220,13 +221,19 @@ var TerminalReporter = class {
|
|
|
220
221
|
* and stays that way. It is last so a screenshot can crop to it, unstyled so
|
|
221
222
|
* a copy carries nothing but the words, and it repeats the headline's
|
|
222
223
|
* transmitted-only count for the same reason the headline keeps memory files
|
|
223
|
-
* out of it.
|
|
224
|
+
* out of it. A partial scan says so here in counts, because this is the line
|
|
225
|
+
* that gets cropped to: without it a pasted "no credentials" overclaims.
|
|
224
226
|
*/
|
|
225
227
|
#share(sent, stats) {
|
|
226
228
|
const credentials = sent === 0 ? "no credentials" : plural(sent, "credential");
|
|
229
|
+
const missed = [
|
|
230
|
+
...stats.unscannable > 0 ? [plural(stats.unscannable, "message")] : [],
|
|
231
|
+
...stats.unread.length > 0 ? [plural(stats.unread.length, "file")] : []
|
|
232
|
+
];
|
|
233
|
+
const partial = missed.length === 0 ? "" : ` (${missed.join(" and ")} not scanned)`;
|
|
227
234
|
this.#write("");
|
|
228
235
|
this.#write(
|
|
229
|
-
`whatileaked: ${credentials} sent to a model provider, across ${plural(stats.transcripts, "transcript")}.`
|
|
236
|
+
`whatileaked: ${credentials} sent to a model provider, across ${plural(stats.transcripts, "transcript")}${partial}.`
|
|
230
237
|
);
|
|
231
238
|
}
|
|
232
239
|
};
|
|
@@ -2426,6 +2433,7 @@ async function listDirectories(root) {
|
|
|
2426
2433
|
}
|
|
2427
2434
|
var FileExtension = {
|
|
2428
2435
|
jsonl: ".jsonl",
|
|
2436
|
+
json: ".json",
|
|
2429
2437
|
markdown: ".md",
|
|
2430
2438
|
text: ".txt",
|
|
2431
2439
|
database: ".db"
|
|
@@ -2601,6 +2609,7 @@ var SourceName = {
|
|
|
2601
2609
|
"claude-code": "claude-code",
|
|
2602
2610
|
codex: "codex",
|
|
2603
2611
|
cursor: "cursor",
|
|
2612
|
+
"gemini-cli": "gemini-cli",
|
|
2604
2613
|
opencode: "opencode"
|
|
2605
2614
|
};
|
|
2606
2615
|
|
|
@@ -2620,20 +2629,22 @@ function objectField(source, key) {
|
|
|
2620
2629
|
// src/sources/claude-code-source.ts
|
|
2621
2630
|
var ClaudeCodeSource = class {
|
|
2622
2631
|
name = SourceName["claude-code"];
|
|
2623
|
-
#
|
|
2624
|
-
|
|
2625
|
-
|
|
2632
|
+
#dir;
|
|
2633
|
+
/** `dir` is where Claude Code keeps its state: `~/.claude`, or wherever
|
|
2634
|
+
* `CLAUDE_CONFIG_DIR` moved it. */
|
|
2635
|
+
constructor(home2, dir = join2(home2, ".claude")) {
|
|
2636
|
+
this.#dir = dir;
|
|
2626
2637
|
}
|
|
2627
2638
|
async *discover() {
|
|
2628
|
-
const instructions = await resolveFile(join2(this.#
|
|
2639
|
+
const instructions = await resolveFile(join2(this.#dir, "CLAUDE.md"));
|
|
2629
2640
|
if (instructions !== null) {
|
|
2630
2641
|
yield { source: this.name, kind: FileKind.memory, path: instructions, project: this.name };
|
|
2631
2642
|
}
|
|
2632
|
-
const history = await resolveFile(join2(this.#
|
|
2643
|
+
const history = await resolveFile(join2(this.#dir, "history.jsonl"));
|
|
2633
2644
|
if (history !== null) {
|
|
2634
2645
|
yield { source: this.name, kind: FileKind.transcript, path: history, project: this.name };
|
|
2635
2646
|
}
|
|
2636
|
-
const pastes = join2(this.#
|
|
2647
|
+
const pastes = join2(this.#dir, "paste-cache");
|
|
2637
2648
|
for (const file of await listFiles(pastes, FileExtension.text)) {
|
|
2638
2649
|
yield {
|
|
2639
2650
|
source: this.name,
|
|
@@ -2642,7 +2653,7 @@ var ClaudeCodeSource = class {
|
|
|
2642
2653
|
project: this.name
|
|
2643
2654
|
};
|
|
2644
2655
|
}
|
|
2645
|
-
const root = join2(this.#
|
|
2656
|
+
const root = join2(this.#dir, "projects");
|
|
2646
2657
|
for (const slug of await listDirectories(root)) {
|
|
2647
2658
|
const dir = join2(root, slug);
|
|
2648
2659
|
let project = slug;
|
|
@@ -2686,23 +2697,25 @@ async function firstCwd(path) {
|
|
|
2686
2697
|
import { basename as basename2, join as join3 } from "path";
|
|
2687
2698
|
var CodexSource = class {
|
|
2688
2699
|
name = SourceName.codex;
|
|
2689
|
-
#
|
|
2690
|
-
|
|
2691
|
-
|
|
2700
|
+
#dir;
|
|
2701
|
+
/** `dir` is where Codex keeps its state: `~/.codex`, or wherever
|
|
2702
|
+
* `CODEX_HOME` moved it. */
|
|
2703
|
+
constructor(home2, dir = join3(home2, ".codex")) {
|
|
2704
|
+
this.#dir = dir;
|
|
2692
2705
|
}
|
|
2693
2706
|
async *discover() {
|
|
2694
2707
|
for (const name of ["AGENTS.override.md", "AGENTS.md"]) {
|
|
2695
|
-
const instructions = await resolveFile(join3(this.#
|
|
2708
|
+
const instructions = await resolveFile(join3(this.#dir, name));
|
|
2696
2709
|
if (instructions !== null) {
|
|
2697
2710
|
yield { source: this.name, kind: FileKind.memory, path: instructions, project: this.name };
|
|
2698
2711
|
}
|
|
2699
2712
|
}
|
|
2700
|
-
const history = await resolveFile(join3(this.#
|
|
2713
|
+
const history = await resolveFile(join3(this.#dir, "history.jsonl"));
|
|
2701
2714
|
if (history !== null) {
|
|
2702
2715
|
yield { source: this.name, kind: FileKind.transcript, path: history, project: this.name };
|
|
2703
2716
|
}
|
|
2704
2717
|
for (const root of ["sessions", "archived_sessions"]) {
|
|
2705
|
-
for (const dir of await walkDirectories(join3(this.#
|
|
2718
|
+
for (const dir of await walkDirectories(join3(this.#dir, root))) {
|
|
2706
2719
|
for (const file of await listFiles(dir, FileExtension.jsonl)) {
|
|
2707
2720
|
const path = join3(dir, file);
|
|
2708
2721
|
const cwd = await firstCwd2(path);
|
|
@@ -2784,8 +2797,42 @@ async function isDirectory(path) {
|
|
|
2784
2797
|
}
|
|
2785
2798
|
}
|
|
2786
2799
|
|
|
2800
|
+
// src/sources/gemini-cli-source.ts
|
|
2801
|
+
import { join as join5 } from "path";
|
|
2802
|
+
var GeminiCliSource = class {
|
|
2803
|
+
name = SourceName["gemini-cli"];
|
|
2804
|
+
#dir;
|
|
2805
|
+
/** `dir` is `~/.gemini`, or `$GEMINI_CLI_HOME/.gemini` when that is set. */
|
|
2806
|
+
constructor(home2, dir = join5(home2, ".gemini")) {
|
|
2807
|
+
this.#dir = dir;
|
|
2808
|
+
}
|
|
2809
|
+
async *discover() {
|
|
2810
|
+
const instructions = await resolveFile(join5(this.#dir, "GEMINI.md"));
|
|
2811
|
+
if (instructions !== null) {
|
|
2812
|
+
yield { source: this.name, kind: FileKind.memory, path: instructions, project: this.name };
|
|
2813
|
+
}
|
|
2814
|
+
const root = join5(this.#dir, "tmp");
|
|
2815
|
+
for (const id of await listDirectories(root)) {
|
|
2816
|
+
const project = legacyHash.test(id) ? this.name : id;
|
|
2817
|
+
for (const dir of await walkDirectories(join5(root, id, "chats"))) {
|
|
2818
|
+
for (const extension of [FileExtension.jsonl, FileExtension.json]) {
|
|
2819
|
+
for (const file of await listFiles(dir, extension)) {
|
|
2820
|
+
yield { source: this.name, kind: FileKind.transcript, path: join5(dir, file), project };
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
for (const dir of await walkDirectories(join5(root, id, "memory"))) {
|
|
2825
|
+
for (const file of await listFiles(dir, FileExtension.markdown)) {
|
|
2826
|
+
yield { source: this.name, kind: FileKind.memory, path: join5(dir, file), project };
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
var legacyHash = /^[0-9a-f]{64}$/;
|
|
2833
|
+
|
|
2787
2834
|
// src/sources/opencode-source.ts
|
|
2788
|
-
import { basename as basename4, join as
|
|
2835
|
+
import { basename as basename4, join as join6 } from "path";
|
|
2789
2836
|
var OpencodeSource = class {
|
|
2790
2837
|
name = SourceName.opencode;
|
|
2791
2838
|
#home;
|
|
@@ -2795,9 +2842,9 @@ var OpencodeSource = class {
|
|
|
2795
2842
|
this.#load = load;
|
|
2796
2843
|
}
|
|
2797
2844
|
async *discover() {
|
|
2798
|
-
const dir =
|
|
2845
|
+
const dir = join6(this.#home, ".local", "share", "opencode");
|
|
2799
2846
|
for (const file of await listFiles(dir, FileExtension.database)) {
|
|
2800
|
-
const path =
|
|
2847
|
+
const path = join6(dir, file);
|
|
2801
2848
|
const sqlite = await this.#load();
|
|
2802
2849
|
yield {
|
|
2803
2850
|
source: this.name,
|
|
@@ -2851,6 +2898,17 @@ async function importSqlite() {
|
|
|
2851
2898
|
}
|
|
2852
2899
|
}
|
|
2853
2900
|
|
|
2901
|
+
// src/sources/state-dirs.ts
|
|
2902
|
+
import { join as join7, resolve } from "path";
|
|
2903
|
+
function stateDirs(home2, folder, moved) {
|
|
2904
|
+
const dirs = [join7(home2, folder)];
|
|
2905
|
+
if (moved !== void 0 && moved !== "") {
|
|
2906
|
+
const dir = resolve(moved);
|
|
2907
|
+
if (!dirs.includes(dir)) dirs.push(dir);
|
|
2908
|
+
}
|
|
2909
|
+
return dirs;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2854
2912
|
// src/wipe/confirm.ts
|
|
2855
2913
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
2856
2914
|
var CONFIRMATION = "wipe";
|
|
@@ -3015,9 +3073,19 @@ async function main(argv) {
|
|
|
3015
3073
|
return 0;
|
|
3016
3074
|
}
|
|
3017
3075
|
const sources = [
|
|
3018
|
-
|
|
3019
|
-
|
|
3076
|
+
...stateDirs(homedir(), ".claude", process.env.CLAUDE_CONFIG_DIR).map(
|
|
3077
|
+
(dir) => new ClaudeCodeSource(homedir(), dir)
|
|
3078
|
+
),
|
|
3079
|
+
...stateDirs(homedir(), ".codex", process.env.CODEX_HOME).map(
|
|
3080
|
+
(dir) => new CodexSource(homedir(), dir)
|
|
3081
|
+
),
|
|
3020
3082
|
new CursorSource(homedir()),
|
|
3083
|
+
// GEMINI_CLI_HOME stands in for the home directory, not for `.gemini`.
|
|
3084
|
+
...stateDirs(
|
|
3085
|
+
homedir(),
|
|
3086
|
+
".gemini",
|
|
3087
|
+
process.env.GEMINI_CLI_HOME ? join8(process.env.GEMINI_CLI_HOME, ".gemini") : void 0
|
|
3088
|
+
).map((dir) => new GeminiCliSource(homedir(), dir)),
|
|
3021
3089
|
new OpencodeSource(homedir())
|
|
3022
3090
|
];
|
|
3023
3091
|
const scanner = new Scanner(new KeywordIndex(compileRules(SECRET_RULE_SOURCES)));
|
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, Cursor and
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Scan your local Claude Code, Codex, Cursor, opencode and Gemini CLI transcripts for credentials you already sent to a model provider.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"imports": {
|
|
7
7
|
"#scripts/*": "./scripts/*.ts",
|