From c238afbd4150ef53737f16e7dd84a565d2838ecc Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 23 Jul 2026 20:48:44 -0500 Subject: 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. --- .../typescript/claude/hooks/validate-typescript.sh | 94 ++++++++++++++++++++++ languages/typescript/claude/settings.json | 80 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100755 languages/typescript/claude/hooks/validate-typescript.sh create mode 100644 languages/typescript/claude/settings.json (limited to 'languages/typescript/claude') diff --git a/languages/typescript/claude/hooks/validate-typescript.sh b/languages/typescript/claude/hooks/validate-typescript.sh new file mode 100755 index 0000000..b76f1df --- /dev/null +++ b/languages/typescript/claude/hooks/validate-typescript.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Validate TypeScript/JavaScript files after Edit/Write/MultiEdit. +# PostToolUse hook: receives tool-call JSON on stdin. +# +# On success: exit 0 silent. +# On failure: emit JSON with hookSpecificOutput.additionalContext so Claude +# sees a structured error in its context, THEN exit 2 to block the tool +# pipeline. stderr still echoes the error for terminal visibility. +# +# Gate: parseability. A file that doesn't parse is never worth passing on. +# Full type checking is deliberately NOT enforced here — it needs the whole +# project graph and its dependencies resolved, which is a build-scale +# operation, not a per-keystroke one. A type error that parses cleanly passes +# this hook; `make typecheck` / `tsc --noEmit` over the project owns it. +# +# Formatting (prettier) is also out: a project picks its own style, so blocking +# on an unconfigured default would impose a contested choice. +# +# Two checkers, because one tool can't do both jobs: +# +# .js/.jsx/.mjs/.cjs → `node --check`, a straight parse. +# .ts/.tsx/.mts/.cts → `tsc`, filtered to syntax-category diagnostics. +# +# `node --check` must NOT be used on TypeScript. It ignores +# --experimental-strip-types, so it is wrong in *both* directions: it rejects +# valid TS (an `interface` declaration reads as a syntax error) and accepts +# broken TS (a genuinely unparseable file exits 0). Measured on node v26.4.0, +# 2026-07-23. tsc is the only correct parser for these extensions. +# +# The tsc call is filtered to TS1xxx codes, which is TypeScript's syntactic +# diagnostic range; TS2xxx and up are semantic (type) errors and are out of +# scope by the paragraph above. Without the filter this hook would block every +# unresolved import in a file whose dependencies aren't installed yet. + +set -u + +# Emit a JSON failure payload and exit 2. Arguments: +# $1 — short failure type (e.g. "TYPESCRIPT SYNTAX ERROR") +# $2 — file path +# $3 — tool output (error body) +fail_json() { + local ctx + ctx="$(printf '%s: %s\n\n%s\n\nFix before proceeding.' "$1" "$2" "$3" \ + | jq -Rs .)" + cat <&2 + exit 2 +} + +f="$(jq -r '.tool_input.file_path // .tool_response.filePath // empty')" +[ -z "$f" ] && exit 0 +[ -f "$f" ] || exit 0 + +# Classify by extension. Match on the basename, not the full path — a parent +# dir can carry a dot (e.g. my.project/) and confuse a path-wide match. +kind="" +base="${f##*/}" +case "$base" in + *.ts | *.tsx | *.mts | *.cts) kind="ts" ;; + *.js | *.jsx | *.mjs | *.cjs) kind="js" ;; + *) exit 0 ;; +esac + +if [ "$kind" = "js" ]; then + command -v node >/dev/null 2>&1 || exit 0 + if ! out="$(node --check "$f" 2>&1)"; then + fail_json "JAVASCRIPT SYNTAX ERROR" "$f" "$out" + fi + exit 0 +fi + +# TypeScript. Prefer a project-local tsc so the project's own version decides, +# falling back to one on PATH. +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" +else + exit 0 # no TypeScript compiler available — don't block the edit +fi + +# --moduleDetection force so a file with no import/export still parses as a +# module rather than tripping global-scope collisions against lib types. +out="$("$tsc_bin" --noEmit --skipLibCheck --target es2022 --moduleDetection force "$f" 2>&1 || true)" +syntax_errors="$(printf '%s\n' "$out" | grep -E 'error TS1[0-9]{3}:' || true)" + +if [ -n "$syntax_errors" ]; then + fail_json "TYPESCRIPT SYNTAX ERROR" "$f" "$syntax_errors" +fi + +exit 0 diff --git a/languages/typescript/claude/settings.json b/languages/typescript/claude/settings.json new file mode 100644 index 0000000..f4c9211 --- /dev/null +++ b/languages/typescript/claude/settings.json @@ -0,0 +1,80 @@ +{ + "attribution": { + "commit": "", + "pr": "" + }, + "permissions": { + "allow": [ + "Bash(make)", + "Bash(make help)", + "Bash(make targets)", + "Bash(make test)", + "Bash(make test *)", + "Bash(make lint)", + "Bash(make fmt)", + "Bash(make coverage)", + "Bash(make coverage-summary)", + "Bash(make typecheck)", + "Bash(make build)", + "Bash(npm test)", + "Bash(npm test *)", + "Bash(npm run *)", + "Bash(npm ci)", + "Bash(npm ls *)", + "Bash(node --check *)", + "Bash(node --test *)", + "Bash(node --version)", + "Bash(tsc --noEmit *)", + "Bash(npx tsc --noEmit *)", + "Bash(eslint *)", + "Bash(prettier --check *)", + "Bash(git status)", + "Bash(git status *)", + "Bash(git diff)", + "Bash(git diff *)", + "Bash(git log)", + "Bash(git log *)", + "Bash(git show)", + "Bash(git show *)", + "Bash(git blame *)", + "Bash(git branch)", + "Bash(git branch -v)", + "Bash(git branch -a)", + "Bash(git branch --list *)", + "Bash(git remote)", + "Bash(git remote -v)", + "Bash(git remote show *)", + "Bash(git ls-files *)", + "Bash(git rev-parse *)", + "Bash(git cat-file *)", + "Bash(git stash list)", + "Bash(git stash show *)", + "Bash(jq *)", + "Bash(date)", + "Bash(date *)", + "Bash(which *)", + "Bash(file *)", + "Bash(ls)", + "Bash(ls *)", + "Bash(wc *)", + "Bash(du *)", + "Bash(readlink *)", + "Bash(realpath *)", + "Bash(basename *)", + "Bash(dirname *)" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-typescript.sh" + } + ] + } + ] + } +} -- cgit v1.2.3