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.
· 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 repositoriesA 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 accesspush-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.gitThe 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 minutesFull 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.
| Rules | Default | Fires on |
|---|---|---|
REF-DELETE, REF-NON-FF, REF-TAG-MOVE | red | A branch or tag is deleted, a branch is rewritten, a tag is moved |
REF-NAMESPACE, REF-COUNT | red | A ref outside refs/heads/* and refs/tags/*; more than 10 refs in one push |
PATH-RED / PATH-YELLOW | red / yellow | A 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-ALLOW | red | gitleaks finds a secret in any new commit; an added gitleaks:allow or trufflehog:ignore |
CONTENT-INVISIBLE | red | Bidi controls, zero-width or Unicode tag characters |
CONTENT-BINARY, CONTENT-BLOB, CONTENT-PAGES-SCRIPT | yellow | Binary files, long base64 or hex runs, new script hosts on the Pages branch |
MODE-*, META-* | yellow | New executables, symlinks or submodules; missing signatures, backdated or future commits |
SIZE-* | red / yellow | Very large pushes, large files, mass deletions |
RATE-LIMIT, RATE-YELLOW-STREAK | red | Too 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:
| Credential | Push Guard (read and write) | Backup Guard (read only) |
|---|---|---|
| GitHub deploy key (SSH) | with Allow write access | without it (the default) |
| GitHub fine-grained token | Contents: read and write | Contents: read-only |
| GitHub App installation token | Contents: read and write | Contents: read-only |
| GitLab deploy key (SSH) | with write permissions | without them |
| GitLab project access token | write_repository, Developer or Maintainer | read_repository |
| GitLab deploy token | not possible (read scope only) | read_repository, with credential_username |
Sources and details: Credentials and the rights they need.
#Architecture & layout
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.
| Path | Contents |
|---|---|
cmd/push-guard, cmd/backup-guard | The two binaries |
internal/rules | Decision core: delta normalisation, the deterministic rules, the verdict; no platform knowledge |
internal/pushguard | Hook, forwarding, approvals, rate limit and streak, serve, replay |
internal/backupguard | Configuration, preflight, bridge and backup setup, the git-everref runner, backup.jsonl |
internal/config, internal/gitx | Shared 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.nix | Container image and Nix flake with pinned git, gitleaks and git-everref |
SPEC.md | Design, decisions, the risk register R1–R22 and what Git Warden does not cover |