Projects/Personal Site/Architecture Decisions

ADR 014: Husky Precommit

Context

Without automated checks before commits, several quality issues slip through:

  • Inconsistent Formatting: Code style diverges across commits, creating noisy diffs and merge conflicts
  • Broken Builds in CI: Linting errors, type errors, or malformed files fail in CI after pushing, slowing down the feedback loop
  • Documentation Drift: Markdown/MDX files with syntax errors or frontmatter issues break the site build
  • Wasted CI Resources: Running full linting suites in CI for every commit when issues could be caught locally
  • Context Switching Cost: Discovering issues post-push requires switching back to fix trivial problems

I need a pre-commit workflow that:

  • Runs Fast: Only check changed files, not the entire codebase
  • Integrates with Mise: Build on top of the task system established in ADR 004: Mise, not duplicate configuration
  • Supports Multiple Languages: TypeScript, Terraform, Markdown, MDX, YAML across a monorepo
  • Fails Early: Catch errors before they enter git history
  • Stays Simple: Boring technology with deep LLM knowledge bases for AI-assisted development

Decision

I will use Husky for git hook management combined with lint-staged to run targeted checks on staged files.

Husky manages the .git/hooks/pre-commit script, delegating to a mise task that orchestrates lint-staged. The lint-staged configuration in package.json defines file patterns and their corresponding mise tasks, creating a clean delegation hierarchy:

Git commit → Husky → mise run //:pre-commit → lint-staged → mise lint tasks

Toolchain

Git Hook Management:

  • husky: Manages git hooks, runs .husky/pre-commit which calls mise run //:pre-commit

Staged File Processing:

  • lint-staged: Filters staged files by pattern and runs appropriate mise tasks

Linting Libraries:

  • markdownlint-cli: Lints Markdown files for syntax, style, and frontmatter consistency
  • remark-cli with plugins: Lints and auto-fixes MDX files
  • remark-frontmatter: Parse MDX frontmatter
  • remark-gfm: GitHub Flavored Markdown support
  • remark-lint: Linting foundation
  • remark-mdx: MDX-specific syntax support
  • remark-preset-lint-recommended: Sensible defaults for Markdown best practices
  • yaml-lint: Validates YAML syntax and structure

Configuration

The lint-staged configuration in package.json maps file patterns to mise tasks:

{
  "lint-staged": {
    "ui/**/*.{ts,tsx,js,jsx,json}": ["mise run //ui:format"],
    "infra/**/*.tf": ["mise run //infra:format", "mise run //infra:precommit-lint"],
    "**/*.md": ["mise run //:lint:markdown"],
    "**/*.mdx": ["mise run //:lint:mdx"],
    "*.{yml,yaml}": ["mise run //:lint:yaml"]
  }
}

This builds directly on top of mise tasks from ADR 004, avoiding duplication. The tasks in .mise.toml accept file arguments, allowing lint-staged to pass only staged files rather than running against the entire codebase.

Alternatives Considered

pre-commit.com (Python-based framework):

  • Pros: Language-agnostic, extensive plugin ecosystem, declarative YAML configuration, manages hook installation automatically
  • Cons: Introduces Python as a dependency for the entire repo (same monorepo-wide language dependency issue as Husky, just with Python instead of Node). Adds another configuration format (.pre-commit-config.yaml) instead of leveraging existing mise tasks. Less familiarity in the Node.js ecosystem where this project is primarily based.
  • Why not chosen: Would create duplication between .pre-commit-config.yaml and mise tasks, violating the "single source of truth" principle. Python dependency is less natural for a repo with Node.js at the root.

Lefthook (Go-based alternative):

  • Pros: Faster than Husky, language-agnostic, supports parallel execution, single binary with no runtime dependencies
  • Cons: Less mature ecosystem, fewer npm downloads (~900K/week vs Husky's 18M/week), less LLM training data for AI-assisted development
  • Why not chosen: Speed difference is negligible for this project's pre-commit time (2-5 seconds). The "boring technology" principle favors Husky's wider adoption and better LLM knowledge base.

Custom shell scripts in .git/hooks:

  • Pros: Simple, no dependencies, direct control
  • Cons: Not tracked in git by default, requires manual distribution to all contributors, no automatic installation, harder to maintain across team
  • Why not chosen: Violates "low maintenance" requirement. Every contributor would need manual setup instructions.

CI-only checks (no local hooks):

  • Pros: Simplest setup, no local tooling required, guaranteed consistency
  • Cons: Slow feedback loop (minutes vs seconds), wastes CI resources, context switching cost to fix trivial issues post-push
  • Why not chosen: Fails the "fast feedback loop" requirement. Particularly bad for AI-assisted development where Claude Code can make many small commits.

git-hooks via core.hooksPath:

  • Pros: Git-native configuration, no third-party tools
  • Cons: Still requires manual setup per developer, hooks tracked in a separate directory, no automatic installation on clone
  • Why not chosen: Same distribution problem as custom shell scripts.

Consequences

Positive:

  • Fast Feedback Loop: Linters run in seconds on staged files instead of minutes on the entire codebase. Typical pre-commit time: 2-5 seconds vs 30+ seconds for full repo checks.
  • Fails Before CI: Catches formatting, syntax, and linting errors before they enter git history. CI becomes a verification layer, not the first line of defense.
  • Single Source of Truth: Lint tasks are defined once in .mise.toml and reused by both pre-commit hooks and CI. No duplication between local checks and GitHub Actions.
  • Monorepo Awareness: Supports workspace-specific tasks (//ui:format, //infra:format) while maintaining repo-wide consistency for shared file types (Markdown, YAML).
  • Low Maintenance: Husky auto-installs hooks via the prepare script on pnpm install. No manual setup or git hook configuration required.
  • Boring Technology: Husky is the industry standard, with excellent documentation and deep LLM training data. Works seamlessly with the LLM-optimized development workflow.
  • Selective Execution: lint-staged only processes files that are staged, reducing noise and focusing checks on what's actually changing.
  • Claude Code Guardrails: Prevents Claude Code from drifting too far and committing malformed code. Pre-commit hooks act as an immediate correction signal, keeping the AI agent aligned with project standards without requiring constant human oversight.

Negative:

  • Commit Latency: Adds 2-5 seconds to every commit. For trivial changes (typo fixes, documentation tweaks), this can feel like friction. Mitigation: git commit --no-verify to bypass when necessary.
  • Setup Complexity: Requires three tools working together (Husky, lint-staged, mise) rather than simple shell scripts. The abstraction is justified by the flexibility and speed gains, but adds cognitive overhead for new contributors.
  • Silent Failures: If Husky or mise aren't installed (e.g., someone clones without running pnpm install), hooks silently skip. CI catches this eventually, but the local safety net is gone.
  • Merge Conflicts in Hooks: Husky hooks are tracked in git (.husky/ directory). During merges or rebases, these files can conflict, requiring manual resolution. Rare but annoying when it happens.
  • Monorepo-Wide Node.js Dependency: Husky and lint-staged live in the root package.json, requiring Node.js setup even for contributors working exclusively on non-Node parts of the monorepo. A developer making Terraform-only changes or contributing to a hypothetical Python service still needs to install Node.js and run pnpm install at the root to get working git hooks. This is mitigated by mise managing Node at the root level (simplifying setup to just mise install), but it still means additional tooling overhead and a package.json at the repo root for what is fundamentally a multi-language monorepo. If this project ever migrates away from Node entirely, hooks will need reimplementation in a language-agnostic tool.