wtguard

module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 18, 2026 License: MIT

README

wtguard

Go CLI that physically prevents commits to protected branches (main, master, …) when an LLM or human is supposed to be working in a worktree. Three layers of enforcement so an automated agent never misses.

Why

LLM-driven development agents occasionally git commit straight to main instead of a feature branch. A plain pre-commit hook is not enough: agents can pass --no-verify, delete the hook, or work in a fresh clone. wtguard closes those holes by stacking three independent defenses behind a single install command.

Defense in depth

LLM runs `git commit ...`
   │
   ├─[1] Proxy        ~/.wtguard/bin/git is first in PATH.
   │                  Catches `--no-verify` and missing hooks.
   │
   ├─[2] Hook         .git/hooks/pre-commit checks the same rule.
   │                  Catches the case where proxy is bypassed
   │                  (e.g. /usr/bin/git called directly).
   │
   └─[3] Branch       GitHub branch protection rejects direct
        protection    pushes to `main`. Server-side, unbypassable.
        (opt-in)

Each layer alone is bypassable. All three together are not.

Install (one line)

curl -fsSL https://raw.githubusercontent.com/cuongtranba/wtguard/main/scripts/install.sh | sh

The script downloads the latest release binary (or falls back to go install if no binary is available for your platform), drops it at ~/.wtguard/bin/wtguard, symlinks the git proxy, sets git config --global init.templateDir so every new clone ships with the hook, and patches your shell rc to put ~/.wtguard/bin on PATH.

Open a new shell or source ~/.zshrc (or .bashrc) afterwards.

Uninstall (one line)
curl -fsSL https://raw.githubusercontent.com/cuongtranba/wtguard/main/scripts/uninstall.sh | sh

Reverses everything the installer did: removes ~/.wtguard/, unsets the global init.templateDir (only if it still points at us), and strips the marker block from your shell rc. GitHub branch protection rules — if you applied them via wtguard install --remote-protect — are not auto-removed.

Manual install
go install github.com/cuongtranba/wtguard/cmd/wtguard@latest
wtguard install                  # layers 1 + 2 + global init template
wtguard install --remote-protect # also apply layer 3 via `gh api`

Or grab a binary from Releases.

wtguard install is idempotent and does the following:

  1. Symlinks ~/.wtguard/bin/gitwtguard.
  2. Patches your shell rc with export PATH="$HOME/.wtguard/bin:$PATH" (guarded by a marker block, safe to re-run).
  3. Sets git config --global init.templateDir ~/.wtguard/template so every new git init / git clone ships with the hook.
  4. Drops the hook into the current repo.

wtguard uninstall reverses 1–4 (branch protection on the remote stays — it affects shared resources).

Quick start

cd my-repo
wtguard create feat-x          # creates ../my-repo-feat-x, installs hook
cd ../my-repo-feat-x           # work here

# ... commits on feat-x are fine.
# ... commits on main from anywhere are BLOCKED:
#
#   wtguard: blocked `git commit` on protected branch 'main'
#     reason: 1 active worktree(s) — commit there instead
#     worktrees:
#       ../my-repo-feat-x  [feat-x]
#     next:
#       cd ../my-repo-feat-x && git commit ...

Commands

wtguard install [--remote-protect]   # set up proxy + hook + (opt) branch protection
wtguard uninstall                    # remove proxy + hook
wtguard status                       # what's installed, where, on what
wtguard create <branch> [--path P]   # git worktree add + ensure hook installed
wtguard remove <path|branch>         # git worktree remove
wtguard list                         # list worktrees, mark protected branches
wtguard config get|set <key> [value] # read / write wtguard.<key>
wtguard config protected add|rm <branch>
wtguard explain                      # full LLM-friendly reference (markdown)

How it works

Layer 1 — proxy

A symlink at ~/.wtguard/bin/git points to the wtguard binary. Because that directory is first on PATH, every git call goes through wtguard. The wrapper inspects the subcommand:

  • git commit and git push to a protected ref → block with a structured stderr message and a non-zero exit code.
  • everything else → forwarded bit-for-bit to the real git (resolved via PATH-strip, with WTGUARD_REAL_GIT as override).

The proxy refuses regardless of --no-verify because it runs before git invokes any hooks.

Layer 2 — hook

A pre-commit shim in .git/hooks (and in ~/.wtguard/template/hooks for new clones) execs wtguard hook pre-commit and applies the same rule. Catches the case where someone invokes /usr/bin/git directly, bypassing PATH.

If you already had a pre-commit hook, it is preserved as pre-commit.local and chained after the guard check.

Layer 3 — GitHub branch protection (opt-in)

wtguard install --remote-protect calls gh api to require pull requests for main and friends. Even if both client layers are bypassed, the server rejects the push. This is the only truly unbypassable layer.

The block rule
block iff:  current branch ∈ wtguard.protected     (policy=always, default)

The default policy is always: every commit/push to a protected branch is rejected, regardless of whether a feature worktree exists.

Set wtguard.policy = worktree-active to relax the rule and only block when git worktree list reports more than one entry (i.e. a feature worktree has been created). Useful if you legitimately make occasional small commits directly to main (e.g. version bumps) and only want protection once feature work has started.

Bypass

Real emergency: WTGUARD_BYPASS=1 git commit ... skips the check at both proxy and hook layers. Bypasses are appended to ~/.wtguard/audit.jsonl and .git/wtguard.log with timestamp, repo, and commit subject.

Configuration

All settings live in .git/config under wtguard.*:

Key Default Notes
wtguard.protected main,master comma-separated branch list
wtguard.policy always or worktree-active
wtguard.worktreeDir ../ base dir for new worktrees
wtguard.bypassLog true log bypassed commits
wtguard.chainHook true run pre-commit.local after guard check

Environment variables:

Var Effect
WTGUARD_BYPASS=1 skip block for one commit (logged)
WTGUARD_REAL_GIT absolute path to the real git binary (proxy override)

Status

Early. Repo scaffolded with release-please + GoReleaser; design docs in docs/plans/. Subcommand bodies land per the proxy / defense-in-depth design 2026-05-10.

License

MIT

Directories

Path Synopsis
cmd
wtguard command
internal
audit
Package audit writes one JSON line per wtguard decision to the global and per-repo audit logs.
Package audit writes one JSON line per wtguard decision to the global and per-repo audit logs.
cli
config
Package config is a typed view over the wtguard.* keys in `git config`.
Package config is a typed view over the wtguard.* keys in `git config`.
git
Package git is a typed wrapper over `git` invoked via os/exec.
Package git is a typed wrapper over `git` invoked via os/exec.
guard
Package guard is the single source of truth for "should this commit be blocked?".
Package guard is the single source of truth for "should this commit be blocked?".
hook
Package hook installs / removes the wtguard pre-commit shim, chaining any pre-existing user hook to pre-commit.local.
Package hook installs / removes the wtguard pre-commit shim, chaining any pre-existing user hook to pre-commit.local.
proxy
Package proxy implements wtguard's git-wrapper mode.
Package proxy implements wtguard's git-wrapper mode.
remote
Package remote applies GitHub branch protection via the `gh` CLI.
Package remote applies GitHub branch protection via the `gh` CLI.
shellrc
Package shellrc patches a user's shell rc file with a marker-guarded block that prepends wtguard's bin dir to PATH.
Package shellrc patches a user's shell rc file with a marker-guarded block that prepends wtguard's bin dir to PATH.
template
Package template writes a hook shim into ~/.wtguard/template/hooks so every future `git init`/`git clone` ships with the wtguard hook (when init.templateDir is pointed at us).
Package template writes a hook shim into ~/.wtguard/template/hooks so every future `git init`/`git clone` ships with the wtguard hook (when init.templateDir is pointed at us).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL