diff --git a/.claude/settings.json b/.claude/settings.json index c305f9c9..dadd3f81 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -8,7 +8,31 @@ "Edit(Client/src/lib/protocolTypes.ts)", "Write(Client/src/lib/protocolTypes.ts)", "Edit(Client/src/generated/**)", - "Write(Client/src/generated/**)" + "Write(Client/src/generated/**)", + "Read(**/.env)" + ] + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "node \"${CLAUDE_PROJECT_DIR}/scripts/claude-hook.mjs\" session-start" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "node \"${CLAUDE_PROJECT_DIR}/scripts/claude-hook.mjs\" pre-bash" + } + ] + } ] } } diff --git a/Client/tsconfig.json b/Client/tsconfig.json index a7323d2c..d8a91153 100644 --- a/Client/tsconfig.json +++ b/Client/tsconfig.json @@ -5,6 +5,7 @@ "moduleResolution": "bundler", "strict": true, "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, diff --git a/Server/.golangci.yml b/Server/.golangci.yml index 279d416b..25bdb2ed 100644 --- a/Server/.golangci.yml +++ b/Server/.golangci.yml @@ -28,6 +28,7 @@ linters: # Silent-drop and sentinel-error classes (added 2026-08-30). - exhaustive # a switch over an enum-like type with no default that misses a member - errorlint # errors.Is/As instead of == and type assertions on wrapped errors; %w in Errorf + - durationcheck # time.Duration multiplied by a Duration-typed value, e.g. d*time.Second where d is already a Duration settings: exhaustive: diff --git a/scripts/claude-hook.mjs b/scripts/claude-hook.mjs new file mode 100644 index 00000000..7b270c20 --- /dev/null +++ b/scripts/claude-hook.mjs @@ -0,0 +1,49 @@ +// Claude Code hooks for this repository, wired in .claude/settings.json. +// +// session-start warn when the repo git hooks are not installed, so a clone +// or a new machine cannot silently run without them. +// pre-bash refuse a top-level `cd`. The Bash tool's shell is +// persistent, so a `cd` leaks its directory into every later +// command; a relative path then resolves somewhere else and a +// gate can report a green result from the wrong directory. +// Use a subshell `( cd DIR && ... )`, `git -C DIR`, or paths +// from the repository root instead. +// +// Exit code 2 from a PreToolUse hook blocks the tool call and shows stderr to +// the model; anything else lets it through. +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +const mode = process.argv[2]; + +if (mode === "session-start") { + let hooksPath = ""; + try { + hooksPath = execFileSync("git", ["config", "core.hooksPath"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + } catch { + // unset: git exits 1 + } + if (hooksPath !== ".githooks") { + console.log( + "WARNING: the repo git hooks are not installed (core.hooksPath is not .githooks). Run `npm run hooks:install` at the repository root.", + ); + } +} else if (mode === "pre-bash") { + let command = ""; + try { + command = JSON.parse(readFileSync(0, "utf8")).tool_input?.command ?? ""; + } catch { + // no or malformed input: nothing to check + } + // A `cd` that starts the command or follows a chain operator or newline is a + // top-level statement; `( cd DIR && ... )` is not matched. + if (/(^|\n|&&|\|\||;)\s*cd(\s|$)/.test(command)) { + console.error( + "Blocked: a top-level `cd` leaks the persistent shell cwd into every later command. Use a subshell `( cd DIR && ... )`, `git -C DIR`, or paths from the repository root.", + ); + process.exit(2); + } +}