# Pre Commit Audit

> Deliver a fast pre-commit safety scan: file size, anonymity (author / affiliation strings in tex/bib), hardcoded secrets, and invisible-Unicode carriers. Use when the user requests a fast pre-commit safety scan: file size, anonymity (author / affiliation strings in tex/bib), hardcoded secrets, and invisible-Unicode carriers. Triggers: 'audit before commit', 'check before push', 'pre-commit scan', 'safety check'.

- Skill: `flonat/pre-commit-audit` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add flonat/pre-commit-audit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/flonat/pre-commit-audit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: flonat (https://skillmd.com/u/flonat)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/flonat/pre-commit-audit

---


# 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

1. **Size pass blocks anything >10MB unless gitignored OR user explicitly approves.** Threshold matches `block-large-files.sh` hook for consistency.
2. **Anonymity pass runs only on paper-relevant paths**: `paper-*/`, `*.tex`, `*.bib`, `*.md` inside paper directories. Scope is data-driven — we don't false-flag README authorship.
3. **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.
4. **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.
5. **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

6. Output a single consolidated table (file × pass × verdict) — not four separate reports.
7. Severity tiers: BLOCK (must fix), WARN (proceed with confirm), OK.
8. Always show the file path and line number when flagging — make it copy-paste-fixable.

## When to Use

- Before any `git commit` on 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-close` Phase 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

```bash
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`](../_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` / `\grant` revealing 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:

```bash
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):

```bash
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 -A` mistakes.
- **Don't** require this skill on infra-only commits. Add a path filter or let the user opt out for those — but `--unicode-only` remains safe and useful there.
- **Don't** run Phase 4's `--apply` as 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 failure `reconcile-before-rewriting.md` exists 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.

