Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
633bf1a
Make the Signed-Commit Verification Tech-Agnostic
ptr727 Aug 14, 2026
8eb9050
Fix Review Findings: ASCII Punctuation and STANDUP.md Consistency
ptr727 Aug 14, 2026
1d705d4
Fix Broken References, Exit-Status Masking, and a Third Stale Probe
ptr727 Aug 14, 2026
d3fbb1c
Check Both Committer and Author Identity in the Scratch Probe
ptr727 Aug 14, 2026
9b2e941
Quote $d and Use try/finally in the PowerShell Probe
ptr727 Aug 14, 2026
0553d34
Make the Scratch-Probe mktemp Call Portable to macOS
ptr727 Aug 14, 2026
21aec10
Test the Default Signing Config, Not a Forced Signature
ptr727 Aug 14, 2026
c3127fe
Merge develop, Fix a dead-path Drift That Merge Exposed
ptr727 Aug 14, 2026
af0714c
Remove Foreign Content Swept In by git add -A
ptr727 Aug 14, 2026
d50b305
Fix a Real CI Failure and Three More Review Findings
ptr727 Aug 14, 2026
bf4108d
Make the Probe Actually Gate on sig=G, Not Just Print It
ptr727 Aug 14, 2026
3686e44
Extend the Gate to Identity, Not Just Signature
ptr727 Aug 14, 2026
8f25083
Accept sig=U and Verify the Noreply Pattern, Not Just a Config Match
ptr727 Aug 14, 2026
f1fa79a
Collapse the POSIX Probe to One Line, Not Just Remove Backslashes
ptr727 Aug 14, 2026
bd19e8a
Merge develop (PR #710 landed)
ptr727 Aug 14, 2026
4f03107
Stop Masking a git log Failure Behind Tee-Object in PowerShell
ptr727 Aug 14, 2026
63336a3
Correct My Own sig=U Claim: SSH Hits It Too
ptr727 Aug 14, 2026
840388d
Fix PowerShell parse error in the signing/identity verify block
ptr727 Aug 14, 2026
c7fb717
Collapse the Last Backslash-Continued Snippet in host-setup.md
ptr727 Aug 14, 2026
1d7cc89
Read --global user.email in the allowed_signers Setup Line
ptr727 Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 69 additions & 8 deletions .agents/skills/git-commit-conventions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,68 @@ scope-widened commit, a rewritten shared history, a destructive reset).

- **Every commit must be cryptographically signed (SSH or GPG).** Branch protection enforces this
on every fleet branch, and an unsigned commit is rejected on push. Signing depends on
environment configuration: `git config commit.gpgsign true`, a configured `user.signingkey`, and
a working signing agent (`ssh-agent` for SSH, `gpg-agent` for GPG). **If signing is not
configured, do not commit.** Surface the missing config to the developer and stop at `git add`.
Verify before the first agent-authored commit, don't assume a prior session left it set:
`git config --get commit.gpgsign && ssh-add -L`, or the GPG equivalent.
environment configuration (`commit.gpgsign`, `user.signingkey`, `gpg.format`), but none of those
values prove signing actually works: `gpg.format=ssh` can sign straight from a key file with no
`ssh-agent` running at all (the common case on Git for Windows), just as GPG can sign
agent-backed or straight from a keyring. **Probing agent liveness (`ssh-add -L`, a `gpg-agent`
check) is not a valid test and must not be used.** It tests one specific delivery path, not
whether a commit actually ends up signed, and a host that signs straight from a key file fails
that probe while signing correctly.
- **Verify with a real scratch commit, read back with git's own verdict, not a text grep.** This
single probe is tech-agnostic (SSH agent-backed, SSH key-file, GPG agent-backed, and GPG keyring
all exercise the same code path) and doubles as the identity check below. Run it once before the
first agent-authored commit of a session. Don't assume a prior session left config correct. The
commit below is plain, deliberately no `-S`: forcing it would still succeed on a host where
`commit.gpgsign` is unset or false, which is the exact default-config gap this probe exists to
catch, since every real commit an agent makes is plain too:

This file is CRLF (the repo's Markdown default), and a `\` line continuation stops working
the moment a stray `\r` lands after it, so the probe is one physical line, not backslash-joined
ones:

```sh
d=$(mktemp -d "${TMPDIR:-/tmp}/sign-check.XXXXXX") && ( trap 'rm -rf "$d"' 0; email=$(git config --global --get user.email) && git init -q "$d" && git -C "$d" commit --allow-empty -q -m check && out=$(git -C "$d" log -1 --format='sig=%G? author=%an <%ae> committer=%cn <%ce>') && echo "$out" && ae=$(git -C "$d" log -1 --format='%ae') && ce=$(git -C "$d" log -1 --format='%ce') && case "$out" in sig=G\ *|sig=U\ *) true ;; *) false ;; esac && case "$email" in *@users.noreply.github.com) true ;; *) false ;; esac && [ "$ae" = "$email" ] && [ "$ce" = "$email" ] )
```

PowerShell equivalent:

```powershell
$d = Join-Path $env:TEMP ([guid]::NewGuid())
try {
$email = git config --global --get user.email
git init -q "$d" `
&& git -C "$d" commit --allow-empty -q -m check
$out = git -C "$d" log -1 --format='sig=%G? author=%an <%ae> committer=%cn <%ce>'
$out
$ae = git -C "$d" log -1 --format='%ae'
$ce = git -C "$d" log -1 --format='%ce'
if ($out -notmatch '^sig=[GU] ' -or $email -notmatch '@users\.noreply\.github\.com$' `
-or $ae -ne $email -or $ce -ne $email) {
throw "signing/identity check failed: $out"
}
} finally {
if (Test-Path "$d") { Remove-Item -Recurse -Force "$d" }
}
```

`sig` must read `G` (good signature) or `U` (good signature, unrecognized signer). For GPG, `U`
is a valid signature from a key whose trust level is merely undefined, common right after
generating a new key. For SSH, it's a valid signature from a key not found in the local
`allowed_signers` file, which doesn't affect whether GitHub itself verifies the commit, only
local `git verify-commit` output. `sig` is git's own verdict char. Don't grep localized
"Good" text, since that varies by git version and locale. Anything else, or the commit failing
outright, means **do not commit**: surface the actual error to the developer and stop at
`git add`. Nothing else is contrary evidence: not an unreachable agent, not a config value, not a
signature type you can't otherwise explain in past history (see below).
- **A mix of SSH- and GPG-signed commits in history is structural, not a host to track down.**
`git log --pretty='%G? %GK'` shows two distinct shapes, not two health states: a commit committed
by the PR's own author carries that host's own signature type, while a commit committed by
`GitHub <noreply@github.com>` is a squash-merge: GitHub creates and signs that commit itself,
server-side, with GitHub's own GPG key, regardless of what the PR author signed with locally.
Every commit on `develop`/`main` past its first squash-merge shows `GitHub` as committer and a
GPG signature. That's expected on every fleet repo, on every host, and is not evidence anything
is misconfigured. Check `commit.committer.name` before treating a differing signature type as a
clue worth chasing.
- **Signing must be live before the *first* commit, not retrofitted.** Turning on a
require-signed-commits rule against a branch that already carries unsigned commits forces a
rewrite of that entire history to re-sign it, changing every commit SHA and making whoever does
Expand All @@ -65,9 +122,13 @@ scope-widened commit, a rewritten shared history, a destructive reset).
**Commit under the committing account's own GitHub `noreply` identity, never a private, personal,
or invented address.** `author` and `committer` on every agent-authored commit are the GitHub
`noreply` address of the account whose key signs the commit, in `username@users.noreply.github.com`
or `ID+username@users.noreply.github.com` form. **Verify it, do not set it**: check
`git config --get user.email` matches that address before committing, rather than writing a
repo-local override. The identity is host configuration set globally once, so a repo-local
or `ID+username@users.noreply.github.com` form. **Verify it, do not set it**: the scratch commit
from the signing check above already proves this end-to-end. Read its `author=`/`committer=`
output rather than trusting `git config --get user.email` alone, since a global config value
doesn't prove what actually lands on a commit object, and read both rather than the author alone
since a rebase, amend, or cherry-pick can rewrite the committer while leaving the author
untouched. Match both against that address before committing, rather than
writing a repo-local override. The identity is host configuration set globally once, so a repo-local
`user.email` is redundant where the global is right and a silently-shadowing wrong identity where
it is not. A mismatch is a host fault to surface to the maintainer, not to patch per repo, because
a local override hides a broken host that then commits wrong in every other repo on that machine.
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/fleet-skills/.source-digest
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f01d13d03542af68
f9e5473792ab198e
77 changes: 69 additions & 8 deletions .claude-plugin/fleet-skills/skills/git-commit-conventions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,68 @@ scope-widened commit, a rewritten shared history, a destructive reset).

- **Every commit must be cryptographically signed (SSH or GPG).** Branch protection enforces this
on every fleet branch, and an unsigned commit is rejected on push. Signing depends on
environment configuration: `git config commit.gpgsign true`, a configured `user.signingkey`, and
a working signing agent (`ssh-agent` for SSH, `gpg-agent` for GPG). **If signing is not
configured, do not commit.** Surface the missing config to the developer and stop at `git add`.
Verify before the first agent-authored commit, don't assume a prior session left it set:
`git config --get commit.gpgsign && ssh-add -L`, or the GPG equivalent.
environment configuration (`commit.gpgsign`, `user.signingkey`, `gpg.format`), but none of those
values prove signing actually works: `gpg.format=ssh` can sign straight from a key file with no
`ssh-agent` running at all (the common case on Git for Windows), just as GPG can sign
agent-backed or straight from a keyring. **Probing agent liveness (`ssh-add -L`, a `gpg-agent`
check) is not a valid test and must not be used.** It tests one specific delivery path, not
whether a commit actually ends up signed, and a host that signs straight from a key file fails
that probe while signing correctly.
- **Verify with a real scratch commit, read back with git's own verdict, not a text grep.** This
single probe is tech-agnostic (SSH agent-backed, SSH key-file, GPG agent-backed, and GPG keyring
all exercise the same code path) and doubles as the identity check below. Run it once before the
first agent-authored commit of a session. Don't assume a prior session left config correct. The
commit below is plain, deliberately no `-S`: forcing it would still succeed on a host where
`commit.gpgsign` is unset or false, which is the exact default-config gap this probe exists to
catch, since every real commit an agent makes is plain too:

This file is CRLF (the repo's Markdown default), and a `\` line continuation stops working
the moment a stray `\r` lands after it, so the probe is one physical line, not backslash-joined
ones:

```sh
d=$(mktemp -d "${TMPDIR:-/tmp}/sign-check.XXXXXX") && ( trap 'rm -rf "$d"' 0; email=$(git config --global --get user.email) && git init -q "$d" && git -C "$d" commit --allow-empty -q -m check && out=$(git -C "$d" log -1 --format='sig=%G? author=%an <%ae> committer=%cn <%ce>') && echo "$out" && ae=$(git -C "$d" log -1 --format='%ae') && ce=$(git -C "$d" log -1 --format='%ce') && case "$out" in sig=G\ *|sig=U\ *) true ;; *) false ;; esac && case "$email" in *@users.noreply.github.com) true ;; *) false ;; esac && [ "$ae" = "$email" ] && [ "$ce" = "$email" ] )
```

PowerShell equivalent:

```powershell
$d = Join-Path $env:TEMP ([guid]::NewGuid())
try {
$email = git config --global --get user.email
git init -q "$d" `
&& git -C "$d" commit --allow-empty -q -m check
$out = git -C "$d" log -1 --format='sig=%G? author=%an <%ae> committer=%cn <%ce>'
$out
$ae = git -C "$d" log -1 --format='%ae'
$ce = git -C "$d" log -1 --format='%ce'
if ($out -notmatch '^sig=[GU] ' -or $email -notmatch '@users\.noreply\.github\.com$' `
-or $ae -ne $email -or $ce -ne $email) {
throw "signing/identity check failed: $out"
}
} finally {
if (Test-Path "$d") { Remove-Item -Recurse -Force "$d" }
}
```

`sig` must read `G` (good signature) or `U` (good signature, unrecognized signer). For GPG, `U`
is a valid signature from a key whose trust level is merely undefined, common right after
generating a new key. For SSH, it's a valid signature from a key not found in the local
`allowed_signers` file, which doesn't affect whether GitHub itself verifies the commit, only
local `git verify-commit` output. `sig` is git's own verdict char. Don't grep localized
"Good" text, since that varies by git version and locale. Anything else, or the commit failing
outright, means **do not commit**: surface the actual error to the developer and stop at
`git add`. Nothing else is contrary evidence: not an unreachable agent, not a config value, not a
signature type you can't otherwise explain in past history (see below).
- **A mix of SSH- and GPG-signed commits in history is structural, not a host to track down.**
`git log --pretty='%G? %GK'` shows two distinct shapes, not two health states: a commit committed
by the PR's own author carries that host's own signature type, while a commit committed by
`GitHub <noreply@github.com>` is a squash-merge: GitHub creates and signs that commit itself,
server-side, with GitHub's own GPG key, regardless of what the PR author signed with locally.
Every commit on `develop`/`main` past its first squash-merge shows `GitHub` as committer and a
GPG signature. That's expected on every fleet repo, on every host, and is not evidence anything
is misconfigured. Check `commit.committer.name` before treating a differing signature type as a
clue worth chasing.
- **Signing must be live before the *first* commit, not retrofitted.** Turning on a
require-signed-commits rule against a branch that already carries unsigned commits forces a
rewrite of that entire history to re-sign it, changing every commit SHA and making whoever does
Expand All @@ -65,9 +122,13 @@ scope-widened commit, a rewritten shared history, a destructive reset).
**Commit under the committing account's own GitHub `noreply` identity, never a private, personal,
or invented address.** `author` and `committer` on every agent-authored commit are the GitHub
`noreply` address of the account whose key signs the commit, in `username@users.noreply.github.com`
or `ID+username@users.noreply.github.com` form. **Verify it, do not set it**: check
`git config --get user.email` matches that address before committing, rather than writing a
repo-local override. The identity is host configuration set globally once, so a repo-local
or `ID+username@users.noreply.github.com` form. **Verify it, do not set it**: the scratch commit
from the signing check above already proves this end-to-end. Read its `author=`/`committer=`
output rather than trusting `git config --get user.email` alone, since a global config value
doesn't prove what actually lands on a commit object, and read both rather than the author alone
since a rebase, amend, or cherry-pick can rewrite the committer while leaving the author
untouched. Match both against that address before committing, rather than
writing a repo-local override. The identity is host configuration set globally once, so a repo-local
`user.email` is redundant where the global is right and a silently-shadowing wrong identity where
it is not. A mismatch is a host fault to surface to the maintainer, not to patch per repo, because
a local override hides a broken host that then commits wrong in every other repo on that machine.
Expand Down
13 changes: 10 additions & 3 deletions STANDUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@ git config --global --get commit.gpgsign # true
git config --global --get user.signingkey # set
git config --global --get gpg.format # ssh for an SSH key; unset or openpgp for GPG

# the agent holding the key, selected by the format above
if [ "$(git config --global --get gpg.format)" = ssh ]; then ssh-add -L; else gpg --list-secret-keys; fi
# prove signing works with a live scratch commit in a disposable scratch repo, not this
# repo (its own git init is still section 0B, below), and not an agent-liveness probe
# (ssh-add -L, gpg --list-secret-keys): a host that signs straight from a key file with no
# agent running passes cleanly and fails that probe. See
# .agents/skills/git-commit-conventions/SKILL.md "Signing, verified not configured" for why.
# One physical line, not backslash-joined: this file is CRLF (the repo's Markdown default),
# and a `\` continuation stops working the moment a stray `\r` lands after it.
d=$(mktemp -d "${TMPDIR:-/tmp}/sign-check.XXXXXX") && ( trap 'rm -rf "$d"' 0; email=$(git config --global --get user.email) && git init -q "$d" && git -C "$d" commit --allow-empty -q -m check && out=$(git -C "$d" log -1 --format='sig=%G? author=%an <%ae> committer=%cn <%ce>') && echo "$out" && ae=$(git -C "$d" log -1 --format='%ae') && ce=$(git -C "$d" log -1 --format='%ce') && case "$out" in sig=G\ *|sig=U\ *) true ;; *) false ;; esac && case "$email" in *@users.noreply.github.com) true ;; *) false ;; esac && [ "$ae" = "$email" ] && [ "$ce" = "$email" ] )
```

`--global` rather than the effective config, because the effective value depends on where the command runs: inside any existing repository a repo-local override wins, so a bare `git config --get user.email` there reports that repository's identity and hides the host setting this step exists to check. The two scopes together are what make the result sound, since this block proves the host is right and the block below proves nothing shadows it.
Expand All @@ -53,7 +59,7 @@ python3 scripts/host_gate.py --repo <path-to-target-checkout> # after section

A finding at either point is a **host** misconfiguration to fix on the machine or surface to the maintainer, never something to patch per repo, and [`docs/host-setup.md`][host-setup] is the contract it checks.

The agent check branches rather than listing both forms, because they are alternatives and running the wrong one fails on a correctly configured host: an SSH host need not have `gpg` installed at all. Signing is **SSH or GPG**, so judge the format and its agent together rather than requiring `ssh`: what matters is that the configured format has a matching agent holding the key, which is the check [GOVERNANCE.md "Git and Commit Rules"][governance-git-and-commit-rules] prescribes. Any of these wrong or absent is a **host** misconfiguration to surface to the maintainer ([`docs/host-setup.md`][host-setup] is the setup procedure), not something to patch per repo. Patching it locally hides a broken host that then produces wrong identities in every other repo on that machine.
The scratch commit exercises the whole signing pipeline rather than one delivery path, since `ssh-add -L` or `gpg --list-secret-keys` only prove an agent holds a key and say nothing about a host that signs straight from a key file with no agent running at all, a live and correctly configured case [git-commit-conventions][git-commit-conventions] documents in "Signing, verified not configured", the same rules [GOVERNANCE.md "Git and Commit Rules"][governance-git-and-commit-rules] points to. Signing is **SSH or GPG**, so this judges the configured format by its actual result (`sig=G`, or `sig=U` for a cryptographically good signature from an unrecognized signer, either a GPG key whose trust is merely undefined or an SSH key missing from the local `allowed_signers` file), never by which delivery path produced it. A missing `--global` value, `sig` not reading `G` or `U`, or either printed email not matching the noreply address is a **host** misconfiguration to surface to the maintainer ([`docs/host-setup.md`][host-setup] is the setup procedure), not something to patch per repo. Patching it locally hides a broken host that then produces wrong identities in every other repo on that machine.

After `git init` and before the first commit, confirm the repo added no override of its own. This one needs a repository, since `--local` fails outside one. Read it here and run it in section 0B, which places it between the init and the first commit, so nothing here is a prompt to init early:

Expand Down Expand Up @@ -222,6 +228,7 @@ The same [`AUDIT.md`][audit] run is the on-demand audit for any known repo, and
[content-import]: ./docs/content-import.md
[files]: ./spec/files.json
[fleet-map]: ./docs/fleet-map.md
[git-commit-conventions]: ./.agents/skills/git-commit-conventions/SKILL.md
[governance]: ./GOVERNANCE.md
[governance-git-and-commit-rules]: ./GOVERNANCE.md#git-and-commit-rules
[governance-repository-boundaries-and-write-safety]: ./GOVERNANCE.md#repository-boundaries-and-write-safety
Expand Down
Loading