Skip to content

About

Require a Touch ID confirmation before git push reaches your important remotes - with a prompt that names the actual repo, branch and commits

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

git-push-gate

Require a Touch ID confirmation before git push reaches the remotes you care about — and make the prompt say exactly what is being pushed.

Every other remote passes through silently. Fetches, pulls and pushes to your own forks are untouched.

git-push-gate-dialog

Why this exists

If you hold commit rights to a shared upstream organisation, that access is normally ambient: any process running as you — a script, an IDE, a misconfigured CI helper, an AI coding agent — can push to it with no interaction at all. Most of the time that is convenient. Occasionally it is how an unintended commit lands on a branch that many people depend on.

git-push-gate puts one deliberate human action in front of those pushes, and only those.

Why not sudo / pam_tid, or security authorize?

Both exist, both work, and both give you an anonymous prompt:

Approach Prompt text
sudo + /etc/pam.d/sudo_local → pam_tid.so Fixed: "sudo is trying to…". Also needs root to set up, grants root as a side effect, and the sudo timestamp cache means the second push in five minutes isn't gated at all unless you also set timestamp_timeout=0.
security authorize -u <right> Fixed, drawn from the right's description in the authorization policy database. Cannot vary per call.
LAContext.localizedReason Whatever you pass in — repo, branch, commit count, sha range.

That difference is the whole point. A generic "Authentication required" trains you to approve reflexively; after a week you are touching the sensor without reading. A prompt that names the repo, the branch and the commit range is the difference between a confirmation and actual consent — it lets you notice that you are about to push a branch you have never heard of.

There is no Homebrew formula that does this. pamtester is not in homebrew-core, and pam-reattach is a PAM module for reattaching to the GUI session under tmux, not a prompter. Hence ~40 lines of Swift.

Requirements

  • macOS with Touch ID
  • Xcode command line tools (for swiftc and codesign) — xcode-select --install

Install

git clone https://github.com/cominvent/git-push-gate.git
cd git-push-gate
./install.sh

Or by hand:

swiftc -O -o ~/.local/bin/git-push-gate git-push-gate.swift
codesign -s - --force ~/.local/bin/git-push-gate

mkdir -p ~/.config/git/hooks
cp hooks/pre-push ~/.config/git/hooks/pre-push
chmod +x ~/.config/git/hooks/pre-push

The ad-hoc signing step is not optional. An unsigned command line binary can be refused the biometric prompt outright — you get a silent failure rather than a dialog. codesign -s - is enough; you do not need a developer certificate.

Then enable it globally:

git config --global core.hooksPath ~/.config/git/hooks
git config --global pushgate.pattern 'github\.com[:/](myorg|otherorg)/'

Configuration

Key Meaning
pushgate.pattern Case-insensitive extended regex matched against the remote URL. If it matches, the push is gated. There is no default — until you set this, nothing is gated and the hook prints a one-line reminder on each push.
PUSHGATE_HELPER (env) Override the path to the helper binary. Defaults to $HOME/.local/bin/git-push-gate.

The pattern is matched against the URL, not the remote name, so it covers both forms:

git@github.com:myorg/repo.git
https://github.com/myorg/repo.git

…including an explicit git push git@github.com:myorg/repo.git main with no configured remote at all.

Match several orgs with alternation, and be careful to anchor the trailing slash so that myorg-sandbox/ does not match myorg/:

git config --global pushgate.pattern 'github\.com[:/](myorg|myorg-infra)/'

Testing it

Invoke the hook directly with a synthetic ref list. This exercises the real matching and prompt logic without touching the network:

SHA=$(git rev-parse HEAD)
OLD=$(git rev-parse HEAD~3)

# should prompt
printf 'refs/heads/main %s refs/heads/main %s\n' "$SHA" "$OLD" \
  | ~/.config/git/hooks/pre-push upstream git@github.com:myorg/repo.git
echo "exit=$?"

# should be silent, exit 0
printf 'refs/heads/main %s refs/heads/main %s\n' "$SHA" "$OLD" \
  | ~/.config/git/hooks/pre-push origin git@github.com:me/repo.git
echo "exit=$?"

Do not test by pushing to a repository that does not exist. It is the obvious "safe" experiment and it produces a false negative: git performs remote ref discovery before running pre-push, so a missing repository fails during discovery and the hook never fires at all. No prompt appears, and you conclude the gate is broken — or worse, that it is working when you have tested nothing.

git push --dry-run does run the hook (verified), so that is a safe end-to-end check against a remote that really exists.

How it works

pre-push receives the remote name as $1, the remote URL as $2, and the list of refs being pushed on stdin as <local-ref> <local-sha> <remote-ref> <remote-sha>. The hook:

  1. Matches $2 against pushgate.pattern; on no match it exits 0 immediately.
  2. Reads stdin once, then derives a human summary — commit count, branch, abbreviated sha range, handling new branches and branch deletions.
  3. Calls the helper with that summary as the dialog text. Non-zero exit aborts the push.
  4. Chains to a repo-local .git/hooks/pre-push if one exists and is executable, passing the same arguments and replaying stdin.

The helper prefers biometrics and falls back to password authentication when no sensor is available, so a clamshell or external-keyboard session degrades instead of locking you out. Any error path exits non-zero: it fails closed.

Exit codes: 0 authenticated, 1 cancelled or failed, 2 no authentication mechanism available.

Limitations

Be clear-eyed about what this is. It is a deliberate-consent gate against accident and automation, not a security boundary against someone who already controls your shell.

  • git push --no-verify skips all pre-push hooks. There is no way for a hook to prevent that.
  • Anything that does not go through git push is unaffected — IDE integrations pushing via their own stored tokens, REST/GraphQL API calls, web-UI merges.
  • Anyone who can edit your git config or the hook file can disable it.
  • core.hooksPath is global and overrides every repository's .git/hooks. That is why this hook chains to a repo-local pre-push if one exists — but be aware of the interaction if you use tooling that installs hooks per-repository, such as Husky or pre-commit.
  • It gates pushes only. Fetching, cloning and reading are never prompted.

If you want a guarantee at the key level rather than the workflow level, look at storing your SSH key in the Secure Enclave instead — for example with Secretive, where the private key is unextractable and every SSH operation requires a touch. That is a stronger property, at the cost of a prompt on every fetch as well, and it cannot tell one organisation's remote from another's.

License

Apache License 2.0. See LICENSE.

About

Require a Touch ID confirmation before git push reaches your important remotes - with a prompt that names the actual repo, branch and commits

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages