finos/git-proxy is the upstream reference — a Node.js/Express server that inspects git pushes and enforces policy. It works. But the stack imposes real constraints on what kind of proxy it can become.
The root problem: express-http-proxy is a dumb TCP relay. It forwards bytes. It has no understanding of the git smart HTTP protocol, no ability to intercept sideband channels, and no mechanism to stream real-time feedback to the developer during a push. The git smart HTTP protocol is stateful and streaming — not a typical request-response exchange.
Building a policy-enforcing proxy in Node requires:
- Manually constructing git packet-line frames and sideband channel bytes (
0x01/0x02/0x03) — there is no JS library equivalent to JGit'sReceivePackorSideBandOutputStream - Spawning child git processes (
git receive-pack,git unpack-objects) and plumbing their stdin/stdout, with credentials risking exposure in the process environment - Monkey-patching
res.status()andres.writeHead()to force HTTP 200 on error responses — git clients ignore protocol bodies on non-200 status codes, and Express has no standard wrapper pattern for intercepting status codes set by downstream middleware - Managing Node stream lifecycle manually — push pack data must be consumed once for parsing and once for forwarding, but Node streams are single-consume; this requires explicit
PassThroughcloning - Dealing with the event loop during long-running synchronous operations (GPG verification, secret scanning, approval polls) — either block the loop or fight async/await throughout a streaming response handler
None of this is impossible — git-proxy proves that. But it's all engineering effort spent on plumbing rather than policy.
JGit is a complete git server in Java. ReceivePack handles the git smart HTTP push protocol natively — pack parsing, object insertion into a bare repository, and hook execution. Commit metadata (author, message, signature, diff) is available via a typed Java API. No child processes, no protocol hand-rolling.
Sideband streaming is a first-class API. ReceivePack.sendMessage() writes to SideBandOutputStream; calling flush() propagates through Jetty's chunked transfer encoding directly to the developer's terminal as remote: output — live, during the push. This is the capability that makes per-step validation feedback possible. In JGit, it's three lines. In Node, it would require implementing the full sideband protocol framing from scratch.
Jetty's servlet abstractions make protocol manipulation clean and safe. HttpServletResponseWrapper intercepts setStatus() from downstream components — used to force HTTP 200 so git clients read the ERR pkt-line body instead of a generic error page. HttpServletRequestWrapper caches the pack request body once so every filter and the proxy backend can re-read it independently. These are standard documented APIs, not patches.
Thread-per-request model is natural for long-running approval workflows. An approval hook can block for minutes while streaming keepalive messages without starving any other request. With Java 21 virtual threads, this extends to thousands of concurrent blocked pushes without OS thread exhaustion.
See the full technical comparison for a side-by-side breakdown of the stack differences.
git-proxy-java is designed for two primary patterns:
-
OSS contribution gateway — engineers pushing from an internal clone to a public upstream. Policy controls which destinations are allowed, who can push, what content is permitted, and whether human approval is required. This is the original git-proxy use case.
-
Private-to-private proxying — controlled code exchange between isolated environments: M&A integration, contractor access, regulated development. The proxy sits at the boundary, enforces policy in both directions, and maintains a full audit trail.
Both patterns share the same dual-mode proxy (store-and-forward and transparent proxy), the same filter chain, and the same approval UX.
The developer should see exactly what the proxy is doing, in real time, in their terminal — not a single opaque blocked/allowed after 30 seconds.
- ✅ Real-time per-hook sideband progress during validation
- ✅ Aggregate failure reporting — all failures in one push, not stop-at-first
- ✅ ANSI color + emoji in sideband output;
NO_COLOR/GITPROXY_NO_EMOJItoggles - ✅ Shareable dashboard link in blocked push message
- Heartbeat packets — defeat ALB/reverse-proxy idle timeouts during long validations #23
Long-running validation (secret scanning, external approval workflows) on flaky connections should not require starting over. Approved pushes should be forwardable independently of the developer's active session.
- Checkpoint-based filter resumption — persist completed hook results keyed to the commit range; skip finished steps on re-push #25
- Deferred forwarding — park approved pushes in a durable async queue; forward after approval without requiring the developer to re-push #6
- Notification system — internal event bus covering all push lifecycle transitions (RECEIVED → BLOCKED → APPROVED → FORWARDED); outbound webhooks for monitoring integration #28
- Auth backends — local, LDAP/AD, OIDC, Entra ID/JWKS (private key credentials) #15
- Attestation on approval — configurable review questions the authoriser must answer before approving #22
- SCM OAuth integration — PR/MR creation from the dashboard, bound to the push audit record
- Plugin SPI — ServiceLoader-based filter discovery for org-specific implementations #24
- Pre-receive hook registry — execute existing shell hooks from the proxy chain #20
- Tag push support — handle
refs/tags/*through the same validation pipeline #19 - Concurrent/DAG pipeline — independent filter steps execute in parallel #26
- SSH protocol (Apache MINA SSHD) — proxy SSH push/fetch through the same policy pipeline; developers using
git@remotes currently bypass HTTP proxies entirely - Email patch relay — intercept
git send-emailvia an SMTP relay, apply the validation pipeline, forward to the destination mailing list; relevant for Linux-kernel-style mailing list contribution workflows where HTTP push is not the model
git-proxy-java is not a git hosting platform (Gitea, Forgejo, GitLab). It is a policy layer that sits in front of an existing upstream — it receives pushes, inspects them, and decides whether and when to forward them. The developer's remote points at git-proxy-java; git-proxy-java's remote points at the real upstream.