Adds git hooks (husky + lint-staged) so local commits/pushes catch the failures we've been hitting in CI. Prompted by a real miss this session: an SPA lint error reached CI because typecheck/build/vitest were run after an edit but not npm run lint. These hooks close that gap.
Hooks
Hook
Runs
Catches
pre-commit
lint-staged (eslint --fix + typecheck) on staged web/ files; then dotnet format --verify-no-changes on staged .cs (repo root)
web lint/type errors + .NET formatting drift, scoped to staged files
pre-push
cd web && check:api && lint && typecheck && build
generated v1.json/v1.d.ts drift + a staged change that breaks an unstaged file (lint-staged can't see it) — the "green locally, red in CI" class
commit-msg
requires a Co-Authored-By: trailer (merge commits exempt)
CLAUDE.md commit protocol
Monorepo / worktree handling (the tricky bits)
No root package.json; husky lives in web/. prepare does cd .. && husky so it installs .husky at the repo root and sets core.hooksPath correctly (husky v9 rejects a .. target dir, and npm keeps web/node_modules/.bin on PATH across the cd).
pre-push unsets GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE — git exports these while running hooks, and inside a worktree they make nested git (e.g. check:api's git diff) mislocate the tree and silently pass. Without this, drift slips through in worktrees.
pre-commit skips the dotnet format step entirely when no .cs is staged (avoids the ~20-40s sln load for web-only commits). Note: commits that touch C# take ~20-40s at pre-commit for the scoped format verify — the accepted cost.
Verification (evidence)
Wiring: fresh web/npm install → git config core.hooksPath resolves into the committed .husky.
pre-commit eslint: staged web file with an unused var → commit blocked; clean → passes; a non-web/ commit isn't slowed (~0.7s, "no staged files matching").
commit-msg: message without the trailer → exit 1 (block); with it → exit 0.
pre-push: an unstaged lint error → exit 1 (block, the exact class that bit us); clean tree check:api+lint → pass (no false positive).
Docs: docs/ci-cd.md gains a "Pre-commit hooks" section.
Enabling for others: hooks activate after a web/npm install (the prepare script). Anyone who prefers to skip a hook once can git commit/push --no-verify.
Adds git hooks (husky + lint-staged) so local commits/pushes catch the failures we've been hitting in CI. Prompted by a real miss this session: an SPA lint error reached CI because `typecheck`/`build`/`vitest` were run after an edit but not `npm run lint`. These hooks close that gap.
## Hooks
| Hook | Runs | Catches |
|---|---|---|
| **pre-commit** | `lint-staged` (eslint --fix + typecheck) on staged `web/` files; then `dotnet format --verify-no-changes` on staged `.cs` (repo root) | web lint/type errors + .NET formatting drift, scoped to staged files |
| **pre-push** | `cd web && check:api && lint && typecheck && build` | generated `v1.json`/`v1.d.ts` drift + a staged change that breaks an **unstaged** file (lint-staged can't see it) — the "green locally, red in CI" class |
| **commit-msg** | requires a `Co-Authored-By:` trailer (merge commits exempt) | CLAUDE.md commit protocol |
## Monorepo / worktree handling (the tricky bits)
- No root `package.json`; husky lives in `web/`. `prepare` does `cd .. && husky` so it installs `.husky` at the **repo root** and sets `core.hooksPath` correctly (husky v9 rejects a `..` target dir, and `npm` keeps `web/node_modules/.bin` on PATH across the `cd`).
- pre-push **unsets `GIT_DIR`/`GIT_WORK_TREE`/`GIT_INDEX_FILE`** — git exports these while running hooks, and inside a worktree they make nested `git` (e.g. `check:api`'s `git diff`) mislocate the tree and silently pass. Without this, drift slips through in worktrees.
- pre-commit skips the `dotnet format` step entirely when no `.cs` is staged (avoids the ~20-40s sln load for web-only commits). **Note:** commits that touch C# take ~20-40s at pre-commit for the scoped format verify — the accepted cost.
## Verification (evidence)
- **Wiring**: fresh `web/` `npm install` → `git config core.hooksPath` resolves into the committed `.husky`.
- **pre-commit eslint**: staged web file with an unused var → commit **blocked**; clean → passes; a non-`web/` commit isn't slowed (~0.7s, "no staged files matching").
- **pre-commit dotnet-format**: staged badly-formatted `.cs` → `--verify-no-changes` exit 2 (**block**).
- **commit-msg**: message without the trailer → exit 1 (**block**); with it → exit 0.
- **pre-push**: an **unstaged** lint error → exit 1 (**block**, the exact class that bit us); clean tree `check:api`+`lint` → pass (no false positive).
Docs: `docs/ci-cd.md` gains a "Pre-commit hooks" section.
Enabling for others: hooks activate after a `web/` `npm install` (the `prepare` script). Anyone who prefers to skip a hook once can `git commit/push --no-verify`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy
added the ci-cd label 2026-07-11 20:49:16 +02:00
Installs husky git hooks (via web/'s lint-staged + npm, since the JS/TS
project lives in web/ with no root package.json) to catch lint, format,
type, and generated-API-drift errors locally before they reach CI.
Hooks (committed at repo root under .husky/):
- pre-commit: (a) lint-staged runs eslint --fix on staged
web/src/**/*.{ts,tsx} + a project-wide typecheck; (b) if any *.cs are
staged, dotnet format --verify-no-changes on just those files (skipped
when no .cs staged, so web-only commits skip the sln load).
- pre-push: CI-parity gate — cd web && check:api && lint && typecheck &&
build. Blocks pushing drift or a change that breaks an unstaged file.
- commit-msg: requires a Co-Authored-By trailer (merge commits exempt).
Wiring: web/package.json gains husky + lint-staged devDeps, a lint-staged
config, and a `prepare` script (cd .. && husky) that points git's
core.hooksPath at the repo-root .husky dir on npm install. A fresh
`web/` npm install installs all four hooks automatically.
Monorepo/worktree gotchas handled:
- husky init hard-checks for .git in cwd, so `prepare` cd's to the repo
root before invoking husky (npm keeps web/node_modules/.bin on PATH).
- git exports GIT_DIR while running hooks; in a worktree/subdir that made
pre-push's `git diff` (check:api) mislocate the working tree and pass
silently on drift — pre-push now unsets GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE.
docs/ci-cd.md: new "Pre-commit hooks (web/)" section covering all four.
Verified: eslint error blocks commit; clean commit passes; bad-format .cs
blocks (dotnet format ~6-7s scoped), good .cs passes; check:api drift and
a lint error each block `git push --dry-run`, clean state passes; missing
Co-Authored-By blocks commit-msg, present passes; non-web/.cs commits skip
lint/format. npm run lint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Adds git hooks (husky + lint-staged) so local commits/pushes catch the failures we've been hitting in CI. Prompted by a real miss this session: an SPA lint error reached CI because
typecheck/build/vitestwere run after an edit but notnpm run lint. These hooks close that gap.Hooks
lint-staged(eslint --fix + typecheck) on stagedweb/files; thendotnet format --verify-no-changeson staged.cs(repo root)cd web && check:api && lint && typecheck && buildv1.json/v1.d.tsdrift + a staged change that breaks an unstaged file (lint-staged can't see it) — the "green locally, red in CI" classCo-Authored-By:trailer (merge commits exempt)Monorepo / worktree handling (the tricky bits)
package.json; husky lives inweb/.preparedoescd .. && huskyso it installs.huskyat the repo root and setscore.hooksPathcorrectly (husky v9 rejects a..target dir, andnpmkeepsweb/node_modules/.binon PATH across thecd).GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE— git exports these while running hooks, and inside a worktree they make nestedgit(e.g.check:api'sgit diff) mislocate the tree and silently pass. Without this, drift slips through in worktrees.dotnet formatstep entirely when no.csis staged (avoids the ~20-40s sln load for web-only commits). Note: commits that touch C# take ~20-40s at pre-commit for the scoped format verify — the accepted cost.Verification (evidence)
web/npm install→git config core.hooksPathresolves into the committed.husky.web/commit isn't slowed (~0.7s, "no staged files matching")..cs→--verify-no-changesexit 2 (block).check:api+lint→ pass (no false positive).Docs:
docs/ci-cd.mdgains a "Pre-commit hooks" section.Enabling for others: hooks activate after a
web/npm install(thepreparescript). Anyone who prefers to skip a hook once cangit commit/push --no-verify.🤖 Generated with Claude Code
Installs husky git hooks (via web/'s lint-staged + npm, since the JS/TS project lives in web/ with no root package.json) to catch lint, format, type, and generated-API-drift errors locally before they reach CI. Hooks (committed at repo root under .husky/): - pre-commit: (a) lint-staged runs eslint --fix on staged web/src/**/*.{ts,tsx} + a project-wide typecheck; (b) if any *.cs are staged, dotnet format --verify-no-changes on just those files (skipped when no .cs staged, so web-only commits skip the sln load). - pre-push: CI-parity gate — cd web && check:api && lint && typecheck && build. Blocks pushing drift or a change that breaks an unstaged file. - commit-msg: requires a Co-Authored-By trailer (merge commits exempt). Wiring: web/package.json gains husky + lint-staged devDeps, a lint-staged config, and a `prepare` script (cd .. && husky) that points git's core.hooksPath at the repo-root .husky dir on npm install. A fresh `web/` npm install installs all four hooks automatically. Monorepo/worktree gotchas handled: - husky init hard-checks for .git in cwd, so `prepare` cd's to the repo root before invoking husky (npm keeps web/node_modules/.bin on PATH). - git exports GIT_DIR while running hooks; in a worktree/subdir that made pre-push's `git diff` (check:api) mislocate the working tree and pass silently on drift — pre-push now unsets GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE. docs/ci-cd.md: new "Pre-commit hooks (web/)" section covering all four. Verified: eslint error blocks commit; clean commit passes; bad-format .cs blocks (dotnet format ~6-7s scoped), good .cs passes; check:api drift and a lint error each block `git push --dry-run`, clean state passes; missing Co-Authored-By blocks commit-msg, present passes; non-web/.cs commits skip lint/format. npm run lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>