Git: pre-receive hook fails closed on pushes with more than ~20 refs; denial reasons never reach the pusher
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
# Git: pre-receive hook fails closed on pushes with more than ~20 refs, and the denial reason never reaches the pusher
## Summary
Pushing a real-world repository into Buzz's git hosting is effectively impossible without insider knowledge: any push carrying more than roughly 20 ref updates is atomically rejected with a bare `pre-receive hook declined` on every ref, and the human-readable denial reasons the policy endpoint computes are never surfaced to the client.
## Reproduction
Mirroring `OriginTrail/dkg` (1,339 heads+tags; 2,898 refs with GitHub's `refs/pull/*`) into a repo announced via kind 30617 with a valid `buzz-channel` binding, pushed by the **repo owner key** (so role = Owner, which `evaluate_push` should wave through):
| Push shape | Result |
|---|---|
| `git push --mirror` (2,898 refs) | every ref `! [remote rejected] … (pre-receive hook declined)` |
| 25-ref batches | rejected |
| ≤20-ref batches | **accepted** |
| single refs | accepted |
Same commit content, same pusher, same repo — only the ref count per push changes the outcome. The relay log shows only:
```
WARN git receive-pack report-status contains a rejected (ng) ref update
WARN receive-pack exited non-zero (e.g. pre-receive hook decline); skipping CAS publish and kind:30618
```
— no policy denial is logged and none is transmitted in the sideband, so the pusher cannot distinguish "role denied" from "hook infrastructure gave up".
## Likely cause
`crates/buzz-relay/src/api/git/policy.rs`:
- `MAX_CALLBACK_AGE_SECS = 30` — "Push is synchronous so 30s is generous". The hook computes `git merge-base --is-ancestor` **per ref** before POSTing the HMAC-signed callback; with thousands (or even dozens) of refs on a cold ephemeral hydration, the callback timestamp exceeds the TTL by the time it is assembled → fail-closed 403 → every ref denied (push is atomic per `evaluate_push`).
- Fail-closed is the right default, but the failure mode here is indistinguishable from a permissions denial.
## Suggested fixes (independent, any subset helps)
1. **Surface denial reasons.** `HookCallbackResponse` already carries per-ref reasons; print them to the git sideband (`pre-receive` stderr is shown to the pusher verbatim) so `requires admin role`, `callback expired`, etc. are visible.
2. **Start the TTL clock at callback assembly, or scale it with ref count** — the current constant assumes per-push cost is O(1) when it is O(refs).
3. **Batch or skip the per-ref ancestor checks** (`git merge-base --is-ancestor` per ref is the expensive step; for creates from the zero OID it is a constant `false` and needs no subprocess at all — which is every ref of an initial mirror push).
## Environment
Relay at `63496cc1d`, macOS host, pushes over HTTPS via Tailscale Serve with `git-credential-nostr`; observed 2026-08-01.
Contributor guide
Assessment
This issue has not been assessed yet.