Verified commit and tag signatures on GitHub
The "Unverified" label is not about the commit message format: it means GitHub cannot prove that the commit was made by its declared author. The name and email on a commit are free text — anyone can write anything there. To prove authorship, the commit is signed with a cryptographic key, and GitHub verifies it against the corresponding public key.
This guide signs with SSH, so you can reuse the key you already use for GitHub (simpler than setting up GPG from scratch).
Locate or create an SSH key
ls ~/.ssh/*.pub
If none exists, create one:
ssh-keygen -t ed25519 -C "your-email@example.com"
Configure Git to sign with SSH
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true # sign every commit automatically
git config --global tag.gpgsign true # also sign tags
(The settings are still named "gpg" for historical reasons, but with gpg.format ssh signing happens via SSH.)
Register the public key on GitHub as a Signing Key
Copy the public key to the clipboard:
cat ~/.ssh/id_ed25519.pub | pbcopy # on macOS. On Linux: xclip -sel clip < ~/.ssh/id_ed25519.pub
On GitHub: Settings → SSH and GPG keys → New SSH key. In the Key type dropdown, select Signing Key. Paste the key and save.
Verify the account email
GitHub only marks a commit as "Verified" if the commit email is among the verified emails on the account.
git config user.email
That email must appear, verified, under Settings → Emails on GitHub.
Sign existing commits
Turning on signing does not change previous commits; to sign them you have to rewrite history. To sign every commit on the branch from the start:
git rebase --exec 'git commit --amend --no-edit -S --no-verify' --root
-S: signs each commit.--no-verify: skips hooks during the rebase (those commits were already validated when they were created).
If you also need to fix the email on old commits, set the correct email first:
git config user.email "correct@example.com"
git rebase --exec 'git commit --amend --no-edit -S --no-verify --reset-author' --root
--reset-author updates the author and email to match the current Git config.
If a rebase stops mid-way because of an error, cancel it with git rebase --abort and try again.
Push the rewritten history
Rewriting history that was already published requires a force push:
git push --force-with-lease
--force-with-lease only overwrites if nobody else has pushed new changes in the meantime (safer than --force). On a shared repository, tell the team before rewriting published history.
Sign tags
With tag.gpgsign true (configured above), tags are signed automatically. Manually:
git tag -s v1.0.0 -m "v1.0.0"
git push origin v1.0.0
Verify a signature locally:
git tag -v v1.0.0
git log --show-signature -1
Signing in non-interactive environments (agents & CI)
If you sign fine from your terminal but an automated agent (Claude Code, a CI job, a git hook, a background script) fails with something like "cannot sign — key not loaded", the cause is almost always this: your signing key is passphrase-protected and not present in ssh-agent. An interactive shell would prompt you for the passphrase; a non-interactive one can't, so ssh-keygen -Y sign fails and the commit is rejected.
The fix is to sign with a key that can be used without a prompt — one that is either passphrase-free on disk, or already loaded into your agent.
Diagnose
First, see which key you're signing with and which keys your agent actually holds:
git config --get user.signingkey
ssh-add -l
The identities in ssh-add -l are shown by fingerprint, not filename. Match each fingerprint back to a file:
for f in ~/.ssh/*.pub; do ssh-keygen -lf "$f"; done
Then check whether a given private key needs a passphrase (this only reads the key, it changes nothing):
ssh-keygen -y -P '' -f ~/.ssh/id_ed25519 >/dev/null 2>&1 \
&& echo "no passphrase — usable non-interactively" \
|| echo "passphrase-protected"
You're looking for a key that is both loaded in the agent (or passphrase-free on disk) and whose identity matches your committer email (git config user.email).
Fix:
Repoint Git at that key:
git config --global user.signingkey ~/.ssh/your_signing_key.pub
- If user.signingkey is also set at the repository level, the local value shadows the global one — check with git config --get user.signingkey inside the repo and unset the redundant override so it can't win:
git config --unset user.signingkey # run inside the repo, only if a local override exists
A passphrase-free key on disk is the most robust choice: ssh-keygen can read it directly whether or not the agent is running, so signing keeps working across reboots and fresh shells.
Verify without making a commit:
Confirm signing succeeds non-interactively — no throwaway commit needed. This is the exact operation Git runs under the hood:
printf 'test' | ssh-keygen -Y sign -n git -f ~/.ssh/your_signing_key.pub
If it prints a -----BEGIN SSH SIGNATURE----- block with no passphrase prompt, your agent/CI can now sign commits. Remember that the Verified badge still requires this same public key to be registered as a Signing Key on GitHub (see the earlier section) — signing succeeding locally and the badge appearing are two separate things.