- Scheme 88.1%
- Shell 7.1%
- Makefile 4.6%
- Common Lisp 0.2%
| .jerboa | ||
| docs | ||
| gitsafe | ||
| scripts | ||
| support | ||
| test | ||
| .build.yml | ||
| .gitignore | ||
| .jerbuild | ||
| AGENTS.md | ||
| build-binary.ss | ||
| build-common.ss | ||
| build-gitsafe-cross.ss | ||
| build-gitsafe-freebsd-cross.ss | ||
| false.md | ||
| GITLEAKS_IMPLEMENTATION_NOTES.md | ||
| gitsafe.json | ||
| jpkg.lock | ||
| jpkg.policy.sexp | ||
| jpkg.sexp | ||
| LICENSE | ||
| Makefile | ||
| PLAN.md | ||
| README.md | ||
| SECURITY.md | ||
gitsafe
A fast, zero-dependency git hook that blocks commits and pushes containing leaked secrets, API keys, tokens, and credentials. Compiles to a single static binary — no runtime dependencies on the target machine.
Security And Release Status
This repo is preview security tooling until the release checklist is complete. Local release gates:
make verify
make release-evidence
Security notes are in SECURITY.md, docs/threat-model.md, docs/security-hardening.md, docs/corpus-policy.md, and docs/release-evidence.md.
Quick Start
git clone https://git.sr.ht/~lisp/jerboa-gitsafe
cd jerboa-gitsafe
make binary install
This builds a native binary and installs it to ~/.local/bin/gitsafe, then configures global git hooks so every new repo is protected automatically. No Jerboa checkout needed: if jerbuild isn't already installed, the build downloads the prebuilt Jerboa toolchain for your os/arch into .jerboa/bin (macOS arm64, Linux amd64/arm64, FreeBSD amd64). The only requirement is a C compiler.
Install
make install
This builds the binary (fetching the Jerboa toolchain on first run if needed) and then:
- Copies the binary to
~/.local/bin/gitsafe - Creates pre-commit and pre-push hooks in
~/.git-templates/hooks/ - Sets
git config --global init.templateDir ~/.git-templates
If a global template hook already exists and does not contain the
Installed by gitsafe marker, make install refuses to overwrite it. Back up or
merge that hook manually, or use per-repo gitsafe install.
Make sure ~/.local/bin is on your PATH:
# Add to ~/.bashrc or ~/.zshrc if not already present
export PATH="$HOME/.local/bin:$PATH"
Build only (no install)
make binary # Native build → ./gitsafe-bin
make linux-amd64 # Linux x86_64 musl static → ./gitsafe-linux-amd64
make linux-arm64 # Linux arm64 musl static → ./gitsafe-linux-arm64
make freebsd-amd64 # FreeBSD amd64 dynamic → ./gitsafe-freebsd-amd64
make verify # Security checks, tests, and binary build
make release-evidence # Capture release evidence under dist/release-evidence/
To pin a toolchain explicitly: make binary JERBUILD=/path/to/jerbuild or JERBOA_VERSION=<tag>.
Cross targets use JERBOA_HOME for Chez cross prefixes and still run ensure-jerboa-tools, so a missing jerbuild is bootstrapped the same way as native builds.
Production support also requires a reviewed JSAFE_TARGET_PROOF_FILE with the
markers documented in docs/release-evidence.md. JSAFE_REQUIRE_TARGET_PROOF=1
fails closed when that target proof is missing, incomplete, or contains secret
material.
Setting Up Existing Repos
make install configures git's global template directory, which applies to all new repos (git init / git clone). To add gitsafe hooks to repos that already exist, you have two options:
Option 1: Re-initialize (safe, non-destructive)
Run git init inside existing repos. This copies the template hooks without touching your code, branches, or history:
# Single repo
cd ~/projects/my-repo && git init
# All repos under a directory
find ~/projects -name .git -type d -execdir git init \;
Option 2: Per-repo install
cd ~/projects/my-repo
gitsafe install
This writes gitsafe hooks directly into .git/hooks/ for that repo.
Option 3: Per-repo uninstall
cd ~/projects/my-repo
gitsafe uninstall
Removes only hooks that were installed by gitsafe. Leaves other hooks untouched.
Global Configuration
The global git template is set during make install:
~/.git-templates/hooks/pre-commit → exec gitsafe pre-commit
~/.git-templates/hooks/pre-push → exec gitsafe pre-push
Both generated hooks include an Installed by gitsafe marker so future gitsafe
installs can update them without overwriting unrelated hook content.
You can verify this is active:
git config --global init.templateDir
# → /home/you/.git-templates
Per-Repo Configuration
Place a .gitsafe.json in any repo root to customize behavior for that project:
{
"severity": "medium",
"entropy": true,
"patterns": {
"disabled": ["high-entropy-hex"],
"custom": []
},
"exclude": [
"*.lock",
"go.sum",
"*.md",
"vendor/**",
"node_modules/**",
"*.min.js",
"*.min.css",
"test/fixtures/**"
],
"allowlist": {
"files": [],
"patterns": [
"EXAMPLE_KEY",
"YOUR_API_KEY_HERE"
]
},
"max_decode_depth": 0,
"baseline": "gitsafe-baseline.json"
}
Config Fields
| Field | Default | Description |
|---|---|---|
severity |
"medium" |
Minimum severity to report: low, medium, high, critical |
entropy |
true |
Enable Shannon entropy analysis for detecting random-looking strings |
patterns.disabled |
[] |
Pattern IDs to skip (e.g. ["high-entropy-hex", "jwt"]) |
patterns.custom |
[] |
Custom pattern definitions with optional Gitleaks-style metadata |
exclude |
Lock files, docs, vendor dirs | Glob patterns for paths to skip |
allowlist.files |
[] |
File paths or globs to skip entirely |
allowlist.patterns |
[] |
Known-safe strings to ignore (e.g. placeholder keys) |
max_decode_depth |
0 |
Recursively scan decoded percent, unicode, hex, and base64 candidates |
baseline |
unset | Previous JSON or SARIF report whose fingerprints should be suppressed |
Custom patterns use this shape:
{
"id": "internal-service-key",
"name": "Internal Service Key",
"severity": "high",
"pattern": "service_key\\s*=\\s*\"(ISK_[A-Za-z0-9]{32})\"",
"secretGroup": 1,
"entropy": 3.5,
"path": "^src/",
"keywords": ["service_key"],
"tags": ["internal", "service"],
"allowlists": [
{
"stopwords": ["ISK_EXAMPLE"],
"regexTarget": "secret"
}
],
"description": "Internal service authentication key"
}
regex is accepted as an alias for pattern. path/paths are regular
expressions against the file path; pathGlobs/path_globs are glob matches.
Rule allowlists support paths, regexes, stopwords, regexTarget
(secret, match, or line), and condition (OR or AND).
Default Excludes
Without a .gitsafe.json, these paths are excluded automatically:
*.lock, go.sum, *.md, vendor/**, node_modules/**, *.min.js, *.min.css
How It Works
gitsafe uses a multi-layered detection pipeline to catch leaked secrets with high accuracy and low false positives. Each line of scanned content passes through several stages before a finding is reported.
Detection Pipeline
line of code
|
[1. Pattern Match] -- regex against 28 known secret formats
|
[2. Rule Metadata] -- path and keyword prefilters
|
[3. Capture Extract] -- pull the secret value from capture groups
|
[4. Validator] -- pattern-specific checks (length, prefix, entropy)
|
[5. Placeholder Filter] -- reject example/dummy values
|
[6. Allowlist Check] -- skip known-safe strings from config or rule metadata
|
[7. Inline/Baseline Suppression]
|
FINDING
1. Pattern Matching
gitsafe ships with 28 built-in regex patterns organized by severity. Each pattern targets a specific secret format (e.g. AKIA prefix for AWS keys, ghp_ for GitHub PATs, eyJ for JWTs). Patterns are compiled once at startup and matched against every line of scanned content.
When a pattern has capture groups, gitsafe extracts the last non-empty capture group as the matched value. This ensures validators receive just the secret (e.g. AKIAIOSFODNN7EXAMPLE) rather than the full match with surrounding context characters.
Custom patterns can override capture extraction with secretGroup, apply a
per-rule entropy threshold, restrict matches by path/paths or
pathGlobs, prefilter by keywords, attach tags, and define rule-local
allowlists.
2. Validators
Many patterns include a validator function that runs additional checks on the extracted match:
- Length checks -- AWS access keys must be exactly 20 characters, Google API keys exactly 39
- Prefix checks -- Anthropic keys must start with
sk-ant-, GitHub PATs withgh - Marker checks -- OpenAI keys must contain the
T3BlbkFJmarker - Entropy checks -- Generic patterns (API key assignments, bearer tokens) require the matched value to exceed a Shannon entropy threshold, rejecting low-randomness values that are unlikely to be real secrets
- Placeholder rejection -- URL credential patterns reject common test values like
user:password@localhost
3. Shannon Entropy Analysis
For patterns that match broad formats (generic key assignments, high-entropy hex/base64), gitsafe computes the Shannon entropy of the matched string -- a measure of randomness in bits per character. Real secrets tend to have high entropy; configuration values and identifiers tend to have low entropy.
Thresholds vary by character set:
| Character Set | Threshold | Example |
|---|---|---|
Hex (0-9a-f) |
3.0 bits | deadbeef1234... |
Base64 (A-Za-z0-9+/=) |
4.0 bits | SGVsbG8gV29y... |
| Generic (validator-level) | 3.0--3.5 bits | Used by bearer tokens, API key assignments |
A string like "aaaaaaaaaaaaaaaa" has 0.0 bits of entropy and is ignored. A string like "xK9mP2nQrT5vWy8zA3bCdEfGhJk" has >4.0 bits and triggers a finding.
4. Placeholder Filtering
The not-placeholder? filter rejects common test and example values that structurally match secret formats but aren't real credentials. It catches:
- Repeated single characters (
XXXXXXXX,aaaaaaaa) - Strings shorter than 8 characters
- Placeholder words at word boundaries:
example,placeholder,dummy,sample,your-api-key,replace-me,change-me,insert-here
Word-boundary matching prevents false negatives -- a real secret like testX9mP2nQrT5vWy8zA3b won't be rejected just because it contains the substring "test".
5. Scan Modes
gitsafe scans different content depending on how it's invoked:
- Pre-commit (
gitsafe pre-commit): Reads the git index viagit diff --cached. For modified files, only added lines in diff hunks are scanned. For entirely new files, the full staged content is scanned. This means gitsafe only flags secrets you're about to commit, not existing content. - Pre-push (
gitsafe pre-push): Computesgit rev-listfor the commit range being pushed and scans one unified diff across all changed files in that range. - Manual scan (
gitsafe scan PATH...): Reads and scans entire file contents, recursing into directories. - Stdin scan (
gitsafe stdin): Reads content from standard input and scans it as<stdin>.
In all modes, binary files (images, archives, compiled objects, .lock files) are skipped based on file extension. Paths matching exclude globs from .gitsafe.json are also skipped.
6. False Positive Reduction
Several layers work together to minimize noise:
- Severity filtering: Only patterns at or above the configured severity level are active (default:
medium). Set tocriticalto only catch the most dangerous leaks. - Env-var detection: Lines like
os.getenv('API_KEY')oros.environ.get('TOKEN')match generic patterns but the extracted values (API_KEY') fail entropy checks. - Long line skip: Lines over 2000 characters (minified JS, generated code) are skipped entirely.
- Allowlist strings: Known-safe values (e.g.
EXAMPLE_KEY) in.gitsafe.jsonare ignored when found in any match. - Rule allowlists: Custom rules can suppress matches by path, regex, or stopword.
.gitsafeignore: A gitignore-style file for excluding paths, or exact finding fingerprints copied from gitsafe output.- Baselines:
--baseline PATHor.gitsafe.jsonbaselinesuppresses fingerprints from previous JSON or SARIF reports. - Inline suppression:
# gitsafe:ignoreor// gitsafe:ignore=pattern-idon a line suppresses that finding.
What It Detects
Critical
- AWS Access Key IDs (
AKIA...) and Secret Access Keys - GitHub Personal Access Tokens (classic
ghp_and fine-grainedgithub_pat_) - OpenAI API Keys (standard, project-scoped
sk-proj-, and service accountsk-svcacct-) - Anthropic API Keys (
sk-ant-) - Stripe Secret and Restricted Keys (
sk_live_,rk_live_) - PEM-encoded Private Keys (RSA, DSA, EC, OpenSSH, PGP, encrypted)
- PuTTY Private Keys (PPK format)
High
- Generic API key/secret/token/password assignments (with entropy validation)
- Bearer tokens
- Slack tokens (
xoxb-,xoxp-, etc.) and webhook URLs - Google API keys (
AIza...) - Twilio, SendGrid, Mailgun API keys
- NPM and PyPI tokens
- JWTs (
eyJ...three-part base64url) - Credentials embedded in URLs (with placeholder rejection)
Medium
- Database connection strings with credentials
- High-entropy hex strings (40+ chars)
- High-entropy base64 strings (40+ chars)
Usage
As git hooks (automatic)
Once installed, gitsafe runs automatically on git commit and git push. If secrets are found, the operation is blocked and findings are printed.
Manual scan
gitsafe scan path/to/file.json
gitsafe scan src/
cat config.env | gitsafe stdin
gitsafe scan src/ --format sarif
CLI Options
gitsafe [MODE] [OPTIONS]
Modes:
pre-commit Scan staged files (default)
pre-push Scan commits being pushed
scan PATH... Scan specific files or directories
stdin Scan content from stdin
install Install git hooks in .git/hooks/
uninstall Remove gitsafe-installed hooks
Options:
--config PATH Path to .gitsafe.json (default: .gitsafe.json)
--format text|json|sarif
Output format (default: text)
--severity LEVEL Minimum severity: low|medium|high|critical
--no-entropy Disable entropy analysis
--max-decode-depth N
Recursively scan decoded percent/unicode/hex/base64 text
--baseline PATH Suppress findings from a previous JSON or SARIF report
--verbose Show scan statistics
--version Print version
--help Print this help
Suppression
- Inline: append
# gitsafe:ignoreto a line - Per-pattern:
# gitsafe:ignore=aws-access-key - Per-file: add paths to
excludein.gitsafe.json - Per-string: add known-safe values to
allowlist.patternsin.gitsafe.json - Per-finding: add a reported fingerprint like
config.env:generic-secret:12to.gitsafeignore - Baseline: run with
--baseline previous-report.jsonto suppress already-known fingerprints
Make Targets
make binary Native build → ./gitsafe-bin (auto-downloads jerbuild)
make linux-amd64 Cross-build Linux x86_64 musl static binary
make linux-arm64 Cross-build Linux arm64 musl static binary
make freebsd-amd64 Cross-build FreeBSD amd64 dynamic binary
make install Build + install to ~/.local/bin + global hooks
make run ARGS='...' Build + run ./gitsafe-bin
make test Run test suite
make clean Remove all build artifacts