`lefthook run post-checkout` blocks until stdin EOF, deadlocking when the caller holds stdin open
Summary
On v2.1.10, lefthook run post-checkout does not run its jobs (or exit)
until its stdin reaches EOF. When the invoking context holds stdin open
without writing — an automation harness with a socket stdin, a task runner
pipe, a CI wrapper — the hook deadlocks indefinitely: banner printed, no
child processes, main thread parked (futex_do_wait on Linux,
__psynch_cvwait on macOS).
Git hands post-checkout no stdin payload (its inputs are the three
arguments), so blocking on stdin here has no upside; the payload-carrying
hooks are the ones that legitimately read stdin. Of the hooks we tested,
only post-checkout exhibits this — pre-commit and pre-push return
promptly under the same conditions.
Where this bites in practice: any scripted git worktree add / checkout
automation whose hooks run with an inherited, long-lived stdin. Because
TTYs and /dev/null EOF immediately while pipes and sockets never do, the
hang looks intermittent across invocation contexts and is easy to
misdiagnose as a race.
Minimal reproduction
d=$(mktemp -d)/r && mkdir -p "$d" && cd "$d" && git init -q .
git config user.email [email protected] && git config user.name a
cat > lefthook.yml <<'EOF'
post-checkout:
commands:
noop:
run: "true"
EOF
git add -A && LEFTHOOK=0 git commit -qm init && lefthook install -f
# completes in ~0.06s:
timeout 10 lefthook run post-checkout </dev/null; echo "eof-stdin exit=$?"
# hangs until killed (exit 124), job never runs:
mkfifo pipe; sleep 60 > pipe &
timeout 10 lefthook run post-checkout < pipe; echo "open-stdin exit=$?"Same behavior with the identical script comparing hooks: pre-commit and
pre-push exit 0 under the open-pipe stdin; post-checkout exits 124
(killed), deterministically.
Environment
- lefthook v2.1.10 (latest at time of filing), reproduced on:
- macOS 15.x, Homebrew install, Apple Silicon
- Debian-based Linux container, arm64, release binary
- Observed originally as intermittent CI/test-suite deadlocks; the repro above is deterministic once stdin is controlled.
Expected
post-checkout jobs run without waiting on stdin, since git supplies that
hook no stdin data — or at minimum, stdin consumption is opt-in the way
payload-carrying hooks need it.
Possibly related history: #588 (stdin swallowed from the terminal), #733 (request for an option to redirect stdin to /dev/null), #836 (pre-push stdin hang with LFS).
Downstream diagnosis with additional detail: https://github.com/evanharmon1/harmon-init/pull/904
Source: evilmartians/lefthook