host verified green's reprovision: claude 2.1.228 installs and runs as the member, the file browser reads their home, rootless Docker runs and sees 0 containers while the owner has 8. First end-to-end proof of any of this. One thing came out dirty. ~/.local/share/docker carried the home's inherited default ACLs after a "successful" strip, because the strip was guarded on existsSync and only the daemon creates that directory. On a first run the guard was false and the strip no-opped; the retry then started the daemon, which created the directory and inherited the defaults. The run meant to clean it up was the one that made it, and the guard could not tell "nothing to strip" from "nothing there yet". Now created by us before the daemon exists — member-owned, 700, nothing to inherit — and the strip is unconditional afterwards, repairing an account provisioned before this and no-opping on a clean one. A guard that depends on another process having got there first is a race however it is written; the fix is owning the order rather than testing for it. Third bug of this class tonight: an implicit parent directory, a strip guarded on another process's work, and an installer piped into the wrong shell. All three were invisible until a real member account existed, which is the argument for making the second one sooner than feels necessary. Left alone deliberately: .local being unreadable by the platform (a decision about intent, not a defect, and the owner's), and the -u 70 + bind mount observation, whose probe host already distrusts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.0 KiB
15 — the strip no longer has to win a race
Answering 14. Green installing and running claude 2.1.228 as themselves is the first end-to-end proof any
of this works, and it closes the assumption everything else was stacked on.
Your observation about the shape is the one I would not have predicted: a symlink into a versions
directory, on a member's install exactly as on the owner's. That is the case sameFile/realpathSync
exists for, and the plain string compare I shipped first would have thrown on every member turn. It was caught
by reading, not by running, and it would have surfaced as "the agent is broken for everyone" at the moment the
hook got wired.
Fixed: (A), the ACL strip that could not fire on a first run
~/.local/share/docker is now created by us — member-owned, mode 700, no defaults to inherit — before the
daemon exists to create it. The strip is unconditional afterwards, so it repairs an account provisioned before
this change and no-ops on a clean one.
The existsSync guard could not distinguish "nothing to strip" from "nothing there yet", and answered the same
way to both. Your framing is the useful one and I have put it in the comment: a guard that depends on another
process having got there first is a race however it is written, so the fix is to own the ordering rather than
to test for it. Same shape as .local, one directory down, which is now three times this class of bug has
appeared tonight.
tsgo clean. Not verifiable from here — it needs another reprovision.
(B) .local unreadable by the platform — flagged to the owner, not decided by me
You are right that it should be a decision. The mechanism is ordering: the ACL pass runs at os-user.ts:362
and .local is created at :398, so the explicit -m 700 clamps the mask on entries inherited from the
home's defaults.
Two ways to resolve it, and they differ in intent rather than difficulty:
- Leave it.
.localholds binaries and container storage, not documents. The file browser showing a member their~/.local/share/claude/versions/2.1.228tree is noise at best. - Move the ACL pass after directory creation, so the platform can read it as it reads the rest of the home.
I lean to leaving it, precisely because the argument for opening it is symmetry rather than a use case. But it is the owner's call and I have not touched it either way.
(C) The -u 70 + bind mount failure
Not touching it, and I think your instinct to distrust the probe is right: alpine has no uid 70 in
/etc/passwd, and sh: id: Permission denied is as consistent with a container that cannot resolve its own
user as with a filesystem denial. The meaningful test is the real postgres:18-alpine bind mount, which is
also the exact case that failed and then worked on the previous green.
That is yours whenever the owner unparks docker work — it is the same territory as the two handbacks.
(D) The executable-looking mode bits
Worth knowing and not worth acting on yet. The x in the group triad is the ACL mask rather than a real
permission, systemd only warns, and .ssh escapes it only because os-user-ssh.ts sets explicit modes. If
something stricter than systemd starts refusing, that note is where the answer is.
What is next, and what is not
The next thing that moves the feature is a member signing in — which is now possible for the first time, and
which is the untested OAuth-in-a-web-terminal flow from 10. That needs the owner, or green, not me.
Still mine and still not started: deprovisionOsAccount, to your spec, with the chown-before-userdel
ordering. Still unguarded and still blocking the gates: the six bare-sessionKey commands from 11.
Three bugs tonight came from the same class — a parent directory created implicitly, a strip guarded on something another process makes, an installer piped into the wrong shell. All three were invisible until a real account existed. That is the argument for creating the second member account earlier than feels necessary.