skip to content

How do you check from the command line whether Git commits and tags are signed?

level: middleimportance: should knowfreq 32%

answer

  1. One flag on log prints the verdict
  2. A plumbing command gives an exit status
  3. A pretty placeholder gives a status column
  4. Missing key is not the same as unsigned
  5. Merge and pull have a verify option

basics

~20 s

In Git, use git log --show-signature for a range, git verify-commit or git verify-tag for a single object and a scriptable exit status, and the %G? pretty format placeholder for a compact per-commit status column.

solid answer

~50 s

For reading, `git log --show-signature` prints the verification result above each commit, and `git show --show-signature` does the same for one object. For scripting, `git verify-commit <rev>` and `git verify-tag <tag>` (also spelled `git tag -v`) exit non-zero when a signature is missing or bad, which is what a check should branch on rather than parsing text. For a compact survey of a range, `git log --pretty="%h %G? %GS %s"` prints a status code and the signer per commit: `G` good, `B` bad, `U` good but untrusted, `X` expired signature, `Y` expired key, `R` revoked key, `E` cannot check, `N` unsigned. Merging or pulling can also demand it: `git merge --verify-signatures` refuses a merge whose tip commit does not carry a valid signature. All of this needs the signer's public key locally — a keyring for OpenPGP, or the allowed-signers file for SSH — otherwise every commit reports as uncheckable.

code

bash · 10 lines
bash
git log --show-signature -1

git log --pretty="%h %G? %GS %s" origin/main..HEAD

# Scriptable: branch on the exit status, not the text
for c in $(git rev-list origin/main..HEAD); do
    git verify-commit "$c" || { echo "unsigned or bad: $c"; exit 1; }
done

git verify-tag v1.4.0

go deeper

for a junior

Know git log --show-signature for looking at a commit and git tag -v for a tag, and that an unsigned commit simply prints no signature block.

for a middle

Explain the scriptable path — git verify-commit/git verify-tag and their exit status — and read the %G? codes rather than saying only signed or unsigned.

for a senior

Show that verification depends on locally available key material, so a checker on a bare machine reports uncheckable and can pass vacuously; design the gate to fail closed.

for a principal

Decide where verification actually runs and what it must prove — tip-only versus every commit, which principals are trusted, and how that trust list is distributed and revoked.

## Three levels of checking **Human reading.** `git log --show-signature` inserts the backend's verification output above each commit's header. `git show --show-signature <rev>` does the same for a single commit, and `git tag -v <tag>` prints the verification of a tag object followed by its message. This is what you use when investigating one artifact by hand. **Scripting.** `git verify-commit <rev>` and `git verify-tag <tag>` are the plumbing-flavoured entry points: quiet by default, verbose with `-v`, and — critically — their **exit status** is the answer. Zero means a good signature; non-zero means missing, bad, or unverifiable. A gate should test the exit status, never grep the human-readable output, which is produced by the signing backend and is not a stable interface. **Bulk survey.** The pretty-format placeholders let you render signature state as a column: `%G?` is a one-character status, `%GS` the signer's name as recorded in the signature, `%GK` the key used. `git log --pretty="%h %G? %GS %s" origin/main..HEAD` tells you in one screen which of your branch's commits are signed and by whom. ## Reading the status codes `%G?` distinguishes cases that a naive "is it signed?" question collapses: - `G` — good signature. - `B` — bad signature: the object does not match what was signed. This is the alarming one. - `U` — good signature from a key whose trust is unknown or marginal. Cryptographically fine; you simply have not asserted that this key belongs to anyone. - `X` — good signature that has since expired; `Y` — good signature made by a key that has expired; `R` — good signature made by a revoked key. - `E` — the signature cannot be checked, typically because the key is missing locally. - `N` — no signature at all. The practical lesson is that `E` and `N` are very different findings and both are different from `B`. A checker that treats "not `G`" as "tampered" will cry wolf constantly; one that treats `E` as acceptable verifies nothing. ## Verification needs a trust input Verification is not self-contained: it compares a signature against a public key you must already have, plus an assertion about who that key belongs to. - With OpenPGP, the key must be in the local keyring, and its trust level determines whether you get `G` or `U`. - With SSH signing, the key must appear in the file named by `gpg.ssh.allowedSignersFile`, whose lines pair a principal with a public key and the `git` namespace. Optionally `gpg.ssh.revocationFile` lists keys that must be rejected. On a machine with neither, every commit reports `E`/"cannot check", and a script that only asks "did the command print something?" will happily pass. This is the single most common way local signature checking is set up wrong. ## Verifying at the point of integration Two porcelain commands fold verification into an operation rather than a report: `git merge --verify-signatures <ref>` aborts the merge unless the tip commit being merged carries a valid signature, and `git pull --verify-signatures` does the same for the fetched tip. These check the tip only, not every commit in the range — a deliberate design, since the tip's signature covers the tree and, through the parent hashes, the ancestry, but says nothing about who authored the intermediate commits. For tags, the equivalent discipline is verifying the tag before building from it: `git verify-tag v1.4.0` in the release script, branching on the exit status. ## A worked check A reasonable local gate over a feature branch is: list the commits in `origin/main..HEAD`, run `git verify-commit` on each, and fail on the first non-zero exit. It is a few lines of shell, it is deterministic, and it fails loudly if the key material is missing rather than passing vacuously — provided you have satisfied yourself that a known-good commit really does verify on that machine first. ## Interview framing Name the reading command, the scriptable command with its exit status, and the `%G?` placeholder; then show judgement by distinguishing `N`, `E`, `U` and `B`, and by pointing out that verification silently degrades to "cannot check" when the trust input is missing.

  • What is the difference between the %G? codes N, E and B?
    `N` means the object carries no signature at all. `E` means there is a signature but it could not be checked, almost always because the signer's public key is not available locally. `B` means the signature is present, checkable, and does not match the object — the only one of the three that indicates tampering rather than a gap in setup.
  • Why does git merge --verify-signatures check only the tip commit?
    Because the tip's signature covers that commit object, which names its tree and its parents by hash, so accepting it accepts that exact content and ancestry. It does not assert anything about who signed the intermediate commits, so a policy that cares about every author still has to walk the range and verify each commit.
  • Why should a gate branch on verify-commit's exit status rather than its output?
    The printed text comes from the signing backend and varies by backend, version and locale, so parsing it is brittle. The exit status is Git's own stable contract: zero for a good signature, non-zero otherwise. A grep-based check also passes vacuously when the command prints nothing at all.

saying these in an interview costs you the question

  • Greps human-readable output instead of the exit status
  • Treats cannot-check as equivalent to verified
  • Thinks verification works without the signer's public key
  • Assumes merge --verify-signatures checks every commit
  • Confuses an untrusted-key result with a bad signature

context