looselips-guard 0.2.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.
@@ -0,0 +1,28 @@
1
+ looselips-guard bundles secret-detection rules ported from gitleaks.
2
+ Source: https://github.com/gitleaks/gitleaks (config/gitleaks.toml)
3
+ The rules live in gitleaks_rules.py and are regenerated by
4
+ scripts/port_gitleaks_rules.py. gitleaks itself is not bundled.
5
+
6
+ gitleaks is distributed under the MIT Licence:
7
+
8
+ MIT License
9
+
10
+ Copyright (c) 2019 Zachary Rice
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
@@ -0,0 +1,490 @@
1
+ Metadata-Version: 2.4
2
+ Name: looselips-guard
3
+ Version: 0.2.0
4
+ Summary: Stops your coding agent publishing your data to the world
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/ed-is-ai/looselips-guard
7
+ Keywords: security,agent,hooks,egress,claude-code,codex,cursor
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Security
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: NOTICE
14
+ Dynamic: license-file
15
+
16
+ <p align="center">
17
+ <picture>
18
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
19
+ <img src="assets/logo.svg" alt="looselips-guard" width="360">
20
+ </picture>
21
+ </p>
22
+
23
+ <p align="center">
24
+ <img src="https://img.shields.io/badge/hook-PreToolUse-cf222e" alt="PreToolUse hook">
25
+ <img src="https://img.shields.io/badge/python-3.8%2B-3776ab" alt="Python 3.8+">
26
+ <img src="https://img.shields.io/badge/dependencies-none-2da44e" alt="No dependencies">
27
+ <img src="https://img.shields.io/badge/secret%20rules-221-8250df" alt="221 secret rules">
28
+ <img src="https://img.shields.io/badge/status-alpha-d29922" alt="Alpha">
29
+ </p>
30
+
31
+ <p align="center"><em>Loose lips sink shops.</em></p>
32
+
33
+ You want to run agents unattended. Sure, you can sandbox them and run a local
34
+ model — but to do anything useful you have to let them access the outside world: the
35
+ internet, git, your email. And nothing there stops an agent firing your personal
36
+ data when it shouldn't, because the model doesn't know any
37
+ better.
38
+
39
+ Existing agent tools focus on stopping your secrets reaching the model.
40
+ **looselips-guard catches an agent *accidentally* putting your data where the
41
+ world can see it.** It blocks a command *before it runs* when the outbound
42
+ payload contains, in plaintext, a banned string from your denylist, a
43
+ credential, or a suspiciously big file that's probably a database dump. The
44
+ credential check is [gitleaks](https://github.com/gitleaks/gitleaks)' 221
45
+ secret-detection rules ported to run in-process, not a separate scanner you
46
+ install or invoke.
47
+
48
+ It is a **plaintext denylist**, not a containment boundary. It will not stop an
49
+ agent that base64s the value first, splits it across two calls, or otherwise
50
+ means to get around it — see [What it does not stop](#what-it-does-not-stop). In
51
+ the incident that motivated this, every leak was accidental and in the clear; a
52
+ denylist covers that case well and nothing fancier was needed.
53
+
54
+ It watches `gh`, `git`, `curl`/`wget`, `scp`/`rsync`, `nc` and MCP tool calls
55
+ (full list [below](#what-it-intercepts)) — but the two that nothing else covers
56
+ are the reason it exists:
57
+
58
+ - **issue and PR bodies** — they never become git objects, so a git-history
59
+ scanner never sees them
60
+ - **`git add -A`** quietly sweeping a live database onto a public branch
61
+
62
+ ---
63
+
64
+ ## Getting started
65
+
66
+ **1. Get `looselips-guard` on your machine.** One of:
67
+
68
+ ```bash
69
+ npm install -g looselips-guard # puts `looselips-guard` on PATH; needs python3
70
+ ```
71
+
72
+ ```bash
73
+ git clone https://github.com/ed-is-ai/looselips-guard.git ~/looselips-guard
74
+ alias looselips-guard='python3 ~/looselips-guard/looselips_guard.py' # so the commands below work as written
75
+ ```
76
+
77
+ It's one dependency-free Python file either way. In Claude Code,
78
+
79
+ ```
80
+ /plugin marketplace add ed-is-ai/looselips-guard
81
+ /plugin install looselips-guard@reinvently
82
+ ```
83
+
84
+ also wires the Claude hook — but you still want one of the above for
85
+ `init` / `add` / `check`.
86
+
87
+ **2. Wire it and describe your data**, from the project you want guarded:
88
+
89
+ ```bash
90
+ looselips-guard init # scaffold config, detect your agent, wire its hook
91
+ looselips-guard add ZQXF VNTR Acct-99001122
92
+ looselips-guard add "ZQXF,VNTR,ACME Corp,Acct-99001122" # or one comma-separated list
93
+ looselips-guard add --like Acct-99001122 # block the format: \bAcct-\d{8}\b
94
+ looselips-guard presets # optional starter regex sets (internal, cloud, k8s)
95
+ looselips-guard routes # list egress routes; `routes disable git-push` to stop checking one
96
+ looselips-guard check # confirm it's guarding you
97
+ ```
98
+
99
+ Every route is checked by default. `looselips-guard routes disable nc curl`
100
+ turns individual ones off (persisted to `.looselips-guard.json`);
101
+ `routes enable` turns them back on. Routes: `gh`, `git-add`, `git-commit`,
102
+ `git-push`, `curl`, `scp`, `nc`, `mcp`.
103
+
104
+ `init` bakes the hook command in the form you invoked it — the `looselips-guard`
105
+ bin if you npm-installed, the script's own path if you cloned — so it keeps
106
+ working. It also writes `.looselips-guard.json`, detects your host from
107
+ `~/.claude`, `~/.codex`, `~/.cursor`, `~/.hermes` or `.github/`, and merges the
108
+ hook into that host's config file in place, leaving your other hooks alone.
109
+
110
+ Even before any `add`, the hook already blocks oversized files being staged and
111
+ any credential the ported [gitleaks](https://github.com/gitleaks/gitleaks) rules
112
+ recognise. `add` extends it with your own strings — tickers, account ids,
113
+ balances — that generic PII rules can't spot. For data that changes often, point
114
+ a `source` at it instead (see [Config](#config)) and it is re-read on every scan.
115
+ `.looselips-guard.json`, `.looselips-guard.list` and `.looselips-guard.snooze`
116
+ are never committed;
117
+ [`.looselips-blocklist-example.json`](.looselips-blocklist-example.json) is the
118
+ template `init` copies from.
119
+
120
+ ### Per host
121
+
122
+ `looselips-guard init <host>` when detection misses; `--global` writes the
123
+ home-directory config instead of the project one. What `init` does per host, and
124
+ the one thing worth knowing:
125
+
126
+ | `host` | `init` wires | Worth knowing |
127
+ |---|---|---|
128
+ | `claude` | `.claude/settings.json` | or `/plugin install looselips-guard@reinvently` in Claude Code |
129
+ | `codex` | `~/.codex/hooks.json` | same event shape as Claude, `exit 2` blocks |
130
+ | `copilot` | `.github/hooks/looselips-guard.json` | MCP needs `mcp_servers` set (below); known bugs, not ours — plugin hooks don't always fire ([#2540](https://github.com/github/copilot-cli/issues/2540)), subagents ungated ([#2392](https://github.com/github/copilot-cli/issues/2392)) |
131
+ | `cursor` | `~/.cursor/hooks.json` | `failClosed: true` — blocks on a slow hook instead of failing open |
132
+ | `hermes` | prints YAML for `~/.hermes/config.yaml` | shell tool is `terminal`; `fail_closed: true`, like Cursor |
133
+
134
+ **MCP coverage.** MCP tool names use the same `mcp__<server>__<tool>` convention
135
+ across Claude Code, Codex and Hermes, so `init` sets the matcher to
136
+ `Bash|mcp__.*` / `terminal|^mcp__` and the guard scans write-y MCP calls there
137
+ too. Cursor gets a separate `beforeMCPExecution` hook; the OpenClaw plugin
138
+ forwards MCP calls as well (ids are `<server>__<tool>`).
139
+
140
+ **Copilot** names MCP tools `<server>-<tool>` with no prefix — nothing to key on
141
+ by shape — so list the servers you want scanned in `mcp_servers` (see
142
+ [Config](#config)). `init` already sets a matcher that fires the hook on
143
+ write-verb tool names; `mcp_servers` is what decides whether the call is
144
+ actually scanned, so an unlisted server or a built-in `write_file` is left
145
+ alone. Its `preToolUse` is the current event (an older `toolCall` is gone) and
146
+ `exit 2` denies.
147
+
148
+ Two hosts need a hand because they have no shell-command hook:
149
+
150
+ - **OpenClaw / OpenClaw 2** — in-process TS plugin.
151
+ `cp integrations/openclaw/looselips-guard.plugin.ts ~/.openclaw/policies/`, then
152
+ add `"~/.openclaw/policies/looselips-guard.plugin.ts"` to `plugins.load.paths`
153
+ in `~/.openclaw/openclaw.json`. It forwards `exec` and MCP calls to
154
+ `looselips-guard` and returns `{ block }` on exit 2 or on a spawn error, so a
155
+ *crash* fails closed; OpenClaw's `before_tool_call` sets no default handler
156
+ timeout and its policy for a hung handler is undocumented, so a true *hang*
157
+ would stall the turn rather than fail either way.
158
+ - **DeepSeek Harness** — enable its Claude Code / Codex `hooks.json` bridge
159
+ ([`dsh-hooks-claude-code`](https://github.com/deepseek-ai/deepseek-harness)) and
160
+ point it at [`hooks/codex-hooks.json`](hooks/codex-hooks.json). Its pre-execute
161
+ waterfall honours exit 2; any other failure is "logged as non-blocking, action
162
+ proceeds" — fail-open, with no documented timeout. Unverified end to end.
163
+
164
+ Every host is fail-open on a slow hook except Cursor and Hermes. Claude Code and
165
+ Codex give it a 600 s window (see [How it works](#how-it-works)); OpenClaw,
166
+ DeepSeek, Copilot and opencode don't publish theirs.
167
+
168
+ ---
169
+
170
+ ## How it works
171
+
172
+ Four steps in one pass, and the host-specific part is almost nothing.
173
+
174
+ ```
175
+ agent runs a command
176
+ │
177
+ ▼
178
+ ┌───────────────────┐ the host pauses the tool call and pipes the
179
+ │ host pre-tool │ command to us as JSON on stdin
180
+ │ hook │
181
+ └─────────┬─────────┘
182
+ ▼
183
+ ┌───────────────────┐ read `command` and `cwd` from the event;
184
+ │ host adapter │ later, block with `exit 2` — that's the whole of it
185
+ └─────────┬─────────┘
186
+ ▼
187
+ ┌───────────────────┐ is this a write to the outside world? gh issue/pr,
188
+ │ matcher │ gh api, git add/commit/push, curl/wget, scp/rsync, nc, MCP
189
+ └─────────┬─────────┘
190
+ ▼
191
+ ┌───────────────────┐ --body-file, curl @file, scp/rsync sources read from
192
+ │ payload │ disk; git add via --dry-run, push via rev-list; MCP args walked
193
+ └─────────┬─────────┘
194
+ ▼
195
+ ┌───────────────────┐ denylist + your regex patterns · 221 secret
196
+ │ rules │ rules · staged/uploaded-file size limit
197
+ └─────────┬─────────┘
198
+ ▼
199
+ allow or block, naming exactly what matched and where
200
+ ```
201
+
202
+ **Why a pre-tool hook.** It is the only layer that sees both leak routes,
203
+ because it sits where the agent *acts* rather than at any one destination. A
204
+ scanner at the git boundary never runs on an issue body; a guard at the model
205
+ boundary sees the agent legitimately read your database and then says nothing
206
+ when it pastes the balance into a public issue.
207
+
208
+ **There is barely a host adapter.** Every host gives us the same thing — a
209
+ command, before it runs. Claude Code, Codex, Copilot, Hermes and the DeepSeek
210
+ bridge send `tool_input.command`; Cursor sends `command` top-level; all pass
211
+ `cwd` and all read `exit 2` as a block. So one script reads the command out of
212
+ whichever key holds it and covers every one of them with no `--host` flag. Only
213
+ OpenClaw is genuinely different — no shell-command hook at all — and gets a small
214
+ in-process plugin.
215
+
216
+ **Three independent rule sources**, because they fail in different directions:
217
+
218
+ | Source | Catches | Why this shape |
219
+ |---|---|---|
220
+ | Your denylist, derived from your own data (plus opt-in regex `patterns`) | Tickers, balances, account ids and their formats | Generic PII regexes are useless here — see below |
221
+ | 221 rules ported from gitleaks | API keys, tokens, private keys | Credentials *do* have recognisable shapes |
222
+ | A size limit on staged / uploaded files | The 1.1 MB SQLite backup | One rule closes the whole "ship the database" route |
223
+
224
+ **Speed is a correctness requirement, not a nicety.** Most hosts are fail-open on
225
+ timeout — Claude Code's own docs say not to count on a stalled hook as a gate
226
+ ([hooks reference](https://code.claude.com/docs/en/hooks)) — and only Cursor and
227
+ Hermes can be told to fail closed. A slow hook doesn't annoy you, it silently
228
+ stops guarding. So: no dependencies, nothing imported that isn't needed (argparse
229
+ only when a subcommand is given, never on the hook path), and a keyword prefilter
230
+ in front of the secret rules.
231
+
232
+ | Operation | Measured |
233
+ |---|---|
234
+ | Whole hook invocation | 21 ms (15 ms of it is Python starting up) |
235
+ | Matching work itself | 6 ms |
236
+ | Secret prefilter, clean payload | 0.25 ms — no regex compiled at all |
237
+ | Secret rules when something matches | ~1 ms |
238
+ | Compiling all 221 rules, if we didn't prefilter | 19.8 ms |
239
+
240
+ **In practice the timeout race isn't close.** Claude Code's default `PreToolUse`
241
+ timeout is **600 seconds** ([hooks reference](https://code.claude.com/docs/en/hooks)),
242
+ and Codex's is the same ([Codex hooks](https://developers.openai.com/codex/hooks)).
243
+ A 21 ms hook against a ten-minute ceiling doesn't fail open by accident — it would
244
+ have to *hang*: block forever on unreadable input, or catch a pathological regex.
245
+ The design closes those off specifically — no dependencies to hang in, lazy
246
+ imports, `--body-file -` blocked rather than read, a bad `patterns` entry skipped
247
+ rather than run. So the residual fail-open risk isn't a slow hook; it's a
248
+ genuinely stuck process or an adversary who can deliberately stall it.
249
+
250
+ For that, a fail-closed host (Cursor, Hermes) is the real answer — it blocks on
251
+ timeout regardless. If you've already set that up: well done you. You are a hero ❤️
252
+
253
+ ---
254
+
255
+ ## Hosts
256
+
257
+ The scanning is identical everywhere. What differs is how each host hands us the
258
+ command and how we say no.
259
+
260
+ Wiring for each is in [Getting started › Per host](#per-host); `looselips-guard
261
+ init` does it for you.
262
+
263
+ | Host | Integration | Status |
264
+ |---|---|---|
265
+ | Claude Code | `PreToolUse`, exit 2 | **works today** |
266
+ | Codex | `PreToolUse`, exit 2 | **works today** |
267
+ | GitHub Copilot | `PreToolUse` (PascalCase), exit 2 | **works today** |
268
+ | Hermes Agent | `pre_tool_call` shell hook, exit 2 | **works today** |
269
+ | Cursor | `beforeShellExecution`, exit 2 | **works today** |
270
+ | OpenClaw / OpenClaw 2 | `before_tool_call` plugin, `{ block }` | **works today** — bundled plugin |
271
+ | DeepSeek Harness | Claude Code / Codex hook bridge | works via bridge — unverified |
272
+ | opencode | `tool.execute.before`, throw | designed |
273
+
274
+ **works today** means the hook fires and `exit 2` blocks on that host — the
275
+ integration is wired and tested. It is not a claim about detection strength; see
276
+ [What it does not stop](#what-it-does-not-stop).
277
+
278
+ Two things worth knowing before you rely on this:
279
+
280
+ - **Every command-hook host is fail-open on timeout** except Hermes and Cursor,
281
+ which honour `fail_closed` / `failClosed` on the pre-execution hook. The window
282
+ is wide though — Claude Code and Codex default to a 600 s hook timeout, so a
283
+ 21 ms hook only fails open if it truly hangs (see
284
+ [How it works](#how-it-works), and the per-host notes above).
285
+ - **opencode does not intercept subagent tool calls**
286
+ ([open issue](https://github.com/anomalyco/opencode/issues/5894)), so a
287
+ delegated `gh` call bypasses the guard there. Not ours to fix, but yours to
288
+ know.
289
+
290
+ Antigravity is deliberately unsupported: there are open reports its hooks never
291
+ fire, and a guard that might not run is worse than no guard.
292
+
293
+ ---
294
+
295
+ ## Config
296
+
297
+ All of this lives in **`.looselips-guard.json`** at the root of the project you are
298
+ guarding — the working directory the command runs in. It is never committed.
299
+ `.looselips-blocklist-example.json` is only a template to copy from;
300
+ `.looselips-guard.list` (below) is one optional data source, not the config itself.
301
+
302
+ The denylist is **generated from your own data**, not from generic PII regexes.
303
+ Generic rules fail here, concretely: in the incident that motivated this tool
304
+ the owner's own email authored 295 of 445 commits, so a "block emails" rule
305
+ blocks every commit they make; the only emails in tracked files were
306
+ `user@example.com` placeholders; and in a finance repo a UK phone regex matches
307
+ SEDOLs, ISINs, order ids and timestamps.
308
+
309
+ What worked was deriving the list from the ledger itself, excluding the
310
+ English-word collisions, and matching the rest on word boundaries — 27 leaking
311
+ items found across 258 issues and 306 PRs, with zero false positives.
312
+
313
+ - `values` — literal entries, listed inline in the config.
314
+ - `sources` — pull entries from a file instead, re-read on every scan:
315
+ `txt` (path, one value per line, `#` comments), `csv` (path + column),
316
+ `sqlite` (path + query), `env` (path, the value side of each `KEY=value`).
317
+ - `patterns` — regexes, matched raw (you write your own anchors), for a *format*
318
+ rather than a list: `\bAcct-\d{8}\b`, an internal hostname suffix. This is the
319
+ one place generic-regex risk is yours to own — see the warning above.
320
+ `add --regex '<pattern>'` appends one; `add --like 'Acct-99001122'` derives
321
+ `\bAcct-\d{8}\b` from an example. A pattern that won't compile is warned about
322
+ on stderr and skipped, never fatal. Empty by default.
323
+ - `allow` — collision list, for tickers that are also words (`ALL`, `ON`, `CAT`).
324
+ Matching is case-sensitive with word boundaries, which removes most collisions
325
+ before this list is needed. `allow` does not apply to `patterns`.
326
+ - `max_added_file_bytes` — files larger than this are blocked from `git add` and
327
+ from `scp`/`rsync` uploads without being read (default 500 KB).
328
+ - `routes` — a `{name: bool}` map of which egress routes `check()` enforces.
329
+ Anything not listed is checked; set one `false` to skip it. Names: `gh`,
330
+ `git-add`, `git-commit`, `git-push`, `curl`, `scp`, `nc`, `mcp`. Manage with
331
+ `looselips-guard routes [enable|disable] <name>…` rather than by hand.
332
+ - `mcp_tools` — regex of MCP tool names whose arguments get scanned. Default is a
333
+ set of write verbs (`create|post|send|comment|publish|upload|write|update|…`)
334
+ so reading your own data through an MCP server doesn't trip the denylist. Set
335
+ `".*"` to scan every MCP call, `""` or `false` to scan none.
336
+ - `mcp_servers` — Copilot only. A list of MCP server names (`["github", "slack"]`).
337
+ Copilot's tool names are `<server>-<tool>` with no prefix, so the guard can't
338
+ tell an MCP call from a built-in without this. On hosts that use the
339
+ `mcp__<server>__<tool>` convention it's unnecessary.
340
+
341
+ **Presets.** There is no shipped generic-PII pack, on purpose (see the box above
342
+ for why they backfire). What there is: a few opt-in starter sets of format-based
343
+ patterns. `looselips-guard presets` prints them with sources; nothing is added
344
+ until you run `add --preset`, and `init` never adds them.
345
+
346
+ | preset | patterns | from |
347
+ |---|---|---|
348
+ | `internal` | RFC 1918 IPs (`10/8`, `172.16/12`, `192.168/16`); `*.internal`/`*.corp`/`*.intranet`/`*.lan` | [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918); `.internal` is [ICANN-reserved](https://www.icann.org/en/board-activities-and-meetings/materials/approved-resolutions-special-meeting-of-the-icann-board-24-07-2024-en#section2.a) for private use |
349
+ | `cloud` | `arn:aws:…:<acct>:…`; `s3://…` URIs | [AWS ARN format](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) |
350
+ | `k8s` | `*.svc.cluster.local`, `*.pod.cluster.local` | [Kubernetes cluster DNS](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/) |
351
+
352
+ ```bash
353
+ looselips-guard add --preset internal # pull one in; run again for another
354
+ ```
355
+
356
+ They're a starting point, not a policy — review and trim to your environment.
357
+
358
+ Secrets are handled separately, by 221 rules ported from gitleaks, so there is
359
+ **nothing to install**. Regenerate them with `scripts/port_gitleaks_rules.py`.
360
+
361
+ ---
362
+
363
+ ## What it intercepts
364
+
365
+ - `gh issue|pr|release|gist create|edit|comment` — `--body`, `--title`,
366
+ `--body-file <path>` (resolved and read), `--body-file -` (unreadable,
367
+ therefore unscannable, therefore blocked)
368
+ - `gh api` with a mutation, `POST`, or any `-f`/`-F` field
369
+ - `git add` — every path it would actually stage, by size and by content
370
+ - `git commit -m`
371
+ - `git push` — the diff of commits not yet on any remote (capped at 2 MB)
372
+ - `curl` / `wget` — request body (`-d`/`--data*`/`-F`/`-T`/`--json`/`--post-data`,
373
+ inline or `@file`), and the URL itself
374
+ - `scp` / `rsync` — when the destination is remote, the contents of every local
375
+ source file (directories walked, same size cap as `git add`); a download is
376
+ left alone
377
+ - `nc` / `ncat` / `netcat` with a port — the command line itself and any file it
378
+ pipes in (`cat file | nc …`, `nc … < file`)
379
+ - **MCP tool calls** whose name matches a write verb (`create_issue`,
380
+ `post_message`, …) — every string in the arguments. Tune with `mcp_tools`
381
+ (see [Config](#config)); reads like `query` or `list_*` are skipped by default
382
+
383
+ Each of these is a *route* you can turn off with `looselips-guard routes disable
384
+ <name>` — see [Getting started](#getting-started).
385
+
386
+ ## Override
387
+
388
+ Blocking hard gets a tool bypassed, and then it protects nothing. A real bug
389
+ report may need to name the ticker that exposed the bug. So the hook prints
390
+ exactly what matched and where, and there are two deliberate ways through:
391
+
392
+ ```bash
393
+ LOOSELIPS_GUARD_OK=1 gh issue create --title "…" --body-file issue.md # this one call
394
+ looselips-guard snooze # let the agent through here for 5 min (snooze 15 for longer)
395
+ looselips-guard snooze --clear # …or end the window now
396
+ ```
397
+
398
+ `snooze` writes `.looselips-guard.snooze` (an epoch expiry) in the directory and
399
+ the hook allows matches while it's live. It **fails shut**: a missing, expired or
400
+ unreadable file means blocked, so a snooze only ever loosens the guard for the
401
+ window you asked for.
402
+
403
+ For an MCP call there's no command to prefix — use `snooze`, export
404
+ `LOOSELIPS_GUARD_OK=1` for the session, or narrow `mcp_tools`.
405
+
406
+ Every leak in the motivating incident was accidental. Making the deliberate case
407
+ cheap and the accidental case impossible is the whole design goal.
408
+
409
+ ## Inbound guard (planned, opt-in)
410
+
411
+ Outbound is the product. Inbound — stopping secrets reaching the model — is
412
+ separate and opt-in, because **the two directions need opposite rules**: in the
413
+ incident that motivated this, the model was *supposed* to see the portfolio.
414
+ Applying the outbound denylist inbound would block the agent from working at
415
+ all.
416
+
417
+ So inbound guards secrets only, and redacts rather than blocks:
418
+
419
+ ```
420
+ cat .env → cat .env | looselips-guard redact
421
+ ```
422
+
423
+ That works because a pre-tool hook can rewrite tool *input*. Its hard limit:
424
+ **nothing can rewrite tool output**, so a read through the host's native read
425
+ tool (`Read`, `view`) cannot be sanitised — it warns and allows. Inbound
426
+ protection is best-effort by construction, and says so.
427
+
428
+ ## What it does not stop
429
+
430
+ **Any transformation of the value.** Detection is a substring / regex match on
431
+ the plaintext payload. base64, gzip, hex, `gpg`, `rot13`, or splitting
432
+ `Acct-99001122` across two tool calls — none of that matches `Acct-99001122`, and
433
+ the guard allows it. This is inherent to a denylist and is the honest ceiling of
434
+ the approach: it catches *accidental plaintext*, not a determined exfiltrator.
435
+ For that you need network egress control or a sandbox, not a pre-tool hook.
436
+
437
+ **Parsing divergence.** For every intercepted command the guard re-implements
438
+ enough argument parsing to find the payload — `--body-file` resolution, `curl`
439
+ `@file`, `git add --dry-run`, the unpushed-diff range, walking MCP args. Anywhere
440
+ its model of what-will-be-sent differs from what the tool actually sends is a
441
+ silent bypass: exotic quoting, an encoding it doesn't decode, a redirection or
442
+ heredoc, an argument order the parser didn't expect. `curl --data @-` and
443
+ `--body-file -` (reads from stdin, which the hook can't see) are blocked
444
+ outright for this reason; the rest is best-effort. The
445
+ [test suite](tests/) includes an adversarial group, but it is
446
+ not exhaustive.
447
+
448
+ **Out of scope by design:**
449
+
450
+ - Anything typed into github.com in a browser — a pre-tool hook only sees
451
+ agent-initiated calls.
452
+ - Rewriting outbound payloads. Silently altering an issue body the agent wrote
453
+ is worse than refusing it, so outbound blocks and never edits.
454
+ - A `git commit` message from `$EDITOR` (no `-m`/`-F`) — the content is already
455
+ guarded at `git add`, but the message text isn't seen.
456
+ - MCP calls on Copilot when `mcp_servers` isn't set — no tool-name prefix to key
457
+ on, so you name the servers.
458
+ - Whatever tool comes next. The matcher is a short list of `argv[0]` cases plus
459
+ `nc` anywhere in a pipeline — built to extend, not exhaustive.
460
+
461
+ ## Development
462
+
463
+ ```bash
464
+ python3 tests/run.py # whole suite; or run one file, e.g. tests/test_matchers.py
465
+ git config core.hooksPath .githooks # opt in: run tests before every push
466
+ python3 scripts/port_gitleaks_rules.py # refresh the secret rules from upstream
467
+ scripts/release.sh 0.2.0 # bump, tag, push; CI publishes
468
+ ```
469
+
470
+ ## Test approach
471
+ CI (`.github/workflows/test.yml`) runs the suite on every PR and push to
472
+ `master` on Python 3.8 and 3.12; make it a required check in branch protection to
473
+ block merges on failure.
474
+
475
+ `tests/` is framework-free — `assert`-based `run_tests()` per file, shared corpus
476
+ in `tests/fixtures.py`; [`tests/README.md`](tests/README.md) explains the layout
477
+ and the two adversarial groups. Fixtures are synthetic by policy: a ticker-like
478
+ token beside a currency amount, a balance line, a holdings table, an oversized
479
+ SQLite backup. The real incident data that shaped them is never published.
480
+
481
+ ## Credits
482
+
483
+ Built by the team at [Reinvently](https://reinvently.co.uk/about/).
484
+
485
+ Secret detection rules are ported from **[gitleaks](https://github.com/gitleaks/gitleaks)**
486
+ by **Zachary Rice**, used under the MIT Licence — see [NOTICE](NOTICE). gitleaks
487
+ does the hard part: 221 maintained rules, keyword prefilters and entropy
488
+ thresholds, refined over years of real-world false positives. looselips-guard only
489
+ translates them so they run in-process with no binary to install. For scanning
490
+ git history, use gitleaks itself.