Skip to content

fix(hooks): delegate the generated pre-commit hook to the runtime - #684

Merged
0xPlayerOne merged 10 commits into
mainfrom
fix/delegate-pre-commit-to-runtime
Oct 1, 2026
Merged

0xPlayerOne merged 10 commits into
mainfrom
fix/delegate-pre-commit-to-runtime

Conversation

@0xPlayerOne

Copy link
Copy Markdown
Owner

Closes #683.

Problem

The generated .githooks/pre-commit was a 53-byte whitespace guard:

#!/usr/bin/env sh
set -eu

git diff --cached --check

Meanwhile preCommit() — a language-aware gate for JS/TS, Rust, and Python —
already existed in src/runtime-core.mjs, reachable only through an argv
dispatch that nothing invoked:

if (/\.(js|jsx|ts|tsx|json|md|mdx|yml|yaml)$/.test(changed)) { ci('format'); ci('lint') }
if (/\.rs$|(^|\/)Cargo\.toml$/.test(changed))  { cargo fmt --check; cargo clippy -D warnings }
if (/\.py$|(^|\/)(pyproject\.toml|requirements[^/]*\.txt)$/.test(changed)) { ruff ... }

The template is the intended one, not drift: it is byte-identical to
package/.githooks/pre-commit, and sync --dry-run reports the consumer's
copy as already correct.

Every consumer committed ungated. Formatting, lint, and type errors were
discovered on CI instead.

Changes

The template delegates. It launches the CLI, which dispatches to
preCommit(). The whitespace check stays as an offline fallback so an
unavailable npx cannot block a commit.

preCommit() is exported so the CLI can reach it. runtime-core's argv
dispatch is now guarded by an entry-point check — without it, importing the
module for preCommit would run the gate a second time at import.

preCommit() runs ci('type_check'). It did format and lint but never
typecheck, though ci('type_check') already existed. Type errors were the
most common reason a green-looking commit failed CI later.

doctor can see this now. It asserted only that core.hooksPath was set,
so a correctly enabled but inert hook reported Repository doctor passed. It
now warns when the hook is missing or does not delegate.

Agent directories moved to the shared template. .zcode/, .kiro/, and
.goose/ were being added per repository; they now ship in
src/templates/gitignore so they are ignored once.

Verification

  • 557 tests pass; lint, format, and type-check clean.
  • Against a consumer repo with a staged unused import, the gate blocks the
    commit and prints the oxlint finding.
  • doctor warns on the old template and on a missing hook, and stays silent
    on the new one.

Note on diagnosis

Worth recording, since it cost time here: code-foundry@1.33.0 contains no
mention of hooksPath, pre-commit, or githooks anywhere. Auditing a
consumer against an older CLI suggests hooks are unmanaged when they are in
fact generated. Pin the CLI to runtime_ref before auditing.

Follow-up

sync sources templates from the installed package root
(source: packageRoot), so consumers pick this up on the next release.

The generated `.githooks/pre-commit` was a 53-byte whitespace guard while
`preCommit()` — a language-aware format/lint gate for JS/TS, Rust, and
Python — already existed in `runtime-core.mjs` behind an argv dispatch that
nothing invoked. Every consumer committed ungated and found those errors on
CI instead.

Three gaps:

- The template never called the gate. It now launches the CLI, which
  dispatches to `preCommit()`, with the whitespace check kept as an offline
  fallback so an unavailable npx cannot block a commit.
- `preCommit()` is now exported so the CLI can reach it. runtime-core's own
  argv dispatch is guarded by an entry-point check, because importing it
  would otherwise run the gate a second time on import.
- `preCommit()` did not run `ci('type_check')`, so type errors still reached
  CI. Added to the JS/TS branch, where the dependency graph is already warm.

`doctor` could not detect this: it asserted only that `core.hooksPath` was
set, so a correctly enabled but inert hook reported "Repository doctor
passed". It now checks the hook delegates, and warns when it is missing.

Also moves three agent directories from the consumer gitignores into the
shared template — `.zcode/`, `.kiro/`, and `.goose/` — so they are ignored
once rather than re-added per repository.

Verified: 557 tests pass; lint, format, and type-check clean. Against a
consumer with a staged unused import the gate blocks the commit and prints
the oxlint finding. Doctor warns on the old template and the missing file,
and stays silent on the new one.
@0xPlayerOne
0xPlayerOne marked this pull request as draft October 1, 2026 14:52
The file is copied into every consumer repository, so a multi-paragraph
explanation of why the launcher exists becomes clutter in each one. Keep
the provenance line and the logic.
@0xPlayerOne
0xPlayerOne marked this pull request as ready for review October 1, 2026 15:30
Comment thread .githooks/pre-commit Outdated
The generated hook ran `npx --yes code-foundry@latest`, a mutable dist-tag,
on every commit with --yes suppressing the install prompt. Any publish under
that tag would execute on every developer machine at commit time with the
working tree and local credentials in reach.

sync now substitutes the package version into the hook, so the gate a
repository receives is the gate that generated it. Added three tests: the
version is pinned, no mutable dist-tag survives generation, and the
whitespace fallback stays a fallback rather than a short-circuit.
Comment thread test/sync-pre-commit-hook.test.mjs Fixed
Building a RegExp from the package version drew js/incomplete-sanitization.
The substring check states the same thing and needs no escaping.
@0xPlayerOne
0xPlayerOne merged commit 5d9a571 into main Oct 1, 2026
23 checks passed
@0xPlayerOne
0xPlayerOne deleted the fix/delegate-pre-commit-to-runtime branch October 1, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated .githooks/pre-commit never calls the preCommit() runtime — every consumer is ungated

2 participants