
spac — Secret Protection Against Commits
A Claude Code plugin that blocks git commit, git push, and file writes
that contain API keys, tokens, private keys, and other secrets — before
they leave your machine.
1. Purpose
Claude Code agents produce a lot of code fast, and LLMs occasionally
inline real credentials they've seen elsewhere in context. spac is a
PreToolUse hook that stands between the agent and anything that could
persist a secret. It intercepts:
git commit (including -a/-am/--all, and the -m/-F message
text itself)
git push (including --force / --force-with-lease, scanning the
full rewritten range against the remote merge-base)
Write, Edit, and NotebookEdit — scans both the target path
(to catch .env, id_rsa, *.pem, credentials.json, etc.) and the
new content.
If a finding looks real, spac blocks the operation with structured
JSON (permissionDecision: "deny") plus a human-readable explanation
and rotation guidance. Bypass requires an explicit environment variable,
so agents cannot silently skip the check.
2. Installation
As a Claude Code plugin (recommended)
# Clone wherever you keep plugins
git clone https://github.com/seanrobertwright/spac.git
# Enable it in ~/.claude/settings.json under "hooks" -> "PreToolUse":
{
"matcher": "Bash|Write|Edit|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "python \"/absolute/path/to/spac/scripts/scan_secrets.py\"",
"timeout": 15
}
]
}
Restart Claude Code so the hook config reloads.
Requirements
- Python 3.8+ on
PATH (standard library only — no pip installs).
- git on
PATH.
- Works on Linux, macOS, and Windows (tested on Windows 11 with Git Bash).
Verify it's wired up
Stage a file that contains a fake AWS key and try to commit:
echo "AKIA" + "1234567890ABCDEFG1" > /tmp/secret.txt # fake
git add /tmp/secret.txt
git commit -m "test"
# -> PreToolUse:Bash hook error: spac: BLOCKED `git commit` ...
3. Tech Stack / Project Layout
Tech stack: Python 3 (stdlib only — re, json, subprocess,
hashlib, shlex, fnmatch, math). No runtime dependencies.
Integrates with Claude Code's plugin + hook system (plugin.json,
hooks.json).
spac/
├── .claude-plugin/
│ └── plugin.json # plugin manifest (name, version, author)
├── hooks/
│ └── hooks.json # PreToolUse matcher: Bash|Write|Edit|NotebookEdit
├── scripts/
│ └── scan_secrets.py # the scanner (all logic lives here)
├── tests/
│ ├── test_scan.py # 49 unit tests, run with `python tests/test_scan.py`
│ └── fake_secrets_fixture.txt # intentionally-fake tokens for scanner tests
├── .spacignore # allowlist (one literal substring per line)
├── spac.config.json # optional — thresholds, disabled rules, custom patterns
├── TODO.md
└── README.md
Module overview (scripts/scan_secrets.py)
| Section | Purpose |
|---|
HIGH_CONFIDENCE_PATTERNS | ~30 provider-specific regexes (AWS, GitHub, OpenAI, Anthropic, Google, Slack, Stripe, Azure, GCP, Twilio, SendGrid, Mailgun, Mailchimp, npm, HuggingFace, Vercel, Cloudflare, DigitalOcean, Square, PyPI, Postman, JWT, PEM blocks) |
KEYWORD_RE + entropy | Generic KEY=VALUE rule, gated by Shannon entropy ≥ 3.5 bits/char |
SECRET_FILE_GLOBS | Path-based detection: .env, *.pem, id_rsa, credentials.json, etc. |
classify() / commit_is_all() / push_is_force() | Git command parsing |
load_allowlist() / load_remembered_hashes() | .spacignore + .git/spac-allow |
load_config() / apply_config() | spac.config.json overrides |
emit_block() | Structured JSON decision on stdout + text on stderr + exit 2 |
4. How to use
Normal usage — nothing to do
Once installed, spac runs transparently on every Bash/Write/Edit tool
call from Claude Code. You'll only notice it when something is blocked.
When spac blocks a legitimate change
The block message tells you what was found. You have three options:
-
The finding is a false positive. Add a stable substring (or file
path) to .spacignore:
# .spacignore — one literal substring per line
[REDACTED:AWS Access Key]
docs/examples/fake-jwt.txt
-
You need to commit it anyway this one time (e.g. test fixtures
you've verified). Re-run with the bypass env var — spac always
asks Claude to get yes/no confirmation from you first:
ALLOW_SECRET_COMMIT=1 git commit -m "..."