cc-scrub
v0.1.1
Published
Find and remove exposed secrets from Claude Code session history, plus a live hook to catch them before they land in context.
Maintainers
Readme
cc-scrub
Find, redact, and prevent secrets leaking into local Claude Code session transcripts.
Claude Code stores every session as a .jsonl file under ~/.claude/projects/. If you ever pasted an API key, token, or credential into a session — or Claude read a file/ran a command that echoed one back — it's sitting in that transcript in plaintext. cc-scrub finds it, lets you redact it in place without breaking Claude Code's ability to resume the session, and can install a live hook that catches secrets before they ever land in context.
Quick Start
npx cc-scrub scan # scan every local session for secrets
npx cc-scrub clean <id> # preview redactions for one session
npx cc-scrub clean <id> --yes # redact in place (writes a .bak first)
npx cc-scrub protect # install a live redaction hook going forwardNo required external dependencies. If gitleaks is on your PATH, cc-scrub uses it; otherwise it automatically falls back to a bundled in-process ruleset (see "Detection engines" below). Run cc-scrub doctor to see which one is active.
Usage
| Command | What it does |
|---|---|
| cc-scrub list | List local sessions, newest first |
| cc-scrub scan [session-id] | Scan one session, or all of them if omitted. Exit code 1 if anything is found |
| cc-scrub clean <session-id> | Dry run — show what would be redacted |
| cc-scrub clean <session-id> --yes | Redact in place. Always backs up to <session>.jsonl.bak first, never overwrites an existing backup |
| cc-scrub doctor | Show which detection engine is active and how many rules loaded |
| cc-scrub protect | Add a PostToolUse hook to ~/.claude/settings.json that redacts secrets from tool output before Claude sees it |
| cc-scrub unprotect | Remove that hook |
Add --engine auto\|gitleaks\|local to scan/clean to control which engine runs (default auto: gitleaks binary if installed, else the bundled ruleset). Flag values can be passed as --engine local or --engine=local.
Install as a plugin
If you install cc-scrub via the Claude Code plugin system instead of npm, the bundled .claude-plugin/plugin.json registers the same PostToolUse redaction hook automatically — no need to run protect separately.
Detection engines
cc-scrub ships two interchangeable engines rather than committing to just one:
gitleaks(binary) — shells out to the real gitleaks CLI when it's onPATH. Full fidelity: this is the actual upstream engine, regex dialect and all.local(bundled, in-process) —vendor/gitleaks.tomlis gitleaks' own public MIT-licensed ruleset (pinned to the version this package was built against), vendored directly into the repo.src/rules.tsparses it andsrc/localDetect.tsruns it line-by-line in plain TypeScript: keyword pre-filtering, regex matching, Shannon-entropy thresholding, and per-rule/global allowlist filtering — no subprocess, no temp files, no external binary required.cc-scrub doctorreports how many rules loaded (221 of 222 at last check; the one skip is a path-only rule with no content regex, correctly inapplicable here).
--engine auto (the default) prefers the gitleaks binary when present and falls back to local otherwise — including inside the live protect hook, which no longer goes fully silent just because gitleaks isn't installed.
Why not just ship local as the only engine? Two RE2-to-JS regex dialect differences had to be translated (Go/Python-style named groups (?P<name>...) → JS's (?<name>...), and RE2's scoped inline case-insensitivity toggle (?i)/(?i:...), which JS has no equivalent for — approximated by making the whole pattern case-insensitive instead of just the scoped portion). That's a real, if small, parity gap against the actual gitleaks binary, so auto prefers the binary when it's available and treats local as the (very close, but not byte-for-byte identical) fallback.
RE2 also guarantees linear-time matching by construction; JS's backtracking regex engine doesn't. Since the vendored ruleset was written for RE2, local caps scanned lines at 10,000 characters (real secrets are always far shorter) to bound worst-case regex cost on adversarial input — this is checked, not theoretical (see test/localDetect.test.ts), and skipped lines are reported to stderr rather than silently dropped.
How it works
- Retroactive clean: scans a session file with the selected engine, groups findings by line, and replaces only the exact matched secret substring with
[REDACTED:<rule-id>]— line count and per-line JSON validity are verified before and after every write. If a redaction would corrupt a line's JSON,cc-scrubrefuses to write anything and tells you which line. - Live hook: on
PostToolUseforRead/Bash/Grep/Glob, scans the tool's output with the sameautoengine selection and, if it finds anything, replaces the output via the hook'supdatedToolOutputfield before Claude ever sees it.
Limitations
localengine gaps vs. the real gitleaks binary: line-by-line matching only (no multi-line secrets, e.g. PEM key blocks spanning several lines), and the regex-dialect approximations noted above. See "Detection engines."- Doesn't scan
settings.json. Only session transcripts are covered. Secrets embedded in your Bash permission allowlist or other config are out of scope. - Structured tool output is flattened. If a tool's response is a structured object rather than a plain string, the live hook normalizes it to a JSON string before scanning/redacting — Claude receives a redacted string instead of the original shape.
- Already-sent data can't be recalled. This is local cleanup only. If a secret was already sent to a model provider's API,
cc-scrubcannot undo that — rotate the credential.
Roadmap
Keeping vendor/gitleaks.toml in sync with upstream gitleaks releases is currently a manual re-pin (bump the version in the download step and re-verify against test/localDetect.test.ts) — automating that check is a natural next step. Closing the remaining regex-dialect gaps (proper scoped case-insensitivity, multi-line matching) would bring local to full parity with the binary, but isn't blocking anything today since auto already prefers the binary whenever it's present.
Development
npm install
npm run build # tsc -> dist/
npm test # tsx test/*.test.ts — degrades gracefully if gitleaks isn't installedLicense
MIT
