Signing Git Commits With GPG in CI
git -S signs a commit with the GPG key named in user.signingkey, producing a verifiable commit.
Signed commits prove who authored them. Wiring git to gpg is a few config lines, but CI also needs gpg to run non-interactively, which means loopback pinentry.
What it does
git invokes gpg to create a signature over the commit object. user.signingkey names the key, commit.gpgsign true signs every commit, and gpg.program selects the binary. The signature is what GitHub shows as "Verified".
Common usage
git config user.signingkey "$KEY_ID"
git config commit.gpgsign true
git config gpg.program gpg
# sign one commit explicitly
git commit -S -m "Signed commit"
# make gpg non-interactive for git in CI
echo "pinentry-mode loopback" >> "$GNUPGHOME/gpg.conf"Options
| Setting | What it does |
|---|---|
| user.signingkey <id> | Which GPG key signs commits |
| commit.gpgsign true | Sign every commit automatically |
| gpg.program gpg | Path to the gpg binary git should call |
| git commit -S | Sign a single commit on demand |
| pinentry-mode loopback (gpg.conf) | Let git-invoked gpg run without a prompt |
In CI
git does not pass --pinentry-mode, so set pinentry-mode loopback in gpg.conf (under GNUPGHOME) and use a key with no passphrase or one cached via the agent. Set GPG_TTY only matters interactively. Confirm the signing key has the email matching the commit author or hosts may not mark it verified.
Common errors in CI
"error: gpg failed to sign the data" with "Inappropriate ioctl for device" means gpg tried to prompt; add pinentry-mode loopback to gpg.conf and use a passphrase-free or agent-cached key. "secret key not available" means user.signingkey does not match an imported secret key. "gpg: skipped: No secret key" is the same root cause.