Untrusted installers (curl | bash, unvetted npx packages) run in a transient systemd user unit: $HOME unreadable, only ~/.local writable. The script is a copy of office-admin/tools/run-confined.sh; it aborts rather than run unconfined when AppArmor blocks user namespaces. Generated-by: Claude Opus 5.5
2.9 KiB
name, description
| name | description |
|---|---|
| run-confined | Run confined / sandbox an untrusted command — use for `curl ... | bash` installers, npm/npx/bunx of unvetted packages, or any third-party script that should not see ~/.ssh, ~/.config/gh, tokens, or the rest of $HOME; when someone says "run it confined", "sandbox this", or "don't pipe that into my shell". Runs the command in a transient systemd user unit whose only writable place is ~/.local. |
Run confined: untrusted code in a one-shot systemd user unit
run-confined.sh runs a command in a transient
systemd-run --user service with kernel-enforced sandboxing:
$HOMEis an unreadable tmpfs (ProtectHome=yes): no~/.ssh,~/.config/gh,.envfiles, or other secrets.- The only writable place is
/tmp/confined, a bind mount of~/.local; inside the unit$HOMEpoints there. Whatever gets installed lands in~/.localon the host. - System dirs are read-only;
/tmpand/devare private. - The calling shell's environment is not inherited (no
GH_TOKEN,SSH_AUTH_SOCK, ...). Network is allowed. - Node (newest nvm v22, bound read-only) is on
PATHwhen present.
Before running anything, the script does a wet test: if ~/.ssh is still
visible from inside a probe unit, it aborts with exit 3 and runs nothing.
Usage
.agents/skills/run-confined/run-confined.sh -- cmd [args...]
Arguments naming paths under $HOME or ~/.local are rewritten to the
/tmp/confined equivalents; add --preserve-paths to opt out.
stdin, stdout, and stderr are piped through (systemd-run -P), so a
download-and-run installer works as a pipe. Fetch with curl on the host
(read it first), then run the interpreter confined. Pass environment
explicitly with env, since the unit doesn't inherit any:
curl -fsSL https://bun.com/install -o /tmp/bun-install.sh
less /tmp/bun-install.sh
.agents/skills/run-confined/run-confined.sh -- \
env BUN_INSTALL=/tmp/confined bash < /tmp/bun-install.sh
# -> ~/.local/bin/bun on the host
Point installers at /tmp/confined (e.g. BUN_INSTALL, npm_config_prefix,
which the script already sets), not $HOME/.foo. That way binaries land in
~/.local/bin, which is already on the host PATH. Rc-file edits an
installer makes go to ~/.local/.bashrc and similar, where they have no
effect; check for them and delete them.
When the wet test aborts
On Ubuntu 24.04, kernel.apparmor_restrict_unprivileged_userns=1 (the
default) stops the systemd user manager from creating namespaces, and
systemd silently skips the sandbox. The script refuses rather than run
unconfined. The operator decides whether to lift it:
sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 # until reboot
Don't run the command unconfined as a fallback. Report the abort and let the operator choose. The script header lists the full symptom set.
The canonical copy of this script is
~/projects/office-admin/tools/run-confined.sh.