spillage 0.9.0__tar.gz

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 (51) hide show
  1. spillage-0.9.0/LICENSE +21 -0
  2. spillage-0.9.0/PKG-INFO +541 -0
  3. spillage-0.9.0/README.md +506 -0
  4. spillage-0.9.0/pyproject.toml +68 -0
  5. spillage-0.9.0/setup.cfg +4 -0
  6. spillage-0.9.0/spillage/__init__.py +3 -0
  7. spillage-0.9.0/spillage/__main__.py +6 -0
  8. spillage-0.9.0/spillage/assets/report.html +254 -0
  9. spillage-0.9.0/spillage/cli.py +552 -0
  10. spillage-0.9.0/spillage/completions.py +116 -0
  11. spillage-0.9.0/spillage/doctor.py +121 -0
  12. spillage-0.9.0/spillage/envfiles.py +113 -0
  13. spillage-0.9.0/spillage/guard.py +559 -0
  14. spillage-0.9.0/spillage/html.py +43 -0
  15. spillage-0.9.0/spillage/models.py +185 -0
  16. spillage-0.9.0/spillage/repo.py +166 -0
  17. spillage-0.9.0/spillage/repo_output.py +103 -0
  18. spillage-0.9.0/spillage/reporters.py +286 -0
  19. spillage-0.9.0/spillage/rules.py +868 -0
  20. spillage-0.9.0/spillage/scanner.py +350 -0
  21. spillage-0.9.0/spillage/scrub.py +268 -0
  22. spillage-0.9.0/spillage/sources.py +1095 -0
  23. spillage-0.9.0/spillage/term.py +123 -0
  24. spillage-0.9.0/spillage/walker.py +67 -0
  25. spillage-0.9.0/spillage/watch.py +284 -0
  26. spillage-0.9.0/spillage.egg-info/PKG-INFO +541 -0
  27. spillage-0.9.0/spillage.egg-info/SOURCES.txt +49 -0
  28. spillage-0.9.0/spillage.egg-info/dependency_links.txt +1 -0
  29. spillage-0.9.0/spillage.egg-info/entry_points.txt +2 -0
  30. spillage-0.9.0/spillage.egg-info/requires.txt +5 -0
  31. spillage-0.9.0/spillage.egg-info/top_level.txt +1 -0
  32. spillage-0.9.0/tests/test_cli.py +161 -0
  33. spillage-0.9.0/tests/test_completions.py +70 -0
  34. spillage-0.9.0/tests/test_core_review.py +187 -0
  35. spillage-0.9.0/tests/test_cursor.py +86 -0
  36. spillage-0.9.0/tests/test_doctor.py +68 -0
  37. spillage-0.9.0/tests/test_envfiles.py +176 -0
  38. spillage-0.9.0/tests/test_guard.py +416 -0
  39. spillage-0.9.0/tests/test_guard_shells.py +44 -0
  40. spillage-0.9.0/tests/test_html.py +81 -0
  41. spillage-0.9.0/tests/test_more_agents.py +69 -0
  42. spillage-0.9.0/tests/test_plugin.py +56 -0
  43. spillage-0.9.0/tests/test_project_logs.py +112 -0
  44. spillage-0.9.0/tests/test_repo.py +221 -0
  45. spillage-0.9.0/tests/test_rules.py +329 -0
  46. spillage-0.9.0/tests/test_sarif.py +118 -0
  47. spillage-0.9.0/tests/test_scanner.py +147 -0
  48. spillage-0.9.0/tests/test_scrub.py +306 -0
  49. spillage-0.9.0/tests/test_settings.py +172 -0
  50. spillage-0.9.0/tests/test_sources.py +160 -0
  51. spillage-0.9.0/tests/test_watch.py +252 -0
spillage-0.9.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maximilian Feix
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,541 @@
1
+ Metadata-Version: 2.4
2
+ Name: spillage
3
+ Version: 0.9.0
4
+ Summary: Find the API keys and passwords your coding agents spilled into their logs: Claude Code, Codex, Gemini CLI, Cline and more. Scan, scrub, and block the next one.
5
+ Author: Maximilian Feix
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/maximilianfeix/spillage
8
+ Project-URL: Issues, https://github.com/maximilianfeix/spillage/issues
9
+ Project-URL: Changelog, https://github.com/maximilianfeix/spillage/blob/main/CHANGELOG.md
10
+ Keywords: secrets,secret-scanner,claude-code,codex,gemini-cli,ai-agents,security,api-keys,credentials,redaction,devsecops
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Security
26
+ Classifier: Topic :: Software Development :: Quality Assurance
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest; extra == "dev"
32
+ Requires-Dist: hypothesis; extra == "dev"
33
+ Requires-Dist: ruff==0.16.9; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ <div align="center">
37
+
38
+ <picture>
39
+ <source media="(prefers-color-scheme: dark)" srcset="docs/banner-dark.svg">
40
+ <source media="(prefers-color-scheme: light)" srcset="docs/banner-light.svg">
41
+ <img src="docs/banner-dark.svg" alt="spillage – your coding agents spill secrets. Find them, scrub them, block the next one." width="100%">
42
+ </picture>
43
+
44
+ [![tests](https://github.com/maximilianfeix/spillage/actions/workflows/tests.yml/badge.svg)](https://github.com/maximilianfeix/spillage/actions/workflows/tests.yml)
45
+ [![Release](https://img.shields.io/github/v/release/maximilianfeix/spillage?style=flat-square&color=FF6B4A&labelColor=0E0F13)](https://github.com/maximilianfeix/spillage/releases/latest)
46
+ [![Python](https://img.shields.io/badge/python-3.9–3.14-FF6B4A?style=flat-square&labelColor=0E0F13)](pyproject.toml)
47
+ [![Dependencies](https://img.shields.io/badge/dependencies-0-FF6B4A?style=flat-square&labelColor=0E0F13)](pyproject.toml)
48
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/maximilianfeix/spillage/badge)](https://scorecard.dev/viewer/?uri=github.com/maximilianfeix/spillage)
49
+ [![Network](https://img.shields.io/badge/network-never-FF6B4A?style=flat-square&labelColor=0E0F13)](#faq)
50
+ [![License](https://img.shields.io/badge/license-MIT-FF6B4A?style=flat-square&labelColor=0E0F13)](LICENSE)
51
+
52
+ <a href="#install"><img src="https://img.shields.io/badge/Install-FF6B4A?style=for-the-badge&labelColor=0E0F13" alt="Install"></a>
53
+ <a href="#scrub"><img src="https://img.shields.io/badge/Scrub-0E0F13?style=for-the-badge" alt="Scrub"></a>
54
+ <a href="#guard"><img src="https://img.shields.io/badge/Guard_hooks-0E0F13?style=for-the-badge" alt="Guard hooks for Claude Code, Codex and Gemini CLI"></a>
55
+ <a href="#what-it-finds"><img src="https://img.shields.io/badge/87_rules-0E0F13?style=for-the-badge" alt="87 rules"></a>
56
+
57
+ [Website](https://maximilianfeix.github.io/spillage/) · [Install](#install) · [Where it looks](#where-it-looks) · [What it finds](#what-it-finds) · [Scrub](#scrub) · [Guard](#guard) · [Repos & CI](#repo) · [Compared](#compared) · [How it works](#how-it-works) · [FAQ](#faq)
58
+
59
+ </div>
60
+
61
+ ---
62
+
63
+ Your coding agent writes down everything. Every `.env` it read, every key you pasted "just to test this one call", every `printenv` it ran to debug something. Claude Code, Codex, Gemini CLI, Cline and the rest keep those conversations on your disk as plain JSON, and every one of those keys was also sent to the model provider when it happened.
64
+
65
+ The Claude Code docs say it themselves: *"Transcripts and history are not encrypted at rest. OS file permissions are the only protection. If a tool reads a `.env` file or a command prints a credential, that value is written to `projects/<project>/<session>.jsonl`."* ([source](https://code.claude.com/docs/en/claude-directory))
66
+
67
+ **spillage** finds them. It reads the logs of thirteen coding agents, tells you which keys leaked, *how* they got there (you pasted it, a tool printed it, the model repeated it) and links you to the page where you rotate each one. Then it scrubs them from disk and installs hooks so the next one gets blocked before it's sent.
68
+
69
+ <div align="center">
70
+ <img src="docs/demo.svg" alt="Animated demo: spillage finds a GitHub token, a Stripe key and an Anthropic key in Claude Code and Codex logs, scrubs them and installs the guard hooks" width="860">
71
+ </div>
72
+
73
+ - **Zero dependencies, zero network.** Standard library only. Nothing leaves your machine, ever. Secrets are only ever shown masked.
74
+ - **Knows the formats.** Tells a pasted prompt from tool output from a model answer, per agent. Finds keys inside JSON-escaped text, like a private key a tool printed with `\n` in it.
75
+ - **Few false alarms.** GitHub tokens are checked against their built-in CRC32, JWTs have to decode, Discord tokens have to hold a real user id, placeholders like `sk-...your-key-here` are skipped.
76
+ - **Fast.** Rules run over the raw files with literal-prefix regexes on all cores. About 240 MB of real agent history in under 4 seconds on a laptop.
77
+ - **Fixes it, too.** `scrub` redacts in place without breaking the JSON your agent reads back for `--resume`. `guard` blocks the next leak in Claude Code, Codex and Gemini CLI.
78
+
79
+ <details>
80
+ <summary><b>Table of contents</b></summary>
81
+
82
+ - [Install](#install)
83
+ - [Where it looks](#where-it-looks)
84
+ - [What it finds](#what-it-finds)
85
+ - [Commands](#commands) · [Reports](#reports)
86
+ - [Scrub](#scrub)
87
+ - [Guard](#guard) · [Watch](#watch)
88
+ - [Committed transcripts](#repo): GitHub Action, pre-commit
89
+ - [Compared with gitleaks, TruffleHog and ggshield](#compared)
90
+ - [How it works](#how-it-works)
91
+ - [From Python](#from-python)
92
+ - [FAQ](#faq)
93
+ - [Roadmap](#roadmap) · [Community](#community) · [Contributing](#contributing)
94
+
95
+ </details>
96
+
97
+ <a id="install"></a>
98
+
99
+ ## Install
100
+
101
+ ```bash
102
+ brew install maximilianfeix/tap/spillage
103
+ spillage
104
+ ```
105
+
106
+ Or with [pipx](https://pipx.pypa.io/):
107
+
108
+ ```bash
109
+ pipx install git+https://github.com/maximilianfeix/spillage
110
+ ```
111
+
112
+ Or run it once without installing anything, with [uv](https://docs.astral.sh/uv/):
113
+
114
+ ```bash
115
+ uvx --from git+https://github.com/maximilianfeix/spillage spillage
116
+ ```
117
+
118
+ Python 3.9 or newer, on macOS, Linux and Windows. No dependencies to audit, which seems fair for a tool you point at your secrets.
119
+
120
+ ### As a Claude Code plugin
121
+
122
+ Only want the [guard hooks](#guard) in Claude Code? Then there is nothing to install besides the plugin. Inside Claude Code:
123
+
124
+ ```
125
+ /plugin install spillage --marketplace maximilianfeix/spillage
126
+ ```
127
+
128
+ It blocks prompts that contain a key, keeps the agent from reading `.env` files and printing secrets, and scrubs the transcript when a session ends. It runs from the plugin's own folder with the Python you already have (3.9 or newer). Scanning and scrubbing what is already in your logs needs the command line tool above; with it installed, use either the plugin or `spillage guard install`, not both.
129
+
130
+ <a id="where-it-looks"></a>
131
+
132
+ ## Where it looks
133
+
134
+ | Agent | What gets read |
135
+ | --- | --- |
136
+ | **Claude Code** | `~/.claude/projects/**/*.jsonl` (sessions), `history.jsonl`, `file-history/` (backups of files it edited), `paste-cache/`, `shell-snapshots/`, `todos/`. Honors `CLAUDE_CONFIG_DIR` |
137
+ | **Codex CLI** | `~/.codex/sessions/`, `archived_sessions/`, `history.jsonl`, `log/`. Honors `CODEX_HOME` |
138
+ | **Gemini CLI** | `~/.gemini/tmp/*/chats/`, `logs.json`, checkpoints |
139
+ | **OpenCode** | `~/.local/share/opencode/storage/` |
140
+ | **Cline, Roo Code, Kilo Code** | task histories in the extension storage of VS Code, Cursor, Windsurf, VSCodium and Kiro |
141
+ | **Continue** | `~/.continue/sessions/` |
142
+ | **Cursor** | chats in `state.vscdb` (SQLite), global and per workspace. Only chat rows are read, never Cursor's own login |
143
+ | **Qwen Code** | `~/.qwen/tmp/*/chats/`, logs and checkpoints |
144
+ | **Goose** | `~/.local/share/goose/sessions/sessions.db` (SQLite) and older `~/.config/goose/sessions/*.jsonl` |
145
+ | **Crush** | `.crush/crush.db` (SQLite) in each project it knows about |
146
+ | **Aider** | `.aider.chat.history.md` and `.aider.input.history` in your projects |
147
+ | **SpecStory** | `.specstory/history/*.md` in your projects |
148
+ | **GitHub Copilot CLI** | `~/.copilot/session-state/`, `history-session-state/` |
149
+ | | Aider and SpecStory write into the project folder, so spillage checks the current folder plus every folder your Claude Code and Codex sessions ran in |
150
+ | **Agent settings** | MCP servers and allowed commands, see [below](#settings) |
151
+ | **anything else** | `spillage --path <file or folder>`, any mix of JSONL, JSON and text |
152
+
153
+ `spillage agents` shows what it found on your machine:
154
+
155
+ ```
156
+ ● claude Claude Code 441 files, 216.4 MB
157
+ ~/.claude
158
+ ● codex Codex CLI 4 files, 55.0 MB
159
+ ~/.codex
160
+ ○ gemini Gemini CLI not found
161
+ ~/.gemini
162
+ ```
163
+
164
+ <a id="settings"></a>
165
+
166
+ ### Agent settings
167
+
168
+ Keys don't only end up in transcripts. They sit in plain text in the agents' settings, which every agent reads on start:
169
+
170
+ - **MCP servers** with a token in their `env` or `headers`: `~/.claude.json`, `.mcp.json`, `~/.cursor/mcp.json`, Claude Desktop's `claude_desktop_config.json`, `~/.codex/config.toml`, `~/.gemini/settings.json`, Windsurf, VS Code's `mcp.json`, Cline/Roo/Kilo, OpenCode, Crush, Goose, Continue and Qwen
171
+ - **commands you allowed once**: approve `curl -H "Authorization: Bearer …"` in Claude Code and the whole command, key included, is saved in `.claude/settings.local.json`
172
+ - **old prompt history** that Claude Code kept per project in `~/.claude.json`
173
+
174
+ spillage checks the global files and the ones in your project folders, and shows them as *saved in an agent's settings*. Claude Code's own login in `~/.claude.json` is not a leak and is left out. `scrub` doesn't touch the MCP servers and commands in these files, since whatever needs the key would stop working (prompts in that old history do get redacted); move the key into an environment variable (Claude Code expands `"GITHUB_TOKEN": "${GITHUB_TOKEN}"` in `.mcp.json` from your shell), then rotate it. `spillage scan --agent config` looks at nothing else.
175
+
176
+ <a id="what-it-finds"></a>
177
+
178
+ ## What it finds
179
+
180
+ 87 rules. Each one knows the key's exact shape and where to revoke it.
181
+
182
+ | | |
183
+ | --- | --- |
184
+ | **AI providers** | Anthropic (API, admin, and Claude Code OAuth tokens from `claude setup-token`), OpenAI (user, project, service account, admin), OpenRouter, Google AI / Gemini, Hugging Face, Groq, xAI, Perplexity, Replicate |
185
+ | **AI app stack** | Supabase secret keys, LangSmith, Pinecone, Tavily, Firecrawl, Resend, PostHog, Vercel Blob. Most of these aren't in gitleaks' default rules |
186
+ | **Code and packages** | GitHub (classic, OAuth, app, refresh, fine-grained; checksum-verified), GitLab, npm, PyPI |
187
+ | **Deploy and hosting** | Docker Hub, Fly.io, Netlify, Render, Heroku, Scalingo, Pulumi, Infracost, Tailscale, Cloudflare Origin CA keys, Azure storage account keys, age secret keys, RubyGems, Clojars, Bitbucket app passwords, GitLab agent tokens |
188
+ | **Cloud and infra** | AWS access key id and secret key, Google OAuth client secrets and refresh tokens, DigitalOcean, Databricks, Doppler, HashiCorp Vault, 1Password service accounts, PlanetScale |
189
+ | **Payments and SaaS** | Stripe (live is critical, test is low), Slack tokens and webhooks, Discord bot tokens and webhooks, Telegram bots, SendGrid, Brevo, Twilio, Shopify, Linear, Notion, Sentry, Grafana, Postman, Atlassian, Figma, Square, Stripe webhook secrets, Slack app tokens, New Relic, Mapbox, Airtable, ElevenLabs, E2B, Prefect, ReadMe, Duffel, EasyPost, Frame.io |
190
+ | **Everything else** | private keys (RSA, EC, OpenSSH, PGP, …), database URLs with a password in them, JWTs (Supabase `anon` keys count as low), and `API_KEY=…` / `"client_secret": "…"` / `Bearer …` with a high-entropy value |
191
+
192
+ **Plus your own secrets.** Patterns can't recognise a random database password or an Azure key. So spillage also reads the `.env` files in your projects (the current folder and every folder your agent sessions ran in), takes the values of anything named like a key, token, secret or password, and looks for those exact strings in the logs. They show up as *Value of POSTGRES_PASSWORD from ~/code/shop/.env*, never with the value itself. `--no-env` turns that off; the `.env` files are only read, never changed.
193
+
194
+ `spillage rules` lists them with their ids. Leave some out with `--skip-rules jwt,generic-secret`.
195
+
196
+ <a id="commands"></a>
197
+
198
+ ## Commands
199
+
200
+ ```
201
+ spillage scan every agent it knows (same as `spillage scan`)
202
+ spillage scan --since 7d only logs written in the last week (also 12h, 2w, 2026-09-01)
203
+ spillage scan --agent claude only some agents, comma separated
204
+ spillage scan --min-severity high skip the low and medium stuff
205
+ spillage scan -v every place a secret was seen, not just the first
206
+ spillage scrub remove what it found from the logs (asks first)
207
+ spillage guard install agent hooks that block the next leak
208
+ spillage watch tell me the moment a new key lands in any agent's logs
209
+ spillage repo agent transcripts committed to this git repo, and secrets in them
210
+ spillage check "some text" scan a string, or stdin: pbpaste | spillage check
211
+ spillage ignore <fingerprint> stop reporting a secret (a test key, say)
212
+ spillage doctor where you stand: leaks, settings, guard hooks, this repo
213
+ spillage agents which agents were found, and where
214
+ spillage rules what it looks for
215
+ spillage completions zsh tab completion for zsh, bash or fish
216
+ ```
217
+
218
+ Tab completion for commands and options: add `eval "$(spillage completions zsh)"` to your `~/.zshrc` (or `bash` to `~/.bashrc`), or for fish run `spillage completions fish > ~/.config/fish/completions/spillage.fish`.
219
+
220
+ The exit code is `1` when something was found and `0` when not, so it drops into cron, a shell hook or CI as is. `--exit-zero` turns that off.
221
+
222
+ <a id="doctor"></a>
223
+
224
+ ### Doctor
225
+
226
+ One screen that says what is still open, and the command for each point:
227
+
228
+ ```
229
+ spillage doctor
230
+
231
+ ✓ logs 441 files from Claude Code, Codex CLI, Cursor
232
+ ✗ leaks 3 secrets in the logs, 2 critical
233
+ → spillage (rotate them, then: spillage scrub)
234
+ ✓ settings no keys in plain text in agent settings
235
+ ✗ guard hooks are off for Codex CLI
236
+ → spillage guard install
237
+ ✓ env looking for 12 values from your projects' .env files
238
+ ✓ repo no agent transcripts committed in this repository
239
+
240
+ 2 to fix. Run this again when you are done.
241
+ ```
242
+
243
+ It exits with `1` while something is open. `spillage doctor -f json` gives the same as data.
244
+
245
+ <a id="reports"></a>
246
+
247
+ ### Reports
248
+
249
+ ```bash
250
+ spillage scan -f html -o report.html # a page to open in the browser
251
+ spillage scan -f json # for scripts: masked values, fingerprints, every location
252
+ spillage scan -f markdown # for an issue or a PR
253
+ spillage scan -f sarif # SARIF 2.1.0 for code scanning tools
254
+ ```
255
+
256
+ The HTML report is a single file with no CDN or web fonts, so it works offline and doesn't load anything. It shows the severity split, how the secrets got there, a timeline of when they first leaked, and every place each one was seen, with filters and search. Light and dark.
257
+
258
+ <div align="center">
259
+ <img src="docs/report.png" alt="The HTML report: three secrets spilled, how they got there, a timeline, and one card per key with a rotate link" width="860">
260
+ </div>
261
+
262
+ None of the formats ever contain a full secret. They show the first few characters and a fingerprint (the first 12 hex characters of the secret's SHA-256), which is also what `spillage ignore` takes.
263
+
264
+ <a id="scrub"></a>
265
+
266
+ ## Scrub
267
+
268
+ ```bash
269
+ spillage scrub # shows what it will change and asks
270
+ spillage scrub --dry-run # only shows
271
+ spillage scrub --yes --only dfd5fe8c9655,a51887eac2a0
272
+ ```
273
+
274
+ Every occurrence is replaced with a marker like `[REDACTED:github-token:dfd5fe8c9655]`, so you can still see that something was there and which key it was.
275
+
276
+ It is careful with the files, because your agent reads them back when you resume a session:
277
+
278
+ - works on the raw text, so every byte it doesn't redact stays exactly as it was
279
+ - parses every changed JSON line again before writing; if a line would break, the file is left alone
280
+ - writes atomically and keeps the file mode, the modification time (so `claude --resume` still sorts right) and the line endings
281
+ - skips files written in the last minute, since that's most likely the session you're running it from, and leaves a file alone if it changes while being scrubbed
282
+ - leaves Claude's signed thinking blocks untouched (editing them breaks `--resume`) and tells you how many keys stayed in them
283
+
284
+ > [!IMPORTANT]
285
+ > Scrubbing cleans your disk. It does not un-send anything. Every key in these logs went to the model provider when the conversation happened, so **rotate first**, then scrub. The report links to the right page for each key.
286
+
287
+ <a id="guard"></a>
288
+
289
+ ## Guard
290
+
291
+ ```bash
292
+ spillage guard install # every agent it finds; --agent claude,codex,gemini to pick
293
+ spillage guard status
294
+ spillage guard uninstall
295
+ ```
296
+
297
+ Adds three hooks to **Claude Code**, **Codex CLI** and **Gemini CLI**:
298
+
299
+ | Hook | Claude Code / Codex | Gemini CLI | What it does |
300
+ | --- | --- | --- | --- |
301
+ | prompt | `UserPromptSubmit` | `BeforeAgent` | Blocks a prompt that contains a key, before it's sent. Put `spillage:allow` in the prompt if you really mean it. |
302
+ | tool | `PreToolUse` | `BeforeTool` | Blocks reading `.env` files, private keys and credential files (`.npmrc`, `.aws/credentials`, `*.pem`, …) and shell commands that would print secrets: `cat .env`, `printenv`, `echo $STRIPE_SECRET_KEY`, `gh auth token`, `security find-generic-password -w`, … in Bash and in PowerShell. The reason goes back to the model, so it asks you instead. `.env.example` and friends stay readable. |
303
+ | session end | `SessionEnd` | `SessionEnd` | Scrubs the transcript of the session that just ended. |
304
+
305
+ That last one exists because of something that came up while testing the first against the real Claude Code: **a blocked prompt still gets written into the session file.** It's never sent, but it ends up on disk as a `queue-operation` record. The session-end hook cleans that up, along with anything a tool printed that the other hook didn't catch.
306
+
307
+ The config goes where each agent expects it: `~/.claude/settings.json`, `~/.codex/hooks.json`, `~/.gemini/settings.json` (or the repo's folder with `--scope project`; `--scope local` exists only for Claude Code's `settings.local.json`). Gemini's settings may contain comments; they're read fine, but not written back, and the original is kept as a backup. Your other hooks and settings are left alone, a backup is kept the first time, and a file that isn't valid JSON is refused rather than overwritten. Each hook call takes about a tenth of a second.
308
+
309
+ The hooks are a guard rail, not a sandbox. They stop the ways a key usually ends up in a conversation: the agent reads `.env` to "check the config", runs `printenv` to debug, or you paste a key. An agent that is set on reading a file will find a way the hooks don't know (`cp .env notes.txt`, then read that), so they don't replace keeping keys out of the folders your agent works in, and a real sandbox if you need one.
310
+
311
+ > [!NOTE]
312
+ > Codex runs new hooks only after you've trusted them once: open Codex and run `/hooks`. Claude Code and Codex were tested end to end with their real CLIs; Gemini CLI follows its documented hook format.
313
+
314
+ <a id="watch"></a>
315
+
316
+ ## Watch
317
+
318
+ ```bash
319
+ spillage watch # report new secrets the moment an agent writes them
320
+ spillage watch --scrub # and scrub the file once it's been quiet for 90 s
321
+ ```
322
+
323
+ Hooks only exist for Claude Code, Codex and Gemini. `watch` covers every agent, Cursor, Cline and Aider included: it keeps an eye on all their logs, reads only what was appended since the last look, and shows a desktop notification (macOS, Linux and Windows) plus a line in the terminal as soon as a key lands:
324
+
325
+ ```
326
+ 09:12:53 [HIGH] Firecrawl API key fc-bcc…(35 chars) Claude Code · ~/code/shop
327
+ a tool printed it (a file read or a command) · rotate: https://www.firecrawl.dev/app/api-keys
328
+ ```
329
+
330
+ Keys that were already there when it started aren't reported again, that's what `spillage scan` is for. It polls every 2 seconds (`--interval`), which costs next to nothing and keeps spillage free of dependencies.
331
+
332
+ <a id="repo"></a>
333
+
334
+ ## Committed transcripts
335
+
336
+ Some agents write their chat logs into the project, and from there they get committed. GitHub's code search finds about **16,000 SpecStory chat logs** and **5,000 Aider histories** in public repositories (September 2026). Once a key is in a pushed repo, it's not a local problem anymore.
337
+
338
+ ```bash
339
+ spillage repo # this repository
340
+ spillage repo --strict # also fail on transcripts without secrets in them
341
+ spillage repo --all-files # scan every tracked file, not just transcripts
342
+ ```
343
+
344
+ ### GitHub Action
345
+
346
+ ```yaml
347
+ # .github/workflows/spillage.yml
348
+ on: [push, pull_request]
349
+ jobs:
350
+ spillage:
351
+ runs-on: ubuntu-latest
352
+ steps:
353
+ - uses: actions/checkout@v4
354
+ - uses: maximilianfeix/spillage@v0.9.0
355
+ with:
356
+ strict: true # fail on any committed transcript
357
+ ```
358
+
359
+ Each secret becomes an error annotation on the file and line, and the job summary gets a table with rotate links. Outputs: `transcripts` and `secrets` (counts). A test fixture that trips it? Put its fingerprint into a `.spillageignore` file in the repo.
360
+
361
+ To see them in the **Security tab** and on pull requests too, have it write SARIF and upload that:
362
+
363
+ ```yaml
364
+ permissions:
365
+ contents: read
366
+ security-events: write
367
+ steps:
368
+ - uses: actions/checkout@v4
369
+ - uses: maximilianfeix/spillage@v0.9.0
370
+ with:
371
+ sarif: spillage.sarif
372
+ - uses: github/codeql-action/upload-sarif@v3
373
+ if: always() # spillage fails the step when it finds something
374
+ with:
375
+ sarif_file: spillage.sarif
376
+ ```
377
+
378
+ Each secret becomes one alert per file, with the masked value, the fingerprint, how it got there and the rotate link. `spillage repo -f sarif` prints the same.
379
+
380
+ ### pre-commit
381
+
382
+ ```yaml
383
+ # .pre-commit-config.yaml
384
+ repos:
385
+ - repo: https://github.com/maximilianfeix/spillage
386
+ rev: v0.9.0
387
+ hooks:
388
+ - id: spillage # block transcripts that contain secrets
389
+ # - id: no-agent-transcripts # or block agent transcripts altogether
390
+ ```
391
+
392
+ <a id="compared"></a>
393
+
394
+ ## Compared with gitleaks, TruffleHog and ggshield
395
+
396
+ Run them too. They are built for repositories and live sessions, spillage is built for what your agent already kept on your disk.
397
+
398
+ | | spillage | gitleaks, TruffleHog | ggshield AI hooks |
399
+ | --- | --- | --- | --- |
400
+ | **Built for** | agent logs and settings on your machine | git repositories and their history | live agent sessions |
401
+ | **Keys already in the logs** | found, with the session, the project and whether you, a tool or the model wrote them | found as plain text, without the conversation around them | not covered, it scans sessions as they happen |
402
+ | **Cleaning up** | redacts in place, `--resume` keeps working | no | no |
403
+ | **Blocking the next one** | hooks for Claude Code, Codex and Gemini CLI | pre-commit, not inside the agent | hooks for Cursor, Claude Code, Codex, Copilot CLI and more |
404
+ | **Detection** | 87 rules plus the exact values from your `.env` files | hundreds of detectors; TruffleHog also tests whether a key is live | 600+ secret types |
405
+ | **Account and network** | none | none for gitleaks; TruffleHog calls the provider to verify | GitGuardian account, scans go through its API |
406
+
407
+ Where the others are ahead: far more detectors, live verification of keys (TruffleHog), and hooks for more tools (ggshield). If you only want to check that a few known keys are not in a folder you are about to publish, Simon Willison's [scan-for-secrets](https://github.com/simonw/scan-for-secrets) does exactly that.
408
+
409
+ Sources: the [gitleaks](https://github.com/gitleaks/gitleaks) and [TruffleHog](https://github.com/trufflesecurity/trufflehog) READMEs and GitGuardian's [AI coding tools docs](https://docs.gitguardian.com/ggshield-docs/integrations/ai-coding-tools/secret-scanning-for-ai-coding-tools), October 2026.
410
+
411
+ <a id="how-it-works"></a>
412
+
413
+ ## How it works
414
+
415
+ ```
416
+ agent logs ──▶ discover ──▶ raw text ──▶ 87 rules ──▶ validate ──▶ locate ──▶ dedupe ──▶ report
417
+ (13 agents) per agent per file literal- checksums, parse only one finding
418
+ prefix entropy, the JSON per secret
419
+ regexes placeholders line with
420
+ on all cores a match
421
+ ```
422
+
423
+ 1. **Discover.** Each agent has a small adapter that knows where its files live and how its records say who's talking.
424
+ 2. **Scan the raw text.** The rules run over each file as it is on disk, not over parsed JSON. Almost every rule starts with a literal (`ghp_`, `sk-ant-`, `AKIA`), which lets Python's regex engine jump straight to candidates instead of trying every position: 0.2 s instead of 6 s per rule on a few hundred MB. Patterns without a literal prefix (database URLs, `API_KEY=` assignments, Discord tokens) look up an anchor with `str.find` first and only run the regex there. Big scans fan out over up to 8 processes, and huge session files are cut into parts at line breaks so one 100 MB session doesn't keep a single core busy while the others wait.
425
+ 3. **Validate.** GitHub tokens carry a CRC32 of themselves and have to match it. JWTs have to decode to JSON with an `alg`. Discord bot tokens have to start with a real user id. Generic secrets need a key-ish name, enough entropy, mixed character classes and no placeholder words.
426
+ 4. **Locate.** Only the JSON line that holds a match gets parsed, to find out which session and project it belongs to and whether it came from a prompt, a tool result or the model. A match that only exists inside a base64 image or a thinking signature is dropped as noise.
427
+ 5. **Dedupe.** One finding per distinct secret, with every place it was seen, across agents and sessions.
428
+
429
+ Sources, rules and output formats are each a small registry, so adding an agent, a key type or a format is one class or function. See [CONTRIBUTING.md](CONTRIBUTING.md) and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for how the pieces fit.
430
+
431
+ <a id="from-python"></a>
432
+
433
+ ## From Python
434
+
435
+ ```python
436
+ from spillage.scanner import Scanner, scan_text
437
+ from spillage.sources import build_sources
438
+
439
+ result = Scanner().scan(build_sources(["claude", "codex"]))
440
+ for finding in result.findings:
441
+ print(finding.severity.label, finding.rule_name, finding.masked, len(finding.locations))
442
+
443
+ scan_text("does this contain a key?") # -> [] or a list of findings
444
+ ```
445
+
446
+ <a id="faq"></a>
447
+
448
+ ## FAQ
449
+
450
+ <details>
451
+ <summary><b>Does it send anything anywhere?</b></summary>
452
+ <br>
453
+
454
+ No. There is no network code in it at all: no update check, no telemetry, and it doesn't test whether keys are still valid (that would mean sending them somewhere). It reads files, and only writes when you run `scrub`, `ignore` or `guard`.
455
+
456
+ </details>
457
+
458
+ <details>
459
+ <summary><b>How do I know a release is what's in this repository?</b></summary>
460
+ <br>
461
+
462
+ From 0.9.0 on, every released file is built by the [release workflow](.github/workflows/release.yml) and carries a signed build attestation. With the GitHub CLI:
463
+
464
+ ```bash
465
+ gh attestation verify spillage-0.9.0-py3-none-any.whl --repo maximilianfeix/spillage
466
+ ```
467
+
468
+ It passes only for a file that this repository's workflow built from the tagged commit. The package has no dependencies, so that one file is everything that gets installed.
469
+
470
+ </details>
471
+
472
+ <details>
473
+ <summary><b>I deleted the key from the logs. Am I fine?</b></summary>
474
+ <br>
475
+
476
+ No. The key was sent to the model provider as part of the conversation when it happened. Rotate it. `spillage scrub` is for your disk, your backups and your Time Machine, not for undoing that.
477
+
478
+ </details>
479
+
480
+ <details>
481
+ <summary><b>Why not just run gitleaks or trufflehog on ~/.claude?</b></summary>
482
+ <br>
483
+
484
+ You can, and they're great at what they do. They're built for repositories, though: they don't know that a private key in a JSONL log is written with `\n` escapes, that a match in a base64 screenshot is noise, which session or project a line belongs to, or whether you pasted the key or a tool printed it. And they don't scrub the logs without breaking them, or hook into your agent.
485
+
486
+ </details>
487
+
488
+ <details>
489
+ <summary><b>It flagged something that isn't a secret.</b></summary>
490
+ <br>
491
+
492
+ `spillage ignore <fingerprint>` hides that one from now on (the fingerprints are in `~/.config/spillage/ignore`, one per line). If a whole rule is noisy for you, `--skip-rules`. And please [open an issue](https://github.com/maximilianfeix/spillage/issues/new?template=bug_report.yml) with the masked value and what it actually was, the rules get better from those.
493
+
494
+ </details>
495
+
496
+ <details>
497
+ <summary><b>It missed a key.</b></summary>
498
+ <br>
499
+
500
+ If it's a kind of key spillage doesn't know, [request a rule](https://github.com/maximilianfeix/spillage/issues/new?template=new_rule.yml) (describe its shape, don't paste it). If it's a format it should know, run `spillage check "…"` on a made-up key with the same shape and open a bug with that.
501
+
502
+ </details>
503
+
504
+ <details>
505
+ <summary><b>Does it work with Cursor?</b></summary>
506
+ <br>
507
+
508
+ Yes. Cursor keeps its chats in SQLite (`state.vscdb`), spillage opens those read-only and without taking a lock, so it's fine while Cursor is running. `scrub` doesn't touch that database yet; delete the chat in Cursor instead. Cline, Roo and Kilo inside Cursor are covered too.
509
+
510
+ </details>
511
+
512
+ <a id="roadmap"></a>
513
+
514
+ ## Roadmap
515
+
516
+ - [x] [CI on all three operating systems](https://github.com/maximilianfeix/spillage/issues/1)
517
+ - [x] [Cursor support](https://github.com/maximilianfeix/spillage/issues/7)
518
+ - [x] [Split huge session files across cores](https://github.com/maximilianfeix/spillage/issues/6)
519
+ - [x] [`spillage doctor`](https://github.com/maximilianfeix/spillage/issues/64), shell completions, 87 rules
520
+ - [x] Claude Code plugin, guard for the PowerShell tool, signed releases
521
+ - [ ] PyPI release
522
+
523
+ <a id="community"></a>
524
+
525
+ ## Community
526
+
527
+ - **Questions, ideas, a setup worth showing:** [Discussions](https://github.com/maximilianfeix/spillage/discussions)
528
+ - **A key it missed or a false alarm:** [open an issue](https://github.com/maximilianfeix/spillage/issues/new/choose), with the masked value only
529
+ - **If it found something on your machine,** a star helps the next person find it before their key does
530
+
531
+ <a id="contributing"></a>
532
+
533
+ ## Contributing
534
+
535
+ Rules and agent adapters are the most useful things to add. [CONTRIBUTING.md](CONTRIBUTING.md) has the setup and the one important rule: never commit a real secret, not even a revoked one. The tests build their fake keys at runtime.
536
+
537
+ Found a way to make spillage leak what it protects? Please report it [privately](https://github.com/maximilianfeix/spillage/security/advisories/new).
538
+
539
+ ## License
540
+
541
+ MIT