aboutsummaryrefslogtreecommitdiff
path: root/languages/python/githooks
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-23 20:48:44 -0500
committerCraig Jennings <c@cjennings.net>2026-07-23 20:48:44 -0500
commitc238afbd4150ef53737f16e7dd84a565d2838ecc (patch)
tree588a25dc318cb854c57d8146639d400674baed18 /languages/python/githooks
parent10ea44b6de3be1872f7f0bd4501ccf3878105bc4 (diff)
downloadrulesets-c238afbd4150ef53737f16e7dd84a565d2838ecc.tar.gz
rulesets-c238afbd4150ef53737f16e7dd84a565d2838ecc.zip
feat(languages): ship the missing python and typescript hooks
The python and typescript bundles carried rules and a coverage script but no pre-commit hook, so any project installing one got no credential scan on commit. Both now ship all four components the README documents: the shared secret scan, a validate-on-edit hook, settings wiring, and a seed CLAUDE.md. Verified against a real repo: a commit carrying an AWS key is refused. install-lang now warns when a bundle is missing a documented component. That's the half that keeps this from recurring. Whoever adds the sixth bundle will forget something too, and today the installer prints success either way. Two things fell out of the build. node --check is unusable on TypeScript: it ignores --experimental-strip-types, so it rejects valid TS and accepts broken TS. The hook uses tsc filtered to syntactic diagnostics instead. And completing the bundles means every pair now collides on settings.json and pre-commit, so no two compose without FORCE=1. The earlier "bundles already compose" reading rested on these two being incomplete. Filed for a real decision; clock-panel is the project that wants it. Also stamps :LAST_REVIEWED: at task creation, with a lint checker to catch misses. Writing a task is reviewing it, so leaving the stamp off pushed every fresh task to the top of the next review batch to be re-derived by someone with less context than its author had. Tonight's sweep sent the staleness count from 13 to 22 while the list got more accurate.
Diffstat (limited to 'languages/python/githooks')
-rwxr-xr-xlanguages/python/githooks/pre-commit79
1 files changed, 79 insertions, 0 deletions
diff --git a/languages/python/githooks/pre-commit b/languages/python/githooks/pre-commit
new file mode 100755
index 0000000..0a578b4
--- /dev/null
+++ b/languages/python/githooks/pre-commit
@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+# Pre-commit hook: secret scan + syntax/lint check on staged Python files.
+# Use `git commit --no-verify` to bypass for confirmed false positives.
+
+set -u
+
+REPO_ROOT="$(git rev-parse --show-toplevel)"
+cd "$REPO_ROOT" || exit 1
+
+# --- 1. Secret scan ---
+# Patterns for common credentials. Scans only added lines in the staged diff.
+#
+# Two passes because case-sensitivity differs. AWS keys are uppercase, sk- keys
+# lowercase, PEM headers fixed, so those match case-SENSITIVELY: under -i,
+# AKIA[0-9A-Z]{16} matches any mixed-case 20-char run, which random base64 in an
+# embedded image blob hits ~6% of the time per 100KB and blocks real commits.
+# Only the keyword=value patterns need -i.
+SECRET_PATTERNS_CS='(AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9_-]{20,}|-----BEGIN (RSA|DSA|EC|OPENSSH|PGP)( PRIVATE)?( KEY| KEY BLOCK)?-----)'
+SECRET_PATTERNS_CI='(api[_-]?key|api[_-]?secret|auth[_-]?token|secret[_-]?key|bearer[_-]?token|access[_-]?token|password)[[:space:]]*[:=][[:space:]]*["'"'"'][^"'"'"']{16,}["'"'"']'
+
+added_lines="$(git diff --cached -U0 --diff-filter=AM \
+ | grep '^+' | grep -v '^+++' || true)"
+
+cs_hits="$(printf '%s\n' "$added_lines" | grep -nE "$SECRET_PATTERNS_CS" || true)"
+ci_hits="$(printf '%s\n' "$added_lines" | grep -niE "$SECRET_PATTERNS_CI" || true)"
+# awk dedupes lines both passes matched, keeping first-seen order.
+secret_hits="$(printf '%s\n%s' "$cs_hits" "$ci_hits" \
+ | grep -v '^[[:space:]]*$' | awk '!seen[$0]++' || true)"
+
+if [ -n "$secret_hits" ]; then
+ echo "pre-commit: potential secret in staged changes:" >&2
+ echo "$secret_hits" >&2
+ echo "" >&2
+ echo "Review the lines above. If this is a false positive (test fixture, documentation)," >&2
+ echo "bypass with: git commit --no-verify" >&2
+ exit 1
+fi
+
+# --- 2. Syntax check on staged Python files ---
+staged_py="$(git diff --cached --name-only --diff-filter=AM \
+ | grep -E '\.pyi?$' || true)"
+
+if [ -n "$staged_py" ] && command -v python3 >/dev/null 2>&1; then
+ failed=""
+ while IFS= read -r f; do
+ [ -z "$f" ] && continue
+ [ -f "$f" ] || continue
+ # compile() rather than py_compile so no __pycache__ lands in the tree.
+ if ! python3 -c 'import sys; compile(open(sys.argv[1], "rb").read(), sys.argv[1], "exec")' "$f" >/dev/null 2>&1; then
+ failed="${failed}${f}"$'\n'
+ fi
+ done <<< "$staged_py"
+
+ if [ -n "$failed" ]; then
+ printf 'pre-commit: Python syntax errors in staged files:\n\n%s\n' "$failed" >&2
+ echo "Run: python3 -m py_compile <file> to see the error, then re-stage." >&2
+ exit 1
+ fi
+fi
+
+# --- 3. ruff on staged Python files (when installed) ---
+if [ -n "$staged_py" ] && command -v ruff >/dev/null 2>&1; then
+ failed=""
+ while IFS= read -r f; do
+ [ -z "$f" ] && continue
+ [ -f "$f" ] || continue
+ if ! ruff check "$f" >/dev/null 2>&1; then
+ failed="${failed}${f}"$'\n'
+ fi
+ done <<< "$staged_py"
+
+ if [ -n "$failed" ]; then
+ printf 'pre-commit: ruff failed on staged files:\n\n%s\n' "$failed" >&2
+ echo "Run: ruff check <file> and fix the findings, then re-stage." >&2
+ exit 1
+ fi
+fi
+
+exit 0