Refuse a Skills Install Under Sudo and Probe Docker's Engine Version - #753
Merged
Merged
Conversation
## What
Two host-setup defects found by running the tooling on a live Ubuntu
25.10 WSL2 host, each of which reports success while doing the wrong
thing.
- **`host-setup/linux/install-skills.sh`** gains `refuse_sudo`, run
straight after `parse_args` so `--help` still answers and every action
stops. Under `sudo` the installer resolved `~` to root's home, so the
skills landed in `/root/.agents/skills`, and it looked `claude` up on
root's `secure_path`, so a CLI on the invoking user's own `PATH` read as
absent and the run reported the marketplace unregistered. Both at exit
0. The guard tests `SUDO_USER` alongside `EUID` rather than `EUID`
alone, because a host whose working account is root, a container or a
Proxmox node, carries no `SUDO_USER` and installs for root correctly.
`--report` is refused with the rest, since under `sudo` it reads root's
stamp and answers for a machine state nobody installed. The `--help`
text and the header comment now say the script takes no elevation, where
the two siblings in the same stand-up flow elevate per command
themselves.
- **`spec/host-tools.json` and `host-setup/linux/install-tools.sh`** ask
the daemon for its version before reading the CLI's banner. The probe
list becomes `docker version --format '{{.Server.Version}}'` then
`docker --version`, and `docker_version()` does the same two readings in
the same order, so the gate and the installer never disagree about one
host. One pattern spans both, `^\s*(?:Docker version )?(\d+(?:\.\d+)*)`,
anchored because an unanchored optional prefix reads the first digits
anywhere in the output.
- **`docs/host-setup.md`** gains the engine-versus-client paragraph, and
the floor's `why` no longer claims the probe reads the engine while
reading the client. The **Present when** column still names the banner,
since presence is what it answers and a host with the daemon stopped
still has `docker` installed.
- **Tests**: five in `test_skills_install.py` covering the guard's four
outcomes and `--help` under `sudo`, and two in `test_host_gate.py` for
the probe order and for the pattern against each probe's real output.
The guard reads `$EUID`, which bash makes read-only, so the sudo'd run
cannot be faked by setting a variable and the cases reach a real EUID 0
through `unshare -r`, skipping where a host restricts unprivileged user
namespaces.
## Why
Both defects fail in the direction that reads as success. The sudo'd
skills install exits 0 having installed for the wrong user, and a later
`--report` as the actual user then calls the same tree uninstalled. The
docker floor is an engine number, and on a WSL host where the CLI on
`PATH` is packaged separately from Docker Desktop's engine the two are
different versions, so a current host failed a floor its engine cleared
comfortably.
## Verification
675 unit tests, `spec/validate.py`, `scripts/repo_gate.py`,
`scripts/build_dist.py --check`, both `prose_lint.py` invocations, ruff
check and format, mypy, shellcheck and editorconfig-checker in their
pinned images. `scripts/host_gate.py` and `install-tools.sh --report`
were each run on the host from the issue and now agree on the engine
version. The guard's four outcomes were exercised directly before the
tests were written.
Fixes #750
Fixes #751
Contributor
There was a problem hiding this comment.
Pull request overview
This pull request fixes two host-setup correctness bugs that could previously report success while doing the wrong thing: (1) installing skills into root’s home when run under sudo, and (2) reading Docker’s client banner instead of the daemon/engine version when enforcing the Docker floor.
Changes:
- Refuse
host-setup/linux/install-skills.shexecution undersudo(while still allowing--help) to prevent silently installing for root. - Update the Docker version probe to prefer the engine version (
docker version --format '{{.Server.Version}}') with a fallback todocker --version, and anchor the shared pattern accordingly. - Add unit tests covering the sudo-refusal outcomes and Docker probe/pattern behavior, plus documentation clarifying engine-vs-client semantics.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| spec/host-tools.json | Switch Docker probes to engine-first with a shared anchored pattern that can parse both probe outputs. |
| host-setup/linux/install-tools.sh | Align docker_version() with the engine-first probe order used by the host gate. |
| host-setup/linux/install-skills.sh | Add an early refuse_sudo guard to prevent per-user installs from running under sudo. |
| scripts/tests/test_skills_install.py | Add Linux-only tests (via unshare -r) validating the sudo guard behavior and --help behavior under sudo conditions. |
| scripts/tests/test_host_gate.py | Assert Docker probe order and ensure the updated pattern matches only intended outputs. |
| docs/host-setup.md | Document why the Docker floor is read from the engine and why the client banner is only a fallback. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ptr727
added a commit
that referenced
this pull request
Aug 16, 2026
… Default (#768) Promote `develop` to `main`, carrying the remaining stages of the hub-hosted reusable-workflow rollout and the changes that landed beside them: - #759 Host Get-Version and Publish-Plan as Hub Reusable Tasks - #760 Host the Validate Task and Reshape the Test Pull Request Stub (settles #729 by design: the hub's validate task runs `uvx <tool>@latest`, since Dependabot tracks the action pins and not a uvx version) - #761 Host the Type-Specific Tasks and Retire the Date Badge - #762 Host the Release Chain and the Docker Core in the Hub - #748 and #752, the staged rollout tracker and the PhotoCleaner merge-bot pilot record - #758 Flip the Fleet Line-Ending Default from CRLF to LF - #753, #755, #756, #764, host-setup and test-collection changes The release that follows this promotion is the first tag carrying every hub task, so it is the pin the stage 2 to 5 adoptions and their catalog snippets use. It is also the first run of the hub's own `publish-release.yml` through `build-release-task.yml` with every target disabled, which is the live proof that `github-release` runs when its build needs are skipped. Closes #729. Refs #521 (hub half shipped, the merge-bot adoption sweep is what remains).
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.
What
Two
host-setup/defects found by running the tooling on a live host, each of which reports success while doing the wrong thing.host-setup/linux/install-skills.shgainsrefuse_sudo, run straight afterparse_argsso--helpstill answers and every action stops. Undersudothe installer resolved~to root's home, so the skills landed in/root/.agents/skills, and it lookedclaudeup on root'ssecure_path, so a CLI on the invoking user's ownPATHread as absent and the run reported the marketplace unregistered. Both at exit 0. The guard testsSUDO_USERalongsideEUIDrather thanEUIDalone, because a host whose working account is root, a container or a Proxmox node, carries noSUDO_USERand installs for root correctly.--reportis refused with the rest, since undersudoit reads root's stamp and answers for a machine state nobody installed. The--helptext and the header comment now say the script takes no elevation, where the two siblings in the same stand-up flow elevate per command themselves. This is shape 1 from install-skills.sh installs skills for root and misreports Claude Code registration when run under sudo #750, kept to the shell script.spec/host-tools.jsonandhost-setup/linux/install-tools.shask the daemon for its version before reading the CLI's banner. The probe list becomesdocker version --format '{{.Server.Version}}'thendocker --version, anddocker_version()does the same two readings in the same order, so the gate and the installer never disagree about one host. One pattern spans both,^\s*(?:Docker version )?(\d+(?:\.\d+)*), anchored because an unanchored optional prefix reads the first digits anywhere in the output. The engine reading applies on every platform rather than only inside WSL: on a native install the CLI and the engine are one package, so the number does not change there, andspec/host-tools.jsonhas no way to express a WSL-conditional probe.docs/host-setup.mdgains the engine-versus-client paragraph, and the floor'swhyno longer claims the probe reads the engine while reading the client. The Present when column still names the banner, since presence is what it answers and a host with the daemon stopped still hasdockerinstalled.Why
Both defects fail in the direction that reads as success. The sudo'd skills install exits 0 having installed for the wrong user, and a later
--reportas the actual user then calls the same tree uninstalled. The docker floor is an engine number, and on a WSL host where the CLI onPATHis packaged separately from Docker Desktop's engine the two are different versions, so a current host failed a floor its engine cleared comfortably.Tests
Five in
test_skills_install.pycovering the guard's four outcomes and--helpundersudo, and two intest_host_gate.pyfor the probe order and for the pattern against each probe's real output.The guard reads
$EUID, which bash makes read-only, so a sudo'd run cannot be faked by setting a variable. The cases reach a real EUID 0 throughunshare -rand skip where a host restricts unprivileged user namespaces, which is probed rather than assumed.SUDO_USERnames a userSUDO_USERSUDO_USER=rootSUDO_USERsetVerification
675 unit tests,
spec/validate.py,scripts/repo_gate.py,scripts/build_dist.py --check, bothprose_lint.pyinvocations, ruff check and format, mypy, shellcheck and editorconfig-checker in their pinned images.scripts/host_gate.pyandinstall-tools.sh --reportwere each run on the host from #751 and now agree on the engine version, where the gate previously failed the floor. The guard's four outcomes were exercised directly before the tests were written.Fixes #750
Fixes #751