# sgit-ai 0.26.7: verify the signer's own SSH or OpenPGP signature on commits and tags, cli.sgit.ai

> Dev build of 28 October 2026, from the CLI's dev at 0d486aaf. A commit or tag can carry its signer's own SSH or OpenPGP signature (user_signature, Git's gpgsig) beside the per-clone signature every commit already has, and this build checks them, with Git's verdicts and Git's trust files. sgit check verify, history log --show-signature, history show --show-signature and vault tag show report them. Nothing in the CLI can make one yet (sgit commit -S was written and parked until the web UI confirms the signing-bytes rule). Reviewed by cli.build. 5,235 unit and security tests pass. Old vaults and old clients are untouched; 0.26.0, 0.26.3 and 0.26.6 read, verify, fsck and push over commits that carry the new member. Maturity good. BR-1 is still open.

*Source: <https://cli.sgit.ai/builds/0-26-7.html> · generated from the same content as the page, so the two cannot drift.*

---
# sgit-ai 0.26.7

```text
┌─ sgit-ai 0.26.7 ── dev build ── 28 Oct 2026, 12:22 UTC ─────────────────────────
│ tag v0.26.7 = 0d486aaf (dev) · PyPI package == tag · a plain install gets this
│ new     user signatures verified: check verify · history log/show --show-signature · tag show
│ not     nothing makes them: commit -S / sgit.gpgSign are parked until the web UI confirms
│ tests   5,235 unit+security ✓ · 18/18 end-to-end ✓ · 0.26.0/.3/.6 read signed commits ✓
│ open    BR-1 (high, every build) · R3-1 · W-1 (a question for the web UI)
│ maturity  #######---  good: use it; it changes what you can see, not what you can write
└──────────────────────────────────────────────────────────────────────────────────
```

```text
$ pip install sgit-ai==0.26.7        # or: sgit update, which takes the newest build
$ sgit version
sgit-ai v0.26.7
```

## What is new {#new}

This build is the first half of Git-parity item 2.2, signing with your own key, from [design 02](../research/git-parity-roadmap.md). It is the half that **reads**.

### A second signature on a commit or tag

- **Every commit already has a per-clone signature:** the clone's own key signs the commit, and `sgit check verify` checks it.
- **A commit or tag can now also carry `user_signature`:** the signer's own SSH or OpenPGP signature, Git's `gpgsig`.
  - It covers the same bytes as the clone signature, and neither covers the other.
  - Either can be checked, or removed, alone.
  - **An unsigned commit is stored byte for byte as before:** the member is left out when it is empty.

### Trust is yours, as in Git

- **The settings come from your global Git config,** never from the vault or a repository's config, so nothing next to a vault can change what is trusted:
  - `gpg.ssh.allowedSignersFile`, `gpg.ssh.revocationFile`, `gpg.minTrustLevel`, `gpg.format`, `user.signingKey`;
  - your gpg keyring for OpenPGP.
- **Who checks what:**
  - SSH signatures are checked in the process: ed25519, ECDSA, RSA-SHA2 and FIDO `sk-` keys;
  - certificates, `cert-authority` lines and revocation lists go to `ssh-keygen`;
  - OpenPGP goes to `gpg`.
- **The verdicts are Git's letters** (G U B X Y R E N), checked by the CLI team against `git log --format=%G?`.

### Where you see it

```text
$ sgit check verify
Checked 5 commit(s): 5 verified, 0 bad, 0 unsigned, 0 without a known key, 0 missing
User signatures: 1 good, 1 untrusted, 0 bad, 0 expired, 0 revoked, 0 cannot check, 3 none
$ sgit check verify --require-user-signature --json     # every commit must be good
$ sgit history log --show-signature
$ sgit history show HEAD --show-signature
$ sgit vault tag show v1                                  # names the tag's signer
```

- **`check verify` fails (exit 1) on a bad user signature.** An untrusted one does not fail, as `git verify-commit` does not.
- **`--require-user-signature`** fails unless every commit is good.
- **`--json`** is new on `check verify`: counts, `user_counts`, `user_by_status` and the first failure.
- **The wording is Git's:** `Good "git" signature for alice@example.com with ED25519 key SHA256:…`, `No principal matched.`

### What is not here

- **Nothing creates a user signature.** `sgit commit -S`, `sgit.gpgSign` and signing in `merge`, `write`, `revert` and `tag` were written (`0709aa86`) and then parked (`96120aa6`) until the vault web UI confirms the rule for the signed bytes (W-1 below).
- **So today `--show-signature` shows signatures that only a library caller can produce.** The CLI team's notes say this plainly; this page says it so that nobody looks for the flag.

## Compatibility {#compat}

- **Vaults and the host:** no new gate, no new minimum client, no change for a vault that never carries a user signature.
- **Old vaults:** unchanged. Vaults made by 0.20.0, 0.22.0, 0.24.0 and 0.26.0 (normal and pack mode) open in 0.26.8, fsck clean, and their old clients pull what the new build pushed.
- **Old clients and a signed commit:** checked by creating a vault with 0.26.8 that holds an unsigned, an SSH-signed and an OpenPGP-signed commit, then:

| Client | clone | fsck | clone signatures | push a commit | 0.26.8 pulls it |
|---|---|---|---|---|---|
| 0.20.0 | ✓ | ok | 4 of 4 verified | ✓ | ✓, 5 of 5 verified |
| 0.22.0 | ✓ | ok | 4 of 4 verified | ✓ | ✓ |
| 0.24.0 | ✓ | ok | 4 of 4 verified | ✓ | ✓ |
| 0.26.0 | ✓ | ok | 4 of 4 verified | ✓ | ✓, 5 of 5 verified |
| 0.26.3 | ✓ | ok | 4 of 4 verified | ✓ | ✓ |
| 0.26.6 | ✓ | ok | 4 of 4 verified | ✓ | ✓ |

- **Why old clients are not bothered:** they load a commit into a typed object that does not know `user_signature`, so the member is dropped on load, and the bytes they check are the bytes that were signed.
- **That is also why the gate matters:** a reader that checks the clone signature over the raw JSON and removes only `signature` would call every signed commit **bad**. Python clients cannot do that by accident; the web UI is the open question (W-1).

## Review by cli.build {#review}

**Verdict: good.**
- Every claim of this build held, including the verdicts, the unchanged bytes of an unsigned commit and the compatibility with older clients.
- The build reads and checks; it never writes a signature, so it cannot damage a vault.
- One high finding is not this build's own: it is in every build tested (BR-1).

```text
┌─ checks on the PyPI package 0.26.7 ───────────────────────────────────────────────
│ ✓     package == tag v0.26.7 (only the version file differs); wheel == sdist
│ ✓     5,235 unit+security tests pass (38 skipped)
│ ✓     everything from 0.26.1–0.26.6 (G1–G2, M1–M5, L1–L2, X1–X4, C1–C5) still holds
│ ✓ S1  SSH signature (in-process key) and OpenPGP signature (gpg): checked by 0.26.8 as untrusted / good
│ ✓ S2  a signature over other bytes, and one with a flipped bit: both bad; clone signatures unaffected
│ ✓ S3  0.26.0, 0.26.3 and 0.26.6 clone, fsck, verify and push over signed commits
│ ✓ S4  vaults of 0.20.0 · 0.22.0 · 0.24.0 · 0.26.0, plain and pack: open, commit, push, old client pulls
│ ✗ P1  one push of 150 commits: fresh clone's fsck fails  ── BR-1, unchanged from 0.26.6
└──────────────────────────────────────────────────────────────────────────────────────
```

### Code read {#code}

The diff against 0.26.6 is 14 changed code files and new ones: `crypto/signing/` (19 modules), `Vault__User_Signatures`, four new schemas and eleven new safe types. Read: the runner, the config reader and resolver, both signers, the commit and tag writers, the verification walk and the CLI wiring.

- **Running programs is done in one place** (`Signing__Command_Runner`): argument lists only, never a shell; bytes on stdin; the working folder is the home folder, never the vault; the environment is a short list (agent sockets, `GNUPGHOME`, Git's own config variables) plus `LC_ALL=C`; a missing program is code 127 and a timeout code 124. Nothing from the vault reaches a command line.
- **The key and the program come from your global Git config only.** A repository's config and the vault are not read, so a cloned vault cannot choose which program runs.
- **A signing key beginning with `-`** cannot become an option: it is always the value of `-f` or `-bsu`.
- **The temporary file for a literal SSH key** is made with `O_EXCL` and mode 0600 in a fresh folder and removed afterwards.
- **A signer checks its own output** before returning it, and a failure stops before anything is stored.
- **The stored bytes:** `stored_json` drops an empty `user_signature`, so an unsigned commit and tag are byte-identical to the format before.
- **One small thing:** `sgit.gpgSign` is the switch, not Git's `commit.gpgSign`, on purpose (an existing setting must not make agents start signing after an upgrade). Say so in the release notes when signing ships.

### W-1 (a question for the web UI, before signing ships) {#w-1}

- **The rule:** the bytes covered by either signature are the commit's JSON without `signature` and without `user_signature`, keys sorted, no spaces, UTF-8.
- **Why it needs the web UI:** if it checks the per-clone signature, it must remove `user_signature` too. If it removes only `signature`, a vault with signed commits shows bad signatures in the web UI.
- **What I could not test:** the web UI. It is not in this repository, and a vault with signed commits needs a real host to load there.
- **A fixture for them:** `scripts/xsign.py new-create` builds a vault with an unsigned, an SSH-signed and an OpenPGP-signed commit; the CLI team can push one to a test host for the web UI to open.
- **Update:** the CLI team un-parked signing in 0.26.10 on that statement. Ask the web UI team for one test with a signed vault.

### Also noted

- **The CLI team's brief for this review names `check verify --user`.** The build has no such flag: user signatures are always counted when a commit carries one, and `--require-user-signature` makes them mandatory. The brief and the CHANGELOG should agree with the build.
- **The BR-1 fix** (0.26.9) and signing itself (0.26.10) came after this build; see [0.26.9](../builds/0-26-9.md) and [0.26.10](../builds/0-26-10.md). W-1: the CLI team says the web UI never verifies commit signatures and ignores members it does not know; not tested by me.
- **The clone key and the signer's key are different things.** `check verify` says "verified" for the clone signature and reports user signatures on their own line. A reader may read "verified" as "signed by a person"; the release notes for 0.27.0 should say it does not.
- **Not tested here:** `ssh-keygen` as the signer or verifier (not installed on the test machine; the in-process path was used), certificates, KRLs, FIDO keys, and a passphrase-protected key.

*Scripts: [`xsign.py`](https://github.com/SGit-AI/SGit-AI__Website__Cli/tree/dev/admin/research/builds-0-26-7-0-26-8-2026-10-28/scripts), `sig_tamper.py`, `legacy_vault.py`, `push_gap.py`. Results: `results/check_0.26.7.json`, `results/suite-v0.26.7.txt`, `results/xsign_signed_commits_old_clients.txt`, `results/sig_tamper_0.26.8.txt`, `results/legacy_vaults_0.20.0_to_0.26.0.txt`. All builds: [the builds page](../builds/index.md).*

---

*[Site index for agents](../llms.txt) · [HTML version](https://cli.sgit.ai/builds/0-26-7.html)*
