Pre-Commit Audit — File Size, Anonymity, Secrets
Three-pass safety scan before commit. Blocks pain-points: oversized commits (1.3GB parquet incident), CCS-style anonymity breaches, leaked credentials. Each pass is a hard gate; user OKs proceed.
Hard Rules
Existential — block proceed
- Size pass blocks anything >10MB unless gitignored OR user explicitly approves. Threshold matches
block-large-files.shhook for consistency. - Anonymity pass runs only on paper-relevant paths:
paper-*/,*.tex,*.bib,*.mdinside paper directories. Scope is data-driven — we don't false-flag README authorship. - Secrets pass uses an entropy heuristic + known-prefix list. No regex-only matching that misses high-entropy keys. Block on confirmed secret; warn on suspicious-but-uncertain.
- All four passes run on the same file set (default: staged for commit). Don't mix staged-only size check with all-files anonymity check.
- The Unicode pass is the one pass that is always in scope, including infra-only commits. It is identity-blind — it reads codepoints, never names — so it cannot false-flag authorship the way the anonymity pass can.
Format — catch in review
- Output a single consolidated table (file × pass × verdict) — not four separate reports.
- Severity tiers: BLOCK (must fix), WARN (proceed with confirm), OK.
- Always show the file path and line number when flagging — make it copy-paste-fixable.
When to Use
- Before any
git commiton a paper / project repo - Pre-submission checklist (research projects)
- Before pushing to anonymous-artifact repo (the highest-risk anonymity surface)
- After a sub-agent has written files (verify before committing their work)
- Wired as a
session-closePhase 4 sub-step (alternate path to running the hook)
When NOT to Use
- Before commits on infrastructure-only changes (skills, hooks, rules) — anonymity scan would false-flag README author lines. Exception: Phase 4 (Unicode) stays valid there and can be run alone:
pre-commit-audit --unicode-only - Auto-generated commits (CI, dependabot) — they don't need the scan
- When the change is purely metadata (
.gitignore,LICENSE, etc.)
Modes
| Invocation | Scope |
|---|---|
pre-commit-audit |
Staged files (default) |
pre-commit-audit --staged |
Same as default |
pre-commit-audit --unstaged |
Modified-but-not-staged files |
pre-commit-audit --all |
Working tree + staged |
pre-commit-audit --unicode-only |
Phase 4 only — safe on infra commits |
Architecture
Phase 1 (size) → find files >10MB; check .gitignore status
Phase 2 (anonymity) → grep paper paths for author/affiliation/email
Phase 3 (secrets) → entropy + prefix heuristics on all staged files
Phase 4 (unicode) → invisible-carrier scan via check_invisible_unicode.py
Phase 5 (consolidate) → single table; user OK to proceed
Phase 1: Size
THRESHOLD_BYTES=$((10 * 1024 * 1024))
git diff --cached --name-only --diff-filter=ACM | while read f; do
if [ -f "$f" ]; then
size=$(stat -f%z "$f" 2>/dev/null || stat -c%s "$f" 2>/dev/null || echo 0)
if [ "$size" -gt "$THRESHOLD_BYTES" ]; then
ignored=$(git check-ignore "$f" 2>/dev/null && echo "ignored" || echo "tracked")
echo "$f|$size|$ignored"
fi
fi
done
Verdict: BLOCK if any tracked file >10MB. WARN if gitignored but staged anyway.
Phase 2: Anonymity
Single source of truth: skills/_shared/double-blind-anonymity-checklist.md holds the authoritative pattern list (paper-side P1-P8, artifact-side A1-A6) plus the CCS 2026 #1328 incident report that motivates the checks. Both pre-commit-audit (this skill) and anonymous-artifact reference it; do not duplicate the patterns inline.
Scope for pre-commit-audit: only files staged for commit at paths matching paper-*/, **/*.tex, **/*.bib, **/*.cls, **/*.sty. The full checklist also covers structured-metadata files (pyproject.toml, package.json, Cargo.toml, CITATION.cff, etc.) — those are checked by anonymous-artifact since they only matter when pushing the artifact, not on every commit.
Apply checks P1-P8 from the shared checklist:
- P1 — title page authors blinded
- P2 — no
\thanks{}/\acknowledgements/\funded/\grantrevealing identity - P3 — first-person self-reference uses third person (no "our prior work")
- P4 — self-citation bib entries blinded if author overlap
- P5 — body text doesn't name authors of self-cited works
- P6 — no identifying URLs (personal sites, lab pages, repos with handles)
- P7 — PDF metadata sanitized (
pdfinfo paper.pdf) - P8 — figures contain no identifying watermarks/captions
For each match: print path:line and the matched text (truncated). Verdict: BLOCK on any match in a path containing paper-*/. WARN on matches outside paper directories.
Exception: a frontmatter-style Made by the user in friends-repo / public-repo READMEs is allowed (intentional attribution).
Phase 3: Secrets
Patterns by category:
| Category | Detection |
|---|---|
| API keys with prefix | sk-, sk-proj-, sk-ant-, hf_, gh[a-zA-Z]_, xoxb-, AIza |
| AWS | AKIA[0-9A-Z]{16}, aws_secret_access_key |
| Generic high-entropy strings | length ≥32, base64-ish character set, no whitespace |
.env-style assignments |
^[A-Z_]+=[a-zA-Z0-9+/=_-]{20,} outside .env.example/.env.template |
Skip .env.template, .env.example, *.example, comment lines starting with #.
Verdict: BLOCK on any match in a non-template file.
Phase 4: Invisible Unicode
Catches characters that are invisible on screen but change what a machine reads — zero-width carriers, bidi overrides, tag characters, and space homoglyphs. These arrive by paste (Word, PDF, web page, LLM output) and fail silently: a U+200B inside a citekey makes BibTeX drop the entry with no error, a U+00A0 makes an exact-match string edit fail on a line that looks identical.
Deterministic — delegate to the script rather than grepping by hand:
git diff --cached --name-only --diff-filter=ACM \
| while read -r f; do [ -f "$f" ] && printf '%s\n' "$f"; done \
| xargs -r uv run --no-sync python "<skill-dir>/scripts/check_invisible_unicode.py" --format json
Map the script's severities onto this skill's tiers:
| Script severity | Meaning | Verdict |
|---|---|---|
critical |
Zero-width / bidi / tag char, or any invisible inside a citekey, crossref, path, url, bib_entry_key, bib_doi |
BLOCK |
warning |
Space homoglyph or soft hyphen in prose — renders fine, breaks exact-match editing and grep |
WARN |
rescued |
Legitimate emoji ZWJ / VS16 sequence | not reported |
Report path:line:col, the U+XXXX label, and the containing construct when the JSON carries a context. The context is most of the signal: the same U+00A0 is a WARN in prose and a BLOCK inside \ref{}.
Fixes are mechanical, so offer them — but never apply silently (Anti-Patterns below):
uv run --no-sync python "<skill-dir>/scripts/check_invisible_unicode.py" <path> --diff # preview
uv run --no-sync python "<skill-dir>/scripts/check_invisible_unicode.py" <path> --apply # write
--apply refuses Overleaf-canonical paths unless given --yes-live-surface, per reconcile-before-rewriting.md; if a flagged file is a live Overleaf surface, surface the finding and let the user fix it in the editor rather than passing that flag on their behalf.
Phase 5: Consolidate + ask
Single table:
File Size Anon Secrets Uni Verdict
────────────────────────────────────────────────────────────────────────────
paper-ccs/paper/main.tex 5KB OK OK OK OK
paper-ccs/paper/references.bib 12KB FAIL OK FAIL BLOCK (line 23: the user; line 8: U+200B in bib_entry_key)
data/raw/results.parquet 1.3GB N/A N/A N/A BLOCK (size; not gitignored)
scripts/sync-creds.sh 2KB OK FAIL OK BLOCK (line 15: hf_<...>)
notes/from-word.md 4KB OK OK WARN WARN (4x U+00A0)
If any BLOCK rows: ask the available structured-question mechanism:
- Fix the issues and rerun — recommended
- Override and proceed — risky, requires explicit confirmation
- Cancel commit — abort
If only WARN rows: ask whether to proceed. If all OK: print "Pre-commit clean ✓" and exit silently.
Cross-References
| Skill / Hook / Rule | Relationship |
|---|---|
block-large-files.sh hook |
Runtime enforcement of Phase 1 (this skill is the manual / verbose counterpart) |
scripts/check_invisible_unicode.py |
Bundled deterministic engine behind Phase 4. Codepoint tables adapted from guillaumemeyer/watermarks-remover (MIT); severity tiers, LaTeX-context detection, and emoji rescue are local |
reconcile-before-rewriting.md rule |
Governs Phase 4's --apply on live Overleaf surfaces |
anonymous-artifact |
The anonymity-on-publish workflow; this skill is the pre-commit equivalent |
session-close Phase 4 |
Can invoke this skill as part of pre-commit verification |
mark-unverified.md rule |
Verbal counterpart for content claims; this skill handles file content |
Anti-Patterns
- Don't scan binary files for anonymity strings — false positives on PDFs, images.
- Don't rely solely on regex for secret detection — entropy heuristics catch keys with novel prefixes.
- Don't auto-fix issues — surface them for the user to handle. Auto-replacement of names risks breaking acknowledgements.
- Don't skip the .gitignore check — large gitignored files staged anyway are a signal of
git add -Amistakes. - Don't require this skill on infra-only commits. Add a path filter or let the user opt out for those — but
--unicode-onlyremains safe and useful there. - Don't run Phase 4's
--applyas part of the audit. The audit reports; the user decides. Auto-rewriting bytes in a file the user is mid-edit on is the exact failurereconcile-before-rewriting.mdexists to prevent. - Don't flag visible non-ASCII. Accented names (
López-Ibáñez), em dashes, and→are legitimate and the script deliberately ignores them; a pass that flags them will be muted within a week.