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:
@@ -106,9 +106,12 @@ export async function provisionClaudeCli(params: { email: string; osUser: string
|
||||
const present = await asMember(params.osUser, ['test', '-x', binPath]);
|
||||
if (present.ok) return { ok: true, binPath, wrote: false };
|
||||
|
||||
// `sh -c` with the pipe inside it, because the pipe has to be interpreted by the member's shell and not by
|
||||
// this process — `runAs` takes an argv, not a command line.
|
||||
const install = await asMember(params.osUser, ['sh', '-c', `set -e; curl -fsSL ${CLAUDE_INSTALL_URL} | sh`]);
|
||||
// Piped into `bash`, not `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 `| sh` died with `Syntax error: "(" unexpected`, which reads like a broken
|
||||
// download rather than the wrong interpreter. Reproduced on the live server: `dash -n` fails there, `bash -n`
|
||||
// is clean.
|
||||
const install = await asMember(params.osUser, ['sh', '-c', `set -e; curl -fsSL ${CLAUDE_INSTALL_URL} | bash`]);
|
||||
|
||||
// The installer's exit code is not the gate — the same lesson as rootless Docker in
|
||||
// `docs/per-user-linux-accounts.md`. What matters is whether the binary is now there and runnable.
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user