Signing Your Git Commits: From Zero to Verified¶
Anyone can commit code to a repository pretending to be you. Git's author fields (user.name and user.email) are free-form text that any client can set to anything. Cryptographic commit signing closes that gap by mathematically binding a commit to a key pair that only you control. Once you add a verified badge to your commits on GitHub or GitLab, every reviewer can be confident that the code actually came from you.
This post walks through the full picture: why signing matters, how to configure your git client correctly, how to generate and publish a GPG key, how to use your platform's no-reply address so you never expose your real email, and how to automate Signed-off-by trailers with git hooks, complete with copy-paste examples for every step.
Why Sign Commits?¶
Anyone Can Impersonate You¶
Git does not validate the user.name or user.email fields. Nothing stops a malicious contributor from running:
git config user.name "Peter Gibbons"
git config user.email "[email protected]"
git commit -m "fix: remove TPS report cover sheet requirement"
That commit will show Peter's name in the log. Without a cryptographic signature, there is no way to prove otherwise.
Verified Badges = Trust¶
When you sign a commit with a GPG or SSH key whose public half is uploaded to GitHub or GitLab, those platforms display a green Verified badge next to every signed commit. Reviewers, auditors, and automated tooling can filter on that badge to ensure only authenticated contributors' changes are merged.
Developer Certificate of Origin (DCO)¶
Many open-source projects, including the Linux kernel and the Cloud Native Computing Foundation (CNCF) project family, require a Signed-off-by trailer in every commit message. This is the Developer Certificate of Origin (DCO): a lightweight legal statement that you have the right to submit the code under the project's licence. It is distinct from (and complementary to) cryptographic signing.
Signed-off-by: Ryan Johnson <[email protected]>
Compliance Requirements¶
Enterprise environments running SOC 2, PCI-DSS, or FedRAMP workloads increasingly require that all code in production be traceable to an authenticated identity. Signed commits, combined with branch-protection rules that reject unsigned commits, satisfy that traceability requirement without expensive additional tooling.
Configuring Git: user.name, user.email, and Signing¶
Global Git Configuration¶
Your git client stores identity and behaviour in ~/.gitconfig (global) or .git/config (per-repository). Start with the basics:
git config --global user.name "Ryan Johnson"
git config --global user.email "[email protected]"
Verify the result:
user.name=Ryan Johnson
[email protected]
Per-Repository Overrides¶
If you use a different identity for work projects, override the global settings inside any individual repository:
cd ~/work/acme-corp
git config user.name "Ryan Johnson"
git config user.email "[email protected]"
These per-repo values take precedence over the global file. You can also maintain completely separate ~/.gitconfig files for personal and work profiles using conditional includes:
# ~/.gitconfig
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
[includeIf "gitdir:~/personal/"]
path = ~/.gitconfig-personal
# ~/.gitconfig-work
[user]
name = Ryan Johnson
email = [email protected]
# ~/.gitconfig-personal
[user]
name = Ryan Johnson
email = [email protected]
Any repository cloned inside ~/work/ automatically uses the work identity, and any repository inside ~/personal/ uses the personal identity.
Generating a GPG Key¶
Install GnuPG¶
Generate a Key Pair¶
The recommended algorithm is Ed25519 (fast, compact signatures, strong security):
At the prompts, select these options:
| Prompt | Recommended Answer |
|---|---|
| Key type | 9 - ECC (sign and encrypt) |
| Elliptic curve | 1 - Curve 25519 |
| Key expiry | 1y (rotate annually) |
| Real name | Your full name |
| Email address | The address you use in git |
| Comment | Leave blank or add a note |
Save Your Passphrase
GnuPG will prompt you for a passphrase. Use a strong, unique passphrase and save it in your password manager (for example, 1Password, Bitwarden, or macOS Keychain) before you continue. If you lose the passphrase, you cannot use or revoke the private key.
Why set an expiry?
An expiry date on a GPG key is a safety net. If your private key is ever stolen or your passphrase compromised, the key will stop being trusted on its own without requiring you to actively revoke it. Set a calendar reminder to renew the key before it expires.
Verify the Generated Key¶
/home/ryan/.gnupg/pubring.kbx
-------------------------------
sec ed25519/3AA5C34371567BD2 2026-03-22 [SC] [expires: 2027-03-22]
ABCDEF1234567890ABCDEF1234567890ABCDEF12
uid [ultimate] Ryan Johnson <[email protected]>
ssb cv25519/4BB6D45482678CE3 2026-03-22 [E] [expires: 2027-03-22]
The long key ID here is 3AA5C34371567BD2. The 40-character fingerprint is ABCDEF1234567890ABCDEF1234567890ABCDEF12.
Export the Public Key¶
To upload to GitHub or GitLab, export the ASCII-armored public key:
Copy the entire block including the -----BEGIN and -----END lines.
Configuring Git to Use Your GPG Key¶
Point Git at Your Key¶
Enable Automatic Signing¶
With these settings every git commit and git tag is automatically signed; no -S flag required.
Point Git at the GPG Binary¶
On some systems or when using Homebrew on macOS, you need to tell git which gpg binary to use:
Configure Pinentry on macOS¶
Without a pinentry programme, GPG cannot prompt for your passphrase in a terminal or GUI context. After installing pinentry-mac:
echo "pinentry-program $(brew --prefix)/bin/pinentry-mac" >> ~/.gnupg/gpg-agent.conf
gpgconf --kill gpg-agent
Restart the agent by running any GPG operation, or:
Complete ~/.gitconfig Reference¶
After all the above, your global git config should look similar to this:
[user]
name = Ryan Johnson
email = [email protected]
signingkey = 3AA5C34371567BD2
[commit]
gpgsign = true
[tag]
gpgsign = true
[gpg]
program = gpg
[core]
editor = vim
autocrlf = input
whitespace = fix,-indent-with-non-tab,trailing-space,cr-at-eol
Becoming Verified¶
CLI Prerequisite
The one-liner below requires the GitHub CLI (gh) to be installed and authenticated. Install it with brew install gh or winget install GitHub.cli, then run gh auth login.
Add Your GPG Key
- Navigate to Settings → SSH and GPG keys → New GPG key.
- Paste the ASCII-armored public key block you exported earlier.
- Click Add GPG key and confirm with your GitHub password or sudo prompt.
Or via the GitHub CLI:
Ensure Your Email Matches
GitHub verifies commits by checking that the email address in the commit and the GPG key UID matches a verified email address on your GitHub account.
- Go to Settings → Emails and confirm the address is listed and verified (green tick).
- The email in
git config user.emailmust match one of your verified GitHub emails exactly.
Enable Vigilant Mode (Optional but Recommended)
Vigilant mode instructs GitHub to flag unsigned commits from your account with an Unverified label rather than simply showing no badge. This makes it immediately obvious if someone pushes a commit pretending to be you without a valid signature.
Enable it at Settings → SSH and GPG keys → Vigilant mode → Flag unsigned commits as unverified.
Vigilant mode and existing commits
Once vigilant mode is on, every unsigned commit attributed to your email, including historical ones, will show Unverified. That is the point. If you have old unsigned commits you care about, you cannot retroactively sign them on GitHub, but you can sign new ones going forward.
Make a Signed Commit and Check
Navigate to the commit on GitHub. You should see a green Verified badge next to the commit hash.
Clicking the badge shows the key fingerprint and the name/email from the GPG key UID.
Enforce Signing via Branch Protection
In repository Settings → Branches → Add branch protection rule:
- Enable Require signed commits.
With this setting, any unsigned commit pushed to the protected branch (directly or via merge) is rejected. Combined with Require pull request reviews, this ensures every line of code in your main branch was authored by a verified identity.
CLI Prerequisite
The one-liner below requires the GitLab CLI (glab) to be installed and authenticated. Install it with brew install glab or winget install GitLab.GLAB, then run glab auth login.
Add Your GPG Key
- Navigate to Preferences → GPG Keys (user avatar → Edit profile → GPG Keys).
- Paste the ASCII-armored public key.
- Click Add key.
Or via the GitLab CLI:
Email Verification
Same requirement as GitHub: the email in the commit and in the GPG key UID must be a confirmed email address on your GitLab account (Preferences → Emails).
Enforce Signing on a GitLab Project
In GitLab, signing enforcement is available at the project or group level:
- Go to Project Settings → Repository → Push rules.
- Enable Reject unsigned commits.
GitLab tier requirement
Reject unsigned commits is available on GitLab Premium and Ultimate (and self-managed EE). On GitLab Free/Community Edition, GPG signatures are displayed but not enforced.
Verifying Signatures Locally¶
You can also verify signatures without trusting a web interface:
commit a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
gpg: Signature made Sun 22 Mar 2026 04:34:16 UTC
gpg: using EDDSA key 3AA5C34371567BD2
gpg: Good signature from "Ryan Johnson <[email protected]>" [ultimate]
Author: Ryan Johnson <[email protected]>
Date: Sun Mar 22 04:34:16 2026 +0000
docs: test signed commit
Using Your Platform's No-Reply Email¶
Both GitHub and GitLab provide a no-reply email address that keeps your real address private while still allowing commits to be Verified.
Find Your No-Reply Address
- Go to Settings → Emails.
- Scroll to Keep my email address private.
- Enable the option. GitHub shows your no-reply address in this format:
For example:
The numeric prefix is your GitHub user ID, which ensures the address is unique and stable even if you change your username.
Configure Git to Use the No-Reply Address
git config --global user.email "[email protected]"
Add the No-Reply Address to Your GPG Key
The GPG key must include the no-reply email as a UID. You can either generate a fresh key or add a new UID to an existing key.
Option A: Generate a new key with the no-reply address:
gpg --full-generate-key
# At the email prompt, enter: [email protected]
Option B: Add a UID to an existing key:
Inside the GPG interactive shell:
gpg> adduid
Real name: Ryan Johnson
Email address: [email protected]
Comment:
You selected this USER-ID:
"Ryan Johnson <[email protected]>"
gpg> save
Re-export and re-upload the updated public key (delete the old key first at Settings → SSH and GPG keys):
Why This Works
GitHub considers a signed commit Verified when:
- The commit email matches a verified email on the account, and
- The GPG key that signed the commit is uploaded to the account, and
- The GPG key's UID includes that same email address.
The no-reply address counts as a verified email on your account (GitHub adds it automatically when you enable email privacy). So a commit signed with a key that has the no-reply address in its UID, using that no-reply address as user.email, is Verified, and your real address never appears in the git log.
Find Your No-Reply Address
- Go to Preferences → Emails.
- Enable Use a private commit email.
- GitLab shows your no-reply address in this format:
For example:
Configure Git to Use the No-Reply Address
git config --global user.email "[email protected]"
Add the No-Reply Address to Your GPG Key
The GPG key must include the no-reply email as a UID. You can either generate a fresh key or add a new UID to an existing key.
Option A: Generate a new key with the no-reply address:
gpg --full-generate-key
# At the email prompt, enter: [email protected]
Option B: Add a UID to an existing key:
Inside the GPG interactive shell:
gpg> adduid
Real name: Ryan Johnson
Email address: [email protected]
Comment:
You selected this USER-ID:
"Ryan Johnson <[email protected]>"
gpg> save
Re-export and re-upload the updated public key (delete the old key first at Preferences → GPG Keys):
Why This Works
GitLab considers a signed commit Verified when:
- The commit email matches a confirmed email on the account, and
- The GPG key that signed the commit is uploaded to the account, and
- The GPG key's UID includes that same email address.
The no-reply address counts as a confirmed email on your account (GitLab adds it automatically when you enable private commit email). So a commit signed with a key that has the no-reply address in its UID, using that no-reply address as user.email, is Verified, and your real address never appears in the git log.
The Signed-off-by Trailer (DCO)¶
Cryptographic signing proves who pushed the commit. The Signed-off-by trailer proves that you agree the contribution is yours to give. They are complementary mechanisms.
What the DCO Says¶
The Developer Certificate of Origin 1.1 contains four clauses. By adding Signed-off-by, you certify that:
- The contribution was created by you, or
- It is based on prior work with a compatible open-source licence, or
- It was provided to you by someone in categories (1) or (2), or
- You understand the contribution is public and permanently recorded.
Adding Signed-off-by Manually¶
The -s (or --signoff) flag appends the trailer automatically using user.name and user.email from your git config:
feat: add OAuth2 support
Signed-off-by: Ryan Johnson <[email protected]>
You can also add it in your commit message template:
# ~/.gitmessage
Signed-off-by: Ryan Johnson <[email protected]>
Enforcing Sign-Off with Integrations¶
GitHub and GitLab both have integrations to enforce Signed-off-by on pull requests and merge requests. These tools read every commit in a contribution and fail the CI status check if any commit is missing a sign-off trailer for the commit's author.
Popular options include:
- GitHub: the DCO GitHub App (a free GitHub App installable on any repository or organization)
- GitLab: the built-in push rule available in project settings
- Self-hosted / generic CI: linting steps in GitHub Actions, GitLab CI, or other pipelines that run
git log --format='%H %ae' | xargs ...to verify trailers
If you forget to sign a commit:
# Amend the last commit
git commit --amend --signoff
# Or rebase to add sign-off to multiple commits
git rebase --signoff HEAD~3
Automating Sign-Off and Signing with Git Hooks¶
Git hooks are shell scripts that run automatically at specific points in the git workflow. They live in .git/hooks/ of each repository. Hooks are not committed to the repository by default, but you can share them via a custom hooks directory.
Hook: Auto-Append Signed-off-by (prepare-commit-msg)¶
Create .git/hooks/prepare-commit-msg and make it executable:
cat > .git/hooks/prepare-commit-msg << 'EOF'
#!/usr/bin/env bash
# Automatically append Signed-off-by if not already present.
COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="$2"
# Skip merge commits, squash commits, and commit templates
if [ "$COMMIT_SOURCE" = "merge" ] || [ "$COMMIT_SOURCE" = "squash" ]; then
exit 0
fi
NAME=$(git config user.name)
EMAIL=$(git config user.email)
SOB="Signed-off-by: $NAME <$EMAIL>"
# Only add if not already present
if ! grep -qF "$SOB" "$COMMIT_MSG_FILE"; then
# Append after the commit message, before any comments
printf '\n%s\n' "$SOB" >> "$COMMIT_MSG_FILE"
fi
EOF
chmod +x .git/hooks/prepare-commit-msg
Every git commit in this repository will now append Signed-off-by automatically. You can still edit the commit message and remove it if needed.
Hook: Validate Signed-off-by Exists (commit-msg)¶
If you want to enforce that all commits have a sign-off (useful in a team setting), add a commit-msg hook that rejects commits without it:
cat > .git/hooks/commit-msg << 'EOF'
#!/usr/bin/env bash
# Reject commits that are missing a Signed-off-by trailer.
COMMIT_MSG_FILE="$1"
NAME=$(git config user.name)
EMAIL=$(git config user.email)
SOB="Signed-off-by: $NAME <$EMAIL>"
if ! grep -qF "$SOB" "$COMMIT_MSG_FILE"; then
echo ""
echo "ERROR: Missing Signed-off-by trailer."
echo ""
echo "Please add the following line to your commit message:"
echo ""
echo " $SOB"
echo ""
echo "You can do this automatically by running: git commit -s"
echo ""
exit 1
fi
EOF
chmod +x .git/hooks/commit-msg
Sharing Hooks Across a Team¶
By default, hooks live in .git/hooks/ and are ignored by git. To share hooks with collaborators, store them in a committed directory and point git at it:
mkdir -p .githooks
# Move hooks there
mv .git/hooks/prepare-commit-msg .githooks/
mv .git/hooks/commit-msg .githooks/
# Tell git to use the shared directory
git config core.hooksPath .githooks
Commit .githooks/ to the repository. Each contributor then runs:
Or add a setup step to your Makefile:
Using lefthook for Cross-Platform Hook Management¶
Lefthook is a fast, dependency-free hook manager that works on any platform and integrates with CI:
# Install
brew install lefthook # macOS
go install github.com/evilmartians/lefthook@latest # Go
# Initialise in a repo
lefthook install
lefthook.yml in the repository root:
# lefthook.yml
pre-commit:
commands:
lint:
run: pre-commit run --files {staged_files}
commit-msg:
commands:
signed-off-by:
run: |
NAME=$(git config user.name)
EMAIL=$(git config user.email)
SOB="Signed-off-by: $NAME <$EMAIL>"
if ! grep -qF "$SOB" {1}; then
echo "ERROR: Missing Signed-off-by. Run: git commit -s"
exit 1
fi
Putting It All Together¶
macOS Example
The commands in this walkthrough use brew and pinentry-mac, which are specific to macOS. Linux users should substitute their package manager (for example, apt or dnf) and omit the pinentry-mac package. Windows users should use winget or the Gpg4win installer.
Here is the complete setup workflow from scratch on a fresh machine:
1. Install Tools¶
2. Configure Git Identity¶
git config --global user.name "Ryan Johnson"
git config --global user.email "[email protected]"
3. Generate GPG Key¶
gpg --full-generate-key
# Choose: ECC, Curve 25519, 1y expiry
# Name: Ryan Johnson
# Email: [email protected]
4. Configure Git Signing¶
KEY_ID=$(gpg --list-secret-keys --keyid-format=long \
| grep '^sec' | awk '{print $2}' | cut -d/ -f2)
git config --global user.signingkey "$KEY_ID"
git config --global commit.gpgsign true
git config --global tag.gpgsign true
git config --global gpg.program gpg
5. Configure Pinentry¶
echo "pinentry-program $(brew --prefix)/bin/pinentry-mac" \
>> ~/.gnupg/gpg-agent.conf
gpgconf --kill gpg-agent
6. Upload Your Key¶
7. Install the Sign-Off Hook in Every Repo¶
mkdir -p ~/.git-template/hooks
cat > ~/.git-template/hooks/prepare-commit-msg << 'EOF'
#!/usr/bin/env bash
COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="$2"
[ "$COMMIT_SOURCE" = "merge" ] || [ "$COMMIT_SOURCE" = "squash" ] && exit 0
NAME=$(git config user.name)
EMAIL=$(git config user.email)
SOB="Signed-off-by: $NAME <$EMAIL>"
grep -qF "$SOB" "$COMMIT_MSG_FILE" || printf '\n%s\n' "$SOB" >> "$COMMIT_MSG_FILE"
EOF
chmod +x ~/.git-template/hooks/prepare-commit-msg
# Tell git to use this template for every new clone / init
git config --global init.templateDir ~/.git-template
With init.templateDir set, every new repository created or cloned will automatically inherit the hook.
For existing repositories, copy the hook in:
8. Make a Signed, Signed-Off Commit¶
Git will invoke GPG to sign the commit (you will be prompted for your passphrase the first time) and the hook will append Signed-off-by. The resulting commit message:
docs: initial signed commit
Signed-off-by: Ryan Johnson <[email protected]>
Verify the signature:
commit a1b2c3d ...
gpg: Signature made Sun 22 Mar 2026 04:34:16 UTC
gpg: using EDDSA key 3AA5C34371567BD2
gpg: Good signature from "Ryan Johnson <[email protected]>" [ultimate]
Author: Ryan Johnson <[email protected]>
Date: Sun Mar 22 04:34:16 2026 +0000
docs: initial signed commit
Signed-off-by: Ryan Johnson <[email protected]>
Key Renewal and Rotation¶
Set a calendar reminder for the expiry date of your key. When the time comes:
Inside the GPG shell:
gpg> expire
Changing expiration time for the primary key.
Please specify how long the key should be valid.
Key is valid for? (0) 1y
gpg> key 1 # select the encryption subkey
gpg> expire
Key is valid for? (0) 1y
gpg> save
Re-export and re-upload the updated public key to GitHub and GitLab. The old key ID remains the same; you are just extending the validity period.
To rotate to a completely new key:
gpg --full-generate-key # generate new key
# Update git config: git config --global user.signingkey <NEW_ID>
# Upload new key to GitHub and GitLab
# Revoke old key: gpg --gen-revoke <OLD_ID> > revoke.asc, then gpg --import revoke.asc
# Upload the revocation to key servers
gpg --keyserver hkps://keys.openpgp.org --send-keys <OLD_ID>
Troubleshooting¶
error: gpg failed to sign the data¶
The most common cause on macOS is a missing or misconfigured pinentry. Check:
If this prompts for a passphrase and succeeds, git signing should work. If it fails with Inappropriate ioctl for device, GPG cannot reach the terminal:
Then kill and restart the agent:
Commits Show as Unverified After Upload¶
- Confirm the key is uploaded: Settings → SSH and GPG keys on GitHub, or Preferences → GPG Keys on GitLab.
- Confirm the email in
git config user.emailexactly matches the UID email in your GPG key. - Confirm that email is verified in Settings → Emails on GitHub, or Preferences → Emails on GitLab.
- Run
git log --show-signaturelocally to see if GPG considers the signature valid.
There is no assurance this key belongs to the named user¶
When verifying someone else's signature you see this if the key is not in your web of trust. For your own key, set it to ultimate trust:
Quick Reference Cheat Sheet¶
# List secret keys (long format)
gpg --list-secret-keys --keyid-format=long
# Export public key (ASCII)
gpg --armor --export <KEY_ID>
# Import a public key
gpg --import <file.asc>
# Set git signing key
git config --global user.signingkey <KEY_ID>
# Enable auto-signing
git config --global commit.gpgsign true
git config --global tag.gpgsign true
# Sign a commit manually
git commit -S -m "message"
# Sign off a commit (DCO)
git commit -s -m "message"
# Both at once
git commit -S -s -m "message"
# Verify last commit signature
git log --show-signature -1
# Amend last commit to add sign-off
git commit --amend --signoff
# Rebase to add sign-off to last N commits
git rebase --signoff HEAD~3
# Upload public key to GitHub (requires gh CLI)
gpg --armor --export <KEY_ID> | gh gpg-key add -
Signing commits is a small operational habit that pays dividends in trust, auditability, and compliance. Whether you are contributing to open source projects that require a DCO, working in an enterprise environment with signature enforcement, or simply wanting that green Verified badge on your public profile, the setup is a one-time investment that runs silently in the background from that point on.