I still think SSH signing is the better default for most engineers. It is easier to explain, easier to rotate, and much less likely to ruin your afternoon.
But sometimes the extra machinery in GPG is the point.
If you care about key expiry, revocation, stricter lifecycle control, or you already work in a team that standardised on OpenPGP, GPG is still the right tool. This post is the setup I would use on macOS when SSH is not enough and I want GitHub verification without the usual GPG pain.
When GPG Is Worth the Trouble
The reason to choose GPG is not that GitHub somehow treats it as more "real" than SSH. On GitHub, once a signed commit has been verified, the verification record persists in the repository network even if the signing key is later rotated, removed, expired, or revoked.
The difference is operational.
GPG keys can expire or be revoked. That gives you lifecycle controls SSH keys do not have in the key material itself. If you want new signatures to stop being valid after a certain date, or you want a formal revocation path, GPG gives you that. SSH is simpler. GPG is stricter.
So my rule is simple. If I just want verified provenance on personal or internal repositories, I use SSH. If I want stronger lifecycle controls or I need to fit an existing OpenPGP workflow, I use GPG.
What This Setup Assumes
This guide assumes macOS, Git, Homebrew, and a verified GitHub email that matches your Git committer email.
It also assumes you are willing to protect the key with a passphrase. If you strip the passphrase off a GPG signing key just to make the prompts go away, you are paying the GPG tax without getting much of the benefit.
Install the Tools First
GPG does not ship with macOS, so install it first. I also install pinentry-mac straight away, because a big chunk of GPG frustration on macOS is really pinentry misconfiguration in disguise.
brew install gnupg pinentry-mac
mkdir -p ~/.gnupg
chmod 700 ~/.gnupgbrew install gnupg pinentry-mac
mkdir -p ~/.gnupg
chmod 700 ~/.gnupgPoint gpg-agent at the macOS pinentry program:
echo "pinentry-program $(which pinentry-mac)" >> ~/.gnupg/gpg-agent.conf
gpgconf --kill gpg-agentecho "pinentry-program $(which pinentry-mac)" >> ~/.gnupg/gpg-agent.conf
gpgconf --kill gpg-agentThen make sure your shell exports GPG_TTY, which GitHub also calls out for shell based setups:
if [ -r ~/.zshrc ]; then
echo 'export GPG_TTY=$(tty)' >> ~/.zshrc
else
echo 'export GPG_TTY=$(tty)' >> ~/.zprofile
fi
export GPG_TTY=$(tty)if [ -r ~/.zshrc ]; then
echo 'export GPG_TTY=$(tty)' >> ~/.zshrc
else
echo 'export GPG_TTY=$(tty)' >> ~/.zprofile
fi
export GPG_TTY=$(tty)That one line saves a lot of gpg failed to sign the data nonsense later.
Generate a New GPG Key
GitHub's current docs still point people to gpg --full-generate-key, and that is what I would use here. It is not elegant, but it is predictable.
gpg --full-generate-keygpg --full-generate-keyWhen GPG walks you through the prompts:
- Use the email address you actually commit with on GitHub
- Use a real passphrase
- If your team has no policy, the default choices are fine
- If the whole reason you picked GPG is lifecycle control, set an expiry instead of leaving it open forever
I usually give signing keys an expiry and rotate them deliberately. Otherwise, I may as well have used SSH and kept life simpler.
Now list your secret keys and copy the long key ID:
gpg --list-secret-keys --keyid-format=longgpg --list-secret-keys --keyid-format=longYou should see output in roughly this shape:
/Users/you/.gnupg/pubring.kbx
-----------------------------
sec ed25519/3AA5C34371567BD2 2026-03-22 [SC] [expires: 2027-03-22]
0123456789ABCDEF0123456789ABCDEF01234567
uid [ultimate] Your Name <you@example.com>
ssb cv25519/4BB6D45482678BE3 2026-03-22 [E]/Users/you/.gnupg/pubring.kbx
-----------------------------
sec ed25519/3AA5C34371567BD2 2026-03-22 [SC] [expires: 2027-03-22]
0123456789ABCDEF0123456789ABCDEF01234567
uid [ultimate] Your Name <you@example.com>
ssb cv25519/4BB6D45482678BE3 2026-03-22 [E]The value you care about is the long ID after the slash, such as 3AA5C34371567BD2.
Add the Public Key to GitHub
Export the public key in ASCII armour:
gpg --armor --export 3AA5C34371567BD2 | pbcopygpg --armor --export 3AA5C34371567BD2 | pbcopyThen add it in Settings -> SSH and GPG keys -> New GPG key on GitHub.
If GitHub does not mark your signed commits as verified later, the first two things to check are always the same: the public key was never added, or the email on the key does not match a verified email on your GitHub account.
Tell Git Which Key to Use
Now wire Git up to the key you just created:
git config --global --unset gpg.format
git config --global gpg.program gpg
git config --global user.signingkey 3AA5C34371567BD2
git config --global commit.gpgsign true
git config --global tag.gpgsign truegit config --global --unset gpg.format
git config --global gpg.program gpg
git config --global user.signingkey 3AA5C34371567BD2
git config --global commit.gpgsign true
git config --global tag.gpgsign trueIf you manage multiple GPG keys or you sign with a dedicated subkey, point user.signingkey at the exact key or subkey you want Git to use. GitHub's docs also note that some setups benefit from appending ! to make the preference explicit.
Your signing related config should end up looking something like this:
[user]
signingkey = 3AA5C34371567BD2
[gpg]
program = gpg
[commit]
gpgsign = true
[tag]
gpgsign = true[user]
signingkey = 3AA5C34371567BD2
[gpg]
program = gpg
[commit]
gpgsign = true
[tag]
gpgsign = trueVerify It Before You Trust It
Do a quick local test before you start assuming everything works:
TMPDIR=$(mktemp -d)
git init "$TMPDIR" -q
git -C "$TMPDIR" config user.name 'Your Name'
git -C "$TMPDIR" config user.email 'you@example.com'
git -C "$TMPDIR" commit --allow-empty -m "test signing" -q
git -C "$TMPDIR" log --show-signature -1TMPDIR=$(mktemp -d)
git init "$TMPDIR" -q
git -C "$TMPDIR" config user.name 'Your Name'
git -C "$TMPDIR" config user.email 'you@example.com'
git -C "$TMPDIR" commit --allow-empty -m "test signing" -q
git -C "$TMPDIR" log --show-signature -1If the setup is healthy, you should see Git report a good signature from your GPG key.
You can also verify specific commits and tags directly:
git verify-commit HEAD
git verify-tag v1.0.0git verify-commit HEAD
git verify-tag v1.0.0Push a signed commit to GitHub after that. If the public key is on your account and the email matches, the commit should show as Verified.
The Three Failure Modes You Actually Hit
Most GPG setup guides bury the useful part under pages of ceremony, so here is the short version.
gpg failed to sign the data usually means pinentry or GPG_TTY is broken. Check ~/.gnupg/gpg-agent.conf, confirm pinentry-mac exists, and make sure the current shell has export GPG_TTY=$(tty).
No secret key usually means Git is pointing at the wrong key ID, or you generated a key under a different GPG home than the one Git is using.
GitHub shows Unverified usually means the public key is not uploaded to GitHub, or the email on the key does not match a verified email on the account.
Those three cover most of the pain in practice.
SSH Versus GPG, Honestly
If you are choosing from scratch and you do not have policy constraints, I would still start with SSH signing. It is a cleaner fit for most engineers.
If you specifically care about expiry, revocation, or stronger lifecycle control, GPG earns its complexity. Just be honest about the trade. You are buying more control by accepting more moving parts.
That is a reasonable trade when you need it. It is a silly one when you do not.
Conclusion
GPG commit signing is not hard because the idea is hard. It is hard because the surrounding tooling is fussy, stateful, and easy to misconfigure.
Once pinentry-mac, GPG_TTY, your key, and your Git config all agree with each other, the setup is stable. After that, GPG does what it is supposed to do: give you verified commits with stronger lifecycle controls than SSH.
If that extra control matters in your environment, use it. If it does not, SSH is still the simpler answer.