Repository navigation
feat(install): install without administrator rights (--no-admin) - #768
Merged
Merged
Conversation
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
pushed a commit
that referenced
this pull request
Nov 6, 2026
…admin) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
|
In the 1.49.0 release. Added while landing: a |
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 this is
install.shis the publiccurl | bashinstaller. Until now it could not finish on an account without administrator rights: when Node.js, tmux or git were missing it went tosudo(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, thecodemanlink 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(orCODEMAN_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
SHASUMS256.txtpublished beside it, unpacked to~/.codeman/runtime/node.~/.codeman/runtime/bin.install.sh cloudflared)/usr/local/bin~/.local/bin, where the server already looks first.node,npm,npxandtmuxare linked from~/.local/bin(never over a file that does not already point into the runtime), and the runtime'snode/bingoes on PATH and on the LaunchAgent's PATH, so a CLI installed withnpm install -gis found too.When it is chosen by itself:
sudois not installed; the account is outside thesudo,wheelandadmingroups andsudo -n truefails; thesudopassword is refused (the installer then offers this route instead of ending the run); on a Mac, the account is not in theadmingroup and there is no Homebrew it can write to.CODEMAN_NO_ADMIN=0switches the detection off. It is never decided by tryingsudo -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>.newand moved into place only afternpm run buildsucceeded, 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 kindarchiveand tells the user to re-run the installer; it used to saynpm i -g aicodeman@latest, which is wrong for a~/.codeman/apptree.What it cannot do
Checked before anything is downloaded, with a message that names the cause:
node-pty@1.1.0ships no Linux prebuild and compiles duringnpm install. A compiler is a system package, so this route cannot supply one.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
sudoprompt. Local network and this-machine access need nothing.Also fixed (affects every install)
check_gitno longer trustscommand -v git. On a Mac without the Xcode Command Line Tools,/usr/bin/gitis 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 ingit clone. That path is now judged byxcode-select -pand never executed; any other git is asked for--version.scripts/git-hooks.mjsanswers "not a checkout" without running git (it returns early when there is no.git). postinstall calls it on everynpm install, and it rangittwice there (measured with a stub in place of git), which on such a Mac means Apple's stub.download()anddownload_to_stdout()resolve their downloader themselves.DOWNLOADERwas only set bymain(), so a subcommand that downloads, such asinstall.sh cloudflared, died onset -uwith the message swallowed by the step log. This is broken on master today.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:
install.sh cloudflared,install.sh uninstallHOME, own instance and port, with sudo, brew, git, the compilers, launchctl and tailscale replaced by tripwire stubsAutomated:
test/install-sh-invariants.test.tsgets 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-adminwas 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 passesplutil -lint.Reviewing the diff
install.shis +822 / -38, which overstates what needs a close read:No-admin routeandEnd of the no-admin route. Nothing outside it changed to make room. Start there.update()moved one level in to sit beside the new archive half (git diff -wshows +798 / -14).resolve_admin_modeanduser_in_admin_group(next toensure_sudo),sudo_session_try(the oldsudo_session_startbody, 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.tsare one newdescribeblock at the end, plus its imports.CLAUDE.md,docs/architecture-invariants.mdanddocs/wiki/Installation.mddocument the route.Risk and judgment calls
^1.1.0. That is why Linux still needs a toolchain on this route.node-pty@1.2.0-beta.18ships 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.TMUX_BUILD_VERSION. The Node.js tarball is checked against aSHASUMS256.txtfetched 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.~/.codeman/appby hand is gone after it. With no git there is nothing to say which files a newer version dropped, and a stalesrc/*.tswould be compiled. State lives in~/.codeman, outside that directory. The swap also rebuildsnode_modulesfrom scratch each time, which is slower than reusing it and cannot carry a broken tree forward.nodethe 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.resolve_admin_modeon Linux treats an account outside the three groups as having no rights unlesssudo -n truesucceeds. An account that sudoers lists by name, with a password, gets the no-admin route by default;CODEMAN_NO_ADMIN=0restores the old behaviour.