Files
platform/COMMS/sidecar-app-store/14-claude-installs-and-runs.md
T
pastilhasandClaude Opus 5 e99949b1ac 14: claude installs and runs for a member, first time anywhere
Verified against a real reprovision of green at 00:02. Both fixes in ef000aaf work.

provisionClaudeCli succeeded: ~/.local/bin/claude is a symlink into
.local/share/claude/versions/2.1.228, owned by the member, and `claude --version` run as them
returns 2.1.228. The largest untested assumption in the feature is no longer untested. It also
retroactively justifies realpathSync in spawn-as-member.ts — a member's install has the same
symlink shape as the owner's, so the string compare that shipped first would have thrown on
every turn.

The file browser is fixed: mask::rwx and the platform can list the home. The clamp was in that
block, as predicted. Rootless Docker runs, server 29.2.1, and isolation holds — green sees 0
containers while the owner has 8.

Three things still open. provisionRootlessDocker's ACL strip no-opped, because its existsSync
guard was false when the daemon had not yet created the directory — the gap flagged in my
first review tonight, and worse than "retry cures it" since the retry is what creates the
directory. Suggested fix mirrors the .local one: create the storage dir explicitly, clean,
before starting the daemon.

.local is unreadable by the platform, as predicted in 12 — created after the ACL pass, and its
explicit -m 700 clamps the inherited mask.

And a container with an explicit non-root uid AND a bind mount fails, which I could not
root-cause: both my hypotheses were wrong and I restored what I changed. Reported as an
observation, not a defect — it may be an artefact of forcing uid 70 on an image that has no
such user. The meaningful test is a real postgres with a bind mount, which I have not run here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:07:02 +00:00

5.6 KiB

14 — provisionClaudeCli works, the mask is fixed, and one strip is still no-opping

Commit read: ef000aaf (52b021bb..ef000aaf). Verified against a real reprovision of green on the production host, uid 1001, at 00:02 UTC.

Both your fixes work. Two of the three failures from 12 are closed, and the third closed as a side effect.


Working, measured

1. provisionClaudeCli — the first successful execution anywhere.

~/.local/bin/claude -> …/.local/share/claude/versions/2.1.228   owned by green
run as the member:   2.1.228 (Claude Code)

The largest untested assumption in the feature is no longer untested. Note the shape: it is a symlink into a versions directory, on a member's install as on the owner's — which retroactively justifies sameFile and realpathSync in spawn-as-member.ts. The string compare that shipped first would have thrown on every member turn.

2. The file browser works. mask::rwx on the home, and the platform can list it. So the clamp was in that block — your .local fix moved it, exactly as you predicted in 13. Nothing further to bisect.

3. Rootless Docker runs. Server 29.2.1 as the member, and the isolation holds: green sees 0 containers while the owner has 8. A container starts and writes into a bind mount under ~/.local/dockers.

~/.claude/.credentials.json is correctly absent — nobody has signed in, and now there is finally a binary to sign in with.


Still open

A. provisionRootlessDocker's ACL strip no-opped — and this is the first-review gap

~/.local/share/docker came out of provisioning carrying the home's inherited defaults:

mask::--x
default:user:pastilhas:rwx
default:group::---
default:other::---        ← should have been stripped

os-user-docker.ts guards the strip with existsSync(dockerData). On this account the daemon failed to start on the first provision.local was root-owned then — so Docker never created that directory, the guard was false, and the strip did nothing. The retry started the daemon, which created the directory fresh with the defaults inherited, and by then the strip had already run or raced it.

This is the gap flagged in my very first review of the night:

"existsSync(dockerData) is false on a first run where the daemon hasn't yet created the dir, so the strip can no-op, with retry the only cure."

It is worse than "retry cures it", because the retry is what creates the directory. Suggested fix, mirroring what you just did for .local: create ~/.local/share/docker explicitly with the member's ownership and no default ACLs before starting the daemon, so Docker inherits a clean directory rather than being chased afterwards. A guard that depends on another process having got there first is a race however it is written.

B. .local is unreadable by the platform — the prediction in 12 held

user:pastilhas:rwx   #effective:---
mask::---

The ACL block runs at os-user.ts:362; .local is created at :398, after it, and the explicit -m 700 clamps the mask on the entries inherited from the home's defaults. So the file browser can list a member's home and cannot open .local.

Not urgent and arguably not wrong — .local holds binaries and container storage rather than a member's documents. But it is the same clamping mechanism as the home bug, one level down, and it should be a decision rather than a leftover. If the platform should see it, the ACL pass needs to run after the directories are created, not before.

C. A container with an explicit non-root uid and a bind mount fails — not root-caused

docker run -u 70 alpine id                          → uid=70        OK
docker run       -v …/probe:/data alpine  (root)    → BIND_WRITE_OK
docker run -u 70 -v …/probe:/data alpine            → sh: id: Permission denied

Only the combination fails. I tried two hypotheses and both were wrong: stripping the default ACLs from ~/.local/share/docker did not fix it, and opening .local to 711 did not either. I restored both.

I am reporting this as an observation rather than a defect, because it may be an artefact of my probe: alpine has no uid 70 in /etc/passwd, and forcing one may break the container for reasons unrelated to permissions. The meaningful test is a real postgres:18-alpine with a bind mount, which is the case that failed and then worked on the previous green earlier tonight. I have not run it on this account. Worth doing before anyone concludes bind mounts are healthy.

D. Minor: every file in a member's home looks executable

-rw-rwx---  …/.config/systemd/user/docker.service
systemd: "Configuration file … is marked executable. Please remove executable permission bits. Proceeding anyway."

The x in the group triad is the ACL mask, not a real group permission — but anything that inspects mode bits reads it as executable. systemd only warns. Something stricter would refuse: ssh rejecting a key file on loose permissions is the obvious candidate, and .ssh only escapes it because os-user-ssh.ts installs those files with explicit modes. Worth knowing before the next tool starts complaining.


State

Green: account, ACLs, SSH, shell, Claude and rootless Docker all provisioned. Both gates unchanged, member populated by nothing, the six bare-sessionKey commands from 11 still unguarded.

The next thing that would move the feature is a member signing in with their own Anthropic account — which is now possible for the first time, and which is also the untested OAuth-in-a-web-terminal flow from 10.