π The Hook That Was There, and Did Nothing
A pre-push hook committed to a repository as a plain script file needs its executable permission bit set before Git will actually run it, and that bit is a Unix filesystem attribute that a checkout on Windows has no real equivalent for – the file arrives on disk looking completely normal, with the correct content and the correct name in the correct hooks folder, but without the permission Git checks for before invoking it. Git does not report an error when a hook exists but lacks the executable bit; it simply skips running it, so a push-time check meant to catch a broken build or an accidental commit of a secret file passes every single push without ever actually running once.
π The Problem
$ ls -la .git/hooks/pre-push -rw-r--r-- 1 dev staff 412 Oct 2 09:14 pre-push # Missing the executable bit (no 'x' in the permission string) - # this file was checked out on Windows, where there is no real # equivalent concept, and the clone on a teammate's Linux or macOS # machine inherited whatever permission the checkout produced. $ git push origin feature/billing-fix Enumerating objects: 9, done. ... * [new branch] feature/billing-fix -> feature/billing-fix # Push succeeds immediately - no hook output at all, because Git # silently skipped a hook file it found but could not execute, # rather than failing the push or printing any warning about it. $ cat .git/hooks/pre-push #!/bin/sh # runs the test suite and blocks the push on failure - except it # never actually ran, on this clone, even once.
β Fix: Make the Executable Bit Part of the Setup, Not an Assumption
- Run a chmod command to restore the executable bit as an explicit step in the project’s documented setup process, right after cloning, rather than assuming the bit survives every combination of operating systems and checkout tools involved.
- Distribute the hook through a setup script or a Git hooks manager that sets the executable permission itself at install time, instead of relying on the bit being preserved correctly by whichever clone or checkout path a given teammate happens to use.
- Add a one-line check to CI, or to the setup script itself, that verifies the hook file is actually executable right after it is installed, so a missing permission bit gets caught immediately instead of silently producing a hook that never runs.
β οΈ Why This Is Easy to Miss
- Git skips a non-executable hook without printing any message at all, so a push that was supposed to be checked looks identical, from the command line, to a push where the hook ran cleanly and passed – there is no visible difference between the two outcomes.
- The permission bit question only comes up at all on a cross-platform team, or when a repository gets cloned through a tool or a Windows filesystem that does not preserve Unix permissions the way a native Linux or macOS checkout normally does.
A hook Git can’t execute is a hook Git won’t run – and it won’t tell you either, because from Git’s point of view, skipping it isn’t an error.
