mattrobenolt/vouch

A git shim that routes git push through the GitHub API so pushed commits land with verified signatures

★ 0Forks 0ZigGitHub ↗Compare

README

vouch

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.

How it works

  • vouch installs as git in PATH. Every subcommand except push is 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 with createCommitOnBranch. The expectedHeadOid argument 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 status stays 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.

Requirements

  • GH_TOKEN or GITHUB_TOKEN with contents:write on the target repo.
  • A GitHub remote.

Install

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

Limits

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-remote vs the local tracking ref) with a millisecond-scale race window; REST update-ref has no old-SHA precondition. Treat --force as --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 status stays clean after repair.
  • Tag pushes are refused. Only refs/heads/* is modeled.
  • --atomic, --tags, --all, and --mirror are refused. --atomic cannot 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.

Environment

  • GH_TOKEN / GITHUB_TOKEN — required for push.
  • VOUCH_REAL_GIT — explicit path to real git. Baked into the Nix package; rarely needed.

Develop

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.

License

Apache-2.0. See LICENSE.