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",