Skip to content

install.sh: support Linux, not just Homebrew - #127

Merged
ThinkOffApp merged 1 commit into
mainfrom
claudemb/install-sh-linux-prereqs
Sep 20, 2026
Merged

ThinkOffApp merged 1 commit into
mainfrom
claudemb/install-sh-linux-prereqs

Conversation

@ThinkOffApp

Copy link
Copy Markdown
Owner

scripts/install.sh is advertised in the README as THE one-line install, but its prereq layer only knew Homebrew. On Linux it told you to "Install Homebrew (https://brew.sh) first", which is nonsense advice. There is a real Ubuntu box (VTA) that needs this today, and every Linux cloner of this public repo hit the same wall.

Do not merge yet — opened for review.

macOS-only assumptions found in install.sh

# Assumption Status
1 need_brew prereq install; "Install Homebrew first" on failure fixed
2 "brew upgrade node" when node < 20 fixed
3 Hook wiring shells out to python3 — stock ubuntu:24.04 has none fixed (rewritten in node)
4 ipconfig getifaddr en0/en1 for the LAN URL fixed (ip -4 route get, hostname -I)
5 brew install qrencode tip fixed (per-manager)
6 macOS Accessibility / osascript block printed unconditionally fixed (Darwin-gated)
7 IAK_INSTALL_WATCHDOG installs a launchd LaunchAgent fixed (Darwin-gated, with a pointer)
8 No systemd equivalent for the watchdog supervisor deferred, called out in output and README
9 osascript GUI-wake scripts (claudemb-wake.sh, claude-gui-*.sh) are macOS-only deferred — out of scope for the installer; Linux users get the portable Stop-hook path, which the installer now says explicitly
10 scripts/start-all.sh uses launchctl deferred — not invoked by install.sh

No BSD sed -i '' anywhere in the repo. The two hook scripts the installer wires (check-rooms-hook.sh, claudecode-stop-resume.sh) and session-bootstrap.sh are already portable.

A full Linux port of the wake layer is bigger than this PR should be, so this is scoped to the prereq layer plus everything in install.sh that was trivially fixable. Items 8-10 are the remainder.

Two bugs found while testing, both producing a half-done install that exited 0

  1. apt ate the installer. apt-get prompted via debconf on tzdata and read from stdin. Under curl | bash stdin is the remaining script text, so apt consumed the rest of the installer and bash exited 0 having only installed packages: no clone, no config, no hooks, success exit code. Plan commands now run with stdin closed, plus DEBIAN_FRONTEND=noninteractive. The evidence was <STDIN> line 254 in a debconf perl warning.
  2. The report claimed a daemon that was not running. It printed Daemon: tmux session 'iak-mcp' and a listener URL unconditionally, but the daemon exits immediately on a fresh install (no mcp.confirmations channels configured) because the starter config has no room. It now checks the session and says so plainly.

node 20+ on Debian/Ubuntu

Measured, not assumed: on Ubuntu 24.04 apt-cache policy nodejs gives candidate 18.19.1, below our floor of 20. So apt-get systems default to NodeSource's official apt repo. That means piping a third-party script into a root shell, so the installer prints the line and a "Heads up" naming deb.nodesource.com before running it, and points at nvm and IAK_NODE_SOURCE=distro as alternatives. With IAK_NODE_SOURCE=distro the post-install re-verify still refuses to continue on node 18 rather than proceeding into a broken install.

Consent and honesty rules

  • Every package install is printed first; sudo is called out explicitly.
  • curl | bash leaves no TTY, so the installer refuses to run an unattended sudo install: it prints the commands and exits 1. IAK_ASSUME_YES=1 is the documented opt-in, after downloading and reading the script.
  • No package manager, or no root and no sudo: exact package names, manual step, exit non-zero.
  • Unknown uname -s: exit non-zero naming the platform.
  • New IAK_DRY_RUN=1 prints the platform and prereq plan and changes nothing.

Testing

Real ubuntu:24.04 containers, nothing preinstalled (no node, git, tmux, python3, curl or sudo), script piped into bash so stdin is the script exactly as under curl | bash.

  • (a) fresh Ubuntu, no prereqs — installs git, tmux, NodeSource node v22.23.2, clones, npm install (92 packages), writes starter config, wires all three hooks, reports LAN IP 172.17.0.2 correctly (not loopback). Exit 0.
  • (b) idempotence — second run: Prereqs OK: node v22.23.2, npm 10.9.8, git, tmux / Already up to date. / Hooks already present. No package work, config preserved, exit 0.
  • (c) no supported package manager — apt-get moved aside: Package manager: none detected, lists node 20+/git/tmux and the manual step, exit 1.
  • (d) macOS unchanged — Darwin verified on the host via IAK_DRY_RUN=1 and a PATH sandbox containing a fake brew that exits 99 loudly if invoked. It was never invoked: the no-TTY refusal fires first. Plan on Darwin is brew install git tmux + brew install node, same as before. The installer was never run for real on this MacBook — its prereqs, ~/.claude/settings.json, config and daemon were not touched.

Hook-wiring rewrite verified by differential test: old python and new node run against the same settings.json (unrelated model/permissions keys, an unrelated UserPromptSubmit hook, an unrelated PreToolUse hook, and one of our three hooks already present) produce byte-identical output apart from a trailing newline, and both are idempotent on a second run.

Negative controls

  • Platform detection can fail: a fake uname reporting Plan9Unsupported platform: 'uname -s' reported 'Plan9', exit 1. (First attempt did not fire — SIP blocked the fake binary and it still reported Darwin — so it was rebuilt and re-run.)
  • Node floor can fail: IAK_NODE_SOURCE=distro on Ubuntu 24.04 installs 18.19.1 → node is still missing or below 20 after the install step. found: v18.19.1, exit 1.
  • Daemon check can fail both ways: shown reporting Daemon: NOT running on a fresh config and Daemon: running in tmux session 'iak-mcp' once a channel was configured.

No credential was read, printed or committed; the starter config ships api_key: "" and the test containers never held a real key.

🤖 Generated with Claude Code

scripts/install.sh is advertised in the README as THE one-line install, but
its prereq layer only knew brew: on any Linux box it told the user to
"Install Homebrew first", which is nonsense advice. There is a real Ubuntu
device (VTA) that needs this today, and every Linux cloner of this public
repo hits the same wall.

Prereq layer:
- Detect the platform with `uname -s` and pick a package manager: brew on
  Darwin, apt-get / dnf / pacman / zypper on Linux. An unrecognised
  `uname -s` now exits non-zero naming the platform and listing the manual
  steps, instead of falling through.
- No package manager, or no root and no sudo, prints the exact package
  names plus the manual step and exits non-zero. Never a silent fall-through
  and never a claim of success.
- Every install is printed before it runs, and sudo is called out
  explicitly. Under `curl | bash` there is no TTY to confirm on, so the
  installer refuses to run an unattended sudo install: it prints the
  commands and exits 1. IAK_ASSUME_YES=1 is the documented opt-in.
- After installing, re-verify node/npm/git/tmux rather than assuming the
  package manager did what was wanted.

node 20+ on Debian/Ubuntu: the distro `nodejs` package is below our floor
(Ubuntu 24.04's candidate is 18.19.1, measured), so apt systems default to
NodeSource's official apt repo. That pipes a third-party script into a root
shell, so the installer says so out loud before doing it and points at nvm
and IAK_NODE_SOURCE=distro as alternatives. With IAK_NODE_SOURCE=distro the
re-verify step still refuses to continue on node 18.

Other macOS-only assumptions fixed in the same pass, since a fix in one
place is not a fix:
- The hook-wiring step shelled out to python3. A stock ubuntu:24.04 has no
  python3, so the installer needed a second language runtime it never
  declared as a prereq. Rewritten in node, which is already a hard prereq
  by that point. Verified byte-identical to the python it replaces against
  a settings.json with pre-existing unrelated hooks, and it now reports
  malformed JSON instead of throwing a traceback.
- `ipconfig getifaddr en0` silently fell back to 127.0.0.1 on Linux, which
  is useless when the whole point is a LAN URL for the phone. Linux now
  asks the routing table (`ip -4 route get`), with `hostname -I` as backup,
  and if it really cannot tell it says the URL will not work and how to
  find the real one.
- The `brew install qrencode` tip is now per-manager.
- The macOS Accessibility / osascript block printed unconditionally. It is
  Darwin-only now; Linux gets a note that the portable Stop-hook resume
  path is what delivers messages there.
- IAK_INSTALL_WATCHDOG installed a launchd LaunchAgent unconditionally. It
  is Darwin-gated now; on Linux it says the supervisor is launchd-only and
  points at running team-watchdog.mjs directly. A systemd unit is NOT part
  of this change.

Two bugs found while testing in the container, both of which produced a
half-done install that exited 0:
- apt's debconf prompted on tzdata and read from stdin. Under `curl | bash`
  stdin IS the remaining script, so apt ate the rest of the installer and
  bash exited 0 having only installed packages: no clone, no config, no
  hooks, and a success exit code. Plan commands now run with stdin closed,
  plus DEBIAN_FRONTEND=noninteractive.
- The final report printed "Daemon: tmux session 'iak-mcp'" and a listener
  URL even when the daemon had already exited, which it always does on a
  fresh install because the starter config has no channel configured. It
  now checks the session and says plainly when the daemon is not running.

Also adds IAK_DRY_RUN=1, which prints the platform and prereq plan and
changes nothing.

Tested in stock ubuntu:24.04 containers (no node/git/tmux/python3/curl/sudo
preinstalled): fresh install end to end, a second run for idempotence
("Hooks already present", no package work), and a no-package-manager box.
Negative controls: a fake `uname` reporting Plan9 exits 1; forcing the
distro node (18.19.1) is rejected by the re-verify step; and the new daemon
check was shown reporting both NOT running and running. The Darwin path was
exercised on macOS via IAK_DRY_RUN and a PATH sandbox with a fake brew that
would have failed loudly if called; this machine's own prereqs, config,
hooks and daemon were never touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-20T11:31:37.397264Z 0364eef PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0364eef6fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/install.sh
Comment on lines +405 to +407
# This used to be a python3 heredoc. A stock ubuntu:24.04 has no python3, so
# that made the installer need a second language runtime it never declared as
# a prereq. node 20+ is already guaranteed by this point, so use it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep Python available for the installed SessionStart hook

On a fresh Linux installation without python3—the exact environment this rewrite targets—the installer now succeeds and registers session-bootstrap.sh, but that script still invokes python3 to read configuration/input and to emit its final hook JSON. Since the final invocation suppresses stderr and exits 0 afterward, every SessionStart silently produces no context, so the advertised self-arming room-agent behavior never starts. Either port session-bootstrap.sh to Node as well or retain Python as a checked/installed prerequisite.

Useful? React with 👍 / 👎.

@ThinkOffApp
ThinkOffApp merged commit 8d0859c into main Sep 20, 2026
3 checks passed
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.

1 participant