Githooks Pins Nix vendorHash After Go Module Drift


Githooks is a Git hooks manager written in Go. It runs hooks checked into a repo and hooks pulled from shared Git repositories, which is how a lot of data pipeline trees keep lint, format, and secret scanning consistent on commit and push. This week main moved by one commit. The change is a Nix vendorHash pin, not a runtime fix. If you build the in tree flake against current go.mod, that pin is the difference between a hash mismatch and a binary.

On 2 September 2026 the maintainer fixed the Nix build hash. The edit is one assignment in nix/pkgs/default.nix. One insertion, one deletion. No Go source changed. No CLI flag changed. No hook runner behavior changed.

buildGo126Module still fetches the module graph as a fixed output derivation, then compiles. If the hash recorded in the package does not match the bytes Nix received, the build stops before githooks-cli exists. The new pin is:

modRoot = "./githooks";
vendorHash = "sha256-bHQJqDo+awZpQvCni28b7p/tbHEJWtHX/fF18mZNObI=";

modRoot is easy to miss. The Go module is not at the repo root. It lives under ./githooks. A wrong vendorHash is not a compiler error. It is a fetch error. A green go test on a laptop does not prove nix build ./nix#default works. The Nix side was stale. Now it is not.

The pin moved because the module graph moved first. On 19 August 2026 a bulk update rewrote githooks/go.mod and most of githooks/go.sum. Direct dependencies jumped in the same change: spf13/cobra from 1.2.1 to 1.10.2, spf13/viper from 1.9.0 to 1.21.0, golang.org/x/crypto from 0.1.0 to 0.55.0, and the Gitea SDK from 0.15.0 to 0.25.1. Indirects moved with them. go.sum shrank by hundreds of lines.

Nix does not download Go modules at compile time the way a laptop go build does. buildGo126Module vendors from go.mod and go.sum, hashes that tree, and compares the digest to vendorHash. When the lockfile changes and the hash does not, every build of the ./nix#default package fails. The mismatch sat on main from 19 August until 2 September. That is a long time to leave just build-nix broken.

The runtime code in that module bump is a separate story. It is not this week’s commit. This week’s commit only records that the Nix fetch hash was still pointing at the old vendor tree. Shipping go.mod without the matching vendorHash is the Nix equivalent of committing a module file without its sum file. The builder will tell you. It just told the flake users first.

The file around the pin is the package operators consume. nix/flake.nix calls pkgs.callPackage ./pkgs/default.nix. The package name is githooks. Version is read from nix/pkgs/version.json and is still 3.0.6. The builder is buildGo126Module, so the toolchain is Go 1.26. ldflags strip the symbol table and DWARF. doCheck is false, so the Nix package does not run Go tests.

After compile, cli, runner, and dialog are renamed to githooks-cli, githooks-runner, and githooks-dialog. wrapProgram puts git on PATH for the CLI and the runner. postConfigure runs go generate -mod=vendor ./.... The vendor tree Nix produced has to be complete enough for generate, not only for compile. A bad vendorHash fails earlier, so generate never runs.

The Go build tag package_manager_enabled is on. That tag compiles PackageManagerEnabled as true. Automatic updates are forbidden. The binary is treated as externally managed. That is the correct default for a Nix install. It is a trap if you expected the same self update path you get from a curl installer build. Package manager builds also skip writing a default githooks.runner Git config, and fall back to whatever is already set.

None of that packaging logic changed this week. It is the context for why a one line hash edit matters for flake users and is unchanged for everyone else.

Blast radius depends on the pin you already have.

If the flake input is a version ref such as github:gabyx/githooks?dir=nix&ref=v3.0.4, you are on that snapshot. This commit does not rewrite it. You see the new hash only when the ref moves to main or to a later tag. The README still documents that form, then tells you to run githooks-cli installer after the package is on PATH. Installing the derivation does not enable hooks by itself.

If you track main through dir=nix, or you run nix build ./nix#default in a checkout, you were broken after the 19 August module bump and you are fixed now. Pull and rebuild. The local helper is just build-nix, which is nix build -L "./nix#default".

If you take pkgs.githooks from nixpkgs, you are on the nixpkgs pin, not on this repo’s main. The githooks flake and the nixpkgs package are not the same bits until nixpkgs copies the hash. Do not mix those two pins and then debug a vendorHash error that only exists on one of them.

A read only global Git config still fails the installer. That is unrelated to the hash, and it is still the first thing to check when the package builds and githooks-cli installer does not.

The next Go module bump needs a vendorHash update in the same change. Splitting them is how flake builds on main sat broken for two weeks. A Nix build step in required checks would have caught it the day go.mod moved.

doCheck = false is still the package default. The Nix derivation will not catch a Go test regression. The Alpine integration scripts in tests/ remain the real gate, and they do not substitute for nix build.

version.json still says 3.0.6 while main now vendors a newer module graph than the version bump commit. Flake inputs that pin a version tag will not pick up this hash until a new tag exists. Until then, tracking main is the only way to get a Nix package that matches current go.sum.