Skip to content

feat(install): install without administrator rights (--no-admin) - #768

Merged
Ark0N merged 1 commit into
masterfrom
feat/no-admin-install
Nov 6, 2026
Merged

Ark0N merged 1 commit into
masterfrom
feat/no-admin-install

Conversation

@Ark0N

@Ark0N Ark0N commented Nov 5, 2026

Copy link
Copy Markdown
Owner

What this is

install.sh is the public curl | bash installer. Until now it could not finish on an account without administrator rights: when Node.js, tmux or git were missing it went to sudo (Linux) or to Homebrew (macOS) and stopped there, with "sudo is required but not installed", "Failed to obtain sudo privileges", or Homebrew's own "Insufficient permissions". Codeman itself never needed those rights. The app, the codeman link and the user-level service all live under the home folder; only the prerequisites needed an administrator.

This PR adds a route that supplies the prerequisites without administrator rights. It is taken by itself when the account has none, and --no-admin (or CODEMAN_NO_ADMIN=1) forces it. The route for accounts that do have the rights is unchanged, apart from the fixes listed under "Also fixed".

On the server side the only change is one new value of the install kind the Updates panel reads (archive), with its own message.

What the route does

Needed With administrator rights (unchanged) Without
Node.js 22 system package or Homebrew The official tarball from nodejs.org, checked against the SHASUMS256.txt published beside it, unpacked to ~/.codeman/runtime/node.
tmux system package or Homebrew The tmux project's own build (tmux/tmux-builds 3.7c), checked against a SHA-256 pinned in the installer, in ~/.codeman/runtime/bin.
git system package or Xcode tools Never installed. Without a working git the source is downloaded as an archive instead of cloned.
cloudflared (install.sh cloudflared) package, or a binary in /usr/local/bin The release binary in ~/.local/bin, where the server already looks first.

node, npm, npx and tmux are linked from ~/.local/bin (never over a file that does not already point into the runtime), and the runtime's node/bin goes on PATH and on the LaunchAgent's PATH, so a CLI installed with npm install -g is found too.

When it is chosen by itself: sudo is not installed; the account is outside the sudo, wheel and admin groups and sudo -n true fails; the sudo password is refused (the installer then offers this route instead of ending the run); on a Mac, the account is not in the admin group and there is no Homebrew it can write to. CODEMAN_NO_ADMIN=0 switches the detection off. It is never decided by trying sudo -v, which prompts, and whose refusal is reported to the machine's administrator for an account outside sudoers.

An install without git carries a marker file (.install-source) and updates by re-running the installer. The new version is fetched and built in <install dir>.new and moved into place only after npm run build succeeded, so a failed download or build leaves the running install as it was. A later re-run on a machine that has gained a working git stages a clone instead, which turns the install back into a normal one. The in-app updater reports such an install as kind archive and tells the user to re-run the installer; it used to say npm i -g aicodeman@latest, which is wrong for a ~/.codeman/app tree.

What it cannot do

Checked before anything is downloaded, with a message that names the cause:

  • Linux without make, a C++ compiler and python3. node-pty@1.1.0 ships no Linux prebuild and compiles during npm install. A compiler is a system package, so this route cannot supply one.
  • macOS older than 15. The official tmux build has that as its deployment target. An administrator has to install tmux once; the rest then works without one.
  • musl (Alpine) and architectures other than x86_64 and arm64: nodejs.org or tmux-builds has no build this route can use.

Tailscale on Linux needs an administrator to install it, to log in and to grant serve rights, so on this route the installer says so and falls back to local access instead of opening a sudo prompt. Local network and this-machine access need nothing.

Also fixed (affects every install)

  1. check_git no longer trusts command -v git. On a Mac without the Xcode Command Line Tools, /usr/bin/git is a stub that opens Apple's "install the developer tools" dialog and exits non-zero. The installer used to report git as found and then fail in git clone. That path is now judged by xcode-select -p and never executed; any other git is asked for --version.
  2. scripts/git-hooks.mjs answers "not a checkout" without running git (it returns early when there is no .git). postinstall calls it on every npm install, and it ran git twice there (measured with a stub in place of git), which on such a Mac means Apple's stub.
  3. download() and download_to_stdout() resolve their downloader themselves. DOWNLOADER was only set by main(), so a subcommand that downloads, such as install.sh cloudflared, died on set -u with the message swallowed by the step log. This is broken on master today.
  4. The AI CLI menu leaves out a Homebrew-only install command where there is no Homebrew (same reasoning as the existing curl filter).
  5. The LaunchAgent's PATH includes the folder Node.js itself lives in when that is outside the fixed list (the no-admin runtime, nvm, fnm). See Risk.

How it was tested

With the real installer, each run that started a server followed by a shell session created through the API and a command round trip through its PTY:

Case Result
Ubuntu 24.04 container, unprivileged user, no sudo, git, Node.js or tmux Install in 60 s, session works, update by re-run (built beside, swapped in), install.sh cloudflared, install.sh uninstall
Same, without a compiler Stops in under a second, nothing downloaded
A git that does not run, then a working git Archive install, then a clone, then a plain git update
sudo installed, account outside the sudo group Route chosen by itself
sudo installed, password refused Falls back to this route
macOS 26 on Apple Silicon, isolated HOME, own instance and port, with sudo, brew, git, the compilers, launchctl and tailscale replaced by tripwire stubs Install in 18 s, session works, update in 16 s; the stubs recorded only the installer's own "does git run" probes and read-only Tailscale status calls
Linux host with system Node.js and tmux but no working git Nothing downloaded for those two, archive install, and the Updates panel shows the new message in a real browser

Automated: test/install-sh-invariants.test.ts gets a block for this route (static guards plus drives of the real functions in a real bash, including an archive install and its replacement); each new guard was checked by mutating the code and watching it fail. CI's bash 3.2 step gets a sub-step for the same helpers; that image is Alpine, so it is also where the musl stop runs for real. npm test, typecheck, lint and format pass.

Not tested: a real standard macOS account. The Mac used here is an administrator's with Xcode installed, so --no-admin was forced and the missing tools were simulated. The service files were not started from a no-admin install either: the systemd unit text is unchanged, and the LaunchAgent PATH is unit-tested and the generated plist passes plutil -lint.

Reviewing the diff

install.sh is +822 / -38, which overstates what needs a close read:

  • 465 lines are one new, self-contained section, between the headers No-admin route and End of the no-admin route. Nothing outside it changed to make room. Start there.
  • About 24 lines are re-indentation: the git half of update() moved one level in to sit beside the new archive half (git diff -w shows +798 / -14).
  • The rest are call sites, each small: resolve_admin_mode and user_in_admin_group (next to ensure_sudo), sudo_session_try (the old sudo_session_start body, returning 1 instead of ending the run), ask_dependencies, fetch_source / finish_source_swap / install_or_update_repo, the four Tailscale spots, cloudflared_subcommand, the LaunchAgent PATH, uninstall, and the dispatcher at the bottom.

The 412 added lines in test/install-sh-invariants.test.ts are one new describe block at the end, plus its imports. CLAUDE.md, docs/architecture-invariants.md and docs/wiki/Installation.md document the route.

Risk and judgment calls

  1. node-pty stays at ^1.1.0. That is why Linux still needs a toolchain on this route. node-pty@1.2.0-beta.18 ships Linux prebuilds and would lift it, but its install script does not detect musl or a glibc older than 2.28, so an Alpine install that compiles today would get a prebuild it cannot load. A dependency decision for its own PR.
  2. Trust. The four tmux digests are pinned in the script and have to be bumped with TMUX_BUILD_VERSION. The Node.js tarball is checked against a SHASUMS256.txt fetched from nodejs.org over the same TLS connection, which proves integrity, not independent provenance (the signature on that file is not checked). cloudflared is taken unverified, as the existing system routes take it.
  3. An archive update replaces the install directory as a whole, so a file someone put into ~/.codeman/app by hand is gone after it. With no git there is nothing to say which files a newer version dropped, and a stale src/*.ts would be compiled. State lives in ~/.codeman, outside that directory. The swap also rebuilds node_modules from scratch each time, which is slower than reusing it and cannot carry a broken tree forward.
  4. The LaunchAgent PATH change applies to every macOS service install: the folder of the node the installer found is put first when it is not already in the list. For an nvm user that is the Node.js the service already runs on.
  5. resolve_admin_mode on Linux treats an account outside the three groups as having no rights unless sudo -n true succeeds. An account that sudoers lists by name, with a password, gets the no-admin route by default; CODEMAN_NO_ADMIN=0 restores the old behaviour.

Codeman itself always lived under $HOME; administrator rights were only
spent on what it needs first (system packages for Node.js, tmux and git,
Homebrew on a Mac). An account without those rights could not install it:
the installer died in ensure_sudo, or in Homebrew's own installer.

install.sh now has a no-admin route, taken by itself when the account has
no administrator rights (no sudo binary; outside sudo/wheel/admin and
`sudo -n true` fails; a refused sudo password; a Mac account outside the
admin group with no writable Homebrew) and forced with --no-admin or
CODEMAN_NO_ADMIN=1:

- Node.js: the official tarball from nodejs.org, checked against the
  SHASUMS256.txt published beside it, into ~/.codeman/runtime/node.
- tmux: the tmux project's own build (tmux/tmux-builds 3.7c), checked
  against a SHA-256 pinned in the installer, into ~/.codeman/runtime/bin.
- Both are linked from ~/.local/bin (never over a file that is not ours),
  and the runtime is on PATH and on the LaunchAgent's PATH.
- git is never installed. Without a working git the source comes as an
  archive, the install is marked with .install-source, and it updates by
  re-running the installer: the new tree is built beside the old one and
  swapped in only after the build succeeded. A later re-run on a machine
  that has gained git turns it back into a clone.
- Nothing on this route runs sudo, Homebrew or `xcode-select --install`,
  and the route is never decided by `sudo -v`.

What it cannot supply is checked before the first download: a C/C++
toolchain on Linux (node-pty 1.1.0 compiles there), musl, and macOS below
15 (the tmux build's deployment target).

Along the way:

- check_git no longer trusts `command -v git`. On a Mac without the Xcode
  Command Line Tools /usr/bin/git is a stub that opens Apple's install
  dialog; it is judged by `xcode-select -p` and never executed. Any other
  git is asked for its version.
- scripts/git-hooks.mjs answers "not a checkout" without running git, for
  the same reason: postinstall calls it on every npm install.
- The in-app updater knows the install kind `archive` and points such an
  install at the installer instead of `npm i -g aicodeman@latest`.
- download() and download_to_stdout() resolve DOWNLOADER themselves. Only
  main() used to set it, so `install.sh cloudflared` died on `set -u` with
  the message swallowed by the step log.
- The AI CLI menu leaves out a Homebrew-only install command where there
  is no Homebrew.

Verified with the real installer: a bare Ubuntu 24.04 container as an
unprivileged user (install, a PTY round trip through the API, update by
re-run, cloudflared, uninstall), the same without a compiler (stops before
any download), with a broken then working git, with sudo present but not
usable, and on macOS 26 (arm64) in an isolated HOME with sudo, brew, git,
the compilers, launchctl and tailscale replaced by tripwires. Not verified
on a real standard macOS account: that account was an administrator.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Ark0N
Ark0N merged commit ae28534 into master Nov 6, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Nov 6, 2026
…admin)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Ark0N

Ark0N commented Nov 6, 2026

Copy link
Copy Markdown
Owner Author

In the 1.49.0 release. Added while landing: a noexec /tmp no longer fails a working build, an unset USER and a trailing slash on CODEMAN_INSTALL_DIR are handled, and a route chosen because sudo could not be confirmed now says so and names CODEMAN_NO_ADMIN=0. A real standard macOS account is still untested.

philmcneely pushed a commit to philmcneely/Codeman that referenced this pull request Nov 8, 2026
The graphical install runs install.sh --no-admin, so this branch needs both
PR Ark0N#763 (the app) and PR Ark0N#768 (the route that needs no password).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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