Skip to content

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

Merged
brettheap merged 5 commits into
mainfrom
l3-1108-ruleset-hint-prompts
Oct 23, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

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 of 4009b65 (#1130 (comment)); 5724035, the second round, after RV1130's review of bc27679 (#1130 (comment)); and 631ea7c, the third round, after RV1130c's review of 5724035 (#1130 (comment)); and dee55dc, a docstring, after RV1130c's last NIT (#1130 (comment)). The files are scaffold-project.py and two test files.

On GitHub, the ruleset hint is followed by a paragraph on the prompts its re-run can still wait at

  • On GitHub, _refused_push now prints RULESET_HINT_PROMPTS after RULESET_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-dir the hint is printed alone, byte for byte as before.
  • The design choice: a GitHub-only append at the scaffold's one call site. The words are not added to the shared constant, and the constant gets no third format field. Why:
  • Decided here; the Direction leaves these open:
    • The paragraph says "The --reuse-empty-repo re-run", not "That re-run". It follows the hint's last paragraph, which ends "a clean re-run", so "That re-run" would name the wrong one (RV1101's N3, about the handbook).
    • It is not printed where a login remedy came before the hint. That remedy already ends with the same sentence about the same re-run, so the sentence is said once per refusal.
  • _refused_push takes a new, required local argument. Both call sites, the legs' and the root's, pass args.local_remote_dir is not None.
  • The two comments that named scaffold-project.py: the ruleset hint sends a person into the --reuse-empty-repo re-run without #915's prompt sentence, and a push refused for its ssh host key gets that hint alone #1108, over REMOTE_ENV and PROMPTS_STILL_ASK, now say where it is said: the constant says nothing of it, and _refused_push appends RULESET_HINT_PROMPTS after 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

  • New _host_key_remedy. Where git's output for a push holds OpenSSH's Host key verification failed. (HOST_KEY_FAILED) in a line of ssh's own, the push prints this remedy and no RULESET_HINT. ssh refused before git sent anything, so no ruleset was asked, and the hint's re-run fails the same way.
  • Two remedies. Each gives the cause, then what to do, then the re-run:
    • Unknown key. The rule sends the push over ssh, the key is in no known_hosts file, and ssh refused it without asking or was answered no. What to do (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 your GIT_SSH_COMMAND or core.sshCommand adds", 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_fingerprints prints them, and ssh shows each with SHA256: before it").
    • Changed key (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 (the ssh-keygen -R line 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.
  • The re-run, matched to the state a refused push leaves (measured):
    • A first run's first push is the spec leg's, after gh repo create made all three repositories. The fake gh logged 3 creates, then the push was refused, so all three are empty.
    • A push refused later leaves each leg pushed before it holding its seed.
    • A host-key failure stops before git sends anything.
    • So the finishing run is the re-run the login remedies already name: "re-run the same command with --reuse-empty-repo added (after -- 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.
    • That tail is now shared as _then_re_run. The login remedies' output is byte for byte what it was, and their rows still pass.
  • No new refusal code. A refused push has none (REFUSED pushing the spec leg.), and neither of scaffold-project.py: a --reuse-empty-repo re-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.
  • Decided here: GitHub's published fingerprints are named by 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 the gh login the scaffold already needs, and --hostname github.com pins it to the host the scaffold's https://github.com/ URLs name, whatever GH_HOST says. I did not run that command (no real gh call but the PR's own); gh api --help lists --hostname, and RV1130, in round 2, ran gh api --hostname github.com meta --jq .ssh_key_fingerprints once on its own: exit 0, three fingerprints (ECDSA, ED25519, RSA) printed without SHA256:.

The fix round (bc27679), RV1130's thread list

  • M1, the changed-key remedy let a person keep a key nobody checked. HOST_KEY_CHECK now names ssh -T with 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: with StrictHostKeyChecking accept-new in ssh_config, a bare ssh -T kept the key without showing the fingerprint or asking (known_hosts 0 to 98 bytes), and with yes it refused without asking, so the unknown-key remedy could not be followed there either. With -o StrictHostKeyChecking=ask first, ssh showed the fingerprint and asked under accept-new, yes and ask, and over a later -o. Answering no left known_hosts unchanged.
  • N1. GITHUB_FINGERPRINTS is gh api --hostname github.com meta --jq .ssh_key_fingerprints, and says ssh shows each value with SHA256: before it.
  • N2. _without_remote_lines: the host-key match skips lines that start with remote:, 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, with remote:, 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.splitlines also splits at \v, \f, \x1c to \x1e, \x85, U+2028 and U+2029, where git starts no new prefix; round 3's S1 adds the test row for that.
  • N3. The HOST_KEY_FAILED comment names three cases, all safe: LogLevel=QUIET prints neither phrase, so the push gets the ruleset hint, as at main; LogLevel=FATAL drops the changed-key banner and keeps the failed line, so a changed key gets the unknown-key remedy, whose ssh -T refuses a changed key again; a revoked key gets the unknown-key remedy too.
  • N4. The _then_re_run docstring says "before any ref is updated", where it said "before git sends anything", which is untrue for LOGIN_REFUSED.
  • N5. The two comments over REMOTE_ENV and PROMPTS_STILL_ASK say what _refused_push appends (above).
  • S1. The body ends with the closing line for scaffold-project.py: the ruleset hint sends a person into the --reuse-empty-repo re-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-repo help, 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 2

  • S1, the filter's comments and test row. Two comments said git prints a server line with a carriage return under one remote: 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 under LC_ALL=C, a local bare repository whose pre-receive hook writes "see the log\rHost key verification failed.\n": cat -A of the push shows remote: 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_lines docstring and the test comment say what git does. The behaviour was right before and is unchanged.
  • N1. HOST_KEY_CHECK names ssh -T -o BatchMode=no -o StrictHostKeyChecking=ask, both options first, so they win over an ssh_config's BatchMode yes and StrictHostKeyChecking, and over any later -o from GIT_SSH_COMMAND or core.sshCommand. The clause "but BatchMode=yes" is gone, since ssh keeps the first value it is given. Measured below. SEE_THE_KEY follows the words.
  • N2, the body. Sourcery's "Summary by Sourcery" block, appended again after the robot line, is stripped.
  • The trailer. bc27679 and 5724035 end Co-Authored-By: Claude Sonnet 5.5, and 4009b65 ends Claude Opus 5.5. Set the one you want in the explicit squash message.

The third round (631ea7c), RV1130c's review

  • S1, M13 was not equivalent. The second round called the mutant that splits the match at every separator equivalent, "on anything git really prints". It is not: str.splitlines also splits at \v, \f, \x1c to \x1e, \x85, U+2028 and U+2029, and git starts a new remote: prefix only after \r and \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. So RULESET_SAYING_HOST_KEY has 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. At 631ea7c the file passes, and M13 fails (below). The behaviour of scaffold-project.py is unchanged by this.
  • N1, the changed-key remedy's last words. They read "answering yes only to a fingerprint GitHub publishes, which can be another of its keys than the one you checked". ssh first asks about the key types it already holds: after a changed RSA key is removed with ssh-keygen -R, ssh -T asks 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_remedy docstring and the pin in CHANGED_HOST_KEY_REMEDY say the same; a grep of the tree for the old words found no other pin. RV1130c took the order from ssh -vv's proposal and RFC 4253 section 7.1; I did not re-measure it.
  • N2, the body. The gh api line names the --hostname github.com form, run once, exit 0, and the left-out probe names -o BatchMode=no -o StrictHostKeyChecking=ask.
  • N3, the body. The two headings under "What changed" say what the PR changes.
  • The docstring (dee55dc), RV1130c's last NIT. The _host_key_remedy docstring said the ssh -T "asks first about a key type ssh holds". That is the push's ssh: after ssh-keygen -R ssh holds no key for the host, and the ssh -T asks 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 the ssh -T asks 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) and tests/test_repo_hygiene.py (100 passed) were re-run at dee55dc, each alone, under the stub gh. The tests and merge tables below are at 631ea7c; the edit is in scaffold-project.py, which no other open PR touches.

Left out of scope

  • The probe. On a re-run over a leg that holds its seed, only_commit runs git ls-remote. When ssh refuses the host key there, the run is still refused as scaffold-remote-exists with the reason "Host key verification failed." and _exists_remedy's "Let git read it" remedy, which names an ssh -T check (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 that ssh -T the same -o BatchMode=no -o StrictHostKeyChecking=ask (RV1130's out-of-scope (a)).
  • family.py's FAMILY_RULESET_HINT and its refused push. That is another tool, with its own hint.
  • A revoked key. A key marked @revoked in known_hosts also ends in Host 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.
  • ssh run with StrictHostKeyChecking=no goes on past a changed key and prints no Host key verification failed., so that is not this case.
  • No README, handbook or docs/cli.md change: none of them describes what a refused push prints beyond the hint's own words.
  • scaffold-project.py: the ruleset hint sends a person into the --reuse-empty-repo re-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:

  • My harnesses are W1108-scratch/meas/measure1108.py (the scaffold's pushes) and W1108-fix-scratch/meas/ssh_ask.py and ssh_ask2.py (ssh -T alone). Both reuse W915's fake_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.
  • The changed-key case uses a known_hosts file holding another ed25519 key for fakehost.invalid. Host * has ProxyCommand /bin/false.
  • Each run is in a pseudo-terminal (run_in_a_terminal), with the tests' fake gh first on PATH and then a stub gh that exits 97. The environment also has GH_TOKEN=bogus, an empty GH_CONFIG_DIR, a scratch GIT_CONFIG_GLOBAL and GIT_CONFIG_NOSYSTEM=1, and every run has a timeout. No real host was reached.
  • The ruleset cases use the tests' pre-receive hook (RULESET_REFUSES). On GitHub they use bare repositories under url.<path>.insteadOf.

The scaffold's pushes, at main 823624d, the first head 4009b65, the second bc27679 and the new head 5724035. k1, k3 and r3 were re-run at bc27679 and again at 5724035; the other cases were measured at 4009b65 and the changes since do not touch their path (k2, k4, k5, r1, r2 not re-run):

case main 4009b65 bc27679 5724035
k1 unknown key, BatchMode=yes, first run exit 2 in 0.9 s; hint alone exit 2 in 0.7 s; unknown-key remedy, no hint exit 2 in 1.2 s; unknown-key remedy with -o StrictHostKeyChecking=ask and --hostname github.com, no hint exit 2 in 0.7 s; unknown-key remedy with -o BatchMode=no -o StrictHostKeyChecking=ask, no hint
k2 unknown key, no BatchMode, typed "no" exit 2 in 0.8 s; hint alone exit 2 in 0.9 s; unknown-key remedy not re-run not re-run
k3 changed key, BatchMode=yes exit 2 in 0.7 s; banner, then hint alone exit 2 in 0.8 s; changed-key remedy exit 2 in 1.3 s; changed-key remedy ending "answering yes only to the fingerprint you checked", no hint exit 2 in 0.7 s; changed-key remedy with the same words, ending "answering yes only to the fingerprint you checked", no hint
k4 changed key, no BatchMode exit 2 in 0.8 s; banner, hint alone exit 2 in 0.7 s; changed-key remedy not re-run not re-run
k5 unknown key, BatchMode=yes, --reuse-empty-repo re-run exit 2 in 1.3 s; hint alone exit 2 in 0.8 s; remedy, "same command with a new --work-dir or none" not re-run not re-run
r1 GitHub, ruleset refuses the spec leg exit 2 in 0.6 s; hint exit 2 in 0.7 s; hint, then the prompts paragraph not re-run not re-run
r2 GitHub, ruleset refuses the root exit 2 in 1.4 s; hint exit 2 in 1.9 s; hint, then that paragraph not re-run not re-run
r3 --local-remote-dir, ruleset refuses the spec leg exit 2 in 0.4 s; hint exit 2 in 0.5 s; hint alone exit 2 in 3.1 s; hint alone, last, unchanged exit 2 in 0.3 s; hint alone, last, unchanged

Round 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's CHANGED_HOST_KEY_REMEDY pin holds the new words.

ssh -T in 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's StrictHostKeyChecking result
bare, accept-new no fingerprint, no question: "Warning: Permanently added 'fakehost.invalid' (ED25519) to the list of known hosts."; known_hosts 0 to 98 bytes (the defect)
bare, yes "No ED25519 host key is known for fakehost.invalid and you have requested strict checking." and "Host key verification failed.", no question
-o StrictHostKeyChecking=ask, accept-new fingerprint shown, asked; typed no: "Host key verification failed."; known_hosts 0 bytes, unchanged
-o StrictHostKeyChecking=ask, yes the same; unchanged
-o StrictHostKeyChecking=ask, ask the same; unchanged
-o StrictHostKeyChecking=ask -o StrictHostKeyChecking=accept-new, accept-new the same: the first value wins over a later -o; unchanged
-o StrictHostKeyChecking=ask -o StrictHostKeyChecking=yes, yes the same; unchanged
-o StrictHostKeyChecking=ask -o BatchMode=yes, accept-new "Host key verification failed." with no question (a BatchMode=yes with no -o BatchMode=no before it wins, which is why that option is named first too)
changed key, -o StrictHostKeyChecking=ask, accept-new and yes the REMOTE HOST IDENTIFICATION HAS CHANGED banner, "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's ssh -T refuses a changed key again
-o BatchMode=no -o StrictHostKeyChecking=ask, ssh_config BatchMode yes and accept-new fingerprint shown, asked; typed no: "Host key verification failed."; known_hosts 0 bytes, unchanged
the same, ssh_config BatchMode yes and yes the same; unchanged
the same, then a later -o BatchMode=yes the same: the first value wins; unchanged
the same, then a later -o BatchMode=yes -o StrictHostKeyChecking=accept-new the same; unchanged
the same, ssh_config accept-new and no BatchMode the same; unchanged
-o StrictHostKeyChecking=ask alone, ssh_config BatchMode yes and accept-new "Host key verification failed." with no question: why -o BatchMode=no is named too
changed key, -o BatchMode=no -o StrictHostKeyChecking=ask, ssh_config BatchMode yes the banner, "Host key for fakehost.invalid has changed and you have requested strict checking." and "Host key verification failed."; known_hosts unchanged
BatchMode=yes -o LogLevel=QUIET, unknown and changed key prints nothing: neither phrase, so the push gets the ruleset hint, as at main
BatchMode=yes -o LogLevel=FATAL, unknown and changed key prints "Host key verification failed." alone: a changed key reads as unknown

Under BatchMode=yes, ssh 9.6 printed Host key verification failed. alone for an unknown key. Run directly, it printed the same line with no terminal (setsid). Under StrictHostKeyChecking=yes it printed No 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 an ssh-keygen -R line, with BatchMode and without.

Tests

Each file ran alone at 631ea7c under the stub gh, with GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null and EMAIL set. The command was /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt/<name> -o tmp_path_retention_policy=failed tests/<file>.

file result
tests/test_scaffold_reuse_seeded_legs.py 121 passed in 58.51s (117 at 4009b65, plus 4 new ids; round 3's row is part of a constant and adds no id)
tests/test_ruleset_hint_tracking_branch.py 23 passed in 32.39s
tests/test_cli_reference.py 23 passed in 31.01s
tests/test_readme_sections.py 76 passed in 0.30s
tests/test_repo_hygiene.py 100 passed in 2.70s
tests/test_agents_md_codes.py 47 passed in 1.96s
tests/test_update_add_codes_documented.py, tests/test_no_auto_maintenance.py, the four importers' --collect-only run at bc27679 (6 passed; 58 passed and 2 skipped; 132 collected), not re-run after bc27679: rounds 2 and 3 change two constants' words, a docstring, comments and one test row

pyflakes (at 631ea7c): scaffold-project.py 0 warnings; test_scaffold_reuse_seeded_legs.py 15, the same as at 4009b65 (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". Only scaffold-project.py and tests/test_scaffold_reuse_seeded_legs.py hold any (SEE_THE_KEY, PUBLISHED and CHANGED_HOST_KEY_REMEDY there, as literals).

What the tests pin:

  • test_what_a_refused_push_prints has 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 whose remote: lines carry Host key verification failed. and REMOTE 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 a remote: 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, after REFUSED pushing, and no hint.
  • test_a_ruleset_s_refusal_on_github_says_what_its_re_run_can_wait_at covers a leg and the root against the ruleset hook. Its stderr must end with the hint's literal, then the paragraph.
  • In 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, plus test_ruleset_hint_tracking_branch.py -k scaffold for the ones that touch local). The file was then restored from a saved copy and checked with cmp; its sha256 was the same before and after each batch, and git show HEAD:scaffold-project.py equals the saved copy. M1 to M9 were run at bc27679 (5, 3 plus 6, 16, 10 plus 6, 5, 1, 1, 16 and 9 failed); M10 to M15 were run again at 5724035, with the changed words. Round 3 ran M13, M11, M11b and M15 at 631ea7c against the WHOLE reuse file (121 ids), each restored and checked with cmp against saved copies of both changed files.

mutant result
M1, the hint without the sentence on GitHub KILLED: 5 failed
M2, the sentence printed under --local-remote-dir KILLED: 3 failed in the reuse file, and 6 failed in test_ruleset_hint_tracking_branch.py
M3, a host-key failure taken for a ruleset refusal KILLED: 16 failed
M4, a ruleset refusal taken for a host-key failure (HOST_KEY_FAILED = ("failed",)) KILLED: 10 failed in the reuse file, and 6 in test_ruleset_hint_tracking_branch.py
M5, the changed-key case given the unknown-key remedy KILLED: 5 failed
M6, the legs' call site passes True for local KILLED: 1 failed
M7, the root's call site passes True for local KILLED: 1 failed
M8, no return after the host-key remedy, so the hint follows it KILLED: 16 failed
M9, the sentence said again after a login remedy KILLED: 9 failed
M10, -o StrictHostKeyChecking=ask dropped from HOST_KEY_CHECK KILLED: 16 failed (every host-key id)
M11, the changed-key remedy's "answering yes only to a fingerprint GitHub publishes, which can be another of its keys than the one you checked" dropped KILLED: 5 failed at 631ea7c (every changed-key id; 5 failed at 5724035 with the old words)
M11b, the same words cut after "GitHub publishes" (the clause on another of its keys dropped) KILLED: 5 failed at 631ea7c
M12, the match reads the remote: lines again KILLED: 4 failed (both new rows, on GitHub and on disk)
M13, output.split("\n") replaced by output.splitlines(), so a form feed (or \v, \x1c to \x1e, \x85, U+2028, U+2029) in a server's line cuts it in two KILLED at 631ea7c: 2 failed (the ruleset row with the form-feed line, on GitHub and on disk). At 5724035 it 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).
M14, gh api meta without --hostname github.com KILLED: 16 failed
M15, -o BatchMode=no dropped from HOST_KEY_CHECK KILLED: 16 failed (the same at 5724035 and 631ea7c)

Merges

I read the open PR list live at 5724035: 24 open PRs; at 631ea7c I 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 touches scaffold-project.py, tests/test_ruleset_hint_tracking_branch.py or tests/test_scaffold_reuse_seeded_legs.py. Main is now 038dafb. The branch is not merged with it, since the landing after the v1.2.0 tag updates the branch.

merge (git merge-tree --write-tree) result
head 631ea7c with origin/main 038dafb clean, tree e31a2f79dfd1
head with #1104 de89aa8 (touches scripts/shape_materialize.py, where RULESET_HINT lives) clean, tree 5a5177fbf65c; it does not change the hint
head with #1105 b28f40d (the same file) clean, tree 7a076a5ce10b; it does not change the hint
head with #1118 2307036 (the same file) clean, tree 31ea584ecbfe; it does not change the hint

Closes #1108

Lane: openreposhape-3 (openRepoShape-3)

🤖 Generated with Claude Code

…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>
@sourcery-ai

sourcery-ai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

The 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 refusal

sequenceDiagram
    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
Loading

Sequence diagram for GitHub ruleset refusal guidance

sequenceDiagram
    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
Loading

Flow diagram for scaffold push-refusal diagnostics

flowchart 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
Loading

File-Level Changes

Change Details Files
Adds targeted handling for SSH host-key refusals during pushes, separating them from ruleset refusals and providing safe GitHub fingerprint verification guidance.
  • Detects unknown and changed host-key failures from SSH output.
  • Prints remediation using ssh -T and gh api meta --jq .ssh_key_fingerprints.
  • Warns against accepting changed keys before fingerprint verification, then recommends the appropriate repository reuse re-run.
  • Suppresses the ruleset hint when SSH refused before a push was sent.
scaffold-project.py
tests/test_scaffold_reuse_seeded_legs.py
Makes ruleset refusal guidance explain the prompts that the GitHub reuse re-run can still encounter without changing shared hint text or local-remote output.
  • Appends the prompt warning only at the scaffold's GitHub call site.
  • Preserves byte-for-byte shared hints and leaves local-remote hints unchanged.
  • Centralizes re-run wording for login and host-key remedies.
  • Passes local-mode context through both push refusal call sites.
scaffold-project.py
tests/test_scaffold_reuse_seeded_legs.py
tests/test_ruleset_hint_tracking_branch.py
Expands regression coverage for host-key classification, remediation text, re-run states, and GitHub versus local ruleset output.
  • Adds unit and end-to-end cases for unknown, strict-checking, changed-key, first-run, and reuse-run scenarios.
  • Verifies host-key remedies exclude the ruleset hint and ruleset hints include the prompt paragraph only on GitHub.
  • Retains existing login, ruleset, tracking-branch, and local-output behavior.
tests/test_scaffold_reuse_seeded_legs.py
tests/test_ruleset_hint_tracking_branch.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1108 When a ruleset refusal on GitHub recommends a --reuse-empty-repo re-run, also tell the user that ssh, credential helpers, or askpass may still cause that run to wait, without adding that sentence to the local-remote-dir output or duplicating it after a login remedy. ✅
#1108 Detect push failures caused by SSH host-key verification, distinguish them from ruleset refusals, and provide a host-key-specific remedy instead of the ruleset hint, including safe guidance for changed keys and an appropriate re-run. ✅

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(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 "ssh -T -o StrictHostKeyChecking=ask ...", and the changed-key branch ends "keep the new one (...), answering yes only to the fingerprint you checked". Update SEE_THE_KEY and CHANGED_HOST_KEY_REMEDY (tests/test_scaffold_reuse_seeded_legs.py:1452, :1472), and add a mutant that drops the new words.

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:

  • :2173 gh api meta follows GH_HOST, and the scaffold only pushes to https://github.com/, so gh api --hostname github.com meta --jq .ssh_key_fingerprints pins the list. gh prints the values without ssh's SHA256: prefix.
  • :2157/:2168 match by substring, remote: lines included; skipping lines that start with remote: would stop server-sent text from switching the remedy.
  • :2146-2156 leave out LogLevel=QUIET (neither phrase, ruleset hint as at main), LogLevel=FATAL (a changed key reads as unknown) and a revoked key (unknown-key remedy); all three are safe.
  • :2189 "fails the push before git sends anything" is untrue for LOGIN_REFUSED; say "before any ref is updated".
  • :534-535 the hint constant says nothing itself; _refused_push appends RULESET_HINT_PROMPTS.

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 ssh -T -o StrictHostKeyChecking=ask, the changed-key branch says to answer yes only to the fingerprint you checked, SEE_THE_KEY and CHANGED_HOST_KEY_REMEDY follow, a new mutant drops the new words, and the body ends with Closes #1108. RV1130 then re-checks the new head. Lane 3 files (a) as a follow-up issue. The fix round starts when the shared window allows. #1130 stays a draft with no milestone and lands after the v1.2.0 tag, on a later word.

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>
@brettheap

Copy link
Copy Markdown
Contributor Author

(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 remote: prefix, whole") are untrue, and the row at :1727 ("remote: see the log\rHost key verification failed.") is a shape git never prints. Measured with git 2.43: a pre-receive hook writing "see the log\rHost key verification failed.\n" comes out as "remote: see the log \rremote: Host key verification failed. "

  • git starts a new prefix after every \r. The behaviour is still right (_host_key_remedy returns "" on that real output). A split at every separator would be right too, so M13 is equivalent on anything git prints. Fix: use git's real shape in the row, and say in both comments that git prefixes each line, and each part after a \r, with remote:, so a line starting with it is the server's from end to end.

NITs:

  • scaffold-project.py:2200 a BatchMode yes in ssh_config still makes ssh -T -o StrictHostKeyChecking=ask refuse without asking (measured); -o BatchMode=no first beside it makes ssh ask (measured). Safe either way; optional.
  • The body: Sourcery appended its "Summary by Sourcery" block after the robot line again. It holds no closing keyword and is ASCII; strip it just before landing if the robot line must come last.
  • bc27679's trailer says Claude Sonnet 5.5 (4009b65 says Opus 5.5); set it in the explicit squash message.

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 remote:. N1 is taken: -o BatchMode=no goes first beside -o StrictHostKeyChecking=ask, with a measurement and a mutant. A fresh opus reviewer then re-checks that head. The Sourcery block is stripped and 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)

…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>
@brettheap

Copy link
Copy Markdown
Contributor Author

(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:

  • scaffold-project.py:2322-2323 (docstring :2303, pin tests/test_scaffold_reuse_seeded_legs.py:1490): "answering yes only to the fingerprint you checked" can name a key ssh will not show. ssh asks first for the key types it holds: ssh -vv with an RSA key held proposes rsa-sha2-512 first, with none held ssh-ed25519 first. So 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 one checked. Safe (a "no" leads to the unknown-key remedy on the next refusal), but GitHub's 2023 RSA change is exactly this case. Fix: "answering yes only to a fingerprint GitHub publishes, which can be another of its keys than the one you checked".
  • Body: line 33 says RV1130 ran gh api meta --jq ...; round 2 ran the --hostname github.com form once, exit 0. "Left out of scope" says "the same -o StrictHostKeyChecking=ask"; it is now -o BatchMode=no -o StrictHostKeyChecking=ask.
  • Body: the two headings under "What changed" describe main's behaviour, not the change. Optional.

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>
@brettheap

Copy link
Copy Markdown
Contributor Author

(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 ssh -T is a new connection, which asks first about a key type ssh holds". That is the push's connection. After ssh-keygen -R, ssh holds no key for the host (measured: an RSA-only known_hosts goes to 0 bytes, and ssh -vv then proposes ssh-ed25519-cert... first, ssh's default). Suggested: "the push's 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, and it can be sent another key than the push's". The body's N1 bullet is right.

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 tests then runs on that head, and lane 3 checks that diff. #1130 is READY once tests passes there. It stays a draft with no milestone, and lands after the v1.2.0 tag on a later word, with the trailer set in the explicit squash message.

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>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-21T23:39:16Z) #1130 is READY at dee55dc: RV1130c's condition is met.

Lane: openreposhape-3 (openRepoShape-3)

@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026

Copy link
Copy Markdown

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 @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 01c1ac3 into main Oct 23, 2026
5 checks passed
@brettheap
brettheap deleted the l3-1108-ruleset-hint-prompts branch October 23, 2026 13:26
brettheap added a commit that referenced this pull request Oct 23, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant