Skip to content
TilloTech Docs

Use Lefthook for repository-managed Git hooks

Status

Accepted. The Nx task selection amendment supersedes staged-file-only execution and the Turborepo references below.

Context

EOS uses pnpm workspaces and Turborepo to coordinate checks across applications, libraries, and versioned plugin packages. Mise provides the development toolchain. Linting, formatting, typechecking, testing, and documentation validation already have repository-level commands and authoritative CI jobs.

Git hooks can provide earlier feedback, but they introduce a separate local execution boundary. A hook system must install reliably for contributors, select only relevant staged files, handle paths across the monorepo, and avoid duplicating the task graph owned by Turborepo. It should also accommodate non-Node tooling if EOS becomes more polyglot.

Running the complete workspace check before every commit would increase commit latency and repeat work already performed by CI. Limiting hooks to fast checks requires file filtering, conditional execution, and safe parallelism. These capabilities can be supplied by a hook manager, composed from multiple Node.js tools, or implemented in repository-owned scripts.

Decision Drivers

  • Fast feedback based on the files staged for the current commit.
  • One repository-level configuration for all pnpm workspaces.
  • Compatibility with the existing mise, pnpm, and Turborepo setup.
  • Conditional execution by path and file type without custom routing scripts.
  • Parallel execution of independent read-only checks.
  • Support for Git hook types beyond pre-commit if they are needed later.
  • A language-neutral mechanism that can invoke future non-Node tooling.
  • Minimal overlap with Turborepo's package graph and CI's authoritative checks.
  • Low configuration and dependency overhead.

Considered Options

Lefthook

Lefthook is a standalone Git hook manager configured from a repository-level YAML file. It provides staged-file templates, glob and path filtering, per-command working directories, parallel jobs, and multiple Git hook types. It can be installed as a pnpm development dependency while remaining neutral about the languages and commands it executes.

Lefthook can perform the required staged-file routing without an additional tool such as lint-staged. Its configuration can invoke the repository's existing locally installed tools and can add jobs for other ecosystems without changing hook managers.

Husky with lint-staged

Husky provides a lightweight connection between Git hooks and repository-owned shell scripts. lint-staged adds staged-file filtering, command concurrency, and handling for partially staged files.

Together they meet the functional requirements and are familiar within the Node.js ecosystem. They require two dependencies and two configuration layers, with hook behavior divided between shell files and lint-staged configuration. Husky alone would require EOS to implement the staged-file orchestration that lint-staged or Lefthook already provides.

pre-commit

pre-commit is a multi-language hook framework that can install tools into isolated environments and run them against matching files. It is well suited to repositories that want the hook system to provision and version toolchains independently of the repository's primary development environment.

EOS already uses mise for its development toolchain and pnpm for Node.js dependencies. Adding pre-commit would introduce a Python-managed installation, cache, and dependency lifecycle beside those existing mechanisms. The local checks do not require that isolation.

simple-git-hooks

simple-git-hooks installs commands from package configuration with little additional machinery. It would keep the dependency and installation surface small.

It does not provide the staged-file filtering, monorepo routing, or job orchestration required by this decision. EOS would need to implement and maintain those capabilities in scripts.

Native Git hooks maintained by EOS

EOS could maintain hook scripts directly and configure Git to use them during repository setup. This would avoid a hook-manager dependency and allow complete control over execution.

The repository would then own cross-platform installation, updates, file selection, quoting, concurrency, and contributor opt-out behavior. Those responsibilities are not specific to EOS and would add maintenance without changing the checks being run.

Decision Outcome

EOS will use Lefthook to manage repository-provided Git hooks.

Lefthook will be installed as a development dependency at the pnpm workspace root, and its required installation script will be explicitly allowed by the workspace's dependency build policy. Hook behavior will be declared in a root lefthook.yml file.

The initial pre-commit hook will run fast, read-only formatting and linting checks only for relevant staged files. Independent checks may run in parallel. It will not run the complete pnpm check command or duplicate Turborepo's workspace dependency graph. Builds, full typechecking, tests, and release validation remain authoritative in CI. A future pre-push hook may invoke affected-package Turbo tasks if commit-time checks prove insufficient, but that is outside this decision.

Consequences

Positive

  • Contributors receive formatting and linting feedback before creating a commit.
  • Staged-file selection, path filtering, and parallel execution are expressed in one repository-level configuration.
  • EOS does not need lint-staged or custom shell orchestration for the initial hook set.
  • The hook configuration can cover multiple workspaces and future languages without changing managers.
  • Turborepo and CI retain clear ownership of package-level and complete repository verification.

Negative

  • Lefthook adds a development dependency and runs an installation script that must be permitted by pnpm's dependency build policy.
  • Contributors must reinstall or repair hooks if their local Git configuration is changed or the installation step is skipped.
  • Commands invoked by hooks still require their underlying tools to be available; Lefthook does not provide isolated tool environments.
  • Contributors unfamiliar with Lefthook must learn its configuration and local opt-out mechanisms.

Neutral

  • Git hooks remain bypassable and therefore provide feedback rather than the final enforcement boundary; CI continues to enforce repository policy.
  • Lefthook manages command execution but does not replace mise, pnpm, or Turborepo.
  • Commits made from terminals with mise active have the required Node.js and pnpm commands. The implementation must validate hook behavior from Git GUI clients or shells without mise. These clients may require a documented setup step.
  • The exact staged-file globs and commands will evolve with the repository's supported file types while preserving the decision boundary described here.

Nx task selection amendment

Date: 2026-09-10

Nx replaces Turborepo as the task runner. Lefthook manages Git events and passes staged or pushed filenames to Nx. Nx selects affected projects, runs the check tools, and manages parallel execution and the task cache.

The pre-commit hook runs lint, format, and Markdown checks for selected projects. These checks read their full inputs in the working tree, including unstaged files. This replaces the initial staged-file-only scope. An unstaged error in a selected project can therefore block a commit.

The pre-push hook runs the repository's offline link check when the push includes Markdown changes. Builds, typechecks, tests, and release checks remain separate from the commit hook. The finalise-change skill and CI use the same Nx targets.

Ultracite amendment

Date: 2026-09-15

The pre-commit hook first runs ultracite fix on supported staged source files. Lefthook adds safe fixes to the Git index with stage_fixed. The hook then runs the Nx checks from the previous amendment. This step follows the Ultracite Git hook guidance.

The hook passes explicit staged filenames to Ultracite. It does not apply fixes to unrelated unstaged source files. Markdown keeps its separate repository checks because the documentation workflow also uses Markdownlint. Lefthook adds the complete fixed file, so a partially staged file needs index review.