Add secret_scan_gate.sh: block a live credential from becoming tracked - #4
Merged
Conversation
added 2 commits
August 7, 2026 23:12
The junk-file gate objects to junk being tracked. It does not read content, so
on 2026-08-06 and 08-07 the same mechanism leaked credentials into
gtmify-config three times and a gate reported success every time:
1. 354 memory files, one quoting a full Supabase token, tracked since
2026-05-11 and readable by five collaborators
2. .env.backup-20260428-004952, a plaintext .env with six live keys, tracked
for three months
3. .n8n_onboarding_env_snapshot.json, swept in by a cc-sync `git add -A`,
holding a LIVE Stripe sk_live_ key and the PRODUCTION onboarding bridge HMAC
Two detection layers, because either alone misses a real case here. The VALUE
layer compares staged content against the actual values in the local env files,
which is near-zero false positive and catches a secret no pattern knows. The
PATTERN layer covers vendor prefixes plus a generic NAME_KEY=<long string>
detector, and that is the layer that catches a credential absent from your env,
which is precisely how the Stripe key arrived.
It never prints a secret. Findings name file, line, and detector, because a
scanner that echoes its findings turns scrollback and CI logs into the next
copy of the leak.
Same conventions as junk_file_gate.sh: --staged, --audit, or a base-ref diff;
only tracked or staged content can fail; sops files and binaries are skipped;
per-repo escape hatch via .secret-scan-allow, and `git commit --no-verify`.
Verified with 14 cases before committing, per the standing rule that an
untriggered hook is not evidence: it blocks Stripe, Slack, Supabase, GitHub,
PEM and generic assignments, blocks a real value from a local env file, and
does NOT fire on <REDACTED>, YOUR_, documented examples, short values,
allowlisted paths, or encrypted sops files.
One implementation note worth keeping: the python body is delivered through a
QUOTED heredoc. The first version used `python3 -c '...'`, and the regexes
contain single quotes, which closed the shell string and made every invocation
a syntax error. A gate that cannot parse itself is worse than no gate, because
a caller can read the crash as a pass.
Ran --audit against the real repo before enabling this as a blocking hook, which is the whole point of having an audit mode. It returned 18 findings and only one was real. A gate that fires on legitimate commits earns a permanent --no-verify, so the noise had to go first. Three exclusions, each from a concrete case in that audit: Non-secret env names. SUPABASE_URL and SUPERGROW_WORKSPACE_ID tripped the value layer: a URL and an identifier, both over 20 chars, neither a secret. Names ending in _URL, _ID, _HOST, _WORKSPACE and similar are now skipped, as is any value containing "://" or "@", since a locator is not a credential. Publishable keys. A Supabase anon key ships in browser JavaScript by design; row-level security protects that data, not secrecy. _ANON_KEY, _PUBLISHABLE_KEY, _PUBLIC_KEY and CLIENT_ID are excluded. Presigned S3 URLs. Nine hits on AKIA/ASIA turned out to be one temporary STS credential inside X-Amz-Credential in bank-statement attachment links, scoped to a single object and long expired. Lines carrying X-Amz-Credential or X-Amz-Signature are skipped. Audit now returns 1 finding on gtmify-config, a hardcoded key in scripts/n8n_fix_wfemail_scaledmail.py that matches no current env value, so a dead credential worth reviewing rather than noise. All 14 original test cases still pass, including the Stripe-in-JSON case.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The junk-file gate objects to junk being tracked. It never reads content. So on 2026-08-06 and 08-07 the same mechanism leaked credentials into
gtmify-configthree times, and a gate reported success each time:.env.backup-20260428-004952, plaintext.envwith six live keys, tracked three months.n8n_onboarding_env_snapshot.json, swept in by acc-syncgit add -A, holding a live Stripesk_live_key and the production onboarding bridge HMACNothing in the pipeline looked at content. This closes that.
Two layers, because either alone misses a real case
VALUE layer. Compares staged content against the actual values in
~/.claude/.env,~/.claude/.env.local, andhooks/.env. Near-zero false positives, and it catches a secret no pattern would recognize. Local only; in CI there is no env file, so it reports itself unavailable rather than pretending to have run.PATTERN layer. Vendor prefixes (Stripe, Slack, Supabase, GitHub, GitLab, AWS, age, PEM, Resend, n8n, JWT) plus a generic
NAME_KEY = <long string>detector. That generic rule is what catches a credential absent from your env, which is exactly how the Stripe key arrived inside a JSON file.It never prints a secret
Findings name file, line, and detector only. A scanner that echoes what it found turns terminal scrollback and CI logs into the next copy of the leak.
Conventions match
junk_file_gate.sh--stagedfor the pre-commit hook,--auditfor cleanups, base-ref diff by default. Only tracked or staged content can fail, so a gitignored file on disk is invisible by construction. sops files and binaries are skipped. Escape hatches:.secret-scan-allowper repo, orgit commit --no-verify.Verified before committing
14 cases, per the standing rule that an untriggered hook is not evidence:
sk_live_inside JSON (the exact case that got through), Slackxoxb, Supabasesbp_, GitHubghp_, PEM private key, genericNAME_SECRET=, and a real value read from a local env file<REDACTED>placeholders,YOUR_placeholders, documented examples, short values, allowlisted paths, encrypted sops filesImplementation note worth keeping
The python body is delivered through a quoted heredoc. The first version used
python3 -c '...'and the regexes contain single quotes, which closed the shell string and made every invocation a syntax error. A gate that cannot parse itself is worse than no gate, since a caller can read the crash as a pass. The test suite caught it immediately.🤖 Generated with Claude Code