GPG Commit Signing
When you write code, Git records your name and email as the author of every commit — but that information is just plain text anyone could type. GPG commit signing lets you attach a cryptographic signature to a commit or tag, proving it was created by someone who holds a specific private key, not merely someone who typed a matching name. GitHub checks that signature against a public key you’ve uploaded and shows a green Verified badge next to the commit. For open-source maintainers, security-conscious teams, and anyone who wants an audit trail they can trust, signed commits turn “this claims to be from Ada” into “this is cryptographically proven to be from Ada’s key.”
Overview: How GPG Commit Signing Works
Git’s author and committer fields (user.name and user.email) are stored as plain text inside the commit object. Anyone can run git config user.name "Ada Lovelace" on their own machine and every commit they make afterward will claim to be from Ada Lovelace. Git has no built-in way to stop this — identity in Git is advisory, not enforced.
GPG (GNU Privacy Guard) is a free implementation of the OpenPGP standard for public-key cryptography. You generate a keypair: a private (secret) key that never leaves your machine, and a public key you can hand out freely — to GitHub, to coworkers, to a public keyserver. Anything signed with your private key can be verified by anyone holding your public key, and only your private key could have produced that signature.
When you run git commit -S, Git builds the commit object exactly as it normally would — the hash of the tree representing your staged snapshot, the parent commit’s hash, the author line, the committer line, and the message — and pipes that content to gpg to produce a detached signature. That signature is embedded into the commit object itself, in a header field called gpgsig, before Git computes the object’s hash and writes it to .git/objects. The signature isn’t a separate file bolted on afterward; it becomes part of the content the commit’s own hash is derived from. Tamper with the commit’s message, tree, or parent after the fact and the signature no longer matches — Git and GitHub will both report it as invalid.
Verification works the same way in reverse: git log --show-signature or GitHub’s UI extracts the gpgsig field, reconstructs the exact bytes that were originally signed, and asks gpg to check the signature against a known public key. On GitHub that means a key uploaded to your account settings and whose identity (UID) email matches a verified email on your account. If both match you get Verified; if the key is unknown, revoked, expired, or the email doesn’t line up, you get Unverified — even though the author name looks correct.
You can sign three things: individual commits (git commit -S), annotated tags (git tag -s), and — as a convenience — every commit automatically once you set commit.gpgsign true. Git also supports signing with an SSH key instead of GPG since Git 2.34 (gpg.format ssh), which is simpler if you already manage SSH keys for authentication, but GPG remains the most widely supported and longest-standing option, and is what this lesson focuses on.
Syntax
The commands span two tools: gpg itself (key management) and git (signing and verifying). The core git-side syntax looks like this:
git commit -S -m "<message>"
git tag -s "<tagname>" -m "<message>"
git log --show-signature
git config --global user.signingkey "<key-id>"
git config --global commit.gpgsign true
| Flag / Command | Meaning |
|---|---|
git commit -S |
Sign this commit with your configured GPG key (asks for your passphrase, cached by gpg-agent) |
git commit --no-gpg-sign |
Skip signing for one commit even if commit.gpgsign is enabled globally |
git tag -s <name> |
Create a signed annotated tag (lightweight tags cannot be signed) |
git tag -v <name> |
Verify a signed tag’s signature |
git log --show-signature |
Show each commit’s signature status inline in the log |
user.signingkey |
The GPG key ID Git should use when signing |
commit.gpgsign |
When true, every git commit is signed automatically without needing -S |
tag.gpgsign |
When true, every git tag -a is signed automatically |
gpg.program |
Path to the GPG binary, useful if Git can’t find it or you’re using GPG Suite / gpg2 |
Examples
Example 1: Generate a GPG key and find its key ID
gpg --full-generate-key
Output:
gpg (GnuPG) 2.4.3; Copyright (C) 2023 g10 Code GmbH
Please select what kind of key you want:
(1) RSA and RSA
(9) ECC (sign and encrypt) *default*
Your selection? 1
What keysize do you want? (3072) 4096
Key is valid for? (0) 1y
Real name: Ada Lovelace
Email address: ada@example.com
...
gpg: key 3AA5C34371567BD2 marked as ultimately trusted
Choose RSA 4096 (or the ECC default if you prefer a smaller, modern key) and set an expiration — 1y or 2y is common, since a key that never expires is harder to retire safely if it’s ever compromised. Git will prompt for a passphrase to encrypt the private key at rest; gpg-agent will ask for it again once its cache expires. List your secret keys to get the ID Git needs:
gpg --list-secret-keys --keyid-format=long
Output:
sec rsa4096/3AA5C34371567BD2 2026-08-03 [SC] [expires: 2027-08-03]
A1B2C3D4E5F67890A1B2C3D4E5F67890ABCDEF01
uid [ultimate] Ada Lovelace <ada@example.com>
ssb rsa4096/9BF5D34372667CE3 2026-08-03 [E]
The string after rsa4096/ on the sec line — 3AA5C34371567BD2 — is your key ID. The uid email, ada@example.com, must match the user.email configured in Git and a verified email on your GitHub account, or GitHub will refuse to mark commits as Verified even when the signature itself is valid.
Example 2: Configure Git and sign a commit
git config --global user.signingkey 3AA5C34371567BD2
git config --global commit.gpgsign true
With commit.gpgsign set to true, every future commit is signed automatically, so -S becomes optional (still worth knowing, since you’ll use it to override the default per commit). Make a commit as usual:
git add validator.py
git commit -m "feat: add login validation"
Output:
[main 4f2a1c9] feat: add login validation
1 file changed, 18 insertions(+)
Behind the scenes, gpg-agent either used a cached passphrase or prompted you for one in a pinentry dialog before Git wrote the commit object. Confirm the signature locally:
git log --show-signature -1
Output:
commit 4f2a1c9e8b3d1a2f9c8e7d6b5a4938271605f4e3
gpg: Signature made Mon Aug 3 14:02:11 2026 UTC
gpg: using RSA key 3AA5C34371567BD2
gpg: Good signature from "Ada Lovelace <ada@example.com>" [ultimate]
Author: Ada Lovelace <ada@example.com>
Date: Mon Aug 3 14:02:11 2026 +0000
feat: add login validation
gpg: Good signature confirms the commit content matches what was signed and that gpg trusts the key. Once this commit reaches GitHub — and your public key is uploaded to your account — the same commit shows a Verified badge in the web UI.
Example 3: Sign a release tag and publish your public key
git tag -s v1.2.0 -m "release: version 1.2.0"
git tag -v v1.2.0
Output:
object 4f2a1c9e8b3d1a2f9c8e7d6b5a4938271605f4e3
type commit
tag v1.2.0
tagger Ada Lovelace <ada@example.com> 1785852131 +0000
release: version 1.2.0
gpg: Signature made Mon Aug 3 14:22:11 2026 UTC
gpg: using RSA key 3AA5C34371567BD2
gpg: Good signature from "Ada Lovelace <ada@example.com>" [ultimate]
Unlike commits, only annotated tags can be signed — a lightweight tag is just a name pointing at a commit, with no tag object of its own to attach a signature to. To let GitHub (and your teammates) verify commits and tags signed with this key, export the public key and paste it into GitHub under Settings → SSH and GPG keys → New GPG key:
gpg --armor --export 3AA5C34371567BD2
Output:
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGSp2xkBEAC7f3z9k2M8pQeV1n...(many lines omitted)...
-----END PGP PUBLIC KEY BLOCK-----
Never share the private key this way — only ever export and distribute the public key, using --export, never --export-secret-keys.
How It Works, Step by Step
When you run git commit -S -m "...", Git performs these steps internally:
- Git assembles the commit’s content exactly as an unsigned commit would: a pointer to the tree object representing the staged snapshot, the parent commit’s hash, author line, committer line, and message.
- Instead of hashing that content immediately, Git pipes it to
gpgbehind the scenes, asking it to produce a detached ASCII-armored signature over exactly those bytes. gpg-agentsupplies your unlocked private key (prompting for your passphrase via pinentry if it isn’t already cached) and returns the signature.- Git inserts that signature into the commit object as a multi-line
gpgsigheader, then computes the hash over the entire object, signature included, and writes the resulting object to.git/objects. - The branch pointer (
refs/heads/main) moves to this new commit hash, same as any commit — signing changes nothing about how branches, merges, or pushes work.
You can see the embedded signature directly by dumping the raw commit object:
git cat-file -p HEAD
Output:
tree 8f3d2e1a9c7b6543210fedcba9876543210feda
parent 1a2b3c4d5e6f7890abcdef1234567890abcdef12
author Ada Lovelace <ada@example.com> 1785849731 +0000
committer Ada Lovelace <ada@example.com> 1785849731 +0000
gpgsig -----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEE...(wrapped signature bytes)...
-----END PGP SIGNATURE-----
feat: add login validation
Verification just reverses the process: strip the gpgsig header back out to reconstruct the originally-signed bytes, then check the signature against the sender’s public key. This is also why amending a signed commit or rewriting history invalidates the old signature — the resulting object’s content differs, so the embedded signature no longer matches, and it must be re-signed.
Common Mistakes
Mistake 1: Signing fails with no TTY in a remote session
$ git commit -S -m "fix: resolve null pointer in parser"
error: gpg failed to sign the data
fatal: failed to write commit object
This is one of the most common GPG errors, and it’s almost never about your key. It happens because gpg needs a terminal to prompt for your passphrase (via pinentry) but doesn’t know which one to use — common over SSH sessions or inside some terminal multiplexers. Tell GPG which terminal to use before committing:
export GPG_TTY=$(tty)
Add that line to your shell profile (~/.bashrc or ~/.zshrc) so it’s set automatically in every new shell, then retry the commit.
Mistake 2: Commit is signed but still shows “Unverified” on GitHub
A perfectly valid local signature (gpg: Good signature in git log --show-signature) can still show up as Unverified on GitHub. The usual cause is that the commit’s author email doesn’t match any email that’s both verified on your GitHub account and listed as a UID on the uploaded public key. Make sure all three line up:
git config --global user.email "ada@example.com"
Then confirm that same address appears under a uid line in gpg --list-secret-keys --keyid-format=long, and that it’s added and verified under GitHub’s Settings → Emails.
Mistake 3: Assuming a key is signing commits when it isn’t
Setting user.signingkey alone does not sign anything — it only tells Git which key to use if asked to sign. Developers sometimes set the signing key once, forget that commit.gpgsign is still false, and are surprised months later that none of their commits are verified. Check both settings together, and remember -S only forces one commit — it doesn’t change the default.
Best Practices
- Generate an RSA 4096-bit key (or a modern ECC key) with a real expiration date —
0for “never expires” makes a compromised key impossible to retire cleanly. - Set
commit.gpgsign trueandtag.gpgsign trueglobally once your key is configured, rather than remembering-S/-son every command. - Keep the
user.emailin your Git config, the UID on your GPG key, and a verified email on your GitHub account all identical. - Back up your private key and its revocation certificate offline (a password manager or encrypted drive) — losing the key means you can never sign as that identity again.
- Renew the key’s expiration before it lapses rather than generating a brand-new key and re-uploading it everywhere.
- On shared or important repositories, enable a branch protection rule requiring signed commits so unsigned or forged pushes are rejected outright.
- Never distribute your private key or paste it anywhere — only ever share the output of
gpg --armor --export, never--export-secret-keys.
Practice Exercises
- Generate a new GPG key for a test identity, configure
user.signingkeyandcommit.gpgsign, and make two commits. Confirm both showgpg: Good signatureingit log --show-signature. - Create an annotated, signed tag for a fake release (e.g.
v0.1.0), verify it withgit tag -v, then export the public key and add it to a GitHub account to see the commit switch to Verified. - Deliberately set
user.emailto something that does not match your key’s UID, make a signed commit, and observe what GitHub reports. Then fix the mismatch and confirm the badge changes.
Summary
- Plain Git author/committer fields are unverified text; GPG signing adds a cryptographic guarantee tied to a private key only you hold.
- The signature lives inside the commit object itself, in a
gpgsigheader, and is part of what gets hashed — editing a signed commit invalidates its signature. git commit -Ssigns one commit;commit.gpgsign truesigns every commit automatically;git tag -ssigns annotated tags (lightweight tags can’t be signed).- GitHub shows Verified only when the signature is valid and the signer’s email matches a verified account email — a valid signature with a mismatched email still shows Unverified.
git log --show-signatureandgit tag -vlet you check signatures locally without relying on GitHub’s UI.- Guard your private key like a password: back it up securely, set an expiration, and never export or share the secret key itself.
