Conversation
A plaintext http:// package mirror lets anyone on the path swap the .deb/.rpm/.pkgz you just fetched for one of theirs. Signature checks catch forged packages but not downgrades to an older, genuinely-signed, vulnerable build -- only TLS on the transport closes that gap. Adds lib/apt-https.sh as a repository transport guard that runs BEFORE anything is fetched: - apt: installs /etc/apt/apt.conf.d/99neohiro-force-https (the key line is Acquire::http::AllowRedirect "false", so an https:// mirror cannot silently downgrade a fetch to plaintext), and rewrites http:// -> https:// on active lines in sources.list, sources.list.d/*.list and DEB822 *.sources. Comments and DEB822 structural fields are left untouched. - Every file is backed up to /var/backups/neohiro-apt-https/ before the first edit and logged to the rollback log. - The rewrite is verified with a real `apt-get update`; if a mirror does not speak TLS the rewrite is rolled back automatically, so a working package manager is never left broken. - dnf/yum/zypper/pacman/flatpak are audited and reported but not blindly rewritten (many mirrors lack TLS on the same paths); opt in with NEOHIRO_APT_HTTPS_REWRITE=1. - Optional belt-and-braces `ufw deny out 80/tcp` behind NEOHIRO_APT_BLOCK_PORT80=1, which declines while plaintext repos remain. Wired into every surface that downloads packages: pkg_update, pkg_install, pkg_upgrade, pkg_autoremove, update_system, update_kernel, updates_only_mode, all _update_* in lib/updater.sh plus the _run_all_updates dispatcher, restore_ssh.sh (before openssh-server), DeepClean.sh (before apt-get autoremove --purge), the Maintenance submenu (option 1, now 21 tools), --step apt_https, the progress checklist, and the --apt-https / --apt-https-audit / --apt-https-off CLI flags on both linuxinstall.sh and the standalone scripts. It is the first workflow step on every profile, including Custom, and it is idempotent and latched to once per process so AUTO_MODE re-runs and the hot-path guards cost nothing. Also fixes two latent bugs found while wiring this up: - helpers that returned their result via command substitution had their own progress messages folded into the return value, which then landed inside an arithmetic expansion; results now go through globals. - _step_apt_https used `if ! cmd; then rc=$?`, which captures the negation's status, so a failed enforcement reported success. test_color_gate.sh now anchors its restore_ssh.sh slice on function names instead of hardcoded line numbers, and README.md had a stray unresolved merge-conflict marker removed. Tests: tests/test_apt_https.sh, 92 hermetic assertions (fake /etc, fake backup dir, fake ufw, NEOHIRO_APT_FAMILY pin) covering the rewrite, the DEB822 path, comment preservation, idempotency, the backup-before-edit guarantee, refusal-without-backup, revert, exit codes, every env kill switch, and that all ten package entry points guard before their first package command. Full suite: 522 passed, 0 failures.
neohiro
enabled auto-merge (squash)
October 4, 2026 02:23
The guard was correct on every path the tests exercised, but a review pass found three places where a package could still be fetched over plaintext HTTP. All three are now closed. 1. curl|bash inline fallback had unguarded updaters. lib/updater.sh is never on disk under `curl | sudo bash`, which is the documented primary install method. linuxinstall.sh carries its own inline copy of every _update_*, and only _update_apt was guarded - so the dnf, yum, zypper, pacman, snap, flatpak, docker, brew and firmware paths ran with no precaution at all, and the inline _run_all_updates dispatcher did not guard either. Every one of them is guarded now. 2. Four lib/updater.sh sub-steps that hit the network were unguarded. _update_docker (docker pull), _update_firmware (fwupdmgr refresh/update against LVFS), _update_geoip and _update_pihole are guarded. The dispatcher already covered them on a normal run, but each is also reachable directly, so they should not depend on the caller's ordering. _update_virsh, _update_suse_snapper and _update_btrfs_balance stay unguarded on purpose - they only read or write local state. 3. Subscripts fetched at runtime lost the guard entirely. run_remote_script pulls DeepClean.sh / OptimizeLinuxASR.sh into $TMP_DIR. A script resolves its helpers relative to its own location, so $TMP_DIR/lib/ does not exist and the fetched subscript found no apt_https_guard at all - meaning DeepClean.sh's `apt-get autoremove --purge` could resolve and fetch dependencies over plaintext HTTP even though the parent process had just enforced HTTPS. The child is a separate process with its own once-per-process latch, so the parent's enforcement does not carry over. run_remote_script now prefetches lib/apt-https.sh next to the subscript, best-effort: a failure there logs and continues, because the parent has already enforced for this operation. A subscript curl|bash'd entirely on its own and finding no library now says so out loud instead of skipping the precaution silently (restore_ssh.sh, DeepClean.sh). Also: GEOIP_URL is the one operator-supplied download target in the update engine, and a GeoIP database decides which country a packet counts as being in, so a tampered copy is a traffic-tunneling primitive. An http:// value is now refused outright rather than fetched. tests/test_apt_https.sh grows to 120 assertions: the inline _update_* guards, the GEOIP_URL refusal (checked both structurally and at runtime for http and https), and a runtime exercise of the lib prefetch that drives run_remote_script with a fake curl - proving the prefetch happens, lands in the right place, is non-fatal when it fails, and leaves no empty stub behind. That last group was negative-controlled: stripping the prefetch makes it fail. Full suite: 551 passed, 0 failures. bash -n clean on every .sh.
Third review pass. restore_ssh.sh, DeepClean.sh and the lib/updater.sh standalone branch all run under `set -e`, which the transport flags were not accounting for. - restore_ssh.sh --apt-https called `apt_https_enforce` bare. On failure `set -e` aborted the script, so the report never printed and the user saw a truncated run. The `return $?` that followed `apt_https_report || true` would also have reported the *report's* status, which is always 0 - so the exit code was right only by coincidence, on the path where it was least needed. - The same latent pattern sat in lib/updater.sh's standalone flag branches, where the correct exit code came out right only because `set -e` happened to propagate it, skipping the report. Both now capture the enforcement status explicitly (`cmd || _rc=$?`), always print the report, and propagate enforcement's own result. apt_https_guard is also now guaranteed to return 0. It is called from pkg_install and friends, some of which run under `set -e`; a precaution should never be the reason a package operation aborts. Enforcement problems surface through apt_https_report and the enforce exit status instead. lib/apt-https.sh's own standalone entry point now sets its exit code explicitly rather than inheriting whichever command ran last, so the documented contract (0 clean / 1 plaintext remains / 2 bad usage) is a guarantee instead of an accident. tests/test_apt_https.sh grows to 145 assertions. The new ones drive the real scripts and assert both the exit code and that the report still printed: restore_ssh.sh through an extracted-main() harness with root and the print helpers stubbed (set -e is deliberately left active, and main() is invoked outside any condition so set -e is not silently disabled inside its body), plus lib/updater.sh run directly, plus apt_https_guard's never-fail contract and the standalone exit-code contract. Negative-controlled: re-introducing the original bare-call-plus-`return 0` in restore_ssh.sh makes 5 assertions fail, including "prints the report even when enforce fails" with the exact predicted reason. Full suite: 576 passed, 0 failures. bash -n clean on every .sh.
Automated capture of uncommitted modifications to tracked files.
The guard was named for apt and audited apt plus four RPM/Arch repo
formats. That left the majority of a real machine's app stores unchecked,
including ones that ship plaintext HTTP by default:
apk (Alpine) /etc/apk/repositories is http:// on many images
pip PIP_INDEX_URL / pip.conf
npm NPM_CONFIG_REGISTRY / .npmrc
cargo CARGO_REGISTRIES_CRATES_IO_INDEX / ~/.cargo/config.toml
gem GEM_SOURCE / .gemrc
nix nix.conf substituters + channel
docker daemon.json registry-mirrors + insecure-registries
brew HOMEBREW_*_GIT_REMOTE / HOMEBREW_API_DOMAIN
fwupd remotes.d UpdateURI
Now 16 stores are audited. Detection is deliberately format-agnostic: any
non-comment line carrying an http:// URL is reported. A per-dialect key
list would miss gpgkey=, metalink=, and whatever the next distro release
adds, and a miss is exactly the failure this exists to prevent. One
documented exception for JSON, which has no comment syntax: http:// must
sit at the start of a JSON string, so {"_comment": "see http://docs"} is
not mistaken for a registry.
"if available" is honoured honestly. Only apt is rewritten unattended,
because it is the one store whose rewrite can be verified with a real
`apt-get update` and rolled back if a mirror cannot speak TLS. Everything
else is reported, and rewritten only under NEOHIRO_APT_HTTPS_REWRITE=1,
since whether https://<same host><same path> exists is not knowable
offline and silently breaking a working mirror is worse than the threat.
The opt-in sweeps every installed store in one pass; stores whose tool is
absent are left alone.
--apt-https-audit now prints a per-store verdict table, and the aggregate
exit code covers all of them.
Three defects fixed while doing it:
1. cp into place is not atomic. A crash mid-copy left a truncated
sources.list, i.e. a broken package manager, which is precisely what
this tool must never cause. Every write is now a staging file in the
target's own directory plus rename(2). Mode and ownership are cloned
with cp -p and content replaced with a second cp, rather than relying
on sed -i preserving the mode, which differs between GNU and busybox.
2. The CA-trust-store warning printed two or three times per run because
both enforce and report call the check. Latched to once per process.
While adding the latch I also rewrote the check itself and broke it:
the case arm fell through to the warning even when the trust store
existed. Both the latch and the early return are now pinned by tests.
3. Result globals and the latch were initialised inside a function
instead of at the top level, so an audit-only run under `set -u` died
with "unbound variable" -- which is exactly how lib/updater.sh's
standalone CLI runs. Caught by a set -u harness that exercises report,
status, per-source and revert.
The curl|bash inline copy is gone. linuxinstall.sh carried ~400 lines of
security-critical code duplicated from lib/apt-https.sh, which is how fixes
and new store coverage silently failed to reach the documented primary
install path. It now resolves the canonical library -- from disk, or by
fetching it from the same REPO_RAW_BASE it already trusts for
DeepClean.sh -- and sources that, so curl|bash users get identical
coverage. If it cannot be loaded, the public entry points become loud
no-ops rather than undefined functions, and the run states plainly that
the precaution is inactive. Net -404 lines from linuxinstall.sh.
tests/test_apt_https.sh grows 247 -> 247 assertions covering: registry
completeness, a label for every store, plaintext detection per store,
comment suppression, the opt-in rewrite writing real https:// into each
config, backup-before-edit, "not touched without opt-in", the one-pass
sweep, skipping stores whose tool is absent, env-configured transports,
aggregate reporting, revert across stores, atomic-replace residue and mode
handling, and the single-implementation invariants.
Also adds repository encoding hygiene (valid UTF-8, no U+FFFD, no cp1252
double-encoding, LF endings per .gitattributes). This bit me twice during
this work: a UTF-8 file read as cp1251 and rewritten, then a repair pass
that injected U+FFFD. The failure mode is nasty because the file still
parses and the tests mostly pass. The CRLF check matches with a shell
`case` rather than `grep $'\r'` because Git Bash's MSYS argument
translation silently swallows a lone CR passed as an argument -- which
made the first version of that check pass unconditionally.
Every new detector is negative-controlled: removing apk from the registry
fails the coverage test, breaking apk's file lookup fails its detection
test, and injecting each class of encoding damage fails the corresponding
hygiene check.
Full suite: 678 passed, 0 failures. bash -n clean on every .sh.
…port
Sign-off review of the multi-store guard. Three concrete defects, one
latent hazard, and a report that could not actually be acted on.
1. Backup filenames collided.
_flattening a path is not injective_: /a/b/c and /a_b/c both became
_a_b_c.orig. A collision means `--apt-https-off` restores one file with
another file's contents -- a silent corruption of a package manager or a
pip/npm config, discovered at the worst possible moment. Backup names now
carry a cksum of the real path. cksum is POSIX and stable across shells,
runs and machines, which matters because these backups get inspected and
moved during an incident. Falls back to the flattened name alone if cksum
is somehow unavailable.
2. The detected family was re-probed 4x per pass.
_apt_https_source_applicable called apt_https_family itself, and the report
and status loops call it once per store. Each call is up to five `command -v`
PATH scans, so a single audit did ~40 of them. Beyond the waste, it was a
consistency hazard: if PATH changed mid-run, a store could be "applicable" in
one check and absent from the next, so the same file could be reported twice
or not at all. Callers now compute the family once and pass it in. Measured
with a probe-counting override: 4 probes per status_text -> 1, and the report
now makes a single consistent decision per run.
3. Unset HOME built paths at the filesystem root.
`"${HOME:-}/.pip/pip.conf"` degrades to "/.pip/pip.conf" when HOME is empty,
so the per-user store detectors probed paths directly under /. Harmless today
because those files do not exist, but it is wrong and would start matching the
moment anything created them. Per-user config is now only consulted when
HOME is non-empty.
4. The report told you something was wrong but not what to do.
It said "repoint at an https:// endpoint" in the abstract. For flatpak and
snap that advice is actively misleading: there is no config file to rewrite,
so the opt-in sweep cannot fix them and no amount of editing helps. Every
dirty store now prints a one-line remediation with the real command or the
real config key -- including `flatpak remote-modify --url=https://...`, and
an honest note that the snapd store cannot be repointed at all.
Also fixed a misstatement in the report footer: it described
NEOHIRO_APT_HTTPS_REWRITE=1 as an "apt only" rewrite, when that flag sweeps
every installed store. apt is the one rewritten *without* the flag.
And corrected a stale comment that still said "the three functions below" now
that the rewrite path has grown a staging helper, two prep functions and a
collision-proof backup path.
Two tests were pinning the old backup filename by reimplementing the naming
scheme. They now ask the library where the backup is, so a future change to
the scheme cannot break them for the wrong reason.
tests/test_apt_https.sh 247 -> 253. New coverage: family probed exactly once
per pass, applicability check does not re-probe when given a hint, backup
names distinct for paths that flatten alike, single path component, and
deterministic across calls. Both fixes are negative-controlled -- ignoring the
family hint fails the probe-count test, and disabling the checksum fails the
collision test.
Full suite: 684 passed, 0 failures. bash -n clean on every .sh.
Skipped, noted rather than guessed:
- Whether `Acquire::Retries` / `AllowRedirect` behave identically on apt 1.x
and 2.x. Verifying that needs a matrix container per apt release; I have no
such environment here, and guessing at apt's parser is worse than leaving the
long-standing keys alone.
- Making flatpak remotes auto-rewritable. It would mean shelling out to
`flatpak remote-modify`, mutating running-session state during a package
operation. That is a behaviour change with real blast radius, not a review
fix, so it wants its own decision and its own tests.
The tmux wrapper existed to make an interrupted install recoverable. It did not actually deliver that, in three separate ways, and the worst of them is exactly the "booted out with no way back" failure it was supposed to prevent. 1. A stale session was silently hijacked. `tmux new-session -A` attaches when the session already exists. A `linux-setup` left behind by a run that did not exit cleanly therefore meant the new run never started: the user was dropped into an old session, possibly running different flags, with no indication their run had not begun and no way back to it. Now the script checks for the session first, warns that it is a leftover, and starts `linux-setup-<pid>` instead. The inner wrapper -- which previously existed as dead code, built and never passed to tmux -- is now actually used and tears the session down on clean exit, so leftovers stop accumulating in the first place. 2. Every command-line flag was dropped by the re-exec. The wrap re-executed `bash "$SCRIPT_PATH"` with no `"$@"`. `--auto`, `--step`, `--dry-run` and the rest vanished, so wrapping silently changed what the run did. Arguments are now threaded through and shell-quoted, so `--note='a b; rm -rf /'` cannot be re-interpreted by tmux's shell. 3. The reattach command was never shown. `exec` replaces the process, so nothing after the wrap could print. If you detached or lost the socket you had to remember `tmux attach -t linux-setup` from the README. The command is now printed before the exec, naming the session this run actually created -- the PID-suffixed one when the standard name was taken. Script hopping (DeepClean.sh / OptimizeLinuxASR.sh via run_remote_script) had two gaps. The wait loop printed nothing, so a long step looked frozen and invited exactly the Ctrl-C that orphans the work: the poll dies, the detached subscript keeps running, and the user is left staring at a stalled run unable to tell if it is alive. It now emits a heartbeat every 30s with the reattach command, a completion line, and how to detach without stopping. And when SSH + interactive has no tmux to protect the hop, it falls through silently -- now it states plainly that the step cannot be recovered and suggests a local terminal. Also fixed two defects in the degraded (library-not-loaded) mode added in the previous pass. Its warning stood in for a hot-path helper called by every pkg_*/update_* entry point, so a single run reprinted four lines a dozen times and buried the installer output; it is now latched to once per process. And apt_https_status_text returns empty there for "found nothing", which _auto_skip_if_done read as "already HTTPS-only" -- the tool claiming a verification it never performed. Callers now consult a new apt_https_available(), which reports 0 for the real guard and 1 for the stub. One test bug worth noting: an env-transport case reused the variable name SRC for the store id, shadowing the path to linuxinstall.sh and silently breaking every later assertion that grepped it. Renamed, and the symptom is recorded here because three unrelated assertions failing at once with no apparent connection is exactly the kind of thing that gets misdiagnosed as a product bug. tests/test_apt_https.sh 261 -> 275. The continuity properties are asserted against the real ensure_tmux_if_ssh driven with a fake tmux and a captured exec: arguments survive the re-exec, -A is never used, a stale session forces a distinct name and is reported, the printed reattach command matches the session actually created, metacharacters are escaped, a bare invocation emits no empty argument, and the staged reaper really does kill the session on clean exit. Script-hop warnings and the heartbeat are pinned too. Full suite: 706 passed, 0 failures. bash -n clean on every .sh.
…stics
CI caught what the previous commit missed. shellcheck reported SC2120 on
ensure_tmux_if_ssh: "references arguments, but none are ever passed."
That is a real bug, not a lint nit. I had fixed the function body to thread
"$@" into the re-exec, and my tests passed -- because they invoked
ensure_tmux_if_ssh with arguments directly. Production called it bare:
ensure_tmux_if_ssh
so the tmux re-exec still restarted `bash "$SCRIPT_PATH"` with no flags. Over
SSH, --auto / --step / --dry-run were silently dropped and the run behaved
differently from what the user asked for. The call site now forwards "$@".
Lesson recorded in the tests: asserting only the function body is not enough
when the interesting bug lives at the call site. The suite now checks the call
site forwards arguments, that no bare call remains, and that the
references-args-but-nobody-passes shape cannot come back -- so the suite
catches this without needing shellcheck installed. Negative-controlled: putting
the bare call back fails all three.
Also fixed four test assertions whose failure messages could never be
trustworthy. They were written as:
if [ $? -eq 0 ]; then ... else fail_t "..." "rc=$?"; fi
where $? in the else branch is the status of the `[ ... ]` test, not of the
command being checked. Every failure message reported a meaningless rc --
precisely when someone needs the diagnostic. The status is now captured
immediately after the command. shellcheck's SC2319 flags this; CI's pinned
shellcheck predates SC2319, so it passed there, but the local newer build
caught it and the finding is real either way.
Verified with the stricter locally-available shellcheck: the whole tree is
now clean at --severity=warning, not just under CI's exclusion set.
Full suite: 709 passed, 0 failures. bash -n clean. All .sh/.md valid UTF-8
with no double-encoding artifacts and LF endings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A plaintext
http://package mirror means anyone who can intercept the route (hostile Wi-Fi, compromised router, upstream CDN node) can swap the.deb/.rpm/.pkgzyou just downloaded for one of theirs. Signature checks catch forged packages, but they do not stop a downgrade to an older, genuinely-signed, vulnerable build. Only TLS on the transport closes that gap.So every entry point in this repo now runs a guard before anything is fetched.
What it does
On apt (Debian / Ubuntu / Mint / Pop!_OS / Kali and derivatives):
/etc/apt/apt.conf.d/99neohiro-force-https:Acquire::https::AllowRedirect "true"- https to https mirror redirects are fineAcquire::http::AllowRedirect "false"- the important line: a redirect fromhttps://down tohttp://is refused instead of silently followedAcquire::https::Verify-Peer/Verify-Host "true",Acquire::Retries "3"http://->https://on active lines in/etc/apt/sources.list,sources.list.d/*.list(classic) andsources.list.d/*.sources(DEB822URIs:field only). Commented-out lines and DEB822 structural fields (Suites:,Components:,Signed-By:) are left byte-identical./var/backups/neohiro-apt-https/before the first edit, and records each in the rollback log solinuxinstall.sh --rollback --applycan undo it too.apt-get update. If a mirror does not serve the same paths over TLS, the rewrite is rolled back automatically - you are never left with an unusable package manager.NEOHIRO_APT_HTTPS_STRICT=1keeps the rewrite and fails loudly instead.On dnf / yum / zypper / pacman / flatpak it audits and reports every plaintext repo URL but deliberately does not blindly rewrite: plenty of upstream mirrors do not serve the same paths over TLS, and silently breaking a working repo is worse than the threat. Opt in with
NEOHIRO_APT_HTTPS_REWRITE=1.Optional belt-and-braces:
NEOHIRO_APT_BLOCK_PORT80=1addsufw deny out 80/tcp. Off by default because a blanket outbound block also affects unrelated plaintext protocols, and the guard declines to add the rule while any plaintext repo is still configured.Where it is wired in
pkg_update/pkg_install/pkg_upgrade/pkg_autoremoveapt_https_guardas the first statementupdate_system,update_kernel,updates_only_modelib/updater.sh_update_*+ the_run_all_updatesdispatcherrestore_ssh.shopenssh-serverDeepClean.shapt-get autoremove --purge--apt-https/--enforce-https,--apt-https-audit,--apt-https-off/--disable-httpsonlinuxinstall.sh,lib/updater.shandrestore_ssh.sh; plussudo bash lib/apt-https.sh [--enforce|--report|--revert]--step--step apt_httpsapt_httpsrow, always shownIt is the first workflow step on every profile, including Custom, is idempotent, and is latched to once per process, so
AUTO_MODEre-runs and the hot-path guards cost nothing. Escape hatches:NEOHIRO_APT_HTTPS=0,NEOHIRO_APT_HTTPS=audit, or the--apt-https-offrevert.OptimizeLinuxASR.shdownloads no packages, so it needs no guard.Two latent bugs fixed along the way
$(...)had their own progress messages folded into the return value, which then landed inside an arithmetic expansion (bash tried to resolve a word likeRestoredas a variable). Results now travel through globals, keeping the message channel and the value channel separate._step_apt_httpsusedif ! cmd; then rc=$?, which captures the negation's status - a failed enforcement reported success. Nowcmd || rc=$?.Verification
tests/test_apt_https.sh- 92 hermetic assertions. Nothing touches the real/etc:NEOHIRO_APT_ETC_DIR/NEOHIRO_APT_BACKUP_DIRrelocate the tree, a fakeufwsits onPATH,_apt_privis stubbed to simulate root, andNEOHIRO_APT_HTTPS_NOVERIFY=1prevents any network call. A newNEOHIRO_APT_FAMILYpin lets the apt / dnf / zypper / pacman / none paths run deterministically on any host, including the bash:4 and bash:latest CI images.Covered: classic + DEB822 rewriting, comment and structural-field preservation, bracketed options, idempotency, backup-before-edit (and that the backup holds the pre-edit contents), refusal-without-backup, no misleading "already https" on refusal, auto-revert, strict mode, exit codes (0/1/2), every env kill switch, the once-per-process latch, the opt-in port-80 block, and that all ten package entry points guard before their first package command. Plus a parity check that
linuxinstall.sh'scurl|bashinline fallback andlib/apt-https.shexpose the same API.End-to-end CLI paths were also exercised with functional
sudo/apt-getshims: happy path, TLS-less mirror (auto-rollback restores both files byte-for-byte, rc=1), strict mode (keeps rewrite, rc=1), and explicit revert.bash -npasses on every.shin the repo.Drive-by
tests/test_color_gate.shslicedrestore_ssh.shby hardcoded line numbers (sed -n '19,54p'), which silently broke as soon as a source block was added near the top. It now anchors on function names, matching the convention used elsewhere in the suite.README.mdhad an unresolved merge-conflict marker (<<<<<<</=======/>>>>>>>) sitting in the middle of the "One-step automated setup" section. Removed.