Skip to content

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

Merged
brettheap merged 6 commits into
mainfrom
fix/760-bin-dir-control-chars
Oct 17, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-5 (openRepoShape-5)
Claim: #760 (comment)

Against main. #726 landed as dcf62ed, and this branch is restacked onto main as 7e4181c0: it merges #726's final head a22b94c4, 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 efd061a7 and git diff origin/main 7e4181c0 have 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.md and docs/cli.md are unchanged (render-cli-reference.py --check: ok): no documented sentence names these refusals, and neither file is a pinned shape copy, so no upstream-changed row 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 is install.sh +40/-3, openRepoShape +30/-16, tests/test_install_sh.py +132/-2, tests/test_openreposhape_command.py +176/-21. README, the handbook, AGENTS.md and docs/cli.md are still unchanged.

The red Linux run on 7569ffa was #645's copytree race (shutil.Error in test_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_DIR set to <dir>/caf, the byte 0xE9, U+009B, 31mFORGED/bin, and openRepoShape --install: exit 0, the command placed, and the value printed raw in four lines (od -c of the output shows c 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 under C.

The fix is lane 1's, tested by them, in both files (install.sh's main and openRepoShape's install_commands), in place of the case:

if (LC_ALL=C; case "$bin_dir" in *[[:cntrl:]]* | *$'\302'[$'\200'-$'\237']*) exit 0 ;; *) exit 1 ;; esac); then
	die "…unchanged…"
fi

The command's has "$dir". Under LC_ALL=C the 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, then 0x80 to 0x9F). 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):

value locale 7569ffa efd061a
U+009B C, POSIX 0, placed, in 4 lines 2, nothing placed, nothing echoed
U+009B C.UTF-8 2 2
U+0080, U+009F C, POSIX 0, placed 2, nothing placed
U+0080, U+009F C.UTF-8 2 2
0xE9 then U+009B then FORGED C, POSIX, C.UTF-8 0, placed, in 4 lines 2, nothing placed, nothing echoed
a :, 0xE9, U+009B all three 2, the colon refusal names the directory, the C1 control in its line 2, the control-character refusal, nothing of the value
an e with an acute accent, U+00A0 all three placed placed
a lone 0x9B byte all three placed placed (see the checklist)
U+2028 C, POSIX placed placed
U+2028 C.UTF-8 2 placed (see below)

Measured, install.sh with a fake curl that logs each request and exits 22, a version named:

value locale 7569ffa efd061a
U+009B, U+0080, U+009F C exit 2 after 1 request exit 2, 0 requests
0xE9 then U+009B C, C.UTF-8 exit 2 after 1 request exit 2, 0 requests
a :, 0xE9, U+009B C, C.UTF-8 exit 2, 0 requests, the colon refusal holds the C1 control exit 2, 0 requests, nothing of the value
an e with an acute accent, U+00A0 C, C.UTF-8 1 request (the value is accepted) 1 request (the value is accepted)

Side 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_CONTROLS places both under C and C.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, POSIX and C.UTF-8, the new arm gives the same answer in all three locales, and the arm without LC_ALL=C; differs from it on exactly one string: U+2029 (0xE2 0x80 0xA9) under C.UTF-8. So on glibc the locale's one observable effect is those two characters, and that is what pins LC_ALL=C in 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-byte iscntrl says 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 case in install.sh has no *) arm and keeps its S131, as nine others open in that file do. The control check's case now 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 in install.sh, openRepoShape and 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 $HOME default 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 on PATH, bogus token, empty GH_CONFIG_DIR; dirty=0)

Logs are in $Q/v116/f804/ ($Q is the lane's scratchpad).

Run Result Log
The new and changed cases (-k "c1_control or unprinted_letter") 36 passed (18 in each file) new-final.log
test_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.py 478 passed, 103.92s runA-final.log
the three files tests-git-bash runs, with every Path.write_text writing CRLF (the plugin that wave 7 used) 274 passed, 65.38s runC-final.log
test_release_workflow.py, test_doctor_validator_rows.py, test_doctor_machine_row.py 93 passed, 50.72s runB-final.log
test_shape_doctor.py -k "shim or installed_doctor or shipped_doctor or refused_doctor or signal" (it runs the launcher's --doctor) 20 passed, 388 deselected runD-final.log
  • What changed in the tests.
    • The C1 case, which skipped where no UTF-8 locale read U+009B as a control, is gone with its helper utf8_locale_with_c1_controls. In its place a table of five values (C1_BIN_DIRS) runs under both C and C.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.
    • The placed-name test takes four names (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 its preflight: line among lines split at a newline only, because str.splitlines also splits at U+2028 and U+2029.
    • A lone 0xE9 is chr(0xDCE9), which reaches the child as that byte through surrogateescape. No escape is typed into any file, and body-guard.py is clean on the four files.
  • On the base. The new and changed cases are 36: 18 fail on 7569ffa's code and 18 pass (new-base2.log). The 18 that pass are guards: U+009B, U+0080 and U+009F under C.UTF-8 (6, which the head already refused), and the placed names under C and the e and U+00A0 under both (12). Of the 18 that fail, 8 surface as a UnicodeDecodeError raised inside subprocess.run (the head echoes the lone 0xE9, and run_cmd and Net.run decode strictly) and 10 as assertions. That is a rough failure message and no more: decoding with errors="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_DIR holding : or a newline is refused by install.sh only 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 (-k of 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/ and mut3/; the runner is mutate2.py; the counts are in mutants2.out and mutants3.out.

Mutation Command install.sh Where it fails
no C1 bytes in the pattern 10 10 every C1 value, under both locales, in the file mutated
LC_ALL=C; dropped 4 2 U+2028 and U+2029 under C.UTF-8 (the installer's test runs the command too)
[[:cntrl:]] dropped, C1 bytes kept 17 9 carriage return, tab, DEL, escape and the drive and colon rows
range starts one late (\201) 2 2 U+0080, both locales
range ends one early (\236) 2 2 U+009F, both locales
range ends one late (\240) 4 2 U+00A0 is refused (the placed-name test)
lead byte \303 for \302 10 10 every C1 value
the whole arm removed 27 19 every control value
arm narrowed to a newline 23 17 every control but a newline
[![:print:]] for the pattern 16 8 the e with an acute accent under C, and more
refusal echoes the value 27 19 every control value
arm after the drive arm (command) 4 the two drive rows (both files)
arm after the colon arm 8 5 the colon and the drive rows, the colon beside a C1 control
arm after the symlink loop (command) / check (install.sh) 9 6 the link case, the colon and drive rows
arm after the release lookup (install.sh) 19 a request was made, for every control value

Not checked, and known limits

  • bash 3.2.57 is not here. Lane 1 built it and measured the same table on it, and the pattern is theirs. I did not run it.
  • macOS and other libcs. LC_ALL=C is what is meant to hold there. Not run.
  • A GBK or GB18030 locale. The bytes 0xC2 0x80 to 0xC2 0x9F are 32 hanzi there (0xC2 0x9B decodes to one with Python's gbk codec), 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.
  • Windows (Git Bash). The C1 and placed-name tests skip there, as before; the three files tests-git-bash runs pass with CRLF write_text emulated (274).

What changed and why

1. The command refuses any control character in the directory, first (install_commands, openRepoShape).

  • Before: only a newline was refused first. Measured on a3cde99: $OPENREPOSHAPE_BIN_DIR set to <dir>/bin, a carriage return, then FORGED placed 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 $PATH and the export PATH line.
  • Now: the arm is *[[:cntrl:]]* and the two bytes of each C1 control (U+0080 to U+009F), run under LC_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).

Decisions and their evidence

Tests (Linux, bash 5.2.21; every run through the lane's run9.sh: gh stub first, bogus token, empty GH_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 under C and C.UTF-8, U+009B under a UTF-8 locale, and a link in a directory holding a carriage return. A shared assert_a_bin_dir_is_refused holds exit 2, the wording, the ending, and that no forged tail, 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 fake curl records no call, nothing placed or made in either world, $TMPDIR empty), the $HOME default with a colon, the symlink case, a letter under both locales (placed, named as typed), U+009B under a UTF-8 locale.
  • On the base a3cde99 (the head's two test files over the base's code): 24 failed, 8 passed in the 32. The 8 are the command's three colon cases and the $HOME default (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's a-newline and a-drive-spelling-and-a-newline fail only on the refusal's new wording.
  • On the head 7569ffa, dirty=0:
    • 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.
    • The three files tests-git-bash runs, with Windows' write_text emulated (a plugin makes every Path.write_text write 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 curl that logs and exits 22, the gh stub first on PATH)

  • install.sh, version named, each of a colon, a newline, a carriage return, an escape: a3cde99: exit 2 after 1 request (the launcher's fetch), refused by the fetch's error or by the command. This head: exit 2, 0 requests, install.sh's own refusal (the colon one names the directory; the others name nothing of it).
  • The command, --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.
  • Needs Windows or another system to be real: none of these rows. See below for what was not run.

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.

Mutation Failed Where
command: no control-character arm 18 the command's cases and install.sh's, which runs it
command: arm narrowed back to a newline 14 carriage return, escape, tab, DEL (both files)
command: [![:print:]] for [[:cntrl:]] 2 the letter under C (both files)
command: arm after the drive arm 4 the two drive spelling cases (both files)
command: arm after the colon arm 6 colon with an escape, the two drive cases
command: arm after the symlink loop 7 the link case, colon with an escape, the two drive cases
command: refusal echoes the value 18 every control value
install.sh: no control-character arm 10 the link case, U+009B, the control values
install.sh: arm narrowed to a newline 8 carriage return, escape, tab, DEL, the link case, U+009B
install.sh: [![:print:]] 1 the letter under C
install.sh: arm after the drive block 2 the two drive spelling cases
install.sh: arm after the symlink check 4 the link case, colon with an escape, the two drive cases
install.sh: arm after the release lookup 10 a request was made, for every control value; and the link refusal named it
install.sh: refusal echoes the value 10 every control value
install.sh: no colon arm 4 the three colon values, the $HOME default
install.sh: colon arm before the drive block 17 #726's drive cases (the respelling never runs)
install.sh: colon arm after the release lookup 4 a request was made, for the colon values
install.sh: colon refusal names nothing 4 the three colon values, the $HOME default

All 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)

What I could not check

Left out

Checklist candidates (not filed)

  1. U+202E, U+200B, U+2028 and U+2029 in $OPENREPOSHAPE_BIN_DIR are 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.
  2. A lone 0x9B byte (an 8-bit C1 control, not UTF-8) matches in no locale, before and after this commit. Only an 8-bit terminal with C1 controls on acts on it. (Lane 1's candidate 2.)
  3. A GBK or GB18030 locale reads 0xC2 0x80 to 0xC2 0x9F as hanzi, so the arm refuses a directory holding one. Measured with codecs only.
  4. Lane 1's other candidates stand as they listed them: a leading - makes --install run mkdir -p "$dir" with no --; U+202E and U+200B; a $HOME holding 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 under en_US.UTF-8 is 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:

  • Prevent openRepoShape --install and install.sh from echoing directory values containing control characters, including carriage returns, escapes, tabs, DEL, and C1 controls.
  • Make install.sh reject colon-containing installation directories before fetching or installing anything.

Enhancements:

  • Align installation-directory validation and refusal ordering between openRepoShape --install and install.sh.
  • Expand coverage for control-character handling, locale behavior, refusal ordering, network requests, symlink interactions, and safe directory names.

Tests:

  • Add comprehensive tests covering control characters, C1 controls, colons, locale-sensitive names, and pre-fetch refusal behavior in both installation paths.

Brett Heap and others added 2 commits October 16, 2026 14:07
…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>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-17T10:49Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR hardens openRepoShape --install and install.sh against control characters and colons in OPENREPOSHAPE_BIN_DIR, implementing matching validation with deliberate ordering before naming, filesystem checks, or network activity, and adds comprehensive tests for output safety, locale behavior, and side effects.

Sequence diagram for install.sh validation before fetching

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

Flow diagram for safe install directory validation

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

File-Level Changes

Change Details Files
Reject unsafe binary-directory values before they can be echoed, interpreted by the terminal, or used for installation.
  • Add a first-priority [[:cntrl:]] validation for OPENREPOSHAPE_BIN_DIR, covering newline, carriage return, escape, tab, DEL, and locale-recognized C1 controls without echoing the value.
  • Preserve the existing Windows-path and colon validation order, ensuring control characters take precedence and colon values are rejected before filesystem or network work.
  • Apply equivalent validation directly in the piped installer so invalid values are rejected before fetching the release or invoking the installed command.
openRepoShape
install.sh
Expand coverage for validation behavior, ordering, output safety, locale handling, and installer side effects.
  • Parameterize command and installer tests across control characters, colon combinations, Windows spellings, symlink targets, and default HOME-derived paths.
  • Assert exit status, refusal wording, absence of forged or terminal-control output, no network request, no installation, and preservation of existing symlinks.
  • Verify [[:cntrl:]] does not reject non-ASCII printable directory names and conditionally test U+009B where the active locale classifies it as a control.
tests/test_openreposhape_command.py
tests/test_install_sh.py

Possibly linked issues


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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T10:51:10.178913Z 7569ffa Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread openRepoShape Outdated
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:]]*)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@brettheap

Copy link
Copy Markdown
Contributor Author

(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 [[:cntrl:]] when the value also holds any byte that is not UTF-8. That makes one sentence in the body false. It also weakens the reason given for the colon ruling. The fix is about five lines over two files, and I have tested it (finding 1). If you would rather not take it, the text replacements below make the PR true as it stands.

How I tested: a local merge of 7569ffa8 with origin/main bf1f099b merged cleanly to tree fdd65802. Cherry-picking #804 onto a predicted squash of #726 on main gives the same tree. Probes ran with a gh stub first on PATH, a fake curl that logs each request and exits 22, a bogus token and an empty GH_CONFIG_DIR.

The issue's lines

1. "install.sh learns of a : or a newline only after its fetch": FIXED.
I ran bash install.sh v9.9.9 with OPENREPOSHAPE_BIN_DIR set to each of a colon, a newline, a carriage return, an escape, a tab and DEL:

main bf1f099b     exit=2 curl_calls=1   (every value)
base a3cde999     exit=2 curl_calls=1   (every value)
merge             exit=2 curl_calls=0   (every value)

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).
I ran openRepoShape --install with the same values:

main   colon/newline/cr/esc  exit=0 placed=['a\rFORGED/bin/openRepoShape'] out_has=['CR','FORGED']   (esc: 'ESC','FORGED')
base   colon/newline         exit=2 ; cr/esc/tab/del exit=0, placed, value in 4 lines
merge  all six               exit=2 placed=[] out_has=[]

Ruling on the colon refusal naming the directory: AGREE

The premise needs narrowing. Reasons for agreeing:

  1. The $HOME default. The person typed nothing, and the refusal is the only place that says which directory is at fault. install.sh and openRepoShape: a Windows-spelled $OPENREPOSHAPE_BIN_DIR is 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 test pins this, and 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's test_the_installer_holds_the_default_directory_to_the_colon_refusal holds install.sh to the same rule.

  2. The order is enforced, not assumed. The control arm is first in both files. Your mutation rows ("arm after the colon arm", "arm after the symlink check") fail the tests. I re-ran two rows myself:

    • install.sh with no control arm: 10 failed.
    • The command's arm narrowed back to *$'\n'*: 14 failed.

    Both match your table.

  3. Naming the value adds no new exposure. A value without the colon is printed in 4 lines on success (installed at, placed in, is not on $PATH, export PATH). Anything that can reach the colon refusal already reaches those lines. Leaving the colon value unnamed would protect nothing that the success path does not print anyway.

  4. The two files agree. The command (install.sh and openRepoShape: a Windows-spelled $OPENREPOSHAPE_BIN_DIR is 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) and install.sh now give the same answer to the same value.

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:

  • under LC_ALL=C, which you state as a limit;
  • under C.UTF-8, when the value also holds a byte that is not UTF-8, which is not stated.

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.

Findings

1. SHOULD-FIX: a C1 control passes the arm under a UTF-8 locale when the value also holds a byte that is not UTF-8

For 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 (\302\233) are not controls, just as under C. I measured the same table on bash 5.2.21 and on bash 3.2.57, which I built from GNU source:

value         C        POSIX    C.UTF-8
u009b         passes   passes   REFUSED
E9+u009b      passes   passes   passes     <- a lone 0xE9, then U+009B
E9+ESC        REFUSED  REFUSED  REFUSED    (ASCII controls still match in the fallback)

Reproduction on the merge:

d=$(mktemp -d)
LC_ALL=C.UTF-8 HOME=$d OPENREPOSHAPE_SETUP_SH=$PWD/setup.sh \
  OPENREPOSHAPE_BIN_DIR="$d/caf"$'\xe9\xc2\x9b'"31mFORGED/bin" \
  bash ./openRepoShape --install > $d/out 2>&1; echo "exit $?"; grep -ac FORGED $d/out
  • main: exit 0, 4 lines (main has no arm at all).
  • merge: exit 0, 4 lines. The value is printed raw: od -c shows c a f 351 302 233 3 1 m F O R G E D in the installed at and placed in lines.

Through install.sh with a colon, OPENREPOSHAPE_BIN_DIR="$d/a:caf"$'\xe9\xc2\x9b'"31mFORGED/bin", under either LC_ALL=C or C.UTF-8, on the merge: exit 2 and 0 requests, but the REFUSED: line holds ' <d>/a:caf 351 302 233 3 1 ....

Fix (tested): run the check under LC_ALL=C and name U+0080 to U+009F's two bytes. In install.sh, replace the case/esac around the control-character die with:

	if (LC_ALL=C; case "$bin_dir" in *[[:cntrl:]]* | *$'\302'[$'\200'-$'\237']*) exit 0 ;; esac; exit 1); then
		die "…unchanged…"
	fi

Make the same change in the command, with "$dir". With this in a scratch copy of the head:

  • tests/test_openreposhape_command.py tests/test_install_sh.py: 226 passed.
  • Probes, under both C and C.UTF-8, through both files: U+009B alone, 0xE9 then U+009B, and the colon form are all exit 2, 0 requests, nothing placed and nothing echoed. café is still placed, and a plain directory is unchanged.
  • On bash 5.2.21 and 3.2.57, the pattern refuses none of é, ü, ā (C4 81), CJK, an emoji (F0 9F 98 80), U+00A0 or U+202E. \302 is never a continuation byte, so a letter never matches.

Side effects of the fix:

  • The arm becomes the same in every locale. U+2028 and U+2029, which only the UTF-8 locale refused, then pass everywhere. Add *$'\342\200'[$'\250\251']* if you want them refused.
  • It closes your checklist candidate 2.
  • The C1 tests no longer need their skip.
  • A regression case should be added: a value holding "\udce9" + C1_CSI under C.UTF-8, and U+009B under C.

If you keep the code as it is, use the text replacements below instead.

2. NIT: two new SonarCloud S131 issues

SonarCloud's PR analysis reports two new issues, both shelldre:S131 "Add a default case (*)", at install.sh lines 382 and 420 (the two new case blocks). The quality gate is OK and new duplication is 0.0%. #726 had the same two, and main has nine S131 issues open in install.sh, so this matches house style. A *) ;; arm clears them if you want them gone.

3. INFO: #804 depends on #726 landing first

Cherry-picking 7569ffa8 onto main alone gives CONFLICT (content): Merge conflict in install.sh. The tests also import #726's BIN_DIR_SPELLINGS, CYGPATH_STATES and typed_bin_dir. Onto a predicted #726 squash, it gives tree fdd65802, the same tree as the merge I tested. Land #726, then retarget #804 using the stacked-PR procedure.

What I verified (the claims that held)

  • Tests on the merge:
  • Git Bash's three files with Windows writes emulated: a plugin made every Path.write_text write CRLF, and a probe file confirmed it wrote b'a\r\nb\r\n'. Result: 244 passed. On the PR, tests-git-bash is SUCCESS at this head.
  • Your "on the base" claim: with the head's two test files over a3cde99's code, 24 failed, 8 passed, as you said.
  • Ordinary values print the same as on main. The command's --install output is byte for byte the same on main and the merge for a plain directory, a space, café, an unset or empty variable, and a*[x]?, under bash 5.2.21 and 3.2.57. install.sh's output on the fetch-failure path is also the same for all six values.
  • Quoting: the value is only ever the quoted subject of case, never the pattern. Spaces, glob characters and an empty value pass through as before. A trailing newline is refused.
  • Every reader of the variable is covered. Only main in install.sh and install_commands read $OPENREPOSHAPE_BIN_DIR. A re-run of install.sh is the update, and there is no uninstall. --doctor, --preflight and the Python package do not read it.
  • Nothing happens before the refusal.
    • install.sh: before the arm come only argument parsing, checked_repo and command -v curl.
    • The command: before the arm comes only refuse_bad_record, which reads and never writes, and the arm comes before collect_commands, so it is also first in the curl … | bash -s -- --install path.
  • bash 3.2.57: with the built shell, install.sh and the command refuse all six values and C1 under C.UTF-8, and place café.
  • Your C1 table rows match what I measured, except for the fallback in finding 1.

Text replacements (if finding 1's code is not taken)

PR body, "What changed" item 1, replace:

so no refusal after it can echo one that forges a line or reaches the terminal.

with:

so no refusal after it can echo an ASCII control character or DEL. A C1 control (U+0080 to U+009F) still passes it under LC_ALL=C, and under a UTF-8 locale when the value also holds a byte that is not UTF-8 (checklist candidate 2).

PR body, decisions, replace:

So under a UTF-8 locale a C1 CSI (U+009B) is refused as an escape is, and under C it is not: the bytes are not controls there.

with:

So under a UTF-8 locale a C1 CSI (U+009B) is refused as an escape is, provided the value is otherwise valid UTF-8. bash matches a value holding any byte that is not UTF-8 one byte at a time, and then U+009B's two bytes are not controls, as under C.

PR body, decisions, replace:

A colon value holds no control character by then, so naming it cannot forge a line.

with:

A colon value holds no ASCII control character or DEL by then, so naming it cannot forge a line. A C1 control the arm does not see can still reach it, as it reaches every line that names the directory on success.

Checklist candidate 2, append:

The same holds under a UTF-8 locale for a value that also holds a byte that is not UTF-8 (measured: caf, 0xE9, U+009B is placed under C.UTF-8).

In both files' comments, replace C.UTF-8 matches every one of them with:

C.UTF-8 matches every one of them in a value that is valid UTF-8 (a single byte that is not UTF-8 makes bash match byte by byte, and then none match)

If finding 1's code is taken, replace the C1 sentences in both comments, and the body's locale bullet, with:

The check runs under LC_ALL=C and names U+0080 to U+009F's two bytes (\302, then \200 to \237), so a C1 control is refused in every locale, whatever else the value holds; U+2028 and U+2029 are refused in none.

For the checklist (outside #760)

  1. A leading -: OPENREPOSHAPE_BIN_DIR=-bin makes --install run mkdir -p "$dir" with no --. On main and on the merge: exit 1, mkdir: invalid option -- 'b', nothing placed. It predates this PR. mkdir -p -- "$dir" fixes it.
  2. A lone 0x9B byte (an 8-bit C1 control, not UTF-8) matches in no locale, before or after the fix. Only an 8-bit terminal with C1 controls enabled acts on it.
  3. U+202E and U+200B are echoed as typed (your candidate 1).
  4. A $HOME holding a control character: the refusal's remedy (set $OPENREPOSHAPE_BIN_DIR) is right, but it does not say that the directory came from $HOME.
  5. install.sh's Windows-spelling refusal names nothing, but the command's names '$dir' (both from install.sh and openRepoShape: a Windows-spelled $OPENREPOSHAPE_BIN_DIR is 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). One value gets two different refusals.
  6. macOS: bash 3.2.57 behaved like 5.2.21 here, but on glibc. macOS libc's [[:cntrl:]] for C1 under en_US.UTF-8 was not measured.

Lane: openreposhape-1 (openRepoShape-1)

Base automatically changed from fix/667-install-bin-dir-spelling to main October 17, 2026 11:42
Brett Heap and others added 3 commits October 17, 2026 16:26
…`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-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	install.sh
#	openRepoShape
#	tests/test_install_sh.py
#	tests/test_openreposhape_command.py
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(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 7e4181c0 and merged it with current main, bad05d21. The merge was clean (tree b8ad230a). Probes ran with a gh stub first on PATH, a fake curl that logs each request and exits 22, a bogus token and an empty GH_CONFIG_DIR. I used bash 5.2.21 and a bash 3.2.57 built from GNU source.

Earlier findings

Finding 1 (C1 control under a UTF-8 locale): RESOLVED. Each row is install.sh v9.9.9 and then openRepoShape --install:

                                     7569ffa8 (old head)                  merge with main
U+009B, LC_ALL=C               installer exit=2 req=1               installer exit=2 req=0
                               command   exit=0 placed, C1+FORGED   command   exit=2 placed=[] out=[]
caf 0xE9 U+009B, C.UTF-8       installer exit=2 req=1               installer exit=2 req=0
                               command   exit=0 placed, C1+FORGED   command   exit=2 placed=[] out=[]
a:caf 0xE9 U+009B, C or C.UTF-8 both exit=2, refusal holds C1+FORGED both exit=2, nothing of the value
  • bash 3.2.57, on the merge: 36 runs, all exit 2 with 0 requests, 0 files placed and nothing echoed. That is both files × C and C.UTF-8 × colon, newline, CR, ESC, tab, DEL, U+009B, 0xE9 then U+009B, and colon with 0xE9 and U+009B.

  • The exact if (LC_ALL=C; case …) line from the head, on both shells, under C and C.UTF-8:

    • Refused: U+0080, U+009F, 0xE9 then U+009B, ESC, DEL.
    • Passed: U+00A0 (C2 A0), a lone C2 at the end, U+2028, é, a lone 0x9B, a plain name.
    • Both shells and both locales give identical answers.
  • Ordinary values are unchanged. Main bad05d21 and the merge give byte-identical exit codes and output in 40 pairs: install.sh and --install under bash 5.2.21 and 3.2.57, for plain, space, é, unset, empty, glob characters, U+00A0, U+2028, CJK and ā.

  • The revert rows hold. The head's tests over 7569ffa's scripts give 18 failed, 44 passed (62 selected), the same as your count.

  • Two mutants of the fix are caught:

    • install.sh range ending at \236: 2 failed, the-last-c1-control-C and -C.UTF-8.
    • LC_ALL=C; dropped from the command: 4 failed, U+2028 and U+2029 under C.UTF-8 in both files.

    Both match your table.

Finding 2 (S131, NIT): PARTLY RESOLVED. SonarCloud analysed 7e4181c0. The quality gate is OK, new duplication is 0.0%, and there is one new issue: shelldre:S131 install.sh:425, the colon case. The control check's S131 is gone. Keeping the last one is fine, as it matches house style.

Finding 3 (depends on #726): RESOLVED. #726 landed as dcf62eda.

  • git diff a3cde999 efd061a7 and git diff 6626a3c5 7e4181c0 have the same patch-id, 835d04d2, and the changed lines are identical file by file.
  • The merge with today's main is clean.
  • On 7e4181c0, CI's tests, tests-git-bash, package and SonarCloud all pass.

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:

  • test_openreposhape_command.py test_install_sh.py test_install_sh_filter_repo.py test_openreposhape_package.py test_repo_hygiene.py test_cli_reference.py: 432 passed in 87s.
  • test_shape_doctor.py -k "shim or installed_doctor or shipped_doctor or refused_doctor or signal": 28 passed.
  • The three files tests-git-bash runs, with every Path.write_text writing CRLF: 274 passed.
  • Tree clean after every run.

New findings, ranked

1. 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 7e4181c0 it is restacked and merges cleanly. Replace:

#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 in install.sh, openRepoShape and 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.

with:

#726 has landed on main as dcf62ed, and this branch is restacked onto main as 7e4181c0 (see the top). Before the restack, efd061a7 alone conflicted with main (40e678e) in install.sh, openRepoShape and both test files, as lane 1 found for a cherry-pick; at 7e4181c0 it merges cleanly with main.

2. TEXT (stale, recommended).

  • In the top paragraph, git diff origin/main 7e4181c0 no longer gives that patch-id, because main has moved 8 commits. Replace git diff origin/main 7e4181c0 with git diff 6626a3c5 7e4181c0 (main when it was restacked).

  • In Finding 2, replace:

    The control check's case now has one, so its S131 should go. That is expected from the code, not measured: SonarCloud's run on this push says.

    with:

    The control check's case now has one, and its S131 is gone: SonarCloud's run on 7e4181c0 reports one new issue, S131 at install.sh:425 (the colon case), with the quality gate OK.

  • Optional, under "Not checked": lane 1 ran this head's arm, install.sh and --install on bash 3.2.57 (results above).

3. INFO (no action): only U+2028 and U+2029 hold the LC_ALL=C subshell in the tests. On glibc the C1 cases pass with or without it, because an invalid string already makes bash match byte by byte. The tests cannot show its purpose for other libcs on Linux; your mutant row already says so.

For the checklist

  • A GBK or GB18030 locale (your candidate): there, C2 80 to C2 9F are hanzi, and the arm now refuses them as control characters.
  • A lone 0x9B byte (an 8-bit C1 control) is still placed and echoed in every locale. Only an 8-bit terminal with C1 controls enabled acts on it.
  • Carried from the first review, all unchanged:
    • OPENREPOSHAPE_BIN_DIR=-bin: mkdir -p "$dir" has no --, so exit 1 with mkdir: invalid option.
    • U+202E and U+200B are echoed as typed.
    • A $HOME holding a control character is refused without naming $HOME.
    • install.sh's Windows-spelling refusal names nothing, but the command's names the directory.

Lane: openreposhape-1 (openRepoShape-1)

@sourcery-ai

sourcery-ai Bot commented Oct 17, 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 2 hours and 18 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 2e476a7 into main Oct 17, 2026
5 checks passed
@brettheap
brettheap deleted the fix/760-bin-dir-control-chars branch October 17, 2026 22:24
brettheap added a commit that referenced this pull request Oct 18, 2026
…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>
brettheap added a commit that referenced this pull request Oct 18, 2026
…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>
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