A git shim that routes git push through the GitHub API, so every pushed
commit lands with GitHub's server-side verified signature.
It exists for environments where a signing key cannot live — CI agents, ephemeral VMs — but a ruleset requires verified commits. The token is the only secret; GitHub signs the commits.
- vouch installs as
gitin PATH. Every subcommand exceptpushis exec'd straight to the real git, adding well under a millisecond. - On
push, vouch asks real git for the plan (git push --dry-run --porcelain), then replays each new commit through the API withcreateCommitOnBranch. TheexpectedHeadOidargument acts as an atomic lease, so the push is race-free like--force-with-lease, but stricter. - After the push, local refs move to the new signed commits. The trees are
byte-identical, so the worktree and index do not move and
git statusstays clean. - A non-fast-forward push fails with git's own rejection text. Non-GitHub remotes pass straight through to real git.
- Anything vouch does not model is a loud error, never a silent unsigned push.
GH_TOKENorGITHUB_TOKENwithcontents:writeon the target repo.- A GitHub remote.
With Nix, the flake github:mattrobenolt/vouch builds a static binary (no
libc on Linux). The package installs as bin/git and wins the buildEnv
collision against pkgs.git via meta.priority. The real-git path is baked
in at build time.
From source, with Zig 0.16:
zig build -Doptimize=ReleaseSafe --prefix ~/.local
These are current-state behaviors, not silent fallbacks:
- Exec bits, symlinks, and submodules route through the REST git-database
API, which only produces signed commits with an app installation token
(
ghs_). With a user token (gho_, PATs) those pushes refuse loudly. Setting an author on a REST commit would zero its signature, so replayed commits are authored by the app bot identity with push-time dates. - Force push works through the same REST path: rewritten history is rebuilt
with arbitrary parents, then the ref moves with
force: true. The lease is read-check-write (ls-remotevs the local tracking ref) with a millisecond-scale race window; REST update-ref has no old-SHA precondition. Treat--forceas--force-with-lease. - REST blob creation caps at 40 MiB per file. The GraphQL route has no such
call, but also only models
100644. - Merge commits replay as linear history on the GraphQL path (the API has
no parents field). The REST path preserves parent edges. Trees are
byte-identical either way;
git statusstays clean after repair. - Tag pushes are refused. Only
refs/heads/*is modeled. --atomic,--tags,--all, and--mirrorare refused.--atomiccannot be honored across separate API calls.- The author of a replayed commit becomes the token owner, and the dates become push time. Neither API has an author field that keeps signing.
- SHA-256 repositories are not supported.
GH_TOKEN/GITHUB_TOKEN— required forpush.VOUCH_REAL_GIT— explicit path to real git. Baked into the Nix package; rarely needed.
zig build, zig build test, ziglint. nix develop (or direnv) provides
zig, zls, ziglint, and zigdoc. The flake's checks run the unit tests in
the build sandbox.
Apache-2.0. See LICENSE.