Let plain Git clients talk to a Forge
The GitHub-compatible smart-HTTP surface: clone, push, and refs/for.
Written By Dustin
Last updated 30 minutes ago
A Forge serves repositories to ordinary Git clients over smart-HTTP v2. Someone with no TOVIO installed can clone from it with the git they already have.
git clone https://<forge>/<owner>/<repo>.gitgit ls-remote https://<forge>/<owner>/<repo>.gitThe owner segment is not optional — the routes are /<owner>/<repo>/..., the shape a Git client already speaks. A URL missing it is a 404, not a permission error.
Reads are membership-gated and fail closed: a non-member or unauthenticated client gets the empty ref advertisement, never a leaked lane name.
Protected paths are opaque, not filtered
A policy-protected file materializes into the Git view as an opaque ciphertext blob and is never decrypted — for every Git client, including one whose owner holds the key. The Git plane has no way to decrypt, so the crypto boundary is held by making the content unreadable rather than by trusting the client. If you need the plaintext, use the native client.
Pushing
Push is served and works: the pack is validated, every commit is imported and given a stable change id, and each lane advances by compare-and-set with a signed audit entry. Re-pushing an already-ingested commit is idempotent — same change id, no duplicate.
A repository must opt in first. Bearer-authorized mutation is default-deny, so a push into a repository that has not enabled it is refused with a 403 before the pack is even read. An owner enables it once:
tovio policy web-auth --allow bearerIf your credentials are right and the push is rejected immediately, this is the first thing to check.
Push is clear paths only. A Git client holds no recipient key and the Forge holds none for your cleartext, so a protected path can be sealed by neither side. The refusal is checked before any blob lands, and the rest of the push still goes through.
Beyond that, the surface keeps Git's own rules, gated on what the presenting token is entitled to: a push is fast-forward by default, a non-fast-forward (force) update needs a token with branch authority, and a ref deletion needs the branch-delete authority. Without the authority, the update is refused rather than silently downgraded.
Opening a proposal from git push
A lane that requires review will not accept a direct push — that would land a change while bypassing the review gate. Push to the magic ref instead:
git push origin HEAD:refs/for/mainThat imports the commits and opens a Proposal targeting main, pins the reviewed commit and the Forge-computed base, and runs the normal land gate. The real lane is never advanced, and the virtual ref reports as accepted. Re-pushing the same commit reuses the open Proposal rather than opening a duplicate. There is a separate article on it.
Authentication
A capability token is presented as a Git credential by the git-credential-tovio helper, a separate binary that ships with the CLI. Wire it up once:
tovio git setup <forge-host>tovio git setup <forge-host> --localIt reads the token from stdin, seals it in the OS keychain, and points Git's credential helper at it — so git, VS Code, IntelliJ and GitHub Desktop authenticate without a stored password. --local writes the configuration to one repository instead of your global Git config.
The helper answers only for hosts it was explicitly configured for, matched exactly including the port, declines silently for everything else, refuses to emit a token over anything but HTTPS, and never persists what Git hands it. A helper on your PATH is consulted for every host Git talks to, which is why those rules are strict.
Refusals
They are reported per-ref in Git's own status protocol, so your client shows the same accept and reject shape it would from GitHub.
Current limits
- Thin packs, and the persisted mapping cache between TOVIO addresses and Git object ids, are not built. The Git view is materialized on demand, which is a scale consideration on a very large repository.
- The GitHub-shaped OAuth authorization-code flow is built end to end — an app registration is resolved and consent returned, a single-use code is issued, and redeeming it mints a conservative content-plane token. What is still refused, with an honest
501, is a metadata-only authorization such aspull_requests:read: there is no capability to map it onto yet, and granting it silently would be worse than saying so. - GraphQL is not served at all. REST and the Git surface are the supported paths.