Compliance Scan
The repo's last line of defense before something compromising hits GitHub. Layered scanner that runs the existing privacy tests, looks for new patterns of leakage, and (optionally) wires itself into git push so a failed scan blocks the push.
When to invoke
Trigger on any of these phrases — and proactively before any push that touches .claude/skills/, scripts/qa/, PRIVACY.md, .gitignore, notebooks/, or fin-guru/data/:
- "compliance scan", "is this safe to push", "scan for PII", "secrets check"
- "pre-commit security", "pre-push security", "check for leaks", "check before I push"
- "PRIVACY.md compliance", "audit privacy", "any leaks?"
- After adding a new credential, deploy URL, dispatcher URL, webhook, or API key
What it checks (seven layers)
| # | Layer | Owner |
|---|---|---|
| 1 | Owner-name leak detector | reuses tests/python/test_no_hardcoded_references.py |
| 2 | Gitignore-coverage integration test | reuses tests/integration/test_gitignore_protection.sh |
| 3 | CRITICAL secret patterns (AWS/GitHub/Anthropic/OpenAI/Slack/JWT/private-key/Google API) | scripts/scan.py |
| 4 | HIGH PII patterns (account numbers, dispatcher URLs, SSNs, owner email) — sourced from scripts/qa/pii-replacements.txt plus built-ins |
scripts/scan.py |
| 5 | Untracked files in sensitive paths (.claude/skills/, notebooks/, fin-guru-private/, .env*) |
scripts/scan.py |
| 6 | PRIVACY.md "never leaves your machine" alignment with .gitignore |
scripts/scan.py |
| 7 | Household financial figures in tracked docs (precise currency amounts) | scripts/scan.py |
What counts as private
references/DataClassification.md is the authority on which values may enter a
tracked file. Read it before adding a number, a name, or a fixture to any
committed file. The short version: this repo is public, code and docs describe
behaviour rather than carrying values, and evidence belongs in a document as a
percentage or ratio rather than an amount.
Severity gate
Default policy:
| Severity | Examples | Behaviour |
|---|---|---|
| CRITICAL | API tokens, private keys, AWS access keys | fail — exit 1, block push |
| HIGH | Account numbers, dispatcher URLs, SSNs, owner email, household financial figures | fail — exit 1, block push |
| MEDIUM | Owner first/last name in non-allowed file | report — does not fail |
| INFO | Untracked file in sensitive path, PRIVACY/.gitignore drift | report — does not fail |
Override with --fail-on CRITICAL for "secrets only" mode or --fail-on INFO for the strictest posture.
How to run
One-shot scan of staged changes (default before git commit)
.claude/skills/compliance-scan/scripts/scan.py --scope staged
Scan everything that's about to be pushed
.claude/skills/compliance-scan/scripts/scan.py --scope push
Full working-tree audit (slower but comprehensive)
.claude/skills/compliance-scan/scripts/scan.py --scope tree
Auto-add gitignore lines for any sensitive untracked files found
.claude/skills/compliance-scan/scripts/scan.py --scope tree --remediate gitignore
The remediation flag is conservative: it only touches .gitignore, only appends to it, and only for paths that match a known-sensitive prefix (.claude/skills/, notebooks/, fin-guru-private/, .env*). Idempotent — re-running won't create duplicate entries.
Output formats
--format text # default, human-readable
--format json # machine-readable; pipe to jq
--format md # markdown report (use for PR descriptions)
Pre-push hook installation
Install once per clone:
.claude/skills/compliance-scan/scripts/install-pre-push.sh
This drops a .git/hooks/pre-push that runs --scope push. Failed scans block the push. Bypass for a one-off (you'll be asked why) with git push --no-verify — but this should be rare and called out in the commit message.
The hook is local to your clone (git hooks are not versioned). Re-run the installer after git clone on a new machine. The installer is idempotent.
Allowlist for accepted exceptions
Some PII is deliberately committed (e.g., a golden test fixture captured from a real production run). Suppress these via .claude/skills/compliance-scan/allowlist.json:
{
"allow": [
{
"path": "tests/python/fixtures/fidelity_history_golden_expected.json",
"reason": "Golden test fixture captured from real Fidelity history import",
"approved_by": "ossie",
"date": "2026-05-16"
}
]
}
path is a fnmatch glob (tests/**/golden_*.json works). Matching files skip ALL pattern scans. Every entry needs a reason, an approver, and a date — that's the audit trail. Don't allowlist a path unless the value being shielded is genuinely meant to stay committed; for "I'll fix it later" use the private-fixture pattern instead (see references/test-pii-pattern.md).
Auto-remediation behavior (gitignore only)
When --remediate gitignore is set, for each untracked file found in a sensitive path that is not currently gitignored, the scanner:
- Identifies the smallest containing directory that's a sensible ignore unit (the immediate parent under
.claude/skills/, the file itself for.env*, etc.) - Appends
<path>/to.gitignoreunder a# Compliance scan auto-remediationblock, creating the block if absent - Re-runs
git check-ignoreto verify the path is now shielded - Reports what was added so you can review the diff
The scanner never deletes, redacts, or rewrites files containing detected secrets — that's your call. It only silences future commits of paths that should already be private.
Decision tree for the agent invoking this skill
User intent: "is this safe to push?" / "compliance scan"
│
├─ Has uncommitted work?
│ ├─ Yes → run --scope staged first; if clean, also run --scope tree to catch untracked files
│ └─ No → run --scope push
│
├─ Findings of severity ≥ HIGH?
│ ├─ Yes → STOP. Show the findings. Do NOT auto-fix CRITICAL/HIGH (those need human judgment).
│ │ Suggest:
│ │ - For CRITICAL: rotate the leaked secret, then either drop the commit or rewrite history (heavy hammer — confirm explicitly).
│ │ - For HIGH: replace with template variable / env var / placeholder; re-stage; re-scan.
│ └─ No → continue
│
├─ INFO finding "untracked sensitive path"?
│ ├─ Yes → Confirm with user: "Add `<path>/` to .gitignore?"
│ │ On yes, re-run with --remediate gitignore.
│ └─ No → done
│
└─ Report PASS with the verdict line from the scanner output
Layer 1 + 2 detail
Layers 1 and 2 are the existing tests. The scanner shells out to them as a single orchestration step:
uv run pytest tests/python/test_no_hardcoded_references.py -q --no-cov
bash tests/integration/test_gitignore_protection.sh
If those tests change shape (e.g., new fixtures), the scanner picks up the change automatically — no skill update required.
Layer 3 + 4 pattern catalog
See references/secret-patterns.md for the full list of detected patterns, why each matters, and how to remediate when one fires.
Layer 5 sensitive-path map
Currently shielded prefixes (extend in scripts/scan.py:SENSITIVE_PATH_PREFIXES):
.claude/skills/apps-script-run/and any other skill withapps-script,dispatcher, orwebhookin the namenotebooks/(entire tree — financial CSVs)fin-guru/data/(except whitelisted methodology files)fin-guru-private/.env,.env.*,*.envcredentials/,private/,sensitive/*.key,*.pem
Layer 6 PRIVACY.md alignment
Reads PRIVACY.md, finds the "What never leaves your machine" section, extracts each bullet, and verifies the corresponding pattern is in .gitignore. Misalignment is INFO severity and lists the missing pattern.
What this skill is NOT
- Not a git-history scrubber. For history rewrites use
scripts/qa/pii-replacements.txtwithgit-filter-repo(a separate, destructive workflow documented in.planning/phases/01-git-history-scrub-security-foundation/). - Not a vulnerability scanner. For dependency CVEs run
uv run pip-audit. - Not a secret rotator. When CRITICAL fires, you rotate the secret out-of-band — the scanner won't help with that.
Files in this skill
SKILL.md— this filescripts/scan.py— Python 3 scanner; no third-party deps; runnable as./scan.pyscripts/install-pre-push.sh— idempotent installer for the git hookreferences/secret-patterns.md— full pattern catalog with remediation guidancereferences/severity-guide.md— when to escalate, when to ignore