From 35edf50f21da1a5b3839fdfe130ed0dfbdd4d241 Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 27 May 2026 15:03:51 -0400 Subject: [PATCH] fix(smolmachines): drop runuser -l in favor of UID switch + explicit HOME/USER MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Interactive claude session hung silently after `attaching interactive claude session...` — `runuser -l` invokes a login shell that triggers PAM session setup / /etc/profile sourcing, and the minimal Debian agent VM doesn't have the PAM config files for that to complete cleanly. claude never got to draw its TUI. Switch UID via plain `runuser -u --` (no `-l`) and inject HOME / USER through `smolvm machine exec -e` so the child process sees them. Avoids login-shell wiring entirely. Same pattern in `exec_claude` and `exec(script)`. `_HOME_FOR` maps the two users the codebase currently asks for (`node`, `root`); anything else falls back to `/home/`. Co-Authored-By: Claude Opus 4.7 --- claude_bottle/backend/smolmachines/bottle.py | 62 +++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/claude_bottle/backend/smolmachines/bottle.py b/claude_bottle/backend/smolmachines/bottle.py index f2bfbb7..f4e4235 100644 --- a/claude_bottle/backend/smolmachines/bottle.py +++ b/claude_bottle/backend/smolmachines/bottle.py @@ -9,20 +9,36 @@ context-manager contract is satisfied. User context: `smolvm machine exec` runs commands as root in the VM, but the agent image's USER is `node` and claude-code refuses to run as root with `--dangerously-skip-permissions`. Both -`exec_claude` and `exec` wrap commands in `runuser -l node -c` -so they execute as the node user (and pick up node's $HOME / -$USER from the login shell) — matches the docker backend's -default-USER behavior.""" +`exec_claude` and `exec` switch to the requested user (default +`node`) via `runuser -u --` and set `HOME` / `USER` +through `smolvm -e` — avoiding `runuser -l`'s login-shell wiring +(PAM session setup, /etc/profile sourcing) which can hang on a +minimal Debian VM with no PAM session config.""" from __future__ import annotations -import shlex import subprocess from .. import Bottle, ExecResult from . import smolvm as _smolvm +# Per-user env the agent image's USER (node) expects. claude +# reads ~/.claude.json + writes session state under ~/.claude/; +# bare `runuser -u` inherits root's HOME=/root, which claude +# can't write to. Set HOME / USER explicitly through smolvm -e +# so the child process sees them. +_HOME_FOR = { + "node": "/home/node", + "root": "/root", +} + + +def _env_flags_for(user: str) -> list[str]: + home = _HOME_FOR.get(user, f"/home/{user}") + return ["-e", f"HOME={home}", "-e", f"USER={user}"] + + class SmolmachinesBottle(Bottle): """Handle returned by `SmolmachinesBottleBackend.launch`. The underlying VM lifecycle (create / start / stop / delete) lives @@ -47,21 +63,18 @@ class SmolmachinesBottle(Bottle): scripted exec, wrong for an interactive shell. Drop down to `subprocess.run` with the TTY inherited. - `runuser -l node -c` runs the inner command under node's - login shell so $HOME / $USER are set. Without that switch - claude bails on `--dangerously-skip-permissions cannot be - used with root/sudo privileges`.""" + UID switches via `runuser -u node --` (not `-l`) so we + avoid login-shell wiring. HOME / USER come from `smolvm + -e` instead, which sets them on the process env.""" flags = ["smolvm", "machine", "exec", "--name", self.name] if tty: flags += ["-i", "-t"] + flags += _env_flags_for("node") claude_argv = ["claude"] if self._prompt_path: claude_argv += ["--append-system-prompt-file", self._prompt_path] claude_argv += argv - # shlex-quote each piece so flags / paths with shell-special - # chars survive the runuser -c shell parse. - inner = " ".join(shlex.quote(p) for p in claude_argv) - flags += ["--", "runuser", "-l", "node", "-c", f"exec {inner}"] + flags += ["--", "runuser", "-u", "node", "--", *claude_argv] result = subprocess.run(flags, check=False) return result.returncode @@ -73,18 +86,23 @@ class SmolmachinesBottle(Bottle): on both backends. Pass `user="root"` for tests that need root. - `smolvm machine exec` runs commands as root in the VM, so - we always need to switch user (even when the caller asked - for root, switching to root is a cheap no-op via - `runuser -l root`).""" - r = _smolvm.machine_exec( - self.name, - ["runuser", "-l", user, "-c", script], + `runuser -u -- /bin/sh -c