Skip to content

Refuse a Skills Install Under Sudo and Probe Docker's Engine Version - #753

Merged
ptr727 merged 1 commit into
developfrom
fix/host-setup-sudo-and-docker-engine-probe
Aug 16, 2026
Merged

Refuse a Skills Install Under Sudo and Probe Docker's Engine Version#753
ptr727 merged 1 commit into
developfrom
fix/host-setup-sudo-and-docker-engine-probe

Conversation

@ptr727

@ptr727 ptr727 commented Aug 16, 2026

Copy link
Copy Markdown
Owner

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.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. 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.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. 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, and spec/host-tools.json has no way to express a WSL-conditional probe.
  • 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.

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.

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 a sudo'd run cannot be faked by setting a variable. The cases reach a real EUID 0 through unshare -r and skip where a host restricts unprivileged user namespaces, which is probed rather than assumed.

run result
root, SUDO_USER names a user refused, exit 1, names that user
root, no SUDO_USER installs, exit 0
root, SUDO_USER=root installs, exit 0
ordinary user, SUDO_USER set installs, exit 0

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 #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

## 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
Copilot AI lite review requested due to automatic review settings August 16, 2026 02:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.sh execution under sudo (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 to docker --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
ptr727 merged commit 5c6decf into develop Aug 16, 2026
6 checks passed
@ptr727
ptr727 deleted the fix/host-setup-sudo-and-docker-engine-probe branch August 16, 2026 03:04
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants