aboutsummaryrefslogtreecommitdiff
path: root/languages/typescript/githooks/pre-commit
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/typescript/githooks/pre-commit
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/typescript/githooks/pre-commit')
-rwxr-xr-xlanguages/typescript/githooks/pre-commit92
1 files changed, 92 insertions, 0 deletions
diff --git a/languages/typescript/githooks/pre-commit b/languages/typescript/githooks/pre-commit
new file mode 100755
index 0000000..1628080
--- /dev/null
+++ b/languages/typescript/githooks/pre-commit
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+# Pre-commit hook: secret scan + syntax check on staged TypeScript/JavaScript 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 TS/JS files ---
+# Two checkers, because one tool can't do both jobs. `node --check` ignores
+# --experimental-strip-types, so on TypeScript it is wrong in BOTH directions:
+# it rejects valid TS (an `interface` reads as a syntax error) and accepts
+# broken TS. Measured on node v26.4.0, 2026-07-23. tsc is the only correct
+# parser for .ts; node is correct and much faster for .js.
+staged_js="$(git diff --cached --name-only --diff-filter=AM \
+ | grep -E '\.(js|jsx|mjs|cjs)$' || true)"
+staged_ts="$(git diff --cached --name-only --diff-filter=AM \
+ | grep -E '\.(ts|tsx|mts|cts)$' || true)"
+
+failed=""
+
+if [ -n "$staged_js" ] && command -v node >/dev/null 2>&1; then
+ while IFS= read -r f; do
+ [ -z "$f" ] && continue
+ [ -f "$f" ] || continue
+ if ! node --check "$f" >/dev/null 2>&1; then
+ failed="${failed}${f}"$'\n'
+ fi
+ done <<< "$staged_js"
+fi
+
+if [ -n "$staged_ts" ]; then
+ tsc_bin=""
+ if [ -x "./node_modules/.bin/tsc" ]; then
+ tsc_bin="./node_modules/.bin/tsc"
+ elif command -v tsc >/dev/null 2>&1; then
+ tsc_bin="tsc"
+ fi
+
+ if [ -n "$tsc_bin" ]; then
+ while IFS= read -r f; do
+ [ -z "$f" ] && continue
+ [ -f "$f" ] || continue
+ # Filter to TS1xxx, TypeScript's syntactic diagnostic range. TS2xxx and
+ # up are type errors, which need the whole project graph and are the
+ # build's job, not this hook's.
+ out="$("$tsc_bin" --noEmit --skipLibCheck --target es2022 \
+ --moduleDetection force "$f" 2>&1 || true)"
+ if printf '%s\n' "$out" | grep -qE 'error TS1[0-9]{3}:'; then
+ failed="${failed}${f}"$'\n'
+ fi
+ done <<< "$staged_ts"
+ fi
+fi
+
+if [ -n "$failed" ]; then
+ printf 'pre-commit: syntax errors in staged files:\n\n%s\n' "$failed" >&2
+ echo "Fix the parse errors above, then re-stage." >&2
+ exit 1
+fi
+
+exit 0