From push-guard
Use before every git push or PR creation to run a systematic 7-dimension code safety scan on modified code
How this skill is triggered — by the user, by Claude, or both
Slash command
/push-guard:pre-push-reviewThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Announce at start:** "I'm using push-guard:pre-push-review to scan modified code before pushing."
Announce at start: "I'm using push-guard:pre-push-review to scan modified code before pushing."
Mandatory — before any git push or PR creation. The hook auto-blocks git push via Bash tool until this skill runs and emits a valid 6-dimension report with verifiable file:line citations.
The hook does not trust a marker file or self-attestation. It reads the Claude Code session transcript and verifies:
file:line that is inside this push's diffYou cannot pass by writing a marker, by reciting verdicts without cites, or by citing arbitrary lines outside the diff. Faking a passable report requires actually reading the diff and finding real lines — at which point you've done the review.
Before this skill's process begins, the working tree must be clean. Untracked or unmanaged files interfere with diff analysis and hide real changes.
Run git status --porcelain and handle any non-clean entries:
| Status | Action |
|---|---|
?? (untracked) | Ignore (add to .gitignore) or commit |
M / MM (modified) | Stage and commit |
A (staged) | Commit |
D (deleted) | Stage and commit |
For files to ignore:
# Add patterns to .gitignore, then commit
echo "dist/" >> .gitignore
git add .gitignore
git commit -m "chore: update .gitignore"
For files to track:
git add <files>
git commit -m "chore: add untracked files"
After handling, re-check git status --porcelain — it must be empty. Then proceed with the process below.
Why re-check matters: The hook verifies this skill was invoked after the HEAD commit. If you commit files during this step, HEAD changes; you must re-invoke this skill so the hook sees a valid invocation timestamp.
# Diff against merge base with main/master (handles single + multi-commit branches)
git diff $(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null) HEAD --stat
# Or for the actual hunks (you'll need line numbers for cites):
git diff $(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null) HEAD
Note which files + functions + line ranges are touched. You will need real line numbers from these hunks for the citations.
Use the Read tool to open every modified file at the relevant line ranges. The hook also verifies that at least one Read call landed on a modified file — pure-text cites without reading anything will be rejected.
For each modified function, work through every checklist item. If any answer is "no" or "unknown": fix the code (or add a test) before continuing.
Dimension 1 — External Call Exception Safety & Resource Leaks
For every external call (subprocess.run, file I/O, network request, DB query, shell command):
try/finally block, are result / fd / conn / similar variables provably bound before use?with or finally?Dimension 2 — I/O Encoding Boundary Safety (SKIPPED only if diff has no encode/decode/seek and no binary↔text conversion)
f.seek(offset) + decode(): is offset guaranteed to land on a character boundary, never mid-multibyte sequence?\n, length-prefixed protocol)?Dimension 3 — External Data Type Validation (SKIPPED only if diff has no json.loads / yaml.safe_load / pickle.loads / response.json and no parsing of env/CLI/HTTP input)
isinstance(x, dict) before .get() or x["key"])?verdict, kind, status, type): does the code branch on every known value AND have an explicit unknown fallback (warn / skip / raise)?Dimension 4 — State / Invariant Completeness (SKIPPED only if diff has no state-machine / dispatcher / handler-registry patterns)
None / 0 / empty?seen = {e.id for e in events if e.event == "X"}): when the same loop emits new entries, is the set .add(...)'d in-iteration?Dimension 5 — Hardcoded Secrets (SKIPPED only if diff introduces no credentials, tokens, keys, or config files)
.gitignore?Dimension 6 — Semantic Logic Correctness (SKIPPED only if diff has no business logic / calculation changes)
Dimension 7 — Test Quality (SKIPPED only if diff has no test files or test-related code)
assert True / assert 1 自欺欺人?The hook treats a diff as large when it adds more than 30 lines OR touches more than 2 files. For a large diff, you must spawn an independent reviewer subagent and the hook will compare its 6-dimension verdicts against yours per dimension. Mismatch on any dimension blocks the push.
Why: the same agent that wrote the code also reviews it. A fresh-context subagent reading the diff cold catches blind spots that confirmation bias missed. Verdict agreement across two independent passes is much stronger evidence than a single self-review.
Small diffs (≤30 added lines AND ≤2 files): subagent is optional, but if you spawn one its verdict still must agree.
How to spawn (verbatim template — the signature is what the hook looks for):
Invoke the Agent tool with subagent_type: "general-purpose" and the prompt below (copy literally; the bracketed signature on the first line is mandatory):
[PUSH-GUARD-INDEPENDENT-REVIEW v1]
You are an independent code reviewer. You have NO context from the parent
conversation, NO access to project memory, and you MUST NOT invoke any Skill,
MUST NOT read ~/.claude/CLAUDE.md, MUST NOT read project memory files under
.claude/projects/*/memory/, and MUST NOT read .claude/settings.json.
Your job: scan the diff at HEAD against its merge-base with main/master, then
emit exactly seven lines in this format:
D{N} {VERDICT} — {file}:{line} ({reason ≤80 chars})
Where N is 1..7, VERDICT is CLEAN | FIXED | SKIPPED, file:line is inside the
diff hunks for CLEAN/FIXED (use file:0 for SKIPPED), reason is ≤80 chars.
The seven dimensions:
D1 — External call exception safety & resource leaks
D2 — I/O encoding boundary safety (SKIPPED if no encode/decode/seek)
D3 — External data type validation (SKIPPED if no json.loads/yaml/etc.)
D4 — State / invariant completeness (SKIPPED if no state machine / registry)
D5 — Hardcoded secrets (SKIPPED if no credentials introduced)
D6 — Semantic logic correctness (SKIPPED if no business logic changes)
D7 — Test quality (SKIPPED if no test files or test-related code)
Process:
1. Run: git diff $(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null) HEAD
2. Read each modified file at the touched line ranges using the Read tool.
3. For each dimension, decide CLEAN / FIXED / SKIPPED on the merits — do NOT
defer to or read any prior review.
4. Output the seven lines. No preamble, no summary, no extra text.
After the subagent returns, the hook will parse its 6-line report from the Agent tool_result and require D1–D6 verdicts to match yours. If they disagree, neither push goes through; reconcile the disagreement (fix the code, or re-examine your verdict, or the subagent's) and re-emit both reports.
Output exactly seven lines, one per dimension, in this format:
D{N} {VERDICT} — {file}:{line} ({reason ≤80 chars})
Where:
{N} is 1 … 6{VERDICT} is CLEAN, FIXED, or SKIPPED{file} is the path relative to repo root (must be in this push's diff for CLEAN/FIXED){line} is a real line number inside the diff hunks for CLEAN/FIXED; use 0 for SKIPPED{reason} is a free-text justification (≤80 chars). For SKIPPED, the reason should briefly state why the dimension doesn't apply.The em-dash — between verdict and file:line is required (a regular hyphen - is also accepted).
Example report (passes audit):
D1 CLEAN — hooks/check-push-guard.sh:54 (_is_shell_boundary 是纯函数无 I/O)
D2 SKIPPED — hooks/check-push-guard.sh:0 (无 encode/decode/seek 调用)
D3 CLEAN — hooks/check-push-guard.sh:43 (shlex 输出已是 str,类型契约成立)
D4 SKIPPED — hooks/check-push-guard.sh:0 (无状态机/dispatcher)
D5 SKIPPED — hooks/check-push-guard.sh:0 (无密钥/凭据)
D6 SKIPPED — hooks/check-push-guard.sh:0 (无业务逻辑/计算变更)
D7 SKIPPED — hooks/check-push-guard.sh:0 (无测试文件/测试代码)
FIXED format is the same — cite the file:line you fixed:
D3 FIXED — api.py:42 (added isinstance(resp, dict) before resp.get("data"))
If any dimension was FIXED, run the test suite before emitting the report:
python -m pytest -x -q 2>&1 | tail -5
# or: npm test | cargo test | go test ./...
After emitting the report, run git push directly. The hook reads the transcript, finds your skill invocation + report, validates every cite against the diff hunks, and decides pass/deny. No marker file to write. No "unblock" step.
If the hook denies, the rejection reason will name the specific cite or check that failed (e.g., "D2 cite line 0 with reason 'no encoding' but diff contains .decode\\("). Fix the report (or the underlying issue) and re-emit, then retry push.
Never:
file:line cite that's inside the diff hunkRead-ing any modified file (the hook checks for Read tool calls)Provides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.
npx claudepluginhub soilniba/push-guard --plugin push-guard