Lefthook v2 runs `git lfs pre-push` internally for pre-push hooks (per
docs/usage/features/git-lfs.md; confirmed in internal/run/controller/
lfs.go where the internal handler invokes `git lfs pre-push <remote>
<url>` with a buffered `cachedStdin`). Our manual `git-lfs:` command
in lefthook.yml was a second invocation against the same remote; the
duplicate is directly visible in `LEFTHOOK_VERBOSE=1` output as
`[git-lfs] executing hook` (internal) followed by `[lefthook] run:
git lfs pre-push` (manual).
The previous fix attempt (piped: true, commit 1b06804) was based on a
wrong understanding of `piped`'s semantics — `piped` controls
fail-fast behavior, not stdin routing or sequencing. Default lefthook
behavior is already sequential per docs/configuration/parallel.md.
That "fix" was placebo; incident 2 (F2 push, zero LFS objects, commit
99eedb6) proved it.
Phase A investigation: documentary + source confirmation that lefthook
owns the LFS pre-push call. Phase B sandbox test against a filesystem
remote confirmed the duplicate execution in logs but did NOT reproduce
the production hang — likely because the duplicate manual call against
a local remote has no LFS server to interact with. A network-y remote
(Gitea over SSH/HTTPS) appears to be part of the trigger. Two
mechanisms remain plausible (H1: PTY-stdin without EOF in
`while read` loop per docs/configuration/use_stdin.md; H4: server-side
LFS interaction on the duplicate call). Both are eliminated by the
same fix: remove the manual command. LFS uploads continue to work via
lefthook's internal handler (verified in sandbox post-fix).
Regression coverage: scripts/test-lefthook-pre-push.sh asserts exactly
one internal LFS invocation, zero manual ones, and `Uploading LFS
objects: 100%` present, against a disposable sandbox.
See dev-docs/ADR-LEFTHOOK-LFS-INTEGRATION.md for full context, both
misconceptions to prevent regression, and the alternative-scenarios
playbook if Phase E ever regresses.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Default parallel execution of sync-check and git-lfs commands within
the pre-push hook deadlocks: both read from stdin (git pipes the push
refspec to pre-push hooks), and two parallel readers never reach EOF.
Add piped: true to force sequential execution. sync-check runs first
(only inspects push_files via lefthook templating, doesn't actually
consume stdin), then git-lfs runs second with clean stdin access.
Observed during chore/test-infra-001 sprint: LFS upload completed
100% but pre-push hook hung indefinitely. Workaround was --no-verify;
this commit removes the need for that.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
WS-3 session 1a Task 1.
Lefthook installed as root dev-dependency with postinstall = lefthook
install. The two hand-rolled scripts in .githooks/ (post-commit,
pre-push) are dispatched 1:1 from lefthook.yml: each lefthook command
shells out to the existing .githooks/<hook> script. The script bodies
are kept as the source of truth because the bash logic (merge-commit
detection, .claude-sync.conf parsing, non-blocking pre-push warning)
would be lossy to translate into a YAML run: | block.
Active hook path moved from .githooks/ to .git/hooks/ via lefthook
install (core.hooksPath unset, git falls back to its default). The
.githooks/ directory is preserved and now documented as the
implementation invoked by lefthook plus an emergency rollback target
(README added).
Smoke-tested locally: the post-commit hook fires on every commit
(verified by reverted test commit). The pre-push hook fires on every
real push with new commits — manual `lefthook run pre-push` requires
`--force` because lefthook v2 skips when {push_files} is empty (see
lefthook.yml comment).