Files
mad-skills/run-confined/SKILL.md
T
dckc ab335b3061 Add run-confined skill
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
2026-10-08 07:21:44 -05:00

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:

  • $HOME is an unreadable tmpfs (ProtectHome=yes): no ~/.ssh, ~/.config/gh, .env files, or other secrets.
  • The only writable place is /tmp/confined, a bind mount of ~/.local; inside the unit $HOME points there. Whatever gets installed lands in ~/.local on the host.
  • System dirs are read-only; /tmp and /dev are 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 PATH when 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.