Skip to content

Instantly share code, notes, and snippets.

@ivy
Created April 2, 2026 23:20
Show Gist options
  • Select an option

  • Save ivy/36674310695d5d078a2ca9a25324113d to your computer and use it in GitHub Desktop.

Select an option

Save ivy/36674310695d5d078a2ca9a25324113d to your computer and use it in GitHub Desktop.
Reproduce a bug in lefthook
#!/usr/bin/env bash
# reproduce-bug.sh
#
# Reproduces the bug where `lefthook run` auto-installs hooks into the
# directory returned by `git rev-parse --git-path hooks` without checking
# whether `core.hooksPath` is set, silently overwriting global hooks.
#
# Runs entirely inside a throwaway Docker container.
[[ -n "${DEBUG:-}" ]] && set -o xtrace
set -o errexit
set -o errtrace
set -o nounset
set -o pipefail
readonly IMAGE_TAG="lefthook-bug-repro"
readonly CONTAINER_NAME="lefthook-bug-repro-run"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
# ---------------------------------------------------------------------------
# Cleanup
# ---------------------------------------------------------------------------
cleanup() {
echo ""
echo "--- Cleaning up Docker resources ---"
docker rm --force "${CONTAINER_NAME}" 2>/dev/null || true
docker rmi --force "${IMAGE_TAG}" 2>/dev/null || true
}
trap cleanup EXIT
# ---------------------------------------------------------------------------
# Build the Docker image
# ---------------------------------------------------------------------------
build_image() {
echo "--- Building Docker image ${IMAGE_TAG} ---"
docker build \
--file - \
--tag "${IMAGE_TAG}" \
"${SCRIPT_DIR}" \
<<'DOCKERFILE'
FROM golang:1.26
WORKDIR /src
COPY . .
RUN go build -o /usr/local/bin/lefthook .
RUN git config --global init.defaultBranch main
DOCKERFILE
}
# ---------------------------------------------------------------------------
# Run the reproduction inside a container
# ---------------------------------------------------------------------------
run_reproduction() {
echo ""
echo "--- Running reproduction inside container ---"
docker run \
--name "${CONTAINER_NAME}" \
--rm \
"${IMAGE_TAG}" \
bash -c '
set -o errexit
set -o nounset
set -o pipefail
GLOBAL_HOOKS_DIR="/root/.config/git/hooks"
GLOBAL_PREPUSH="${GLOBAL_HOOKS_DIR}/pre-push"
MARKER="global pre-push hook"
# ---------------------------------------------------------------
# Step 1: Set up a global hooks directory with a custom pre-push hook
# ---------------------------------------------------------------
echo ">>> Step 1: Setting up global hooks directory with pre-push hook"
mkdir -p "${GLOBAL_HOOKS_DIR}"
cat > "${GLOBAL_PREPUSH}" << HOOK
#!/bin/sh
echo "${MARKER}"
HOOK
chmod +x "${GLOBAL_PREPUSH}"
git config --global core.hooksPath "${GLOBAL_HOOKS_DIR}"
echo " Global pre-push hook contents before test:"
cat "${GLOBAL_PREPUSH}"
echo ""
# ---------------------------------------------------------------
# Step 2: Create a test repo with lefthook config (pre-commit only)
# ---------------------------------------------------------------
echo ">>> Step 2: Creating test repo with lefthook config"
cd /tmp && git init test-repo && cd test-repo
git config user.email "test@test.com"
git config user.name "Test"
cat > lefthook.yml << EOF
pre-commit:
commands:
hello:
run: echo "lefthook"
EOF
# ---------------------------------------------------------------
# Step 3: Force-install lefthook (user explicitly opts in)
# ---------------------------------------------------------------
# This installs a lefthook dispatcher for pre-commit into the global
# hooks directory. The pre-push hook is untouched.
echo ""
echo ">>> Step 3: Running lefthook install --force"
lefthook install --force 2>&1 || true
echo ""
echo " Global pre-push after install:"
cat "${GLOBAL_PREPUSH}"
echo ""
# Make an initial commit to establish a checksum baseline.
git add .
git commit -m "initial commit" 2>&1 || true
# ---------------------------------------------------------------
# Step 4: Add pre-push to lefthook config (creates checksum mismatch)
# ---------------------------------------------------------------
# Now the user adds a pre-push hook to lefthook.yml. On the next commit,
# syncHooks detects the config change and calls createHooksIfNeeded,
# which writes a pre-push dispatcher into the global hooks directory,
# overwriting the custom pre-push hook.
echo ""
echo ">>> Step 4: Adding pre-push to lefthook config + committing"
# Ensure the config file timestamp changes (checkHooksSynchronized
# compares Unix seconds, and Docker can run fast enough to stay
# within the same second).
sleep 1
cat >> lefthook.yml << EOF
pre-push:
commands:
check:
run: echo "push check"
EOF
git add .
git commit -m "add pre-push hook" 2>&1 || true
# ---------------------------------------------------------------
# Step 5: Check whether the global pre-push hook was overwritten
# ---------------------------------------------------------------
echo ""
echo ">>> Step 5: Checking global pre-push hook"
echo " Global pre-push contents after test:"
cat "${GLOBAL_PREPUSH}"
echo ""
if grep -q "${MARKER}" "${GLOBAL_PREPUSH}"; then
echo ""
echo "============================================"
echo " PASS - Global pre-push was NOT overwritten."
echo " The bug is not present (or has been fixed)."
echo "============================================"
exit 0
else
echo ""
echo "============================================"
echo " FAIL - Global pre-push WAS overwritten!"
echo " syncHooks wrote a lefthook dispatcher into"
echo " the core.hooksPath directory, clobbering"
echo " the custom pre-push hook."
echo "============================================"
exit 1
fi
'
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
build_image
run_reproduction
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment