Skip to content
Merged
Changes from 1 commit
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
107 changes: 96 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,73 @@ 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 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.

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 / UX Note: Buffering the entire streaming response in full before redacting and re-emitting it defeats the primary benefit of streaming (reducing perceived latency). For long model responses, this will cause the AI assistant's UI to appear to hang and then dump the entire response at once, leading to a degraded user experience.

Consider:

  1. Documenting this latency impact as a known limitation of the proxy in the Limitations section.
  2. Exploring a sliding-window buffering mechanism in the future, which only buffers a small window (e.g., matching the maximum length of a potential secret) before flushing, thereby preserving the streaming experience while still preventing secrets from straddling chunk boundaries.


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.

```

| 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.

## 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 +164,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 +202,16 @@ 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.

## 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