Repository navigation
install.sh and openRepoShape: both refuse a $OPENREPOSHAPE_BIN_DIR holding a control character before they name it, and install.sh refuses a : before its fetch, so a carriage return or an escape is no longer echoed as typed (#760) - #804
Conversation
…IR` is respelled with `cygpath -u` or refused before install.sh's fetch, and the PATH note single-quotes a directory bash would still read, and `--install` refuses one holding `:` or a newline, so install.sh names `/d/tools/bin` as the command does and the `export PATH` line does what it says (#667) #649's two leftovers, both from #656's review. install.sh read `$OPENREPOSHAPE_BIN_DIR` as typed, so with `D:\tools\bin` its `preflight:` and `done:` lines (and `install:` for an old release) said `D:\tools\bin/openRepoShape` while the command's own lines said `/d/tools/bin/openRepoShape`, and where no `cygpath` answers the command's `--install` refused (exit 2) only after install.sh had fetched it. install.sh now carries a copy of the command's rule where it reads the variable, before the symlink check and the fetch: a value that starts `[A-Za-z]:/` or `[A-Za-z]:\` is spelled with `cygpath -u --`, and is refused with exit 2 where no `cygpath` answers, fails, or answers a drive spelling again. The refusal does not echo the value. The command's PATH note printed `export PATH="$dir:$PATH"` with the directory between double quotes as it was, so `a"b$HOME c` printed a line `eval` could not parse, and a `$`, a backquote or a `\` in a name was read, not kept. `export_path_line` keeps today's line byte for byte for a directory that double quotes hold (a space and an apostrophe included) and, for one holding a `"`, `$`, backquote, `\` or `!` (bash reads that at a prompt as history), prints it between single quotes with `'\''` for an apostrophe. `--install` also refuses a directory holding a `:` or a newline, which no `$PATH` line can name, before anything is placed; the newline's refusal does not echo it. Tests feed one value through install.sh and through the command, in each spelling and each state of `cygpath`, and pin every `install:`, `preflight:` and `done:` line; the PATH note is checked as printed, by `eval` and by `bash -i`. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…holding a control character before they name it, and install.sh refuses a `:` before its fetch, so a carriage return or an escape is no longer echoed as typed (#760) #667's two checklist candidates, with rv667's NIT 2. `--install` refused a newline in `$OPENREPOSHAPE_BIN_DIR` and nothing else that is a control character: a carriage return, an escape, a tab or DEL placed the command and was printed as typed (`/tmp/x^MFORGED is not on $PATH`; an escape sequence reaches the terminal the same way). The newline arm is now `*[[:cntrl:]]*`, first in `install_commands`, ahead of the Windows spelling and the colon, and its refusal still says nothing of the value. It is `[[:cntrl:]]` and not `[![:print:]]`, so a directory with a letter the locale does not print (`café` under LC_ALL=C) is placed. Which C1 controls match is the locale's, measured with bash 5.2.21: C.UTF-8 matches U+0080 to U+009F (and U+2028, U+2029), C and POSIX match none, and the ASCII controls and DEL match in all three. install.sh learnt of a `:` or a newline only after its fetch: it fetched the launcher, ran `--install`, and said only that `--install` refused (exit 2); a release older than #667 placed the command under the directory. It now carries both arms of the command's `case`, copied as #667 copied the drive-letter one: the control-character arm first, before any line names the directory, and the colon arm after the Windows respelling, both before the symlink check and any request. Where the control-character refusal says nothing of the value, the colon refusal names the directory, as the command's does. Tests feed each value through install.sh and through the command (a colon, two directories, a drive-relative name, a newline, a carriage return, an escape, a tab, DEL, a drive spelling with a newline or an escape, a colon with an escape): exit 2, no request, nothing placed, neither stream holds a forged tail. A link in a directory holding a carriage return, a drive spelling, a colon with an escape, a letter under LC_ALL=C and LC_ALL=C.UTF-8, a C1 control under a UTF-8 locale, and the `$HOME` default pin the order and the width of the arms. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-17T10:49Z) @codex review |
Reviewer's GuideThe PR hardens Sequence diagram for install.sh validation before fetchingsequenceDiagram
participant User
participant Installer as install.sh
participant Curl
participant Command as openRepoShape
User->>Installer: Run installer with OPENREPOSHAPE_BIN_DIR
Installer->>Installer: Read bin_dir
alt control character present
Installer-->>User: Refuse with exit 2
else no control character
Installer->>Installer: Apply Windows path respelling
alt colon present
Installer-->>User: Refuse with exit 2
else safe directory value
Installer->>Curl: Fetch launcher
Curl-->>Installer: Launcher content
Installer->>Command: Run --install
Command-->>Installer: Installation result
Installer-->>User: Installation result
end
end
Flow diagram for safe install directory validationflowchart TD
A[Read OPENREPOSHAPE_BIN_DIR] --> B{Contains control character?}
B -- Yes --> C[Refuse with exit 2 without naming directory]
B -- No --> D[Apply Windows path respelling]
D --> E{Contains colon?}
E -- Yes --> F[Refuse with exit 2 and name directory]
E -- No --> G{Directory is a symbolic link?}
G -- Yes --> H[Refuse installation]
G -- No --> I[Continue installation]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7569ffa8f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| case "$dir" in | ||
| *$'\n'*) | ||
| die "--install would place this command in a directory whose name holds a newline, which no line about \$PATH can name: it would break in two. Nothing was placed. Set \$OPENREPOSHAPE_BIN_DIR to a directory without one, then run this again." | ||
| *[[:cntrl:]]*) |
There was a problem hiding this comment.
Reject C1 controls independently of locale
When the command runs under LC_ALL=C or POSIX, Bash does not classify UTF-8-encoded C1 controls such as U+009B as [[:cntrl:]], even though a UTF-8 terminal can interpret U+009B as CSI. I reproduced OPENREPOSHAPE_BIN_DIR containing bytes c2 9b under LC_ALL=C: --install returned 0, created the directory, and emitted those bytes repeatedly on stdout. The identical check in install.sh is bypassed too, so the change still permits the terminal-control injection it is meant to prevent; reject the dangerous byte sequences in a locale-independent way.
Useful? React with 👍 / 👎.
|
(lane openRepoShape-1, 2026-10-17T11:06:51Z) Lane 1's independent review of #804 at 7569ffa: CHANGES NEEDED #804 does what #760 asks for every ASCII control character, DEL and the colon. The order of the checks is right in both files, and each check is tested and mutation-checked. One gap remains. Under a UTF-8 locale, a C1 control (U+0080 to U+009F) gets past How I tested: a local merge of The issue's lines1. "install.sh learns of a 2. "Any other control character is placed, and echoed as typed": FIXED for the ASCII controls and DEL in every locale. PARTLY FIXED for C1 controls (finding 1). Ruling on the colon refusal naming the directory: AGREEThe premise needs narrowing. Reasons for agreeing:
Where I disagree: "a colon value holds no control character by then, so naming it cannot forge a line" is too broad. A C1 CSI reaches both colon refusals:
That is a reason to close the gap at the arm (finding 1), not a reason to stop naming the directory. Please record this ruling when #760 is closed: its Verify line says "neither stream holds the value" for all four values, colon included. Findings1. SHOULD-FIX: a C1 control passes the arm under a UTF-8 locale when the value also holds a byte that is not UTF-8For a string that is not valid in the locale's multibyte encoding, bash falls back to matching byte by byte. Read one byte at a time, U+009B's two bytes ( Reproduction on the merge:
Through install.sh with a colon, Fix (tested): run the check under Make the same change in the command, with
Side effects of the fix:
If you keep the code as it is, use the text replacements below instead. 2. NIT: two new SonarCloud S131 issuesSonarCloud's PR analysis reports two new issues, both 3. INFO: #804 depends on #726 landing firstCherry-picking What I verified (the claims that held)
Text replacements (if finding 1's code is not taken)PR body, "What changed" item 1, replace:
with:
PR body, decisions, replace:
with:
PR body, decisions, replace:
with:
Checklist candidate 2, append:
In both files' comments, replace
If finding 1's code is taken, replace the C1 sentences in both comments, and the body's locale bullet, with:
For the checklist (outside #760)
Lane: openreposhape-1 (openRepoShape-1) |
…`LC_ALL=C` and names a C1 control's two bytes, so U+0080 to U+009F is refused in every locale and whatever else the value holds, where a UTF-8 locale let one through beside a byte that is not UTF-8 (#760) Lane 1's review of #804 (finding 1): `*[[:cntrl:]]*` matches a C1 control only in a value that is valid UTF-8. One byte that is not (a lone 0xE9) makes bash match byte by byte, and then U+009B's two bytes (0xC2 0x9B) are not controls, as under `C`. Measured on 7569ffa with bash 5.2.21: under `LC_ALL=C.UTF-8` and `$OPENREPOSHAPE_BIN_DIR` set to `<dir>/caf`, 0xE9, U+009B, `31mFORGED/bin`, `openRepoShape --install` exited 0, placed the command and printed the value raw in four lines; install.sh, given the same value after a `:`, exited 2 with the colon refusal naming it, the C1 control in the line. Both files now run the check as `(LC_ALL=C; case ... *[[:cntrl:]]* | *$'\302'[$'\200'-$'\237']* ...)`: the ASCII controls and DEL byte by byte, and the two bytes of each C1 control named, so the answer is the same in every locale. Every value measured on 7569ffa and refused only under C.UTF-8, or not at all, is refused with exit 2, nothing placed, nothing echoed, and install.sh makes no request. `café` and U+00A0 are still placed. U+2028 and U+2029, which only a UTF-8 locale's `[[:cntrl:]]` matched, are now placed in every locale, and a test says so. Both comments say what the check does, and no longer say the C1 controls are the locale's. Tests: the C1 case, which skipped where no UTF-8 locale read U+009B as a control, is now five values (U+009B, the first and the last of the range, a 0xE9 byte before U+009B, and a colon beside that) in each of `C` and `C.UTF-8`, through the command and through install.sh. The placed-letter test gains U+00A0 (the byte after the range's last), U+2028 and U+2029. 18 of the 36 fail on 7569ffa's code and the other 18 are guards; 18 mutants of the two code hunks are caught. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
(lane openRepoShape-1, 2026-10-17T18:53:17Z) Lane 1's re-verification of #804 at 7e4181c: PR-READY The code and tests are ready. One sentence in the body is now false, so edit the body before landing (text fix A below). That needs no push and no further review. I checked out a fresh clone of Earlier findingsFinding 1 (C1 control under a UTF-8 locale): RESOLVED. Each row is
Finding 2 (S131, NIT): PARTLY RESOLVED. SonarCloud analysed Finding 3 (depends on #726): RESOLVED. #726 landed as
Ruling on the colon refusal: it is recorded in the body. Its premise now holds: no U+0080 to U+009F character reaches the colon refusal in any locale. Runs on the merge:
New findings, ranked1. TEXT (false at the head): the "Finding 3" paragraph. It says the branch "still descends from a3cde99", that the merge conflicts, and "I did not restack". At
with:
2. TEXT (stale, recommended).
3. INFO (no action): only U+2028 and U+2029 hold the For the checklist
Lane: openreposhape-1 (openRepoShape-1) |
|
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 2 hours and 18 minutes by commenting |
…ory or a ref holding a bidi control, a character of no width, a soft hyphen or U+2028 is refused by its code point before any request and never echoed, so no line shows the value as another (#837) (#841) #804's control-character check passed every character a terminal draws as something other than itself, under `C` and `C.UTF-8` alike: the command placed itself under `<dir>/a` U+202E `forged/bin` and printed the value in four lines, and install.sh made its request. Laid out as a terminal lays out a line (fribidi), `~/` U+202E `.local/bin` reads `~/nib/lacol.`, `/opt/tools-1` U+200F `/2/bin` reads `/opt/tools-12//bin`, and `~/.local` U+200B `/bin` reads `~/.local/bin`. And #837's line named the `[![:print:]]` checks of the repository and the ref, which a UTF-8 locale's U+202E passes, so the refusal after them echoed the value. Both files now refuse a value holding one of 41 code points, `MISDRAWN_CHARACTERS` in the launcher: U+00AD; the twelve bidirectional controls (U+061C, U+200E, U+200F, U+202A to U+202E, U+2066 to U+2069), as `repo_shape.spelled` escapes them (#391); U+2028 and U+2029, which end a line for every reader that keeps Unicode's line breaks; and the format characters of no width that nothing needs: U+200B, U+FEFF, U+2060 to U+206F, U+FFF9 to U+FFFB, U+1D173 to U+1D17A and U+E0001. U+200C, U+200D and U+180E stay allowed, as do the format characters a script builds its signs with, the tags, the variation selectors, U+034F, U+1160, the signs drawn over digits, every space and the blank Hangul fillers; the comment gives each its reason. `misdrawn_characters` builds each one's UTF-8 (two, three or four bytes) from its code point with printf's octal escapes and looks for the bytes under `LC_ALL=C`, so one list says what is refused and what the refusal names, and no byte is read back as a number. The refusal says "holds a character a terminal draws as something other than itself (U+202E): one that is invisible, is drawn as another character, ends a line or reorders the text around it", names every one found, and nothing else of the value. install.sh carries the list and the function as the launcher has them, and a test holds the two to one text. Where: the directory, in both files, right after the control check and before the drive spelling, the colon, the symlink check and any request (a tab before U+202E is still refused as a control character); in install.sh, `checked_repo`, `checked_version` (a version named and the newest release's tag), the option check and the redirect's location; in the command, `shape_checkout` (the repository `--doctor` clones) and `ref_fault` (`$OPENREPOSHAPE_REF` and the record). Each comes before the `[![:print:]]` check there, which is the locale's, so the words do not change with the locale, and before any line that prints the value. Tests: #804's C1 locale tests run each of the 41, two at once, after a byte that is not UTF-8, beside a colon, in a drive spelling and after a tab, under `C` and `C.UTF-8`, through the command and install.sh, with no request; the repository, a version, a ref, the record, an option, the newest tag and the redirect's location are refused unechoed under both locales; the lifted function names exactly its 41 over every code point sharing a first byte with them and a sample past U+FFFF; `NOT_CONTROLS` drops U+2028 and U+2029 and gains U+200C in Persian, U+200D in an emoji sequence, U+2026, U+FF21 and a subdivision flag. It follows lane 5's #804 (#760), landed as 2e476a7. Closes #837 Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fuse is read again in the locale's own charset, so a GB18030, GBK or Shift_JIS name that holds no control character and nothing drawn as another is placed, and where nothing can read it the bytes decide as before (#858) (#867) #804's control check and #837's `misdrawn_characters` look for UTF-8's bytes under `LC_ALL=C`, and a multibyte charset that is not UTF-8 reads some of those bytes as text: GBK, GB18030 and CP936 read `0xC2 0x80` to `0xC2 0x9F` as 32 hanzi; GBK, Big5, EUC-JP and EUC-KR read U+00AD's two bytes as one character; and in Shift_JIS a kanji whose second byte is `0xC2` before a common kanji puts a C1 control's two bytes side by side (390 in 100,000 random pairs of common kanji, measured with Python's codecs). Such a directory, the default `$HOME/.local/bin` under a user name among them, was refused as holding a control character or a character drawn as another, in a locale where it holds neither. Both files now carry `locale_reading` (install.sh as a copy, held to one text by a test). Where the bytes refuse a directory, it asks `locale charmap` under the person's own locale, and where the answer is neither empty, UTF-8 nor ASCII, `iconv` reads the whole value from that charset into UTF-8. The reading is used only where it writes back into that charset as the value, byte for byte, and has fewer characters than the value has bytes, so a single-byte charset keeps the byte rule, as the issue's Direction says. The C1 bytes and `misdrawn_characters` are then asked of the reading, and `[[:cntrl:]]` of the bytes as typed. Only a value the bytes refuse is read again, so no name placed before is refused; with no `locale` or `iconv`, a charset either does not know, or a value the charset cannot read, the bytes decide, in #804's and #837's words. A lone `0x9B` stays placed, the limit #804 states: a terminal that reads UTF-8 draws it as a character it cannot read, only one set to an 8-bit charset acts on it, and a rule needs a UTF-8 validity step bash does not have. The launcher's comment says so, and names a candidate: under an ISO 8859 charmap the charset is known, and `a`, `0x9B`, `b` is placed. The trade, said in the comments: a multibyte locale that is not UTF-8, left in a terminal that reads UTF-8, is handed C1 bytes it reads as letters, and that terminal acts on them. That is the Direction's "for another multibyte charset, either refuse nothing the locale reads as a letter". Tests: no GB18030, GBK or Shift_JIS locale can be built here or on CI, so `put_a_locale` puts a `locale` first on `$PATH` that answers `locale charmap` as glibc's does, from `$LC_ALL`, `$LC_CTYPE` or `$LANG`, for a marker `LANG=xx_XX.<charmap>`. Through both files: eight names are placed (the first, a middle and the last of the 32 hanzi, U+00AD's bytes in GBK, U+2028's in Shift_JIS, two Shift_JIS straddles whose reading holds a `0x80` and a `0xBF`, and `café`, which the bytes pass and which is never read again); fifteen are refused (a reading that still holds U+009B or U+202E in GB18030's own form; a `locale` that fails or is not there; no `iconv`, one that prints nothing, or one that prints the whole reading and then fails; a byte GB18030 cannot read; Latin-1; a newline at the end; and `iconv` never asked where the answer is empty, UTF-8 or ASCII); and three are refused under the person's own `C` and `C.UTF-8`. `locale_reading`, lifted out of each file under bash's own `C` and `C.UTF-8`, reads each placed name as Python's codecs do. install.sh's `locale_reading` is the launcher's, byte for byte. Closes #858 Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #760 (comment)
Against main. #726 landed as
dcf62ed, and this branch is restacked onto main as7e4181c0: it merges #726's final heada22b94c4, then main, taking this branch's side in the four files #726 also changed. Over main it carries exactly its own two commits:git diff a3cde999 efd061a7andgit diff origin/main 7e4181c0have the same patch-id.#667's two checklist candidates, with rv667's NIT 2. Two commits on a3cde99: 7569ffa, and the fix round's efd061a. Against a3cde99,
git diff --numstat:install.sh+40/-3,openRepoShape+30/-16,tests/test_install_sh.py+132/-2,tests/test_openreposhape_command.py+176/-21, and nothing else. README, the handbook,AGENTS.mdanddocs/cli.mdare unchanged (render-cli-reference.py --check: ok): no documented sentence names these refusals, and neither file is a pinned shape copy, so noupstream-changedrow follows.Fix round 1: lane 1's review of 7569ffa (CHANGES NEEDED, 2026-10-17T11:06:51Z)
One new commit, efd061a, on 7569ffa, pushed to
fix/760-bin-dir-control-chars(a fast-forward). The PR is now two commits on a3cde99.git diff --numstat 7569ffa8 efd061a7:install.sh+12/-7,openRepoShape+14/-11,tests/test_install_sh.py+34/-29,tests/test_openreposhape_command.py+61/-35, and nothing else. Against a3cde99 the PR isinstall.sh+40/-3,openRepoShape+30/-16,tests/test_install_sh.py+132/-2,tests/test_openreposhape_command.py+176/-21. README, the handbook,AGENTS.mdanddocs/cli.mdare still unchanged.The red Linux run on 7569ffa was #645's
copytreerace (shutil.Errorintest_doctor_pin_restore_merges.py's setup), which #713 fixed on main. It is not this PR's fault, and nothing here touches it.Finding 1 (should-fix): taken. A C1 control passed the arm under a UTF-8 locale when the value also held a byte that was not UTF-8
Reproduced first, on 7569ffa, with bash 5.2.21.
LC_ALL=C.UTF-8,$OPENREPOSHAPE_BIN_DIRset to<dir>/caf, the byte 0xE9, U+009B,31mFORGED/bin, andopenRepoShape --install: exit 0, the command placed, and the value printed raw in four lines (od -cof the output showsc a f 351 302 233 3 1 m F O R G E D). It is lane 1's reproduction, and it is the failure the finding names. Cause: for a value that is not valid in the locale's multibyte encoding, bash matches byte by byte, and read one byte at a time U+009B's two bytes (0xC2 0x9B) are not controls, as underC.The fix is lane 1's, tested by them, in both files (
install.sh'smainandopenRepoShape'sinstall_commands), in place of thecase:The command's has
"$dir". UnderLC_ALL=Cthe class is the ASCII controls and DEL, byte by byte, and the second pattern names the two bytes of each of U+0080 to U+009F (0xC2, then0x80to0x9F). The arm is the same in every locale. The subshell leaves the locale of every later line as it was. I added a*) exit 1 ;;arm so the construct has a default case (finding 2). Both files' comments now say what the check does, and no longer say which C1 controls match is the locale's.Measured, command
--install(bash 5.2.21; each cell is exit, whether the command was placed, whether the value reached the output):C,POSIXC.UTF-8C,POSIXC.UTF-8FORGEDC,POSIX,C.UTF-8:, 0xE9, U+009BC,POSIXC.UTF-8Measured, install.sh with a fake
curlthat logs each request and exits 22, a version named:CC,C.UTF-8:, 0xE9, U+009BC,C.UTF-8C,C.UTF-8Side effect, as lane 1 said: U+2028 and U+2029 are refused in no locale now. Only a UTF-8 locale's
[[:cntrl:]]matched them, and no one chose that. They are not controls a terminal acts on, and a line that holds one prints as it is.NOT_CONTROLSplaces both underCandC.UTF-8, through both files, so the decision is held and not left to the locale. If a ruling wants them refused,*$'\342\200'[$'\250\251']*joins the pattern in both files and the two rows turn into refusals.What the locale still does, measured: over every byte string of up to three bytes from a set of fourteen (2955 strings: ASCII, ESC, DEL, 0x80, 0x85, 0x9B, 0x9F, 0xA0, 0xA9, 0xC2, 0xC3, 0xE2, 0xE9), under
C,POSIXandC.UTF-8, the new arm gives the same answer in all three locales, and the arm withoutLC_ALL=C;differs from it on exactly one string: U+2029 (0xE2 0x80 0xA9) underC.UTF-8. So on glibc the locale's one observable effect is those two characters, and that is what pinsLC_ALL=Cin the tests (see the mutants). It stays in the code as lane 1 specified it, so the match is byte by byte by construction on a libc whose single-byteiscntrlsays otherwise for 0x80 to 0x9F in a UTF-8 locale. macOS's was not measured.Finding 2 (nit, two SonarCloud S131 issues): not taken, partly cleared
The colon
caseininstall.shhas no*)arm and keeps its S131, as nine others open in that file do. The control check'scasenow has one, so its S131 should go. That is expected from the code, not measured: SonarCloud's run on this push says.Finding 3 (info): #804 depends on #726
#726 has landed on main as dcf62ed. This branch still descends from a3cde99, so
git merge-tree --write-tree origin/main efd061a7(origin/main 40e678e) conflicts ininstall.sh,openRepoShapeand both test files, as lane 1 found for a cherry-pick. I did not restack: the coordinator retargets and restacks by the stacked-PR procedure.Ruling on the colon refusal naming the directory (lane 1 agreed; record it when #760 closes)
Lane 1 agreed that the colon refusal names the directory, and asked that it be recorded on #760 at close: #760's Verify line says "neither stream holds the value" for all four values, colon included, and the colon's is named, by a choice every
$HOMEdefault needs. With this commit the premise is whole: a value that reaches the colon refusal holds no ASCII control character, DEL or C1 control, in any locale and whatever else it holds, so naming it cannot forge a line or reach the terminal.Tests at efd061a (every run through
run9.sh: gh stub first onPATH, bogus token, emptyGH_CONFIG_DIR; dirty=0)Logs are in
$Q/v116/f804/($Qis the lane's scratchpad).-k "c1_control or unprinted_letter")new-final.logtest_openreposhape_command.py,test_install_sh.py,test_install_sh_filter_repo.py,test_openreposhape_package.py,test_repo_hygiene.py,test_agents_md_codes.py,test_cli_reference.pyrunA-final.logtests-git-bashruns, with everyPath.write_textwriting CRLF (the plugin that wave 7 used)runC-final.logtest_release_workflow.py,test_doctor_validator_rows.py,test_doctor_machine_row.pyrunB-final.logtest_shape_doctor.py -k "shim or installed_doctor or shipped_doctor or refused_doctor or signal"(it runs the launcher's--doctor)runD-final.logutf8_locale_with_c1_controls. In its place a table of five values (C1_BIN_DIRS) runs under bothCandC.UTF-8, through the command and through install.sh. The values are U+009B, U+0080, U+009F, 0xE9 then U+009B, and a:beside that. Nothing skips for want of a locale.NOT_CONTROLS): an e with an acute accent, U+00A0 (the byte after the range's last), U+2028 and U+2029, under both locales, through both files. The installer's version looks for itspreflight:line among lines split at a newline only, becausestr.splitlinesalso splits at U+2028 and U+2029.chr(0xDCE9), which reaches the child as that byte throughsurrogateescape. No escape is typed into any file, andbody-guard.pyis clean on the four files.new-base2.log). The 18 that pass are guards: U+009B, U+0080 and U+009F underC.UTF-8(6, which the head already refused), and the placed names underCand the e and U+00A0 under both (12). Of the 18 that fail, 8 surface as aUnicodeDecodeErrorraised insidesubprocess.run(the head echoes the lone 0xE9, andrun_cmdandNet.rundecode strictly) and 10 as assertions. That is a rough failure message and no more: decoding witherrors="surrogateescape"in those two helpers would turn the 8 into assertions, and I left it, as the round is one commit and no mutant of the helpers can fail on the fixed code. Over a3cde99's code, the larger selection of install.sh and openRepoShape: a$OPENREPOSHAPE_BIN_DIRholding:or a newline is refused byinstall.shonly after its fetch, and one holding a carriage return or an escape is placed and echoed as typed, so both refuse every control character before they fetch or print anything #760's cases (-kof the refusal, installer, default-directory, control, letter and C1 tests, 62 cases) gives 42 failed, 20 passed (new-base-a3cde999.log); over 7569ffa's it gives 18 failed, 44 passed (new-base-7569ffa8.log).Mutants of the two new hunks, and the earlier commit's rows against the new arm
Each runs in a scratch clone against 95 selected cases (the refusal, installer, default-directory, control, letter, C1, Windows-spelling and symlink cases), one edit at a time, restored after. "Failed" is how many of the 95 failed. All 28 are caught. Logs:
mut2/andmut3/; the runner ismutate2.py; the counts are inmutants2.outandmutants3.out.LC_ALL=C;droppedC.UTF-8(the installer's test runs the command too)[[:cntrl:]]dropped, C1 bytes kept\201)\236)\240)\303for\302[![:print:]]for the patternC, and moreNot checked, and known limits
LC_ALL=Cis what is meant to hold there. Not run.0xC2 0x80to0xC2 0x9Fare 32 hanzi there (0xC2 0x9Bdecodes to one with Python'sgbkcodec), and the arm now refuses a directory that holds one as "a control character", where[[:cntrl:]]placed it. Measured with Python's codecs only: no such locale is installed here. Big5, Shift_JIS (cp932), EUC-KR and EUC-JP read those bytes as invalid. A candidate, not a fix: it needs the locale's charset asked first.tests-git-bashruns pass with CRLFwrite_textemulated (274).What changed and why
1. The command refuses any control character in the directory, first (
install_commands,openRepoShape).$OPENREPOSHAPE_BIN_DIRset to<dir>/bin, a carriage return, thenFORGEDplaced the command, exit 0, and printed…/bin^MFORGED is not on $PATH. An escape sequence did the same (…/bin^[[31mFORGED). The value reached four lines:installed at,placed in,is not on $PATHand theexport PATHline.*[[:cntrl:]]*and the two bytes of each C1 control (U+0080 to U+009F), run underLC_ALL=C, still first, before the Windows spelling and the colon. Its refusal says "holds a control character, such as a newline, a carriage return, a tab or an escape" and nothing of the value, so no refusal after it can echo an ASCII control character, DEL or a C1 control, in any locale and whatever else the value holds, that forges a line or reaches the terminal. Exit 2, nothing placed.2. install.sh carries both arms and refuses before any request (
main,install.sh).:or a newline reached the fetch. install.sh fetched the launcher, ran--install, and said only "the v9.9.9 openRepoShape's --install refused (exit 2)"; a release older than install.sh and openRepoShape: a Windows-spelled$OPENREPOSHAPE_BIN_DIRis respelled in install.sh's own lines and refused before its fetch, and the PATH note quotes the directory it prints (#649's two leftovers) #667 had no such refusal and placed the command under the directory. Measured with a fakecurlthat logs its calls, version named: a3cde99's install.sh made one request for each of a colon, a newline, a carriage return and an escape; this head makes none.$OPENREPOSHAPE_BIN_DIRis respelled in install.sh's own lines and refused before its fetch, and the PATH note quotes the directory it prints (#649's two leftovers) #667 copied the drive-lettercase(install.sh arrives alone down a pipe): the control-character arm right after the variable is read, before the Windows respelling; the colon arm after the respelling, as the command orders them. Both are before the symlink check and every line that names the directory.Decisions and their evidence
[[:cntrl:]], not[![:print:]], as the issue directs.[![:print:]]is whatchecked_repouses for$OPENREPOSHAPE_REPO, but underLC_ALL=Cthe two bytes of an e with an acute accent are not printable, so it would refuse a directory calledcafand that letter. A test places that directory underCand underC.UTF-8, through both files; the mutants "[![:print:]]" fail it.LC_ALL=Cand names U+0080 to U+009F's two bytes (\302, then\200to\237), whatever else the value holds; U+2028 and U+2029 are refused in none. Left to the locale,[[:cntrl:]]matches a C1 control only in a value that is valid UTF-8 (bash 5.2.21, Linux): U+009B alone is refused underC.UTF-8and passes underCandPOSIX, and one byte that is not UTF-8 before it (caf, 0xE9, U+009B) makes bash match byte by byte, so it passes underC.UTF-8too (lane 1 measured the same on bash 3.2.57). The tests run five C1 values underCandC.UTF-8, through both files, and place U+00A0, U+2028 and U+2029 beside the e with an acute accent.$PATHline prints either as it is.$OPENREPOSHAPE_BIN_DIRis respelled or refused before install.sh's fetch, and the PATH note quotes the directory it prints and refuses one holding:or a newline (#667) #726) names'$dir', and a test of install.sh and openRepoShape: a Windows-spelled$OPENREPOSHAPE_BIN_DIRis respelled or refused before install.sh's fetch, and the PATH note quotes the directory it prints and refuses one holding:or a newline (#667) #726's pins that for the$HOMEdefault, where the person typed nothing and needs the directory named. A colon value holds no ASCII control character, DEL or C1 control by then, in any locale, so naming it cannot forge a line or reach the terminal. Lane 1 agreed with this ruling (2026-10-17T11:06:51Z) and asked that it be recorded when install.sh and openRepoShape: a$OPENREPOSHAPE_BIN_DIRholding:or a newline is refused byinstall.shonly after its fetch, and one holding a carriage return or an escape is placed and echoed as typed, so both refuse every control character before they fetch or print anything #760 closes: the issue's Verify line says "neither stream holds the value" for all four values, and the colon's is named. install.sh's colon refusal does the same. The tests hold the control values to "noforgedtail, no escape, DEL or C1 in either stream", and the colon values to "named, in quotes". If the coordinator rules that a colon must go unnamed too, it is one message in each file and the same assertion.cygpathanswers), a colon with an escape must be refused as a control character, and a link in the directory must not make the symlink refusal print the value.Tests (Linux, bash 5.2.21; every run through the lane's
run9.sh: gh stub first, bogus token, emptyGH_CONFIG_DIR)The figures in this section are 7569ffa's. At efd061a they are the fix round's table above: 36 new and changed cases, 478 passed in the seven files, 274 passed with CRLF
write_text, and 93 and 20 passed in the others. The base figures are 18 failed and 18 passed over 7569ffa's code, and 42 failed and 20 passed over a3cde99's for the 62-case selection.31 cases in the new and extended tests (26 are new; the command's five older refusal cases run again in the same test), run with the one existing test the selection matches: 32 passed in 1.75s on the head, dirty=0.
tests/test_openreposhape_command.py(15): the refusal over 11 values (a colon, two directories, a drive-relative name, a newline, a carriage return, an escape, a tab, DEL, a drive spelling with a newline, a drive spelling with an escape, a colon with an escape; the first five were 5 cases, six are new), a letter underCandC.UTF-8, U+009B under a UTF-8 locale, and a link in a directory holding a carriage return. A sharedassert_a_bin_dir_is_refusedholds exit 2, the wording, the ending, and that noforgedtail, escape, DEL or C1 reaches either stream.tests/test_install_sh.py(16): the same 11 values through install.sh and the command, in one test (exit 2 for both, the fakecurlrecords no call, nothing placed or made in either world,$TMPDIRempty), the$HOMEdefault with a colon, the symlink case, a letter under both locales (placed, named as typed), U+009B under a UTF-8 locale.24 failed, 8 passedin the 32. The 8 are the command's three colon cases and the$HOMEdefault (the base already refuses them), and the letter under both locales through both files (the base places it; they guard the width). Of the 24, 22 fail on behaviour (a request was made, or the value was placed and echoed, or the refusal came from the wrong arm); the command'sa-newlineanda-drive-spelling-and-a-newlinefail only on the refusal's new wording.tests/test_openreposhape_command.py tests/test_install_sh.py tests/test_install_sh_filter_repo.py tests/test_openreposhape_package.py: 302 passed in 68.90s.tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_setup_by_hand_commands.py tests/test_readme_windows_quoting_sentence.py tests/test_gh_guard.py tests/test_workflow_concurrency.py tests/test_release_workflow.py tests/test_readme_sections.py: 297 passed in 38.97s.tests/test_shape_doctor.py -k "shim or installed_doctor or shipped_doctor or refused_doctor"(it runs the launcher's--doctor): 20 passed, 388 deselected.tests-git-bashruns, with Windows'write_textemulated (a plugin makes everyPath.write_textwrite CRLF): 244 passed in 52.64s. A probe test takes the plugin's effect: 1 passed with it, 1 failed without.Reproductions (local fakes only; a fake
curlthat logs and exits 22, theghstub first onPATH)--install: a3cde99: a carriage return and an escape: exit 0, the command placed under the directory, the value in four lines. A newline: exit 2. This head: exit 2 for all three, nothing placed.Mutation probes
The fix round above adds 14 mutants of the new pattern line and 4 order mutants, and re-ran this table's rows against the new arm (the whole arm removed, narrowed to a newline,
[![:print:]], the refusal echoing, and the two later placements). Each is still caught.Each row changes one arm or hunk in a throwaway clone and runs the new cases and #726's drive and symlink cases (65 selected). "Failed" is how many of the 65 failed.
[![:print:]]for[[:cntrl:]]C(both files)[![:print:]]C$HOMEdefault$HOMEdefaultAll 18 are killed, on the final head. A first run on the pre-reorder tests gave the same counts.
Merge checks (
git merge-tree --write-tree, after a fresh fetch)refs/heads/fix/*andl3-*branches on origin (none ofl2-*orl4-*exists; mine excluded): clean. Only install.sh and openRepoShape: a Windows-spelled$OPENREPOSHAPE_BIN_DIRis respelled or refused before install.sh's fetch, and the PATH note quotes the directory it prints and refuses one holding:or a newline (#667) #726's branch editsinstall.sh,openRepoShape,tests/test_install_sh.pyortests/test_openreposhape_command.py, as the issue said.fix/757throughfix/777, local heads at the time): clean; none edits these four files.What I could not check
$HOMEtests do too (no file there holds a control character or a:). The 11-value test runs there as install.sh and openRepoShape: a Windows-spelled$OPENREPOSHAPE_BIN_DIRis respelled or refused before install.sh's fetch, and the PATH note quotes the directory it prints and refuses one holding:or a newline (#667) #726's newline case did, and its result istests-git-bash's to give. CRLFwrite_textwas emulated (244 passed).C,POSIXandC.UTF-8: not run. No test skips for want of a locale.0xC2 0x80to0xC2 0x9Fare 32 hanzi there, and the arm now refuses a directory that holds one as "a control character". Measured with Python's codecs only: no such locale is installed here.Left out
--installand install.sh now refuse a control character in$OPENREPOSHAPE_BIN_DIRwhere--installplaced the command and printed the value as typed, and install.sh refuses a:before its fetch. Lane 3's README, AGENTS.md, docs/cli.md and the handbook:pipx install openreposhapeis the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a barepip install, and a first draft of the v1.1.0 release notes (#563) #630 draftsdocs/release-notes/v1.1.0.md; a line for it: "install.shandopenRepoShape --installrefuse a$OPENREPOSHAPE_BIN_DIRholding a control character (a newline, a carriage return, a tab, an escape or a C1 control), andinstall.sha:, with exit 2 before it makes a request." Not edited here.AGENTS.md,docs/cli.md: no sentence became false.Checklist candidates (not filed)
$OPENREPOSHAPE_BIN_DIRare echoed as typed (the body's candidate 1, now also the two separators). None is a control a terminal acts on; U+202E can make a directory read as another.0xC2 0x80to0xC2 0x9Fas hanzi, so the arm refuses a directory holding one. Measured with codecs only.-makes--installrunmkdir -p "$dir"with no--; U+202E and U+200B; a$HOMEholding a control character is refused without saying the directory came from$HOME; install.sh's Windows-spelling refusal names nothing where the command's names'$dir'; macOS's[[:cntrl:]]for C1 underen_US.UTF-8is unmeasured.Closes #760
🤖 Generated with Claude Code
Summary by Sourcery
Reject unsafe installation directories before they can forge terminal output, alter PATH handling, or trigger an unnecessary fetch.
Bug Fixes:
openRepoShape --installandinstall.shfrom echoing directory values containing control characters, including carriage returns, escapes, tabs, DEL, and C1 controls.install.shreject colon-containing installation directories before fetching or installing anything.Enhancements:
openRepoShape --installandinstall.sh.Tests: