aboutsummaryrefslogtreecommitdiff
path: root/languages/typescript/claude
diff options
context:
space:
mode:
Diffstat (limited to 'languages/typescript/claude')
-rwxr-xr-xlanguages/typescript/claude/hooks/validate-typescript.sh94
-rw-r--r--languages/typescript/claude/settings.json80
2 files changed, 174 insertions, 0 deletions
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 <<EOF
+{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": $ctx}}
+EOF
+ printf '%s: %s\n%s\n' "$1" "$2" "$3" >&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"
+ }
+ ]
+ }
+ ]
+ }
+}