Repository navigation
adopt-project.py: _elector and _remote_repository read git config user.name and the origin URL through git_out, which decodes strictly, so a name or URL that is not UTF-8 ends plan (and, for the URL, check and execute) in a UnicodeDecodeError traceback, exit 1, naming neither the git setting nor the flag that avoids it #292
Description
(lane openRepoShape-2, 2026-10-12T00:11:27Z)
Found by this lane's adversarial review of PR #270 at aed9190 (the fix for #255), its follow-up FU1. It is the same on main d94d155, where it was reproduced again today, and at #270's head and its merge with main: the PR neither causes it nor changes it. Nothing is created in any of the runs below. Not claimed.
What is wrong. adopt-project.py asks git two questions whose answer is text it did not write, and reads both with repo_shape.git_out, which decodes the output with .decode("utf-8"). _elector asks git config user.name when --elected-by is not given; _remote_repository asks git remote get-url origin when it opens a local source. Neither catches UnicodeDecodeError: it is a ValueError, not a Refusal, so main() (:3703-3710) does not catch it either, and the run ends in a traceback, exit 1. A refusal would say what to do (pass --elected-by, pass --org); the traceback names neither the setting nor the flag.
# $U: a checkout of openRepoShape at d94d155. $SRC: a three-file git repository (src/app.py, docs/spec.md, README.md), one commit, no origin. Local; no network.
printf '[user]\n\tname = Jos\xe9\n' > "$G" # a global config whose user.name holds a Latin-1 byte
GIT_CONFIG_GLOBAL="$G" GIT_CONFIG_NOSYSTEM=1 python3 "$U/adopt-project.py" plan \
--source "$SRC" --project Northwind --org testorg --elected-on 2026-09-02 --out "$OUT/plan.yaml"
# exit 1, no plan file
# adopt-project.py line 1512, in cmd_plan / line 1419, in _checked_plan_inputs / line 3615, in _elector
# scripts/repo_shape.py line 777, in git_out: return proc.stdout if binary else proc.stdout.decode("utf-8").strip()
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 3: invalid continuation byte
git -C "$SRC" remote add origin "$TMP/caf"$'\xe9'"/repo.git" # an origin URL that is a non-UTF-8 path
python3 "$U/adopt-project.py" plan --source "$SRC" --project Northwind --org testorg \
--elected-by 'T H' --elected-on 2026-09-02 --out "$OUT/plan2.yaml"
# exit 1, no plan file
# adopt-project.py line 1508, in cmd_plan / line 564, in open / line 686, in _remote_repository / repo_shape.py line 777, in git_out
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 137: invalid continuation byte| the input | the run | result on d94d155 |
|---|---|---|
global user.name = Jos\xe9, no --elected-by |
plan |
exit 1, UnicodeDecodeError from _elector (:3615), no plan written |
global user.name = Jose (control) |
plan |
exit 0, elected_by: Jose |
the same Latin-1 user.name, --elected-by 'T H' given (control) |
plan |
exit 0: _elector is not called |
origin URL holding \xe9, a plan written earlier |
check |
exit 1, UnicodeDecodeError via open_source (:1672), Source.open (:564), _remote_repository (:686) |
| the same | execute --yes --local-remote-dir ... |
exit 1, the same traceback, before any leg exists (no leg repository under the local remote dir) |
origin URL holding \xe9, no plan yet |
plan |
exit 1, the same traceback, no plan written |
Why. git_out (scripts/repo_shape.py:765-777) returns proc.stdout.decode("utf-8").strip() unless binary=True, and raises Refusal only for a git exit code. The two callers wrap it in except Refusal (_elector, adopt-project.py:3613-3618; _remote_repository, :684-691, which returns None on one, so that a source with no origin reads as no org/repo). A byte that is not UTF-8 reaches neither handler. Both values are bytes git stores without interpretation (a config value, a URL), so a repository can hold one without anyone having done anything wrong: a name saved in a legacy code page, a clone path under a non-UTF-8 directory.
What it should do.
_remote_repositorytreats an origin it cannot decode as noorg/repo, returning None as it does for a missing origin, soplanmeets its existingadopt-no-orgrefusal ("pass --org") or takes--org, andcheckandexecuteread the source's repository as unknown.Source.openthen no longer dies for a source nobody can adopt by name._electorrefuses by name. Auser.namethat is not UTF-8 is a refusal that says so and says to pass--elected-by(exit 2), before the plan is written. It should not decode witherrors="surrogateescape"and carry on: the lone surrogate would then be the elector the plan records, which is adopt-project.py: since #247 a byte that is not UTF-8 in--elected-by,--referenceor the--sourcedirectory passesplanandcheck, andexecutepushes both legs before it dies withUnicodeEncodeErrorinwrite_lf#255's input, andexecutewould push both legs before it dies writing it.- Mind where the fix goes.
git_outlives inscripts/repo_shape.py, a pinned copy (COPIED_FROM_SHAPE): a change there moves thesha256that every scaffolded and adopted project'scontracts/shape-pin.yamlrecords for it, and every project already cut sees it asupstream-changed. The two call sites are inadopt-project.pyand can be fixed without it (catchUnicodeDecodeErrorbesideRefusal, or call withbinary=Trueand decode there). Whether othergit_outcallers need the same is a separate question.
How to test. In tests/test_adopt_plan.py, with GIT_CONFIG_GLOBAL pointing at a config written with write_bytes (tests/test_scaffold_ignored_shape_files.py:64-77 does the same for excludes):
- (a) a
user.nameholdingb"Jos\xe9"and no--elected-by: exit 2, a refusal that namesuser.nameand--elected-by, noTraceback, no plan file; - (b) a source whose origin URL holds
\xe9, with--org:planexits 0 and records no repository from it; without--org,planexits 2 withadopt-no-org;checkon a plan written beforehand exits 0 or 2 with noTraceback; - (c) controls: an ASCII
user.name, and--elected-bygiven with the Latin-1 config.
Reverting either guard fails its own case. (a) and (c) run on every OS; (b) passes the URL to git remote add as bytes, so it is POSIX only and skipped on Windows.
Related: #255 and PR #270 (a non-UTF-8 byte in --elected-by, --reference or the source path, which #270 refuses as adopt-unwritable-value; this issue is the same byte arriving from git and not from argv, so it never reaches that check); #261 (blank or malformed --elected-by, which the empty answer from a missing user.name also meets); #192 (closed: the same crash in load_yaml, fixed there).
🤖 Generated with Claude Code
Activity
brettheap commentedon Oct 12, 2026
CLAIMED — lane openRepoShape-2, session 5b7b5f3d-dce7-452d-a33d-ab8537d91db3@Eagle, 2026-10-12T11:00:39Z, for #292
Logged in
lanes/log/openRepoShape-2.mdatf6ba56a14f4e519a6885fc1258972a8f04e445df.The three reads (lane-collision-protocol Rule 1):
Merge origin/main (ab623dd) into fix/adopt-git-reads-not-utf8, after #…
adopt-project.py: `adopt-no-org` says all three of its causes, so a s…
tests/test_adopt_git_reads.py: the four `user.name` tests give the na…
adopt-project.py: `_elector` keeps a `user.name` byte that is not UTF…
Merge origin/main dd8d0ea into fix/adopt-root-kept-symlinks: #261's `…
shape_advisory.py and shape-doctor.py: an `origin` URL that is not UT…
bump-leg.py, scaffold-project.py and family.py: `git status --porcela…
adopt-project.py: `_default_branch` keeps a branch name's byte that i…