From d02fe50193d8cdc1ec96e29794c295b9aeafa7f3 Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 27 May 2026 16:08:08 -0400 Subject: [PATCH] fix(smolmachines): run `claude mcp add` as node so config lands in node's home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit provision_supervise dispatched `claude mcp add --scope user` through `smolvm machine_exec`, which runs as root by default. The MCP entry got written to root's ~/.claude.json — but the agent's claude reads /home/node/.claude.json, so `/mcp` showed "No MCP servers configured" inside the bottle. Wrap the exec in `runuser -u node -- env HOME=/home/node ...` so the config writes to the right home. Same pattern as the interactive exec_claude / Bottle.exec wrappers — `smolvm machine_exec` is always root, so any command that touches user state has to switch UID + set HOME explicitly. Co-Authored-By: Claude Opus 4.7 --- .../backend/smolmachines/provision/supervise.py | 7 +++++++ tests/unit/test_smolmachines_provision.py | 10 +++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/claude_bottle/backend/smolmachines/provision/supervise.py b/claude_bottle/backend/smolmachines/provision/supervise.py index 724dafe..c06bf38 100644 --- a/claude_bottle/backend/smolmachines/provision/supervise.py +++ b/claude_bottle/backend/smolmachines/provision/supervise.py @@ -38,9 +38,16 @@ def provision_supervise(plan: SmolmachinesBottlePlan, target: str) -> None: return url = plan.agent_supervise_url info(f"registering supervise MCP server in agent claude config → {url}") + # `claude mcp add --scope user` writes to ~/.claude.json. The + # agent is the `node` user; smolvm machine_exec runs as root + # by default, so we have to switch user explicitly and set + # HOME so the config lands in /home/node/.claude.json (where + # the agent's claude actually reads it from). r = _smolvm.machine_exec( target, [ + "runuser", "-u", "node", "--", + "env", "HOME=/home/node", "claude", "mcp", "add", "--scope", "user", "--transport", "http", diff --git a/tests/unit/test_smolmachines_provision.py b/tests/unit/test_smolmachines_provision.py index bb547ad..f00ea79 100644 --- a/tests/unit/test_smolmachines_provision.py +++ b/tests/unit/test_smolmachines_provision.py @@ -487,11 +487,15 @@ class TestProvisionSupervise(unittest.TestCase): _supervise.provision_supervise(plan, "claude-bottle-demo-abc12") ex.assert_called_once() argv = ex.call_args.args[1] - # claude mcp add --scope user --transport http supervise - # — URL is the agent-side endpoint (host loopback + - # discovered port), not the docker bridge IP. + # `claude mcp add --scope user` writes to ~/.claude.json, + # and the agent is the `node` user — switch UID + set + # HOME so the config lands in /home/node/.claude.json, + # not root's. URL is the agent-side endpoint (host + # loopback + discovered port), not the docker bridge IP. self.assertEqual( [ + "runuser", "-u", "node", "--", + "env", "HOME=/home/node", "claude", "mcp", "add", "--scope", "user", "--transport", "http",