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

Two claims, usually conflatedAn author signature attests who wrote a commit, which for an unknown external contributor establishes very little. An acceptance signature attests which maintainer reviewed and merged the change, which is the claim the trunk's history actually needs."who wrote this?"an author signatureproves the commit came from a keyyou have already decided to trustfor a stranger, this proves little:their key is not in your trust anchor,so a valid signature is still unverifiable"who accepted this?"an acceptance signatureproves which maintainer reviewedthe change and put it on the trunkthis is what the trunk needs:every commit traceable to someoneaccountable inside the organisation

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.

Squash-merge versus a signed merge commitSquash-merging produces a single signed commit on the trunk with the contributor recorded as co-author, so every commit reachable from the trunk is signed. A signed merge commit keeps the original unsigned commits as parents, which is more faithful to history but means the trunk contains unsigned ancestors.A · squash-merge — uniformly signed trunkGGGone signed commit, maintainer's keyCo-authored-by preserves the contributorevery commit reachable from main is signedB · signed merge commit — faithful, but mixedGGNNGmerge commit is signed; its parents are nota strict all-commits check fails on the red oneschoose A when the gate is "every commit signed"; choose B when preserving the contributor's commits matters more

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'
Every required step costs contributionsOf the people who start a first contribution, most complete a fork and a small edit. Requiring a signing key adds a setup step at exactly the moment the contributor is weighing whether the fix is worth the effort, and a noticeable share stop there.reads the code and spots a fixforks and editsopens a pull requestsets up commit signing first← where a required signature cutsre-signing at merge keeps the trunk verifiable without moving the barrier to the widest part of the funnel

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.