Handling unsigned commits from outside contributors Jump to heading
A repository that requires signed commits and accepts external contributions has a conflict built into it. The requirement exists to establish that a commit came from someone you trust; an external contributor is by definition someone who has not yet been established as trusted, and asking them to set up commit signing before their first typo fix loses most of the contributions you would have received. The resolution is to notice that the two concerns are separable. This recipe applies that separation, extending Commit Verification Gates.
When to use this approach Jump to heading
- Your repository is public, or accepts contributions from outside the organisation.
- A signature requirement on the default branch is rejecting pull requests from forks.
- You want the trunk’s history to be fully verifiable without demanding key setup from strangers.
- Maintainers already review every external change, so a human trust decision is being made anyway.
- If every contributor has organisational credentials, do not use this — require signing properly, and treat an unsigned commit as an onboarding gap.
Step 1 — Work out what the requirement actually protects Jump to heading
Once the two are separated, the design follows. The fork can be unsigned because nothing on it is trusted yet; the trunk requires signatures because everything on it has been accepted by someone accountable.
Step 2 — Move the requirement to the merge boundary Jump to heading
# Verify the requirement is scoped to the branch, not to incoming refs
# (platform rulesets: "require signed commits" applies to main and release/*,
# and does NOT apply to pull request head branches from forks) # Check what the current gate actually inspects
git log --format='%H %G? %an' origin/main | head -5
# G = good, U = good but untrusted, N = none, B = bad
# Every commit ON MAIN should be G — commits on a fork branch need not be What changed: nothing yet — but if your CI job verifies every commit in the pull request rather than the resulting trunk commit, that is the thing to move.
#!/bin/sh
# ci/verify-trunk-signatures.sh — run on push to main, not on pull requests
set -eu
bad=0
for sha in $(git rev-list origin/main~20..origin/main); do
status=$(git log -1 --format='%G?' "$sha")
case "$status" in
G) ;;
*) echo "unsigned or untrusted commit on main: $sha ($status)" >&2; bad=1 ;;
esac
done
exit "$bad" Step 3 — Re-sign at merge with a squash or a signed merge commit Jump to heading
Two mechanisms produce a signed trunk commit from an unsigned contribution, and they differ in what history survives.
# Option A — squash-merge: one signed commit, authorship preserved in the trailer
git fetch origin pull/482/head:pr-482
git checkout main
git merge --squash pr-482
git commit -S -m "feat: add retry backoff to the webhook sender (#482)
Co-authored-by: A. Contributor <[email protected]>" # Option B — signed merge commit: the original commits stay, unsigned, under it
git merge --no-ff -S pr-482 -m "Merge pull request #482 from contributor/webhook-retry" # Verify whichever you chose
git log -1 --format='%G? %GS' # expect: G <your identity>
git log --format='%h %G? %an' -5 # under option B, parents show N — by design What changed: the trunk now carries a commit signed by a maintainer. Under option A the contributor’s authorship is preserved in a Co-authored-by trailer and the trunk history is uniformly signed. Under option B the original commits remain and are unsigned, which is honest about their provenance but means a naive “all commits must be signed” check will fail on them.
SAFETY WARNING — when you re-sign someone else’s work you are attesting to it with your own key. That signature says a maintainer reviewed and accepted the change, and it will be cited if the change turns out to be malicious. Do not automate re-signing on merge without a human approval gate: a bot that signs whatever reaches it converts your organisation’s trust anchor into a rubber stamp.
Step 4 — Make signing easy for contributors who want to Jump to heading
<!-- CONTRIBUTING.md -->
## Signing your commits (optional)
Signed commits are welcome but not required — we re-sign contributions when
we merge them. If you would like to sign, SSH signing needs no new keys:
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
Add the same public key to your account as a *signing* key (separate from
your authentication key) so the platform shows your commits as verified. # Verify the instructions work on a clean machine before publishing them
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git commit --allow-empty -S -m "test: signing works"
git log -1 --format='%G? %GS' Framing it as optional is the point. A required signature is a barrier at exactly the moment a first-time contributor is deciding whether the fix is worth the effort; an invitation with three copy-and-paste lines costs nothing and is taken up by the contributors who will be back. The full comparison of backends is in GPG vs SSH Commit Signing.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Does squash-merging an unsigned branch make it signed? Jump to heading
It creates a new commit signed by whoever performed the merge, which attests that a maintainer reviewed and accepted the change — not that the original author is who they claim. That is usually the right attestation for external contributions: the trust you are recording is the maintainer’s review, and the contributor’s identity is established by the review process rather than by cryptography.
Should we ask external contributors to sign their commits? Jump to heading
Invite, do not require. Signing is a meaningful barrier for a first-time contributor fixing a typo, and the marginal security gain is small when a maintainer reviews and re-signs at merge anyway. Document how to sign for those who want to, and keep the requirement on the branches you control.
What about internal contributors who have not set up signing? Jump to heading
That is a different case and should be fixed rather than accommodated: they have credentials, a key can be issued, and the requirement is enforceable. Treat an unsigned commit from someone with write access as an onboarding gap, and give them the setup guide rather than an exemption.
Related Jump to heading
- Commit Verification Gates — the parent guide: where verification runs and what each gate can enforce.
- Enforcing Signed Commits with Branch Protection — the strict version, for repositories with no external contributions.
- GPG vs SSH Commit Signing — the backend choice that determines how easy the optional path is.