Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 102 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![node](https://img.shields.io/node/v/broomsticks.svg)](https://nodejs.org)
[![license](https://img.shields.io/npm/l/broomsticks.svg)](./LICENSE)

> **Status: early access.** The npm name is reserved and the design is locked (see [PLAN.md](./PLAN.md)); the scanner is in active development. The published `broom` command currently prints a notice only — it reads, writes, and transmits nothing until the engine lands. Watch the repo for `v0.1`.
> **Status: early access.** The scanner, redactor, source adapters (Claude Code, Codex, Cursor), allowlist, Claude Code hook installer, and the live redacting proxy are all implemented and covered by tests. Being finalized for the `v0.1` npm release — until then, install from git. Watch the repo.

---

Expand All @@ -20,32 +20,40 @@ AI coding assistants keep a full, local, plaintext record of every session — y

Those transcripts then live on disk indefinitely — `~/.claude/projects/**/*.jsonl`, `~/.codex/*.jsonl`, Cursor's `state.vscdb` SQLite stores — and get swept into Time Machine, Dropbox, `rsync` backups, or a shared machine. A leaked key in a transcript is just as live as one in a committed `.env`, but nothing scans for it.

**broomsticks** is a small, auditable CLI that finds those secrets and scrubs them out of the transcripts in place — safely.
**broomsticks** gives you two complementary tools:

1. **`broom scan` / `broom clean`** — find secrets already written to your transcripts and scrub them out in place, safely.
2. **`broom proxy`** — a local redacting proxy that sits between your AI tools and the provider API, stripping secrets out of requests *before they're ever sent* (and out of responses the model echoes back).

## Design principles

- **Dry-run by default.** Nothing is ever modified unless you pass `--apply`.
- **Dry-run by default.** `clean` never modifies anything unless you pass `--apply`.
- **Back up before every write.** Each touched file is copied to a timestamped backup directory first.
- **Transcripts only.** It never touches credential files themselves (`~/.codex/auth.json`, `~/.aws/credentials`, etc.) — only chat history.
- **Transcripts only.** The scanner never touches credential files themselves (`~/.codex/auth.json`, `~/.aws/credentials`, etc.) — only chat history.
- **Local by default.** `scan`/`clean` do no networking at all (enforced by a test). Only `broom proxy` talks to the network — by design, since it *is* the network path.
- **Auditable supply chain.** Plain ESM JavaScript, **zero runtime dependencies**, no build step. The code published to npm *is* the source — read every line before you trust it with your secrets.
- **Non-reversible, idempotent redaction.** Secrets are replaced with `«BROOM:<rule>:<sha8>»`. The hash lets you correlate without leaking, and re-running never double-redacts.

## Install

```bash
# one-off, no install
# one-off, no install (once published)
npx broomsticks scan

# or install the CLI globally
npm install -g broomsticks
broom scan

# from git, before the npm release
git clone https://github.com/digitaldrreamer/broomsticks
npm install -g ./broomsticks
```

Requires **Node ≥ 22.5** (uses the built-in `node:sqlite` to read Cursor's database no native modules).
Requires **Node ≥ 22.13** — it uses the built-in `node:sqlite` to read Cursor's database (no native modules), which is available without the `--experimental-sqlite` flag as of Node 22.13.0.

Prefer no Node at all? A dependency-light **`scripts/broom.sh`** fallback (using `jq` + the `sqlite3` CLI) is planned for environments where you can't or won't run the package.

## Usage (target CLI)
## Clean up: `scan` / `clean`

```bash
# Scan every supported source, print a redacted report. Exits non-zero if anything is found (CI-friendly).
Expand All @@ -55,6 +63,9 @@ broom scan
broom scan --source claude-code
broom scan --source cursor

# List discovered transcript files across all sources
broom sources

# Preview what would change, without writing
broom clean

Expand All @@ -71,13 +82,77 @@ broom clean --apply --extra ./leaked.txt
| Flag | Meaning |
| --- | --- |
| `--source <id>` | Restrict to `claude-code`, `codex`, or `cursor` (repeatable; default: all) |
| `--apply` | Perform redaction (otherwise dry-run) |
| `--apply` | Perform redaction (`clean` only; otherwise dry-run) |
| `--backup-dir <dir>` | Where backups go (default `~/.broom/backups/<timestamp>/`) |
| `--no-backup` | Skip backups (discouraged) |
| `--extra <file>` | Additional literal/regex secrets to redact |
| `--extra <file>` | Additional literal/regex secrets to redact (one per line) |
| `--allowlist <file>` | Custom allowlist file (default `~/.broom/allowlist.txt`) |
| `--no-allowlist` | Disable allowlist suppression — report every finding |
| `--json` | Emit findings as JSON |
| `--no-fail` | Exit `0` even when secrets are found |

## Prevent: `broom proxy`

Cleaning up after the fact only reduces exposure — the secret still reached the model. The proxy stops the leak at the source. It sits in front of the provider API and redacts secrets out of the text and tool-call content in every outgoing request before it's sent, and out of every response the model echoes back.

```bash
# Start the proxy in the foreground (Ctrl-C to stop)
broom proxy

# Then point your AI tools at it:
export ANTHROPIC_BASE_URL=http://127.0.0.1:7777
export OPENAI_BASE_URL=http://127.0.0.1:7777
```

| Route | Upstream | Used by |
| --- | --- | --- |
| `POST /v1/messages` | `api.anthropic.com` | Claude Code, Aider |
| `POST /v1/chat/completions` | `api.openai.com` | Codex, OpenAI-compatible clients |

Both streaming (SSE) and non-streaming responses are handled — a streaming response is buffered in full before redaction so a secret straddling two chunks can't slip through, then re-emitted as a valid SSE stream. Redaction covers both assistant **text** and **tool-call arguments** (e.g. a secret the model echoes into a `write_file` content or shell-command argument). Extended-thinking blocks are the one exception — see [Limitations](#limitations).

Make it permanent instead of exporting vars by hand:

```bash
# Add ANTHROPIC_BASE_URL / OPENAI_BASE_URL to your shell init files
broom proxy --install

# Also register a login-persistent daemon (launchd on macOS, systemd --user on Linux)
broom proxy --install --daemon

# Remove the daemon later
broom proxy --uninstall

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Usability Note: Running broom proxy --uninstall only removes the background daemon; it does not clean up or remove the ANTHROPIC_BASE_URL and OPENAI_BASE_URL environment variables from the shell profile files (e.g., .zshrc, .bashrc).

If these environment variables are left in the shell profiles after the proxy is uninstalled or stopped, all subsequent AI CLI tools (like Claude Code or Aider) will fail to connect to their upstream APIs (resulting in connection refused errors).

Consider:

  1. Clarifying in the README that users must manually remove these lines from their shell profiles when uninstalling, or
  2. Implementing an automatic cleanup of shell profiles in the --uninstall command (e.g., an uninstallProxyEnv function) to prevent leaving the user's shell in a broken state.

```

> **Note:** `--uninstall` removes both the daemon **and** the `ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL` block it added to your shell profiles. Open a new terminal (or re-source your profile) afterward so your shell stops pointing at the now-stopped proxy.

| Flag | Meaning |
| --- | --- |
| `--port <n>` | Port to listen on (default `7777`) |
| `--verbose` | Log redaction counts to stderr |
| `--allowlist <file>` | Allowlist to suppress known false positives |
| `--install` | Add base-URL env vars to your shell init files |
| `--install --daemon` | Also install a login-persistent daemon (logs to `~/.broom/proxy.log`) |
| `--uninstall` | Remove the daemon |

## Automate: `broom install`

For Claude Code users, `broom install` wires broomsticks into your editor so it sweeps automatically:

```bash
broom install
```

This adds (with your confirmation):

- `~/.claude/skills/broom-sweep/SKILL.md` — a skill that teaches Claude to preview and apply redactions
- `~/.claude/hooks/stop-broom.mjs` — a Stop hook that silently scans your transcripts after every turn

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Performance Note: The installed Stop hook (stop-broom.mjs) falls back to running npx broomsticks scan if the global broom command is not found in the PATH. Running npx on every single turn of Claude Code introduces significant latency (often 1-2+ seconds) due to package resolution overhead, making the chat experience feel sluggish.

Consider adding a note in this section recommending that users install broomsticks globally (npm install -g broomsticks) when using the automatic Claude Code integration to ensure optimal performance.

- a `Stop` hook entry in `~/.claude/settings.json` to register it

Restart Claude Code afterward for the skill to take effect. Pass `--yes` to skip the confirmation prompt.

> **Tip:** install broomsticks globally (`npm install -g broomsticks`) when using the automatic integration. The Stop hook prefers the `broom` command but falls back to `npx broomsticks`, which re-resolves the package on every turn and adds noticeable latency to each response.

## What it detects

A curated, gitleaks-style ruleset for high-confidence provider tokens, plus an entropy-gated catch-all for unknown `KEY=value` / `"token": "…"` shapes:
Expand All @@ -93,7 +168,19 @@ A curated, gitleaks-style ruleset for high-confidence provider tokens, plus an e
| Connection strings | `postgres://`, `mysql://`, `mongodb+srv://`, `redis://` with inline credentials |
| Generic | `api_key` / `secret` / `password` / `token` assignments above an entropy threshold |

The exact rule set, severities, and entropy thresholds live in `src/rules.mjs` once shipped — and are documented in [PLAN.md](./PLAN.md).
The exact rule set, severities, and entropy thresholds live in [`src/rules.mjs`](./src/rules.mjs).

## Allowlist

Well-known documentation placeholders (AWS's `AKIAIOSFODNN7EXAMPLE`, Stripe test keys, `.env.example` shapes) are suppressed out of the box. Add your own known false positives to `~/.broom/allowlist.txt`:

```
# one entry per line; blank lines and # comments ignored
AKIAIOSFODNN7EXAMPLE
/^sk_test_[A-Za-z0-9]+$/
```

Plain lines match a secret exactly; `/regex/flags` lines match by pattern. Disable suppression entirely with `--no-allowlist`.

## How redaction works

Expand All @@ -119,14 +206,18 @@ Paths shown for Linux; macOS/Windows equivalents are resolved automatically.

## Limitations

- It reduces exposure of secrets **already written to disk**; it cannot un-send anything already transmitted to a model provider. **If a secret hit a transcript, treat it as compromised and rotate it** — broomsticks is cleanup, not a substitute for rotation.
- `scan`/`clean` reduce exposure of secrets **already written to disk**; they cannot un-send anything already transmitted to a model provider. **If a secret hit a transcript before you started using the proxy, treat it as compromised and rotate it** — broomsticks is cleanup, not a substitute for rotation.
- Detection is best-effort. Novel or low-entropy secrets may be missed; tune with `--extra`.
- Cursor's schema evolves between versions; broomsticks targets the known chat/composer keys and will be kept current.
- The proxy buffers each streaming response in full before redacting and re-emitting it. This is deliberate — a secret straddling two SSE chunks can't be caught otherwise — but it means the assistant's UI won't show tokens incrementally; long responses appear all at once after a pause. A sliding-window buffer that preserves incremental streaming is a possible future improvement.
- The proxy scans assistant text and tool-call arguments, but **not extended-thinking blocks** — rewriting a thinking block would invalidate its cryptographic signature and break multi-turn thinking+tool loops. A secret echoed only inside a model's thinking is passed through unredacted. (Thinking is not persisted to transcripts either, so `broom clean` won't see it; if a secret reached the model at all, rotate it.)

## Contributing

Issues and PRs welcome — especially new source adapters (Windsurf, Continue, Aider, Zed) and detection rules. See [PLAN.md](./PLAN.md) for architecture and the contribution surface.

Run the test suite with `npm test` (uses the built-in `node:test` runner — no dependencies).

## License

[MIT](./LICENSE) © digitaldrreamer
14 changes: 11 additions & 3 deletions bin/broom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { discoverTargets as claudeCodeTargets } from '../src/sources/claude-code
import { discoverTargets as codexTargets } from '../src/sources/codex.mjs'
import { discoverTargets as cursorTargets } from '../src/sources/cursor.mjs'
import { runInstall, isInstalled } from '../src/install.mjs'
import { startProxy, installProxyEnv, installDaemon, uninstallDaemon } from '../src/proxy.mjs'
import { startProxy, installProxyEnv, uninstallProxyEnv, installDaemon, uninstallDaemon } from '../src/proxy.mjs'

// ── Package metadata ──────────────────────────────────────────────────────────
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
Expand Down Expand Up @@ -70,10 +70,18 @@ if (command === 'proxy') {

if (flag('--uninstall')) {
const removed = uninstallDaemon()
const cleaned = uninstallProxyEnv()
console.log(removed
? '\n broom proxy: daemon removed. Env vars in your shell profile still point to\n the proxy — remove them manually or they will silently fail to connect.\n'
: '\n broom proxy: no daemon found to remove.\n'
? '\n broom proxy: daemon removed.'
: '\n broom proxy: no daemon found to remove.'
)
if (cleaned.length) {
console.log(' Removed proxy env vars from:')
for (const f of cleaned) console.log(` ${f}`)
console.log(' Open a new terminal (or re-source your profile) for it to take effect.\n')
} else {
console.log(' No proxy env vars found in your shell profiles.\n')
}
process.exit(0)
}

Expand Down
Loading