pihme / git-warden

Git Warden

Guard posts between AI agents and their Git remote: a Push Guard that checks every push with deterministic rules, and a Backup Guard that keeps an append-only backup of every branch and tag.

Get startedGitHubReadme

CI · releases tagged vX.Y.Z, one version for both guards

#What it is

Git Warden puts two guard posts between AI agents and their Git remote. The Push Guard (push-guard) stands on the wall around the agent: agents push to it instead of the remote, it checks every push while the push is running, and only it holds a write credential for the remote. A clean push is forwarded with exactly the checked SHAs; a yellow finding is rejected with the reason, a red one is rejected and the human is warned.

The Backup Guard (backup-guard) runs on a timer on a separate backup host. It drives git-everref against each remote with a read-only credential, so a force push, deletion or moved tag upstream becomes a new lineage or a tombstone in the backup, and nothing a run has seen is lost.

No AI decides anything. Every verdict is a deterministic rule, the same input always gives the same answer, and there is no model to persuade. The remote can be GitHub, GitLab, Gitea/Forgejo or a bare repository over SSH. Git Warden works only if the agents hold no other write credential for the remote.

#Current status

Last updated 3 October 2026 · main@45617d9

All tests pass on the main branch in continuous integration, which also builds the container image and the Nix flake. The Push Guard has every rule of its specification, the pre-receive hook, the HTTP server, the human commands and replay; its tests run against real temporary Git repositories, against an SSH server the test starts itself, and live against GitHub over HTTPS on every push to main. The Backup Guard sets up a bridge clone and a backup repository per remote, selects the branches and runs git-everref; its tests use the real git-everref against a local remote, with force pushes, deletions, moved tags, a token-protected HTTP remote and runs killed at every setup step. The latest release is v0.4.0. Releases are cut automatically after the tests pass.

Neither guard is in production use yet, and GitLab and Gitea have not been tried. Git Warden only helps if the agents hold no other write credential for the remote. The Push Guard has no AI and no semantic code review, checks commit signatures only for presence, and reads its whole log on every push, which will need rotation later. The Backup Guard only sees the states present at its runs, and git-everref is still too slow for very large, busy repositories; LFS objects and submodule targets are not backed up. The license allows noncommercial use only. There are no open issues.

#Key concepts

Two guard posts

The Push Guard prevents, the Backup Guard preserves. Each runs on its own and can be left out; they share a configuration model, not a process.

During the push

The Push Guard decides in its pre-receive hook, before anything reaches the remote, so a secret is stopped before it is published.

Exactly the checked SHAs

A green push is forwarded as git push <sha>:<ref>, never with --force. A lease is used only where a rule was exempted or a human approved that SHA.

Fail closed

A missing gitleaks, an unreachable remote or any error answers internal error, try again later. Nothing goes out unchecked.

Credentials stay on the wall

The write credential is a file on the Push Guard host, never in a URL, argument or log, and never in the agent’s environment.

Append-only backup

The Backup Guard never removes a protection. Rewritten branches become frozen lineages, deleted ones tombstones, moved tags keep their old target.

#Getting started

Needs Linux and git 2.42 or newer; the Push Guard needs gitleaks 8.x if CONTENT-SECRET is enabled (the default), the Backup Guard needs git-everref v1.0.0. Git Warden never downloads them at runtime. The releases carry static push-guard-linux-amd64 and backup-guard-linux-amd64 binaries; there is also a Dockerfile and a Nix flake that bring the programs along at pinned versions.

git clone https://github.com/pihme/git-warden.git && cd git-warden
go build -o push-guard ./cmd/push-guard
go build -o backup-guard ./cmd/backup-guard
go test ./...        # offline, against temporary Git repositories

A Push Guard configuration is a directory with a defaults.yaml and one repos/<name>/warden.yaml per remote:

# /etc/warden/repos/hermetarium/warden.yaml
remote: git@github.com:example/hermetarium.git
credential: /etc/warden/keys/hermetarium      # deploy key with write access
push-guard check-config --config /etc/warden --remote
push-guard serve --config /etc/warden --listen 0.0.0.0:8418
# in the agent's clone:
git remote set-url origin http://hermit:$TOKEN@warden.internal:8418/hermetarium.git

The Backup Guard has its own directory with repos/<name>/backup.yaml and runs from a timer:

backup-guard check-config --config /etc/warden-backup --remote
backup-guard run --config /etc/warden-backup        # e.g. every 15 minutes

Full reference, with the same structure for both guards: Push Guard and Backup Guard configuration and operation, and the readme.

#Push Guard rules

Every rule is on by default and can be disabled or narrowed with allow and deny lists per repo. Path and content rules look at the cumulative diff, so splitting a change into many small commits does not help. Before going live, push-guard replay runs the rules over a branch’s real history without forwarding anything.

RulesDefaultFires on
REF-DELETE, REF-NON-FF, REF-TAG-MOVEredA branch or tag is deleted, a branch is rewritten, a tag is moved
REF-NAMESPACE, REF-COUNTredA ref outside refs/heads/* and refs/tags/*; more than 10 refs in one push
PATH-RED / PATH-YELLOWred / yellowA changed path (both names of a rename) is on a list. Red by default: CI workflows, CODEOWNERS, licence, scanner and agent configuration; yellow: dependency manifests and lockfiles, hooks, build files
CONTENT-SECRET, CONTENT-SCANNER-ALLOWredgitleaks finds a secret in any new commit; an added gitleaks:allow or trufflehog:ignore
CONTENT-INVISIBLEredBidi controls, zero-width or Unicode tag characters
CONTENT-BINARY, CONTENT-BLOB, CONTENT-PAGES-SCRIPTyellowBinary files, long base64 or hex runs, new script hosts on the Pages branch
MODE-*, META-*yellowNew executables, symlinks or submodules; missing signatures, backdated or future commits
SIZE-*red / yellowVery large pushes, large files, mass deletions
RATE-LIMIT, RATE-YELLOW-STREAKredToo many pushes per hour; too many yellow rejections in a row, then every push is red until a human approves or resets

The full table with every limit is in docs/push-guard.md, the reasoning per rule in docs/push-guard-rules.md. A red push’s commits are kept as a bundle on the wall; push-guard approve lets exactly one SHA through, push-guard reset-streak ends a yellow streak.

#Backup Guard

Each run lists the remote’s branches, protects new ones with git-everref add (minus exclude_branches), bridges all tags and then calls git-everref run --all. The backup lives in state_dir/repos/<name>/backup.git; every run writes one line to backup.jsonl, and a failed run sends a backup_failed warning. Overlapping runs wait on a per-repo lock. A run that is killed or times out is picked up cleanly by the next one: the setup is atomic.

It only sees the states present at its runs: a state that exists only between two runs is not in the backup. Restoring to the remote is a human’s step, with git-everref’s own commands.

#Configuration & credentials

Both guards use the same keys with the same meaning: remote, credential (a file path, never the value), credential_username (HTTPS only) and known_hosts (SSH only) per repo; notify.command, state_dir, timeout and the external program’s path in defaults.yaml. Unknown keys are errors. SSH runs with -F none and strict host keys; for HTTPS an inline credential helper reads the token file.

The Push Guard writes, the Backup Guard only reads. Give each exactly that, and never the same credential:

CredentialPush Guard (read and write)Backup Guard (read only)
GitHub deploy key (SSH)with Allow write accesswithout it (the default)
GitHub fine-grained tokenContents: read and writeContents: read-only
GitHub App installation tokenContents: read and writeContents: read-only
GitLab deploy key (SSH)with write permissionswithout them
GitLab project access tokenwrite_repository, Developer or Maintainerread_repository
GitLab deploy tokennot possible (read scope only)read_repository, with credential_username

Sources and details: Credentials and the rights they need.

#Architecture & layout

WallBackup hostAI agentpushes only to the guardPush Guardrules + gitleaks, during the pushGit remoteGitHub, GitLab, any GitHumanwarnings · approvebackup.gitappend-only: lineages, tombstonesBackup Guardtimer · git-everrefgit pushchecked SHAsread-only fetchgit-everref runred push: warning

The Push Guard reads the remote fresh for every push, runs the rules and gitleaks over the new commits and forwards exactly the checked SHAs, or rejects and warns through notify.command. The Backup Guard runs on its own host with a read-only credential; git-everref records every branch and tag in the backup repository.

PathContents
cmd/push-guard, cmd/backup-guardThe two binaries
internal/rulesDecision core: delta normalisation, the deterministic rules, the verdict; no platform knowledge
internal/pushguardHook, forwarding, approvals, rate limit and streak, serve, replay
internal/backupguardConfiguration, preflight, bridge and backup setup, the git-everref runner, backup.jsonl
internal/config, internal/gitxShared configuration loading and remote checks; git as a subprocess with timeouts and credential handling
examples/Example configuration for both guards and systemd units for the Backup Guard
Dockerfile, flake.nixContainer image and Nix flake with pinned git, gitleaks and git-everref
SPEC.mdDesign, decisions, the risk register R1–R22 and what Git Warden does not cover