A coding agent can produce a useful patch and still leave an ambiguous authorship trail. A signed-commit gate makes provenance a merge prerequisite: every commit introduced by a pull request must carry a signature that the repository can verify against an approved identity. This control is deliberately narrower than code review. It answers “which signing identity produced this commit?” while tests and review answer “is this change correct and intended?”
Last reviewed: 2026-09-09
Direct answer
Use two enforcement layers. First, run a deterministic check over every commit that the agent proposes to merge. Second, configure the destination branch to require signed commits and a successful status check. The local check gives the agent and operator a fast, reproducible error; the protected-branch rule is the server-side stop that prevents a bypass.
GitHub documents GPG, SSH, and S/MIME as supported commit-signing methods. A cryptographically verifiable signature can receive a “Verified” status, while a signed value that cannot be verified is “Unverified.” The GitHub commit-signature guide also distinguishes signing from merely signing off on a commit. Choose an approved signing identity for each agent execution boundary, publish the corresponding verification material through your normal code-host controls, and keep the private signing operation outside the agent’s prompt and logs.
A practical happy path is:
- The agent works on an isolated branch. The runner records the base revision and proposed head revision, then enumerates the full commit range rather than checking only the tip.
- The gate invokes Git’s verifier for each commit. The Git
verify-commitreference says the command validates the GPG signature created by a signed commit and offers verbose and raw status output. If your policy accepts SSH or S/MIME, pair the local check with the hosting service’s verification result for those methods. - The gate normalizes each result to a small status record. Every commit must be
verifiedwith an allowed reason, such asvalidin the hosting API. Do not copy a raw signature or signed payload into the ordinary run log. - The check reports success under a stable, unique status name from a trusted, pinned workflow producer. Put an always-run wrapper around the signature job if necessary; it must emit failure when the underlying job is skipped or neutral. Configure the protected branch to require that wrapper from the expected application or integration, and prevent the agent identity from publishing or changing it. The protected branch accepts the pull request only when that check and the required review and test checks pass.
- The reviewer compares the verified commit range with the reviewed diff. An amend, rebase, cherry-pick, or conflict resolution creates new commit objects, so the signature gate and review must run again.
The error path is a first-class workflow. If one commit is unsigned or returns unknown_key, bad_email, invalid, malformed_signature, or another non-allowed reason, mark the check failed, leave the branch unmerged, and preserve the failing revision for diagnosis. Ask the operator to configure the approved signer, re-sign or rewrite the affected commits in a controlled branch, and rerun the complete range. Never turn a failed result into a pass by checking only the latest commit.
This provider-neutral shell sketch emits sanitized evidence. Replace placeholders in the runner, not in a published log:
set -eu
base_ref="${BASE_REF:-origin/main}"
head_ref="${HEAD_REF:-HEAD}"
failure=0
approved_signer_class="approved"
commits="$(git rev-list "${base_ref}..${head_ref}")" || {
printf '%s\n' '{"event":"signature_check","result":"fail","signature_status":"unverified","reason":"range_resolution_failed"}'
exit 1
}
if [ -z "$commits" ]; then
printf '%s\n' '{"event":"signature_check","result":"fail","signature_status":"unverified","reason":"empty_commit_range"}'
exit 1
fi
for commit in $commits; do
# The host/API verification step must also compare the signer with the approved allowlist.
if git verify-commit --raw "$commit" >/dev/null 2>/dev/null && verify_host_identity "$commit" "$approved_signer_class"; then
printf '%s\n' '{"event":"signature_check","commit_sha":"[REDACTED]","result":"pass","signature_status":"verified","reason":"valid","signer_identity_class":"approved","status_producer_class":"trusted","execution_state":"executed"}'
else
printf '%s\n' '{"event":"signature_check","commit_sha":"[REDACTED]","result":"fail","signature_status":"unverified","reason":"verification_failed","signer_identity_class":"not_approved","status_producer_class":"trusted","execution_state":"executed"}'
failure=1
fi
done
test "$failure" -eq 0
The script fails closed when range resolution, empty-range validation, signature verification, or identity verification fails. In production, parse raw status in memory or in a short-lived workspace, map it to an explicit allowlist, and delete temporary diagnostics. A local GPG result does not, by itself, prove that an SSH or S/MIME signature is acceptable; use the host’s verification signal for those methods. Keep the base and head revisions immutable for the duration of the check, or invalidate the result when either changes.
Who this is for
This workflow is for repository maintainers, platform engineers, and security reviewers who let coding agents create branches or pull requests. It is especially useful when an agent can run unattended, when several agents share a repository, or when a pull request may be promoted automatically after checks pass.
It is not a replacement for human review, tests, dependency checks, or a policy about what an agent may change. A valid signature proves that a particular signing identity produced the commit object; it does not prove that the identity was used with the right authorization, that the code is correct, or that the agent’s instructions were trustworthy. Treat the signature gate as one control in a layered change process. A written scope and acceptance record can be paired with the change-scope notes workflow so the verified object and the reviewed intent stay connected.
Key takeaways
- Check every commit introduced by the pull request, not only the head commit.
- Define the approved signer identity and accepted signature methods before enabling enforcement.
- Use local verification for fast feedback and a hosting-service result for the merge decision.
- Require signed commits and a dedicated status check on the protected branch. Decide explicitly whether a partially verified status is acceptable under vigilant-mode rules.
- Record commit IDs, result, reason, signer class, and verification time, but never raw signatures, signed payloads, private signing material, or authentication data.
- Re-run the gate after every amend, rebase, cherry-pick, conflict resolution, or merge-method change.
- Keep commit authenticity separate from artifact provenance. The security scan gate guide covers a different question and should remain enabled.
Sources checked
The workflow is grounded in five public references checked for the claims used here.
- About commit signature verification explains GPG, SSH, and S/MIME signing, default and vigilant-mode statuses, persistent verification records, bot-signature constraints, and the rebase-and-merge caveat.
- About protected branches describes branch rules, required status checks, required signed commits, bypass behavior, and how unsigned commits can block a pull request.
- REST API endpoints for commits
defines the verification object and its
verified,reason,signature,payload, andverified_atfields, along with reason values such asunsigned,unknown_key, andinvalid. - Git’s
git-verify-commitmanual specifies the local GPG verification command and its--rawand--verboseoptions. - Sigstore’s overview describes identity-based signing, short-lived certificates, and an append-only transparency log for software artifacts. Those ideas can strengthen release provenance, but they do not remove the need to define a commit-signing policy.
Contract details to verify
Write the gate as a contract before implementing it. The following details are the minimum to settle with the repository owner.
Identity and tool support. Name the agent or workload identity that may sign, list the allowed key types, and decide how identity changes are approved. GitHub notes that SSH signature verification requires Git 2.34 or later and S/MIME verification requires Git 2.19 or later. Pin or check the runner’s Git version so an upgrade does not silently change behavior.
Commit coverage. Define the base revision at the moment the check starts and inspect every object in base..head. If the agent rebases while the check is running, discard the result and start over. Store the final head revision in the status record so a result cannot be reused for a different diff. Include merge commits unless your repository’s policy explicitly says otherwise; silently omitting them creates a coverage gap.
Decision fields. A hosting API response can be normalized into fields like these. The values are redacted placeholders, not credentials or live identifiers.
{
"event": "coding_agent_commit_gate",
"run_id": "[REDACTED]",
"repository": "[REDACTED]",
"base_sha": "[REDACTED]",
"head_sha": "[REDACTED]",
"commit_count": 3,
"verified_count": 3,
"failed_count": 0,
"signature_status": "verified",
"reason": "valid",
"signer_identity_class": "approved",
"verified_at": "[REDACTED]",
"decision": "allow"
}
Keep the raw signature and payload fields out of ordinary logs. The REST reference documents them so an auditor can understand the response shape, but an operational ledger usually needs only a redacted commit ID, the boolean result, the reason, and the timestamp. Restrict access to deeper diagnostics and set a retention period.
Branch enforcement. Enable the protected-branch setting that requires signed commits, add an always-run wrapper as the required status check, and select the expected application or integration as its trusted source where the host supports that restriction. The wrapper must inspect the signature job and return failure when that job is skipped or neutral; a successful, skipped, or neutral result can otherwise satisfy a generic required-check rule. Require the wrapper to execute on every applicable pull request. Prevent the agent or other untrusted write-capable actors from publishing, replacing, or dismissing this check, and decide whether administrators and bypass roles are subject to the rule. Use a unique check name across workflows; GitHub warns that duplicate job names can make status results ambiguous and block merges. Test the rule with a disposable branch before applying it to the default branch.
Merge semantics. GitHub’s documentation says that rebase-and-merge adds head-branch commits to the base branch without signature verification because the hosting service cannot sign on the contributor’s behalf. It also says that the test merge used to evaluate a pull request includes the commits introduced by the head branch, so unsigned commits can block a squash merge even when the final squash commit would be signed by the service. Select a merge method deliberately, and if you create a merge or squash commit locally, sign that resulting object before pushing it.
Historical verification. GitHub stores a verification record and timestamp when it verifies a commit. The documentation says that this record can remain verified after a signing key is later rotated, revoked, or expired, and that the record is reused across the repository network. Decide whether your organization trusts that historical record or requires a currently approved identity for a new merge; do not silently equate the two.
Artifact boundary. If the agent also builds a package or container, consider an artifact-attestation workflow separately. Sigstore’s model binds an artifact to an identity and records signing information in a public log; that is useful evidence about what was built. It should complement, rather than blur, the commit gate’s narrower question about the Git object entering the protected branch.
Failure modes
- Unsigned commit. The API reason is
unsigned, or the local verifier finds no signature. Fail closed, identify every affected commit, and have the operator re-sign the range. Signing only the tip leaves earlier objects untrusted. - Unknown key or identity mismatch.
unknown_key,unverified_email, orbad_emailmeans the signature cannot be tied to the expected account. Verify the public key and committer identity through the code host; do not copy signing material into the agent workspace. - Cryptographic failure.
invalidormalformed_signatureis not a transient test failure. Preserve the revision, quarantine the pull request, and investigate the signing toolchain before retrying. - Expired or revoked key. A hosting service may retain a historical verified record after a key is later revoked or expires. Apply the organization’s stated policy and make the distinction between historical evidence and current approval visible in the gate result.
- Unsupported local verifier. An old Git version may not understand the signature method you selected. Check the runner version and use the hosting-service status as an additional signal where appropriate.
- Partially verified author. With vigilant mode, GitHub can label a commit “Partially verified” when the author and committer have different vigilant-mode conditions. Decide whether that status is allowed for agent commits; a strict provenance policy should treat it as a review queue rather than an automatic pass.
- Verification service outage. The API reason
gpgverify_unavailableorgpgverify_errorindicates that verification could not complete. Fail closed for protected branches, or quarantine the change with an explicit, logged operator override. - Rewritten history after approval. An amend, rebase, cherry-pick, or conflict fix changes the commit objects. Invalidate the old status and require a new signature check and diff review.
- Merge-method surprise. A rebase-and-merge path may not verify the resulting commits as an author-signed object. Exercise each enabled merge method in a test repository and document the accepted behavior.
- Evidence leakage. Raw signatures, signed payloads, signing material, and unredacted identity data can expose more than a reviewer needs. Keep the normal log to the sanitized fields above and place deeper diagnostics behind restricted access.
- Duplicate status names. If two workflows publish the same job name, a branch rule can receive ambiguous results. Give the signature gate a unique, stable name and test the required-check selection.
- Stale success. A status from an earlier head revision must never authorize a later one. Bind the result to the exact base and head revisions and reject it when either changes.
- Untrusted or skipped status. A write-capable actor may forge the expected check, or a conditional workflow may report skipped or neutral while no signature check ran. Bind the required check to its trusted producer, prevent agent-controlled status writes, and require an executed, successful result.
- Range lookup failure. A missing base ref or shallow checkout can make the commit range unavailable. Resolve the range before entering the loop, fail on a nonzero
rev-listresult, and fetch or abort explicitly; an empty iteration is not proof that there were no commits.
FAQ
Does a verified signature mean the patch is safe? No. It establishes cryptographic provenance for the commit object. Tests, review, dependency analysis, and permission boundaries still determine whether the change is safe to merge.
Should every agent share one signing key? Usually not. Give each execution boundary an accountable identity or a narrowly governed bot identity. Shared keys make attribution and rotation harder. Never place signing material in a prompt, repository file, or routine log.
Can an agent use SSH instead of GPG? GitHub supports SSH, GPG, and S/MIME signatures. SSH can be simpler for individuals, while GPG offers expiry and revocation features; S/MIME is often an organizational choice. Confirm tool-version support and the code host’s verification behavior before choosing.
Why store verified_at if the commit is already marked verified? The REST API exposes that timestamp as part of the verification object. It lets an auditor distinguish when the host recorded the verified state from when the agent run happened, without retaining sensitive signature material.
What should happen when a signing key is revoked? Apply the organization’s stated policy. Persistent historical records may stay verified, but a new merge can still require an identity that is currently approved. Make that distinction visible in the gate result.
Does an artifact transparency log replace this check? No. Artifact signing helps consumers verify what was built and where it came from. A commit gate controls which Git objects may enter a branch. Use both when the threat model requires both.
Why check the whole range instead of only the newest commit? Each commit is a distinct object, and a pull request can contain an unsigned earlier commit even when its tip is signed. Range coverage closes that gap and makes the result reproducible.
Reader next step
Start with a disposable repository and one agent branch. Write down the approved signer, accepted statuses, Git version floor, commit range, bypass rule, trusted status producer, skip/neutral policy, and retention period. Add the local range check, emit only the sanitized fields, and intentionally test unsigned, unknown-key, verification-outage, rewritten-history, missing-range, forged-status, and skipped-job cases. Then enable the protected-branch requirement and required status check, and verify every merge method your repository permits.
Once the gate is stable, connect its result to the rest of your change record. Pair it with CI repair loops for coding agents and the security scan gate guide so reviewers can see provenance, intent, and security evidence together. The next successful pull request should leave a compact record: the reviewed head revision, the number of commits checked, the verification decision, the reason, and the time it was recorded.