pipe the installer into bash, and stop inventing a root-owned .local

Green's first provision failed three ways. host caught all three on the live
box; two are fixed here and the third is his to bisect.

THE INSTALLER IS BASH AND WE PIPED IT INTO SH. A script read on stdin never has
its shebang honoured — the interpreter you name is the one that runs it — and
install.sh declares #!/bin/bash and uses [[ =~ ]] on line 9. On Ubuntu /bin/sh
is dash, so it died with `Syntax error: "(" unexpected`, which reads like a
corrupt download rather than the wrong interpreter. scripts/setup.sh carried the
same line for the owner's own install and is fixed too.

INSTALL -D CREATED ~/.local AS ROOT. `install -d` makes missing parents but
applies -o/-g/-m only to the final component, so blessing ~/.local/dockers
invented a root:root .local inside the member's own home. Rootless Docker then
died on `mkdir …/.local/share: permission denied`, and the Claude installer
targets ~/.local/bin, so fixing the shell alone would have hit this next.

That is 71589ae for the second time — same function shape, same silent parent,
same class of consequence. Its own commit message said this surfaces "weeks
later as one tool mysteriously failing"; it took twenty minutes. Grepped the
other install -d/-D sites: os-user-shell already creates its parent explicitly,
os-user-ssh has no implicit parent.

NOT fixed: the file browser's ACL mask on a member home, where access mask is
--- while default:mask is rwx. That pattern means a chmod ran after the setfacl
and clamped only the access side, so the primitive is right and something later
is wrong. host has the live filesystem and has already half-excluded the
suspect; guessing from here would churn a working block. Noted that this commit
adds an install -d before the one he was about to bisect, so it wants a
reprovision first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 23:46:59 +00:00
co-authored by Claude Opus 5
parent 52b021bbe2
commit ef000aaf51
4 changed files with 90 additions and 4 deletions
+25
View File
@@ -393,6 +393,31 @@ export async function confineUserTree(params: {
// The cost is that the file browser cannot read inside it, which is the same trade already accepted for
// Docker's internal storage — consistent rather than a new exception. Not enforced: a member can bind
// mount from anywhere and will hit the denial there. This is the documented place that works.
// `.local` FIRST, explicitly, with the member's ownership. `install -d` creates missing parents but
// applies `-o`/`-g`/`-m` only to the FINAL component, so letting it invent `.local` leaves that directory
// root:root — inside the member's own home, unwritable by them.
//
// This is `71589ae` for the second time. That commit found the identical thing for `~/.config` and wrote
// "a single wrong-owner directory in a home is the kind of thing that surfaces weeks later as one tool
// mysteriously failing". It surfaced in twenty minutes: rootless Docker died on
// `mkdir …/.local/share: permission denied`, and the Claude installer targets `~/.local/bin`, so it was
// blocked by the same directory. Grep before adding another `install -d`/`-D` whose parent is implicit.
const localDir = join(home, '.local');
const madeLocalDir = await run([
'sudo',
'-n',
'install',
'-d',
'-o',
String(params.uid),
'-g',
String(params.gid),
'-m',
'700',
localDir,
]);
if (!madeLocalDir.ok) return { ok: false, error: `could not create ${localDir}: ${madeLocalDir.out}` };
const composeDir = join(home, '.local', 'dockers');
const madeComposeDir = await run([
'sudo',