Harness Intelligence Wiki
Research

Lefthook Pre-Commit Catalogue Integration Research

Lefthook Pre-Commit Catalogue Integration Research

Scope and method

Four readonly lanes inspected the CLI lifecycle, catalogue and desired-state model, existing hook implementations, and prior research/runbooks. The coordinator also inspected the published tool metadata and the upstream Lefthook 2.1.10 source checkout. This report records facts and bounded architectural implications. It does not settle product choices.

Trusted facts

Package identity and upstream behavior

  • The npm name left-hook is not published: npm view left-hook returns a registry 404. The likely intended tool is lefthook, currently published as 2.1.10 with the evilmartians/lefthook repository. This naming must be confirmed before adding a dependency or catalog entry.
  • Lefthook's upstream README documents npm installation, a repository config file, lefthook install, and lefthook run pre-commit: README.md#L20-L69; upstream usage also documents LEFTHOOK=0 git commit: docs/usage.md#L5-L24.
  • Lefthook accepts YAML, TOML, JSON, and JSONC config names and merges a lefthook-local file. Its docs state that lefthook install writes hook scripts under .git/hooks/: docs/configuration.md#L5-L23; docs/index.md#L14-L25.
  • Lefthook supports staged-file placeholders, glob filters, parallel jobs, and stage_fixed for formatters that rewrite files: README.md#L88-L129; docs/index.md#L27-L48; docs/examples/stage_fixed.md#L1-L15.

HI lifecycle and settings

  • hi init is interactive and delegates to repository scaffolding; hi scaffold is the setup/materialization command: apps/cli/src/cli/scaffold-command.ts:210-296.
  • Init computes required tools, writes project settings, and materializes the selected desired scaffold state: apps/cli/src/scaffold/stage.ts:954-968,1022-1029.
  • hi ensure currently only reconfigures provider, repository, and required tool settings. It reads .devpunks/settings.json and writes a ReconfigurationChange; it does not materialize scaffold files: apps/cli/src/cli/ensure-command.ts:17-74.
  • Project settings have no hook-policy field today: apps/cli/src/features/project-settings/model.ts:12-31. The settings service preserves unknown JSON keys when it writes a reconfiguration, but typed reads and changes do not expose a hook choice: apps/cli/src/features/project-settings/service.ts:151-243,292-340.

Catalogue, context plan, and managed output

  • Pack entries already have hooks string IDs, and context plans already have a lifecycle-hook contribution kind: packages/scaffold/src/catalog.ts:12-26; packages/scaffold/src/context-plan.ts:8-55.
  • The current hook catalog contains only the shared scaffold-update-check and format-edited-file assets: apps/cli/src/data/catalog/hooks.ts:1-26. Root context planning always adds the update check, adds pack hooks, and gates the edit formatter on Python or detected Oxfmt: apps/cli/src/features/context-planning/compiler.ts:300-315.
  • Hook source files are copied from the selected baseline into .agents/hooks/ and recorded in the managed output/receipt: apps/cli/src/scaffold/output.ts:3478-3489,3954-3977.
  • Lint assets already describe required dependencies, versions, plugins, config placement, and source files, while scaffold writes lint specs under .devpunks/specs/lint/: apps/cli/src/data/catalog/lint.ts:25-43; apps/cli/src/scaffold/output.ts:3200-3266.
  • Existing lifecycle projection adapters are for Claude, Codex, Cursor, and OpenCode agent events. They have no Git commit event; OpenCode deliberately treats unsupported lifecycle hooks as omissions: apps/cli/src/data/scripts/harness-projection/adapters/{claude,codex,cursor,opencode}.mjs:1-124.

Existing repository hooks are a separate concern

  • HI itself uses .githooks/pre-commit and .githooks/pre-push, installed by scripts/install-git-hooks.mjs, which sets local core.hooksPath and leaves global Git configuration unchanged: .githooks/pre-commit:1-5; .githooks/pre-push:1-10; scripts/install-git-hooks.mjs:23-40.
  • HI's pre-commit selector is worktree-aware, parses staged renames, maps apps/* and packages/* owners, and runs filtered checks or a root check for global/unknown paths: apps/cli/scripts/staged-verification-selector.mjs:19-113.
  • This repository-owned release gate is not consumer scaffold behavior. Adding Lefthook to HI itself would compete with the existing .githooks and core.hooksPath mechanism.

Alignment constraints

  • HI requires a lint/format migration to update package scripts, task pipelines, CI, editor/docs references, and hooks together: docs/reference/dp-requirements.md:244-260; apps/wiki/content/docs/harness/validation-and-tools/hooks.mdx:27-43.
  • The current global tool catalog models operator binaries installed through global Bun, pnpm, or npm commands: apps/cli/src/data/catalog/tools.ts:1-60; apps/cli/src/integrations/tool-management.ts:435-555. That mechanism is a poor fit for a project-local commit gate whose version should resolve from the consumer's package manager and lockfile.

Evidence-backed architectural implications

These are recommendations for a later requirements/specification pass, not accepted decisions:

  1. Keep Git commit policy separate from the existing agent lifecycle-hook projection, or add an explicit Git-hook contribution subtype. This avoids teaching four AI-provider adapters about a Git event they do not own while retaining the existing catalog, context-plan, desired-state, and receipt seams.
  2. Persist the opt-out as typed project policy in .devpunks/settings.json so hi init, hi ensure, hi update, and hi check can derive one deterministic desired state. A command-only flag cannot explain rerun, drift, or removal behavior. The field name and shape remain open.
  3. Prefer a consumer-local lefthook dev dependency plus a generated config and an explicit install/reconcile action. Do not classify it as a global Harness operator tool unless the product intentionally wants a PATH-owned version. Do not add lefthook install blindly to prepare: upstream installation mutates .git/hooks, and existing consumer prepare commands are already reconciled as structured entries.
  4. Define the first rule as a staged pre-commit gate that runs the consumer's canonical format and lint commands, with explicit behavior for formatter rewrites (stage_fixed versus check-only), mixed JS/TS and Python repos, monorepo owner selection, empty/non-source commits, and command failure. Lefthook provides staged-file and glob primitives, but it does not discover HI's nearest-workspace lint policy for us.
  5. Treat the generated config, hook installer state, dependency declaration, and policy as separate managed/structured entries. Preserve unrelated project-owned Husky, lint-staged, .githooks, and core.hooksPath state; fail or ask before taking ownership of an existing hook manager. Disable, uninstall, and recovery semantics must be explicit before implementation.

Conflicts and unresolved product decisions

  • Identity: confirm whether “left-hook” means the published lefthook package, a private/internal executable, or another tool. No implementation should infer this from the spelling alone.
  • Scope: decide whether every consumer receives the gate, or only repos with a discoverable lint/format command and supported language/config.
  • Opt-out semantics: decide whether opt-out prevents installation, execution, or both; whether it removes only HI-owned files; and how a later hi ensure re-enables the gate.
  • Lifecycle placement: decide whether hi ensure remains settings-only and schedules a later scaffold update, or becomes a transaction that also reconciles the dependency, config, and Git hook.
  • Command contract: decide the canonical format/lint command shape, staged versus workspace scope, formatter mutation/staging, package-manager choice, and version pin/range.
  • Ownership/coexistence: decide behavior for an existing core.hooksPath, Husky, lint-staged, Lefthook config, or direct .git/hooks/pre-commit.
  • Authority and drift: decide which files are strict baseline-managed, which settings/config keys are structured and mergeable, and how consumer edits are preserved or reported.

Synthesized conclusion

HI has the right catalog and managed-state primitives, but no current contract for consumer Git hooks. The lowest-friction architecture is a project-policy controlled, project-local Lefthook integration that is planned with scaffold desired state, installed/reconciled through an explicit lifecycle action, and kept distinct from agent edit-hook projections. The package spelling, command semantics, coexistence rules, and opt-out behavior require a requirements decision before code or baseline assets are added.

Next local action

Confirm the intended package identity and answer the unresolved policy questions through a requirements grill. If accepted, compile a spec that assigns catalog, settings, scaffold-state, package-manager, hook-reconciliation, and docs work to their owning surfaces before implementation.

On this page