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.
Files changed (3) hide show
  1. package/README.md +13 -5
  2. package/dist/cli.js +88 -20
  3. 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 or
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. Adding support for another agent is one small file — pull requests
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
- #home;
2624
- constructor(home2) {
2625
- this.#home = home2;
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.#home, ".claude", "CLAUDE.md"));
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.#home, ".claude", "history.jsonl"));
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.#home, ".claude", "paste-cache");
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.#home, ".claude", "projects");
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
- #home;
2690
- constructor(home2) {
2691
- this.#home = home2;
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.#home, ".codex", name));
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.#home, ".codex", "history.jsonl"));
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.#home, ".codex", root))) {
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 join5 } from "path";
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 = join5(this.#home, ".local", "share", "opencode");
2845
+ const dir = join6(this.#home, ".local", "share", "opencode");
2799
2846
  for (const file of await listFiles(dir, FileExtension.database)) {
2800
- const path = join5(dir, file);
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
- new ClaudeCodeSource(homedir()),
3019
- new CodexSource(homedir()),
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.5.2",
4
- "description": "Scan your local Claude Code, Codex, Cursor and opencode transcripts for credentials you already sent to a model provider.",
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",