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

68 lines
2.9 KiB
Markdown

---
name: run-confined
description: 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](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
```sh
.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:
```sh
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:
```sh
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`.