Repository navigation
scaffold-project.py: a push ssh refused the host's key for names how to check that key against GitHub's fingerprints, not the ruleset hint, and the hint on GitHub says what its re-run can still wait at (#1108) - #1130
Conversation
…to check that key against GitHub's fingerprints and keep it, then the re-run, instead of the ruleset hint, and a ruleset's refusal on GitHub says what the hint's re-run can still wait at (#1108) Follow-up to #915 (U037), from RV1101's S1 on #1101. A host key ssh refused. Where a `url.<base>.insteadOf` or `url.<base>.pushInsteadOf` rule sends a push over ssh and ssh cannot verify the host's key, ssh says `Host key verification failed.` and the push printed `RULESET_HINT` alone, whose re-run fails the same way. Measured on main at 823624d with the real OpenSSH 9.6 against a fake server: an unknown key under `BatchMode=yes` (which #1101 advises), answered no, and on a `--reuse-empty-repo` re-run, and a changed key with and without `BatchMode=yes`; each exits 2 in under 2 s with the hint alone. `_host_key_remedy` now names the cause, how to see the key's fingerprint (`ssh -T` without `BatchMode=yes`), that it must be one of the fingerprints GitHub publishes (`gh api meta --jq .ssh_key_fingerprints`), and then the re-run, in place of the hint: ssh refused before git sent anything, so no ruleset was asked. A key that CHANGED (`REMOTE HOST IDENTIFICATION HAS CHANGED`) is never told to be accepted as it comes: nothing is accepted, removed or pushed until its fingerprint is checked. The re-run is the one the login remedies name, now shared as `_then_re_run`: the push fails before git sends anything, so the repositories are left empty or holding their seed, and `--reuse-empty-repo` reuses them. The ruleset hint on GitHub. `RULESET_HINT`'s one exit is the `--reuse-empty-repo` re-run, and it did not say what every other remedy into that re-run says (`PROMPTS_STILL_ASK`). The hint is printed under `--local-remote-dir` too, where no prompt can come, and its text is pinned byte for byte beside `family.py`'s and the adoption's, so the words stay in the hint and `_refused_push` appends `RULESET_HINT_PROMPTS` after it on GitHub alone, and not where a login remedy before it has said the same. `_refused_push` takes `local` from both call sites. The two comments that named #1108 now say where it is said. No refusal code: a refused push is printed as `REFUSED pushing ...`, with no code, as the login remedies of #915 and #993 are. Tests: `test_what_a_refused_push_prints` gains the host-key rows and runs every row on GitHub and on disk; two end-to-end tests run the scaffold on GitHub against a fake ssh saying OpenSSH's measured words and against a ruleset hook; `test_ruleset_hint_tracking_branch.py` pins that the scaffold's hint is the last thing it prints under `--local-remote-dir`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR updates scaffold push-refusal diagnostics to distinguish pre-push SSH host-key failures from ruleset refusals, provide fingerprint-based recovery guidance and safe re-run instructions, and append GitHub-only prompt guidance for genuine ruleset refusals while preserving shared and local hint output. Sequence diagram for SSH host-key push refusalsequenceDiagram
participant Scaffold
participant Git
participant SSH
participant User
participant GitHub
Scaffold->>Git: push
Git->>SSH: connect to host
SSH-->>Git: Host key verification failed.
Git-->>Scaffold: CommandFailed
Scaffold->>Scaffold: _host_key_remedy
Scaffold-->>User: fingerprint-check guidance
User->>SSH: ssh -T with BatchMode omitted
SSH-->>User: show fingerprint and ask
User->>GitHub: gh api meta --jq .ssh_key_fingerprints
GitHub-->>User: published fingerprints
User->>Scaffold: re-run with corrected known_hosts
Scaffold->>Git: push
Sequence diagram for GitHub ruleset refusal guidancesequenceDiagram
participant Scaffold
participant Git
participant Ruleset
participant User
Scaffold->>Git: push
Git->>Ruleset: receive push
Ruleset-->>Git: refuse push
Git-->>Scaffold: CommandFailed
Scaffold->>Scaffold: _login_remedy
Scaffold->>Scaffold: RULESET_HINT.format(repo, tracking)
Scaffold->>Scaffold: RULESET_HINT_PROMPTS
Scaffold-->>User: ruleset hint and GitHub-only prompt guidance
User->>Scaffold: re-run with --reuse-empty-repo
Flow diagram for scaffold push-refusal diagnosticsflowchart TD
A[Push fails] --> B{SSH host-key failure?}
B -->|Yes| C[_host_key_remedy]
C --> D[Check host fingerprint against GitHub]
D --> E[_then_re_run]
B -->|No| F[_login_remedy]
F --> G{Login remedy printed?}
G -->|Yes| H[RULESET_HINT]
G -->|No| I[RULESET_HINT]
I --> J{GitHub mode?}
J -->|Yes| K[RULESET_HINT_PROMPTS]
J -->|No| L[End]
H --> L
K --> L
E --> L
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-3, 2026-10-21T15:42:34Z) RV1130, independent review of #1130 at 4009b65: NOT READY (one MUST, one SHOULD). MUST M1 - scaffold-project.py:2179-2182 (HOST_KEY_CHECK) and :2281-2283. The changed-key remedy checks the fingerprint ssh printed for the refused push, then says "remove the old key ... and keep the new one (ssh -T ... shows the fingerprint ... and asks whether to keep it)". That ssh -T is a new connection, and the branch never says to answer yes only to a published fingerprint (the unknown-key branch does). Measured with OpenSSH 9.6p1 against W915's fake sshd: with StrictHostKeyChecking accept-new in ssh_config, ssh -T printed only "Warning: Permanently added ... to the list of known hosts." and kept the key without showing or asking. With -o StrictHostKeyChecking=ask first on the command line, ssh showed the fingerprint and asked, even over accept-new in the config or a later -o. The same flag also makes the unknown-key remedy work under StrictHostKeyChecking=yes (your an-unknown-key-checked-strictly case), where ssh -T refuses without asking. Fix: HOST_KEY_CHECK names " SHOULD S1 - the body. "Closes #1108." is line 1; the body ends with the merges table, then Lane, then the robot line. Move it to the end as "Closes #1108", before the Lane line (as #1101 does). closingIssuesReferences is already [1108]. NITs:
Verified:
Left out of scope: (a) a follow-up issue - a re-run's probe of a seeded leg refused for its host key still says "Let git read it" and sends the person to ssh -T with no fingerprint advice; (b) nothing - ssh always refuses a revoked key; (c) nothing - the docs do not list refused-push remedies. Lane 3's plan: a fresh writer makes one fix round for M1, S1 and N1 to N5. HOST_KEY_CHECK names Lane: openreposhape-3 (openRepoShape-3) |
… for names `ssh -T -o StrictHostKeyChecking=ask` first, so ssh shows the fingerprint and asks whatever the person's ssh_config says, and a changed key is kept only on a yes to the fingerprint checked; GitHub's fingerprints come from `gh api --hostname github.com meta`; and `remote:` lines the server sent no longer switch the remedy (#1108) RV1130's review of #1130: - M1: `HOST_KEY_CHECK` names `ssh -T -o StrictHostKeyChecking=ask`, that option first. ssh keeps the first value it is given for an option, so it wins over an ssh_config's `accept-new`, which kept the key without showing or asking (measured, known_hosts 0 -> 98 bytes), over its `yes`, which refused without asking, and over a later `-o`. The changed-key remedy ends "answering yes only to the fingerprint you checked", as the unknown-key remedy does. - N1: `GITHUB_FINGERPRINTS` is `gh api --hostname github.com meta --jq .ssh_key_fingerprints` and says ssh shows each with `SHA256:` before it. - N2: the host-key match skips lines that start with `remote:` (`_without_remote_lines`). - N3, N4, N5: the comments say what `LogLevel=QUIET`, `LogLevel=FATAL` and a revoked key give, `_then_re_run` says "before any ref is updated", and the two comments over `REMOTE_ENV` and `PROMPTS_STILL_ASK` say `_refused_push` appends `RULESET_HINT_PROMPTS`. Tests: `SEE_THE_KEY`, `PUBLISHED` and `CHANGED_HOST_KEY_REMEDY` follow the words; `test_what_a_refused_push_prints` gains a ruleset whose `remote:` lines carry ssh's words, a `\r` among them, and an unknown key beside `remote:` lines that say it changed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-21T21:10:38Z) RV1130 round 2, #1130 at bc27679: NOT READY for one SHOULD only. M1 is fixed right; no MUST remains. SHOULD S1 - the comments on N2's filter. scaffold-project.py:2281-2283 (_without_remote_lines: "a \r ... stays in it, as git prints it behind its prefix") and tests/test_scaffold_reuse_seeded_legs.py:1720-1722 ("as git prints a server's line behind its
NITs:
Verified:
Lane 3's plan: W1108-FIX makes one more small round. S1: git's real shape in the row at :1727, and both comments reworded to say git prefixes each line, and each part after a \r, with Lane: openreposhape-3 (openRepoShape-3) |
…atchMode=no -o StrictHostKeyChecking=ask` first, so ssh shows the fingerprint and asks over an ssh_config's `BatchMode yes` and a later `-o`, and the filter's comment says git prefixes each part of a server's line after a carriage return with `remote:` (#1108) RV1130's round 2: - N1: `HOST_KEY_CHECK` names `ssh -T -o BatchMode=no -o StrictHostKeyChecking=ask`, both options first, and drops "but `BatchMode=yes`". Measured in a terminal against the fake sshd: with `BatchMode yes` and `StrictHostKeyChecking accept-new` or `yes` in ssh_config, and again with a later `-o BatchMode=yes` and `-o StrictHostKeyChecking=accept-new`, ssh showed the fingerprint and asked; answering no left known_hosts unchanged. `-o StrictHostKeyChecking=ask` alone refused without asking under `BatchMode yes`. - S1: the `_without_remote_lines` docstring and the test comment say git prefixes each line the server sent, and each part after a carriage return in it, with `remote:`; the test row is git's real shape ("remote: see the log \rremote: Host key verification failed. \n", measured with git 2.43 under LC_ALL=C). Tests: `SEE_THE_KEY` follows the words. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-21T21:34:29Z) RV1130c, independent review round 3 of #1130 at 5724035: NOT READY for one SHOULD (one test row and one body cell). The code is right and safe; no MUST. SHOULD S1 - M13 is not equivalent. The body's Mutants table calls it EQUIVALENT "on anything git really prints". M13 swaps output.split("\n") for output.splitlines(), which also splits at \v, \f, \x1c-\x1e, \x85, U+2028 and U+2029, and git starts a new prefix only after \r and \n. Measured, git 2.43 under LC_ALL=C, a pre-receive hook: cat -A shows "remote: ff^LHost key verification failed. $" (and the same for \v, \x1c and U+2028), one remote: prefix over the separator. Through the scaffold's own run() and _host_key_remedy, the head gives the ruleset hint for each such line and M13 gives the host-key remedy. (In the real path the \r row never reaches the filter: run() reads in text mode, which turns git's \r into \n first.) Fix: add "remote: see the log\x0cHost key verification failed. \n" to RULESET_SAYING_HOST_KEY (tests/test_scaffold_reuse_seeded_legs.py:1725-1733), say so in the comment above it, and mark M13 KILLED in the body. Proven in my clone: the row passes at the head (2 passed) and kills M13 (2 failed); without it M13 survives the whole file (121 passed). NITs:
Verified:
Lane 3's plan: a fresh writer makes round 3. S1: the form-feed row goes into RULESET_SAYING_HOST_KEY, the comment above it says so, and M13 is marked KILLED in the body. N1 is taken: the changed-key remedy ends "answering yes only to a fingerprint GitHub publishes, which can be another of its keys than the one you checked", with the docstring and the pin. N2 and N3 are fixed in the body. RV1130c then re-checks that head. The trailer is set in the explicit squash message at landing. #1130 stays a draft with no milestone, and lands after the v1.2.0 tag on a later word. Lane: openreposhape-3 (openRepoShape-3) |
…to a fingerprint GitHub publishes, which can be another of its keys than the one you checked", since ssh asks first about a key type it holds, and a form feed in a server's `remote:` line keeps it one line for the filter (#1108) RV1130's round 3: - N1: after a changed RSA or ECDSA key is checked and `ssh-keygen -R` removes it, `ssh -T` asks about GitHub's ED25519 key, not the key that was checked (ssh asks first about the key types it already holds, RFC 4253 7.1), so the remedy no longer says "the fingerprint you checked"; the docstring and the pin say the same. - S1: the test row `"remote: see the log\x0cHost key verification failed. \n"` is git's real shape for a form feed (git 2.43, `cat -A`: one `remote:` prefix over the separator), and kills the filter's mutant that splits at every separator `str.splitlines` knows. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-21T21:55:16Z) RV1130c, round 3 re-check of #1130 at 631ea7c: READY once CI's tests job passes on 631ea7c. No MUST, no SHOULD, one NIT. NIT - scaffold-project.py:2304-2306, the _host_key_remedy docstring: "the Verified:
Lane 3's plan: the docstring NIT is fixed now in the reviewer's words, in one commit that changes only the docstring. CI's Lane: openreposhape-3 (openRepoShape-3) |
… ssh asked first for the key type it held, and once that key is removed the `ssh -T` asks first for ssh's default, ED25519 (#1108) RV1130c's NIT: the docstring said the `ssh -T` "asks first about a key type ssh holds", which is the push's ssh. After `ssh-keygen -R` ssh holds no key for the host, so the `ssh -T` asks first for its default, ED25519. A docstring alone: no printed word and no test changes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
|
(lane openRepoShape-3, 2026-10-21T23:39:16Z) #1130 is READY at
Lane: openreposhape-3 (openRepoShape-3) |
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 10 hours and 22 minutes by commenting |
… host key is told that key's fix, checked against GitHub's published fingerprints, in place of "Let git read it" (#1137) (#1156) This follows #1130. A `--reuse-empty-repo` re-run over a leg with commits asks git (`git ls-remote`) whether the leg's one commit is the seed. Where a `url.<base>.insteadOf` rule sends that read over ssh and ssh refuses GitHub's host key, the run is refused as `scaffold-remote-exists`, but the remedy was the login fix ("Let git read it", and an `ssh -T` that says whether GitHub takes your key). That is the wrong fix for a refused host key. `_exists_remedy` now gives #1130's host-key fix in a read's words (`_read_host_key_fix`), then the same re-run sentence. For an unknown key it is `HOST_KEY_CHECK` (`ssh -T -o BatchMode=no -o StrictHostKeyChecking=ask`), answered yes only to a fingerprint GitHub publishes (`GITHUB_FINGERPRINTS`). For a changed key it is to accept no key from that host until the fingerprint ssh prints on a second `git ls-remote` of that URL is checked against GitHub's published ones. Any other refused read (a refused ssh key, a closed connection, an https login) keeps the login fix, byte for byte as before. `scaffold-remote-exists` is kept, no refusal code is added and no docs change. The one line `unread` carries is `Host key verification failed.` for an unknown and a changed key alike, so `_held_seed` now returns git's refused read itself (the `CommandFailed`) and the remedy reads its whole output. A new `_host_key_refused(output)` returns `changed`, `unknown` or nothing, skips `remote:` lines with `_without_remote_lines`, and cuts all from a server's `fatal: remote error:` on, so text a server sends cannot steer a read into the host-key fix. `_host_key_remedy`, the push's, uses it too and behaves as before. `tests/test_scaffold_reuse_seeded_legs.py` has 134 passed (121 at the parent). `test_a_leg_whose_read_ssh_refused_the_host_key_for_names_the_key` runs end to end with a fake ssh for an unknown key, one checked strictly, and a changed key. `test_what_a_leg_git_could_not_read_is_told` has 9 rows in-process, among them `remote:` lines alone and two of a server's error packet, which fail without the cut. Each of the mutants run against the whole file is killed. Closes #1137 Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



What changed
A follow-up outside v1.2.0 (Brett Heap, 2026-10-21T10:32:04Z). It lands only after the v1.2.0 tag, on a later merge word.
This comes from RV1101's S1, the review of #1101 (U037 of #915). Five commits touch three files:
4009b65, the change;bc27679, the fix round after RV1130's review of4009b65(#1130 (comment));5724035, the second round, after RV1130's review ofbc27679(#1130 (comment)); and631ea7c, the third round, after RV1130c's review of5724035(#1130 (comment)); anddee55dc, a docstring, after RV1130c's last NIT (#1130 (comment)). The files arescaffold-project.pyand two test files.On GitHub, the ruleset hint is followed by a paragraph on the prompts its re-run can still wait at
_refused_pushnow printsRULESET_HINT_PROMPTSafterRULESET_HINT, as a paragraph of its own: "The --reuse-empty-repo re-run turns off git's own terminal prompt and no other, ..." (PROMPTS_STILL_ASK). Under--local-remote-dirthe hint is printed alone, byte for byte as before.--local-remote-dirtoo, where no prompt can come. Words in the constant would be printed there.{prompts}) would break the siblings' pins.test_each_hint_takes_two_fields_the_repository_and_the_branchandtest_each_tool_prints_its_own_hint_and_passes_both_fieldshold all three hints to the same two fields, and the byte-for-byte pins call.format(repo=..., tracking=...), which would raiseKeyError. With the append,RULESET_HINTand its pins stay unchanged, and so doFAMILY_RULESET_HINT,ADOPT_RULESET_HINTand theirs.scripts/shape_materialize.pyis not touched._exists_remedy'sstill = "" if local else ...from scaffold-project.py: the--reuse-empty-repohelp, its docs and each remedy that sends a person into that re-run say ssh, a credential helper or askpass can still make it wait, and how to keep it from waiting (#915) #1101._refused_pushtakes a new, requiredlocalargument. Both call sites, the legs' and the root's, passargs.local_remote_dir is not None.--reuse-empty-repore-run without #915's prompt sentence, and a push refused for its ssh host key gets that hint alone #1108, overREMOTE_ENVandPROMPTS_STILL_ASK, now say where it is said: the constant says nothing of it, and_refused_pushappendsRULESET_HINT_PROMPTSafter it, on GitHub alone and where no login remedy has said it already (RV1130's N5).A push refused for its host key gets a host-key remedy in place of the ruleset hint
_host_key_remedy. Where git's output for a push holds OpenSSH'sHost key verification failed.(HOST_KEY_FAILED) in a line of ssh's own, the push prints this remedy and noRULESET_HINT. ssh refused before git sent anything, so no ruleset was asked, and the hint's re-run fails the same way.HOST_KEY_CHECK): "ssh -T -o BatchMode=no -o StrictHostKeyChecking=ask, both options first so that they win over your ssh_config and any later-o, against the user, host and port your rule names, with the rest of what yourGIT_SSH_COMMANDorcore.sshCommandadds", which shows the fingerprint and asks; "answer yes only to a fingerprint GitHub publishes" (GITHUB_FINGERPRINTS: "gh api --hostname github.com meta --jq .ssh_key_fingerprintsprints them, and ssh shows each withSHA256:before it").REMOTE HOST IDENTIFICATION HAS CHANGED,HOST_KEY_CHANGED). The remedy says a machine in the middle of the connection shows this as well as a key GitHub replaced. So it says to "accept no key from that host, and push nothing to it, until you have checked that the fingerprint ssh printed above is one GitHub publishes". Only then: remove the old key (thessh-keygen -Rline ssh printed) and keep the new one (HOST_KEY_CHECK), "answering yes only to a fingerprint GitHub publishes, which can be another of its keys than the one you checked" (round 3 changed these words from "the fingerprint you checked"; see below). It never says to accept the key as it comes.gh repo createmade all three repositories. The fake gh logged 3 creates, then the push was refused, so all three are empty.--through setup.sh) and a new --work-dir or none: it reuses the repositories this run left empty, and a leg whose one commit is the seed this scaffold pushes". When the run was itself that re-run, it is "the same command with a new --work-dir or none". The prompt sentence follows._then_re_run. The login remedies' output is byte for byte what it was, and their rows still pass.REFUSED pushing the spec leg.), and neither of scaffold-project.py: a--reuse-empty-repore-run can stop at a git, ssh or helper prompt on its pushes and seed fetch, and an ssh-rewrite operator's refusal gives git's generic line and a remedy that does not apply #915's and scaffold-project.py: a push whose stored login GitHub rejects (fatal: Authentication failed) is refused with only the ruleset hint, where a push git has no login for is told how to give git one #993's login remedies added one. Every existing code is unchanged, and AGENTS.md is not touched.gh api --hostname github.com meta --jq .ssh_key_fingerprints, not by a docs URL. That is a read over https, outside the ssh connection in doubt, with theghlogin the scaffold already needs, and--hostname github.compins it to the host the scaffold'shttps://github.com/URLs name, whateverGH_HOSTsays. I did not run that command (no realghcall but the PR's own);gh api --helplists--hostname, and RV1130, in round 2, rangh api --hostname github.com meta --jq .ssh_key_fingerprintsonce on its own: exit 0, three fingerprints (ECDSA, ED25519, RSA) printed withoutSHA256:.The fix round (
bc27679), RV1130's thread listHOST_KEY_CHECKnow namesssh -Twith the options below first, and the changed-key remedy ends "answering yes only to the fingerprint you checked", as the unknown-key remedy does. Why, measured below with real ssh in a pseudo-terminal: withStrictHostKeyChecking accept-newin ssh_config, a baressh -Tkept the key without showing the fingerprint or asking (known_hosts 0 to 98 bytes), and withyesit refused without asking, so the unknown-key remedy could not be followed there either. With-o StrictHostKeyChecking=askfirst, ssh showed the fingerprint and asked underaccept-new,yesandask, and over a later-o. Answeringnoleft known_hosts unchanged.GITHUB_FINGERPRINTSisgh api --hostname github.com meta --jq .ssh_key_fingerprints, and says ssh shows each value withSHA256:before it._without_remote_lines: the host-key match skips lines that start withremote:, so words a ruleset's message or a hook carries cannot switch the remedy. It splits at newlines alone. git prefixes each line the server sent, and each part after a carriage return in it, withremote:, so a line that starts with it is the server's from end to end, and a split at newlines alone keeps it whole (round 2's S1 corrects the first wording of this).str.splitlinesalso splits at\v,\f,\x1cto\x1e,\x85, U+2028 and U+2029, where git starts no new prefix; round 3's S1 adds the test row for that.HOST_KEY_FAILEDcomment names three cases, all safe:LogLevel=QUIETprints neither phrase, so the push gets the ruleset hint, as at main;LogLevel=FATALdrops the changed-key banner and keeps the failed line, so a changed key gets the unknown-key remedy, whosessh -Trefuses a changed key again; a revoked key gets the unknown-key remedy too._then_re_rundocstring says "before any ref is updated", where it said "before git sends anything", which is untrue forLOGIN_REFUSED.REMOTE_ENVandPROMPTS_STILL_ASKsay what_refused_pushappends (above).--reuse-empty-repore-run without #915's prompt sentence, and a push refused for its ssh host key gets that hint alone #1108, then the Lane line, then the robot line, as scaffold-project.py: the--reuse-empty-repohelp, its docs and each remedy that sends a person into that re-run say ssh, a credential helper or askpass can still make it wait, and how to keep it from waiting (#915) #1101's did.The second round (
5724035), RV1130's round 2remote:prefix, and the test row had that shape. It does not: git starts a new prefix after every carriage return as well as every newline. Measured with my own probe, git 2.43 underLC_ALL=C, a local bare repository whose pre-receive hook writes "see the log\rHost key verification failed.\n":cat -Aof the push showsremote: see the log ^Mremote: Host key verification failed. $, eight spaces after each part. The row is now"remote: see the log \rremote: Host key verification failed. \n", and the_without_remote_linesdocstring and the test comment say what git does. The behaviour was right before and is unchanged.HOST_KEY_CHECKnamesssh -T -o BatchMode=no -o StrictHostKeyChecking=ask, both options first, so they win over an ssh_config'sBatchMode yesandStrictHostKeyChecking, and over any later-ofromGIT_SSH_COMMANDorcore.sshCommand. The clause "butBatchMode=yes" is gone, since ssh keeps the first value it is given. Measured below.SEE_THE_KEYfollows the words.bc27679and5724035endCo-Authored-By: Claude Sonnet 5.5, and4009b65endsClaude Opus 5.5. Set the one you want in the explicit squash message.The third round (
631ea7c), RV1130c's reviewstr.splitlinesalso splits at\v,\f,\x1cto\x1e,\x85, U+2028 and U+2029, and git starts a newremote:prefix only after\rand\n. RV1130c measured it (git 2.43, a pre-receive hook,cat -A):remote: ff^LHost key verification failed. $, one prefix over the form feed. A split at every separator would cut that server line in two and read its tail as ssh's. SoRULESET_SAYING_HOST_KEYhas a new line,"remote: see the log\x0cHost key verification failed. \n", and the comment above it says git keeps a form feed inside one prefixed line. At631ea7cthe file passes, and M13 fails (below). The behaviour ofscaffold-project.pyis unchanged by this.ssh-keygen -R,ssh -Tasks about GitHub's ED25519 key, not the RSA key the person checked. The old words ("the fingerprint you checked") could name a key ssh would not show. It failed safe (the person answers no, and the next refused push gives the unknown-key remedy), and the new words say so. The_host_key_remedydocstring and the pin inCHANGED_HOST_KEY_REMEDYsay the same; a grep of the tree for the old words found no other pin. RV1130c took the order fromssh -vv's proposal and RFC 4253 section 7.1; I did not re-measure it.gh apiline names the--hostname github.comform, run once, exit 0, and the left-out probe names-o BatchMode=no -o StrictHostKeyChecking=ask.dee55dc), RV1130c's last NIT. The_host_key_remedydocstring said thessh -T"asks first about a key type ssh holds". That is the push's ssh: afterssh-keygen -Rssh holds no key for the host, and thessh -Tasks first for ssh's default, ED25519. It now says the push's ssh asked first for the key type it held, and once that key is removed thessh -Tasks first for ssh's default, ED25519, and it can be sent another key than the push's. A docstring alone: no printed word and no test changed.tests/test_scaffold_reuse_seeded_legs.py(121 passed) andtests/test_repo_hygiene.py(100 passed) were re-run atdee55dc, each alone, under the stub gh. The tests and merge tables below are at631ea7c; the edit is inscaffold-project.py, which no other open PR touches.Left out of scope
only_commitrunsgit ls-remote. When ssh refuses the host key there, the run is still refused asscaffold-remote-existswith the reason "Host key verification failed." and_exists_remedy's "Let git read it" remedy, which names anssh -Tcheck (RV1101's case c). That is not a push and not the ruleset hint, so it is outside this issue. A follow-up could give it the host-key remedy, and thatssh -Tthe same-o BatchMode=no -o StrictHostKeyChecking=ask(RV1130's out-of-scope (a)).family.py'sFAMILY_RULESET_HINTand its refused push. That is another tool, with its own hint.@revokedin known_hosts also ends inHost key verification failed., so it gets the unknown-key remedy. Its cause sentence is then not exact, but its advice is safe, because ssh always refuses a revoked key. Not measured, read from the ssh binary by RV1130.StrictHostKeyChecking=nogoes on past a changed key and prints noHost key verification failed., so that is not this case.docs/cli.mdchange: none of them describes what a refused push prints beyond the hint's own words.--reuse-empty-repore-run without #915's prompt sentence, and a push refused for its ssh host key gets that hint alone #1108 has no "Needs a ruling" or "Documented limit" section.Measurements
Setup:
W1108-scratch/meas/measure1108.py(the scaffold's pushes) andW1108-fix-scratch/meas/ssh_ask.pyandssh_ask2.py(ssh -Talone). Both reuse W915'sfake_sshd.py, run as a ProxyCommand and unchanged. That fake is enough of an ssh server for the real OpenSSH 9.6p1 client to reach its host-key check.fakehost.invalid.Host *hasProxyCommand /bin/false.run_in_a_terminal), with the tests' fake gh first on PATH and then a stub gh that exits 97. The environment also hasGH_TOKEN=bogus, an emptyGH_CONFIG_DIR, a scratchGIT_CONFIG_GLOBALandGIT_CONFIG_NOSYSTEM=1, and every run has a timeout. No real host was reached.pre-receivehook (RULESET_REFUSES). On GitHub they use bare repositories underurl.<path>.insteadOf.The scaffold's pushes, at main
823624d, the first head4009b65, the secondbc27679and the new head5724035. k1, k3 and r3 were re-run atbc27679and again at5724035; the other cases were measured at4009b65and the changes since do not touch their path (k2, k4, k5, r1, r2 not re-run):4009b65bc276795724035BatchMode=yes, first run-o StrictHostKeyChecking=askand--hostname github.com, no hint-o BatchMode=no -o StrictHostKeyChecking=ask, no hintBatchMode=yesBatchMode=yes,--reuse-empty-repore-run--local-remote-dir, ruleset refuses the spec legRound 3 changes the changed-key remedy's last words and one test row, so the k3 row's last words are the old ones, and no push case was re-measured at
631ea7c; the reuse file'sCHANGED_HOST_KEY_REMEDYpin holds the new words.ssh -Tin a pseudo-terminal (real OpenSSH 9.6p1 behind the fake sshd; known_hosts empty, or holding another key for the changed cases; answers typed at the "(yes/no/[fingerprint])" prompt):ssh -T... and ssh_config'sStrictHostKeyCheckingaccept-newyes-o StrictHostKeyChecking=ask,accept-newno: "Host key verification failed."; known_hosts 0 bytes, unchanged-o StrictHostKeyChecking=ask,yes-o StrictHostKeyChecking=ask,ask-o StrictHostKeyChecking=ask -o StrictHostKeyChecking=accept-new,accept-new-o; unchanged-o StrictHostKeyChecking=ask -o StrictHostKeyChecking=yes,yes-o StrictHostKeyChecking=ask -o BatchMode=yes,accept-newBatchMode=yeswith no-o BatchMode=nobefore it wins, which is why that option is named first too)-o StrictHostKeyChecking=ask,accept-newandyesREMOTE HOST IDENTIFICATION HAS CHANGEDbanner, "Host key for fakehost.invalid has changed and you have requested strict checking." and "Host key verification failed."; known_hosts unchanged: the unknown-key remedy'sssh -Trefuses a changed key again-o BatchMode=no -o StrictHostKeyChecking=ask, ssh_configBatchMode yesandaccept-newno: "Host key verification failed."; known_hosts 0 bytes, unchangedBatchMode yesandyes-o BatchMode=yes-o BatchMode=yes -o StrictHostKeyChecking=accept-newaccept-newand noBatchMode-o StrictHostKeyChecking=askalone, ssh_configBatchMode yesandaccept-new-o BatchMode=nois named too-o BatchMode=no -o StrictHostKeyChecking=ask, ssh_configBatchMode yesBatchMode=yes -o LogLevel=QUIET, unknown and changed keyBatchMode=yes -o LogLevel=FATAL, unknown and changed keyUnder
BatchMode=yes, ssh 9.6 printedHost key verification failed.alone for an unknown key. Run directly, it printed the same line with no terminal (setsid). UnderStrictHostKeyChecking=yesit printedNo ED25519 host key is known for ... and you have requested strict checking.first. For a changed key, it printed the banner, the new fingerprint and anssh-keygen -Rline, with BatchMode and without.Tests
Each file ran alone at
631ea7cunder the stub gh, withGH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_SSH_COMMAND=false,GIT_CONFIG_GLOBAL=/dev/nullandEMAILset. The command was/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt/<name> -o tmp_path_retention_policy=failed tests/<file>.tests/test_scaffold_reuse_seeded_legs.py4009b65, plus 4 new ids; round 3's row is part of a constant and adds no id)tests/test_ruleset_hint_tracking_branch.pytests/test_cli_reference.pytests/test_readme_sections.pytests/test_repo_hygiene.pytests/test_agents_md_codes.pytests/test_update_add_codes_documented.py,tests/test_no_auto_maintenance.py, the four importers'--collect-onlybc27679(6 passed; 58 passed and 2 skipped; 132 collected), not re-run afterbc27679: rounds 2 and 3 change two constants' words, a docstring, comments and one test rowpyflakes (at
631ea7c):scaffold-project.py0 warnings;test_scaffold_reuse_seeded_legs.py15, the same as at4009b65(the existing "redefinition of unused 'template'" kind).Which tests pin the words: I grepped the tree for
ssh_key_fingerprints,StrictHostKeyChecking,HOST_KEY_CHECK, "shows the fingerprint" and "asks whether to keep it". Onlyscaffold-project.pyandtests/test_scaffold_reuse_seeded_legs.pyhold any (SEE_THE_KEY,PUBLISHEDandCHANGED_HOST_KEY_REMEDYthere, as literals).What the tests pin:
test_what_a_refused_push_printshas 18 rows, each run on GitHub and on disk, so 36 ids. Its host-key rows use OpenSSH's measured words, and the expected output is built from literals. The fix round adds two: a ruleset whoseremote:lines carryHost key verification failed.andREMOTE HOST IDENTIFICATION HAS CHANGED, one after a carriage return and one after a form feed (round 3's S1), which keep the ruleset hint (N2); and an unknown key beside aremote:line that says "changed", which keeps the unknown-key remedy.test_a_push_ssh_refused_the_host_key_for_names_the_key_not_the_ruleset(4 ids) runs the scaffold on GitHub against a fake ssh that says those words. It checks the remedy once, afterREFUSED pushing, and no hint.test_a_ruleset_s_refusal_on_github_says_what_its_re_run_can_wait_atcovers a leg and the root against the ruleset hook. Its stderr must end with the hint's literal, then the paragraph.test_ruleset_hint_tracking_branch.py, the scaffold's hint must be the last thing printed under--local-remote-dir.Mutants
Each mutant was applied with an exact one-occurrence replacement, then its test run (
-k "refused_push_prints or host_key_for_names or ruleset_s_refusal_on_github"in the reuse file, 42 ids, plustest_ruleset_hint_tracking_branch.py -k scaffoldfor the ones that touchlocal). The file was then restored from a saved copy and checked withcmp; its sha256 was the same before and after each batch, andgit show HEAD:scaffold-project.pyequals the saved copy. M1 to M9 were run atbc27679(5, 3 plus 6, 16, 10 plus 6, 5, 1, 1, 16 and 9 failed); M10 to M15 were run again at5724035, with the changed words. Round 3 ran M13, M11, M11b and M15 at631ea7cagainst the WHOLE reuse file (121 ids), each restored and checked withcmpagainst saved copies of both changed files.--local-remote-dirtest_ruleset_hint_tracking_branch.pyHOST_KEY_FAILED = ("failed",))test_ruleset_hint_tracking_branch.pyTrueforlocalTrueforlocalreturnafter the host-key remedy, so the hint follows it-o StrictHostKeyChecking=askdropped fromHOST_KEY_CHECK631ea7c(every changed-key id; 5 failed at5724035with the old words)631ea7cremote:lines againoutput.split("\n")replaced byoutput.splitlines(), so a form feed (or\v,\x1cto\x1e,\x85, U+2028, U+2029) in a server's line cuts it in two631ea7c: 2 failed (the ruleset row with the form-feed line, on GitHub and on disk). At5724035it survived (42 passed in the 42-id run, 121 in the whole file), and the round 2 text called it equivalent; that was wrong (RV1130c's S1).gh api metawithout--hostname github.com-o BatchMode=nodropped fromHOST_KEY_CHECK5724035and631ea7c)Merges
I read the open PR list live at
5724035: 24 open PRs; at631ea7cI re-read each of #1104, #1105 and #1118 (the head, still open) and main (038dafb), and re-ran every merge below. None other than this one touchesscaffold-project.py,tests/test_ruleset_hint_tracking_branch.pyortests/test_scaffold_reuse_seeded_legs.py. Main is now038dafb. The branch is not merged with it, since the landing after the v1.2.0 tag updates the branch.git merge-tree --write-tree)631ea7cwith origin/main038dafbe31a2f79dfd1de89aa8(touchesscripts/shape_materialize.py, whereRULESET_HINTlives)5a5177fbf65c; it does not change the hintb28f40d(the same file)7a076a5ce10b; it does not change the hint2307036(the same file)31ea584ecbfe; it does not change the hintCloses #1108
Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code