fix(install): refuse a root install instead of publishing a broken moshcode - #106
Merged
Conversation
…shcode
public/install.sh is what moshcoding.com/install.sh serves, so this is
the script everyone actually runs. Every path in it comes from $HOME.
Escalated, that is /root: the CLI lands in /root/.moshcode and the
wrapper in /root/.local/bin, mode 0700.
link_system_bin then makes it worse than a no-op. Because $MOSHCODE_BIN
is not on root's PATH but /usr/local/bin is and is writable, it
publishes
/usr/local/bin/moshcode -> /root/.local/bin/moshcode
so `moshcode` resolves on PATH for every user on the box and executes
for none of them. The install prints "Install complete" and exits 0. The
first sign of trouble arrives later, on an unrelated command:
$ moshcode install secrets
zsh: permission denied: moshcode
This is easy to walk into: `dns enable` needs root and tells you to
re-run under sudo, and `moshcode update` self-updates by re-running this
installer (upgrade.mjs selfSpec) -- so an escalated update quietly
reinstalls the CLI into root's home.
Refuse that case before any work, and name the user who would be locked
out. A bare root shell (containers, CI images, root-only VPS) has no
SUDO_USER and is a legitimate way to install, so only the
escalated-from-a-real-user case is refused, and MOSHCODE_ALLOW_ROOT
overrides even that. `remove` is deliberately left unguarded, since
cleaning up an existing root install is exactly when running as root is
right.
The tests drive the real script with `id` shadowed on PATH, so the root
branch runs without root. They assert the refusal lands before detect_os
-- the first step of run_install -- rather than merely that it happens,
because a guard that fires after the install has started is not a guard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan15 finding(s) HIGH/CRITICAL: 9 | MEDIUM: 6
Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
marked this pull request as ready for review
August 3, 2026 16:29
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
public/install.shis whatmoshcoding.com/install.shserves, so this is the script everyone actually runs. Every path in it derives from$HOME. Escalated, that is/root— the CLI lands in/root/.moshcodeand the wrapper in/root/.local/bin, mode0700.link_system_binthen makes it worse than a no-op.$MOSHCODE_BINis not on root's PATH, but/usr/local/binis and is writable, so it publishes:moshcodenow resolves on PATH for every user on the box and executes for none of them. The install printsInstall completeand exits 0. The first sign of trouble arrives later, on an unrelated command:That is not a hypothetical — it is a real machine, and the symlink above is copied from it.
It is also easy to walk into.
dns enablegenuinely needs root and tells you to re-run under sudo (dns.mjs:2405), andmoshcode updateself-updates by re-running this very installer (upgrade.mjsselfSpec). So an escalated update quietly reinstalls the CLI into root's home. The tool trains the habit, and one of the commands it applies to is the self-updater.What
Refuse before any work, and name the user who would be locked out:
Deliberately narrow, so nothing that works today stops working:
SUDO_USERMOSHCODE_ALLOW_ROOT=1overrides even the refused caseremoveis left unguarded — cleaning up an existing root install is precisely when running as root is correctTests
tests/install-root-guard.test.mjsdrives the real script withidshadowed on PATH, so its root branch is exercised without root, andcurl/miseare tripwires.The load-bearing assertion is that the refusal lands before
detect_os, the first step ofrun_install— not merely that it happens. A guard that fires after the install has started is not a guard, and this bug is specifically a half-completed install that reports success.Seven cases: refusal fires and nothing runs; the message names the locked-out user and both remedies; bare root still proceeds;
MOSHCODE_ALLOW_ROOToverrides; a normal uid is unaffected even withSUDO_USERset (a plain shell inherits it after any earlier escalation);run_removestays unguarded; the header documents the override.Verified non-vacuous — reverting the guard fails 9 assertions.
Suite: 163 pass / 7 fail, and those same 7 fail on
masterwithout this branch (baseline 156 pass / 7 fail).sh -n public/install.shclean.Related
moshcoder/moshcode#272adds the same guard to that repo'sinstall.sh. I opened it first believing it was the served script; it is not — this one is. That PR is still worth having as defense-in-depth, since its header advertises araw.githubusercontent.cominstall path, but this is the fix for the bug.🤖 Generated with Claude Code