# ADR-0002: Pre-commit hook batch review **Date:** 2026-03-28 **Status:** Accepted ## Context Nine PRs proposing new or modified pre-commit hooks were evaluated simultaneously. The repository already has 43 pre-commit hooks; new hooks must clear a high bar for signal-to-noise ratio, speed, and non-duplication with CI. Review was conducted using 86 automated review agents across 4 rounds, cross-validating findings and verifying correctness of each recommendation. ## Decision ### Accepted (4 PRs) | PR | Hook | Fix Applied | |----|------|-------------| | #3220 | fix(security): escape server data in innerHTML | None — merged as-is | | #3221 | fix(config): sync ruff version (v0.14.10 → v0.15.8) | Full `ruff format .` pass to reformat all old-style lambdas | | #3225 | chore(hooks): `raise ... from` enforcement in except blocks | None — merged as-is | | #3219 | chore(hooks): layer-import boundary enforcement | Added `exclude: ^tests/` to prevent false positives on test files | ### Rejected (5 PRs) **#3218 — utcnow callable check:** Directly contradicts the existing `check-utcnow-parens` hook. `sqlalchemy_utc.utcnow` is a `FunctionElement` class, so `utcnow()` creates a SQL expression rendered per-INSERT as `CURRENT_TIMESTAMP`. The existing hook correctly requires parentheses. Having both hooks would block every commit. **#3222 — codespell (typo detection):** False-positives on camelCase identifiers are a known, unsolved problem ([codespell-project/codespell#196](https://github.com/codespell-project/codespell/issues/196), open since 2017). The tool treats `hasTable`, `doesnt`, `crasher`, and similar code identifiers as misspellings, requiring an ever-growing ignore-words-list. **#3227 — innerHTML regex scanner:** The core regex (`INTERPOLATION_RE` using `[^}]+`) truncates at the first `}`, failing on nested braces in template literals. Multiple real JS files are affected: `settings.js:80`, `collection_details.js:210`, `history.js:421,501,503`, `semantic_search.js:197-205`. This causes both false positives and false negatives — unacceptable for a security tool. The project already has ESLint and Bearer for JavaScript security analysis, both of which use proper AST-based parsing. Use ESLint `no-unsanitized` rule instead. **#3230 — vulture (dead code detection):** The hook used `language: system`, which requires vulture on the system PATH and breaks in CI (the pre-commit workflow does not install project dev dependencies). The larger issue is policy: vulture is advisory-only in CI (release gate, not PR gate). Promoting it to a blocking pre-commit hook is a policy escalation that should be a deliberate team decision. Use the existing `vulture-dead-code.yml` CI workflow instead. **#3231 — mypy (type checking):** Most production modules have `ignore_errors = true` in `pyproject.toml` (`web.*`, `database.*`, `settings.*`, `utilities.*`, `security.*`, `api.*`, and 8+ more). mypy already blocks PRs via `mypy-type-check.yml` + `ci-gate.yml`. Adding it as a pre-commit hook would add 10-30 seconds per commit for near-zero detection value on the majority of the codebase. ## Consequences - Four new hooks strengthen the pre-commit pipeline (XSS prevention, ruff consistency, exception chaining, architectural boundaries) - Five proposals are documented as rejected with specific technical reasons, preventing re-proposals without new information - Rejected hooks are listed in `.pre-commit-config.yaml` comments with a link to this document - The total hook count increases from 43 to 45, with negligible performance impact (~100-200ms per commit for the new hooks) ## Principles for future hook proposals 1. **Fast**: target under 3 seconds for a typical staged-file set. 2. **Scoped appropriately**: file-scoped hooks are preferred. Hooks requiring full-repo analysis need a narrow `files:` trigger and team consensus. 3. **High local value**: don't add slow CI checks to pre-commit when the local feedback value is low (e.g., mypy with most modules ignored). 4. **Validated**: run the hook against the existing codebase before proposing. 5. **Low false-positive rate**: if suppressions are needed from day one, reconsider whether the hook belongs here. 6. **Low false-positive on identifiers**: tools designed for natural language may over-fire on camelCase identifiers, abbreviations, and domain terms. 7. **Self-contained**: hooks must work in pre-commit's CI workflow without requiring system-level dependencies (`language: python` with `additional_dependencies`, not `language: system`). ## Related PRs | PR | Title | Decision | |----|-------|----------| | [#3218](https://github.com/LearningCircuit/local-deep-research/pull/3218) | chore(hooks): prevent frozen utcnow() | Rejected — conflicts with existing hook | | [#3219](https://github.com/LearningCircuit/local-deep-research/pull/3219) | chore(hooks): layer-import boundary enforcement | Accepted (fix: `exclude: ^tests/`) | | [#3220](https://github.com/LearningCircuit/local-deep-research/pull/3220) | fix(security): escape server data in innerHTML | Accepted | | [#3221](https://github.com/LearningCircuit/local-deep-research/pull/3221) | fix(config): sync ruff version | Accepted (fix: full `ruff format .`) | | [#3222](https://github.com/LearningCircuit/local-deep-research/pull/3222) | chore(hooks): add codespell typo detection | Rejected — CamelCase false positives | | [#3225](https://github.com/LearningCircuit/local-deep-research/pull/3225) | chore(hooks): `raise ... from` in except blocks | Accepted | | [#3227](https://github.com/LearningCircuit/local-deep-research/pull/3227) | chore(hooks): detect unescaped innerHTML | Rejected — regex bug | | [#3230](https://github.com/LearningCircuit/local-deep-research/pull/3230) | chore(hooks): vulture dead code detection | Rejected — policy escalation | | [#3231](https://github.com/LearningCircuit/local-deep-research/pull/3231) | chore(hooks): mypy type checking | Rejected — already in CI |