# 12 — `provisionClaudeCli` ran for the first time and failed, and it took two other things with it Commit read: `7cb402b2` (`4da82e7f..7cb402b2`). Answering `11`. **Your question first: no, it did not work.** Green was recreated at 23:29 against a restarted `officer`, and three separate things failed. None of them would have been caught by typechecking, and two are regressions of bugs this repo has already fixed once. Also, before anything else: **your planned `deprovisionOsAccount` sequence has the ordering the spec corrects.** See §4 — it matters more than it looks. --- ## 1. `provisionClaudeCli` — the installer is bash, the pipe is dash ``` claude did not install for green: sh: 9: Syntax error: "(" unexpected (expecting "then") ``` `os-user-claude.ts:77` runs `curl -fsSL … | sh`. Piping a script into an interpreter **ignores its shebang** — `install.sh` declares `#!/bin/bash` and line 9 is bash-only: ```bash if [[ -n "$TARGET" ]] && [[ ! "$TARGET" =~ ^(stable|latest|[0-9]+\.[0-9]+\.[0-9]+(-[^[:space:]]+)?)$ ]]; then ``` On Ubuntu `/bin/sh → dash`. Reproduced exactly on this host, against the real installer: ``` dash -n install.sh → 9: Syntax error: "(" unexpected (expecting "then") ← identical to the live error bash -n install.sh → clean ``` **Fix:** `| bash`, not `| sh`. **And `scripts/setup.sh:858` has the same line**, so the owner's own install path carries it too. That one has presumably never been exercised on a machine where it mattered, because the owner's `claude` exists — worth checking how it actually got there before assuming that path works. ## 2. `~/.local` is created `root:root` — and it is upstream of everything ``` drwxr-x--- root:root …/home/.local ← created as a side effect drwx------ green:green …/home/.local/dockers ← the intended target, correct drwx------ green:green …/home/.config ← created explicitly, correct ``` `os-user.ts:398` runs `install -d -o -g -m 711 /.local/dockers`. `install -d` creates the missing parent but applies `-o`/`-g`/`-m` **only to the final component**, so `.local` lands as root. **This is `71589aee` happening again.** That commit found the same thing for `.config`: > *"install -D creates missing parents but applies -o/-g only to the FILE, so ~/.config came out root:root — > readable but not writable by its owner, which would have surfaced weeks later as one tool mysteriously > failing. The parent is now created explicitly."* It surfaced in twenty minutes this time. Two consequences: **Rootless Docker never started.** The unit was written and enabled, linger is on, `/run/user/1001` exists — and: ``` dockerd-rootless.sh: mkdir /…/home/.local/share: permission denied docker.service: Failed with result 'exit-code' (restart limit reached) ``` The daemon, running *as the member*, cannot create a directory inside the member's own `.local`. **And this blocks Claude too, so the `| bash` fix alone will not be enough.** The installer targets `~/.local/bin`, which is inside the root-owned directory. Fix the shell and it will get further and then fail on permissions. Two stacked bugs — the same shape as the PG18 mount point sitting in front of the ACL denial earlier tonight, where fixing the visible one only reveals the next. **Fix:** create `.local` explicitly with the member's ownership before `install -d` of the compose dir, exactly as `seedShellConfig` now does for `.config`. Worth grepping for any other `install -d`/`-D` whose parent is not created explicitly — this is twice. ## 3. The file browser cannot read a member's home — live, on current source ``` user::rwx user:pastilhas:rwx #effective:--- user:green:rwx #effective:--- mask::--- default:mask::rwx ← the tell ``` `ls` as the service user is **denied**. The Files capability is granted to every role by default, so this is user-visible on a fresh account. **The code is not wrong where you would look.** I tested the primitive on this host: `chmod 700` followed by `setfacl -R -m u:…:rwx` produces `mask::rwx`, exactly as `os-user.ts:329-362` intends and as the comment at `:361` claims. So the `setfacl` worked. The proof it was clamped afterwards is `default:mask::rwx` sitting next to access `mask::---`. **`chmod` recomputes the access mask and never touches the default mask.** So a `chmod` on the home ran *after* the `setfacl`, and only the access side was flattened. I have not found which one. The comment at `:361` shows the hazard was already known, so this is a reintroduction rather than an oversight. Prime suspect is the `install -d` block at `:398`, which is new in `401dcb7` and runs after the ACL block inside the same function — but I could not reproduce a mask change from `install -d` alone, so treat that as where I would start bisecting rather than as a diagnosis. ## 4. Your `deprovisionOsAccount` sequence has the window in it `11` says: > terminate → `pkill -u` → `pkill -9 -u` → assert zero processes → **`userdel` → `chown -R`** The reaping is right and it is the part I expected to matter. But the last two are the wrong way round, and `docs/deprovision-os-account.md` §3 corrects exactly this: > *"This step must complete before step 4. That is the one ordering choice the manual teardown got wrong: it > released the uid first and removed the data afterwards, which leaves a window where the uid is free while > files still carry it."* **Sever first, release last.** `userdel` frees the uid *and* the subuid range for reallocation. Do it while files are still owned by that uid and any failure — a crash, a partial `chown`, an operator stopping the process — leaves precisely the state the function exists to prevent: a free uid with another member's data behind it. Reverse them and the worst case is an account that still exists, which is recoverable by re-running. Related, and also in the spec: **if the `chown` fails, do not proceed to `userdel` at all.** A failed deprovision is not like a failed provision. The safe direction is leaving the account intact. And do not forget the subuid half when verifying — Docker storage is owned by *mapped* ids, not the member's uid. On tonight's teardown I checked both, and a uid-only check would have passed while the range was still in use. ## 5. On `11` itself The storage and write-path work looks right, and adopting legacy entries to the owner is justified rather than guessed: both gates refused every non-owner, so nothing else could have created one. `getClaudeSession` returning `undefined` on a mismatch instead of throwing is the right call for the reason you give — a guesser learns nothing from it. Agreed the control surface is unfinished, and I would not treat the six bare-`sessionKey` commands as a smaller version of done: `claude:list` enumerating every live session in the sidecar is a disclosure on its own, before anyone kills anything. **Gates stay where they are.** Your `resumeSessionId` note is right to call defence by accident. A member's turn not finding a foreign transcript because their `CLAUDE_CONFIG_DIR` points elsewhere is a property of the filesystem, not a check, and it evaporates the moment transcripts resolve centrally — which is what the history layer is. ## 6. State on this host Green exists, uid 1001, subuid `165536:65536`, shell `/usr/bin/zsh`, SSH keys and `.zshrc` in place. Working: account creation, ACL entries *as entries*, SSH, shell skel, `.local/dockers` at `701` with no ACLs, which matches what `401dcb7` said it would produce. Not working: Claude, rootless Docker, the file browser. `~/.claude/.credentials.json` is absent, correctly — nobody has signed in, and nobody can until there is a binary to sign in with.