Five providers implement the same Computer interface. They do not offer the same
protection. This page is what each one is worth against an adversary, not against an
accident.
| Provider | isolationKind | Boundary | An injected model can reach |
|---|---|---|---|
docker | kernel | namespaces, cgroups, dropped capabilities | the container, and the network policy you set |
podman | kernel | the same, rootless when run unprivileged | the container, and the network policy you set |
fly | kernel | a Fly microVM, one per computer | the microVM |
ssh | machine | a different physical box | everything your user can reach on that box |
local | guardrails | process-level checks only | everything your user can reach on this machine |
isolationKind exists because a plain boolean overclaims. An ssh box is genuinely
isolated from the laptop you are sitting at, and genuinely not isolated from itself.
husk doctor renders that as isolated from this machine rather than a green
isolated.
docker and podman
Every container is created with the same hardening, and none of it is conditional on what the caller passed:
--cap-drop ALL
--security-opt no-new-privileges
--pids-limit 512
--read-only
--tmpfs /tmp:rw,exec,nosuid,size=512m
--tmpfs /run:rw,nosuid,size=16m
--tmpfs /work:rw,exec,nosuid,size=<diskMb or 2048>m
--cpus=<cpus or 2>
--memory=<memoryMb or 2048>m
--memory-swap=<memoryMb or 2048>m
-u 1000:1000--memory-swapmatches--memorybecause without it the limit only delays the OOM: the container swaps first.--pids-limit 512means a fork bomb inside the machine hits its own ceiling rather than the host's.- The root filesystem is read-only. Anything written outside
/workand/tmpfails loudly instead of mutating an image layer that is about to vanish. -u 1000:1000is passed regardless of the image, so even a public fallback image runs unprivileged.
With persist: true, /work becomes a named volume husk-<id> instead of a tmpfs.
An injected model in this mode can wreck the container. It cannot reach your home directory, your SSH keys, your Docker socket, or your other containers.
The one hole you can open yourself
computer:
mounts:
- source: ./data
target: /work/data
readonly: true # the defaultA host mount is the one hole in a container. readonly defaults to true, and an agent
that only needs to read a repository must not be handed the ability to rewrite it. Set
readonly: false deliberately or not at all.
Rootless podman is reported, not assumed
podman info is asked whether it is rootless, and the answer is repeated back:
✓ podman isolated
podman is running as root -- containers get kernel isolation, but an escape lands on root
fix: run husk as an unprivileged user to get rootless containersRunning as root is still kernel isolation. It is not the isolation this provider advertises, so it is said out loud.
fly
One Fly.io microVM per computer, created through the Machines API with
restart: { policy: 'no' } and an init of mkdir -p <workdir> && exec sleep infinity.
Hardware-level isolation, and the only provider that costs money — metered by the second
while the machine runs.
husk doctor says so in the hint rather than after the fact:
fix: run `fly auth token` and export it as FLY_API_TOKEN (fly machines are metered, not free)ssh
A Linux box you already own. Commands run as your user on that box, over a multiplexed SSH control master.
✓ ssh isolated
ssh · user@host (Linux aarch64)
commands run as your user on that box -- isolated from this machine, not from itselfWorkspaces live under $HOME/.husk-work/<id>/, with work and tmp subdirectories,
both chmod 700. The jail is enforced on the far end — see below.
local
Not a sandbox. A guarded working directory, and the only provider that is always available.
This is not a caveat at the bottom of the page. It is the first thing husk doctor
says, the first thing husk up warns about, and the first thing the MCP server tells
the model:
A model that believes it is contained when it is not will take risks it otherwise would not, which is why the MCP server states it in its first tool result rather than leaving the model to guess.
What it does guarantee
| Control | What it stops |
|---|---|
| Path jail | Every filesystem-API call resolves through realpath and is rejected if the target leaves the workspace, including through a symlink created inside it |
| Environment scrub | Everything not on a small allow-list is dropped before the process starts |
| Command deny list | A short list of unrecoverable commands is refused |
| Output caps | A runaway process cannot exhaust memory through captured output |
| Process-tree kill | A timeout kills the whole process group, not just the shell |
| Mount namespace (WSL2) | /work is bind-mounted per exec inside unshare -mr, so two computers cannot see each other's files |
What it does not stop
The agent shares your kernel, your network and your user account. It can reach anything your user can reach that is not specifically blocked. Verifiably:
The command deny list is a deny list, and deny lists are bypassable by anyone who is
trying. It exists to stop rm -rf / typed by a confused model, not to stop an
adversary who can read the list.
Inside a WSL exec the agent is uid 0 of a user namespace created by unshare -mr, so
id -un prints root. Files it creates are owned by your real user outside the
namespace, and the bind mount disappears when the command exits. It is the same trick
rootless containers use, and it is not a container.
The path jail
The jail presents /work and /tmp to the guest and maps them onto real directories.
It runs in two passes:
- Lexical.
toHostPathnormalises the guest path, rejects anything outside/workor/tmp, and re-checks containment afterresolve()collapses... - Resolved.
assertInJailcallsrealpathon the nearest existing ancestor and confirms the real target is still inside the workspace. This is the pass that catches a symlink created inside the workspace and pointing out of it.
error path is outside the machine's writable area: /etc/passwd
hint: this computer exposes /work and /tmp; use a path under one of themerror path resolves outside the workspace through a symlink
hint: husk refuses to follow links that leave the machineOn ssh the resolved pass cannot be a local realpath — that would be a round trip per
operation — so it is compiled into the same remote shell command that does the work,
using readlink -f, and exits with code 77 on a violation.
What the path jail does not cover
The command deny list
Applied by every provider, on every exec, before the process starts. It is short on purpose: an over-eager deny list gets switched off, and a switched-off deny list protects nobody.
The bar for inclusion is no legitimate agent task needs this, and running it by accident is unrecoverable.
| Refused | Reason reported |
|---|---|
rm -rf /, rm / | recursive delete of / · delete of / |
mkfs, mkswap, fdisk, parted, sgdisk | disk formatting |
dd … of=/dev/sd* (also nvme, hd, disk) | raw write to a block device |
shutdown, reboot, halt, poweroff, init 0 | host power control |
systemctl <x> stop, disable, mask; the same via service | stopping host services |
> or >> into /etc/passwd, shadow, sudoers, hosts | overwriting a system file |
chmod 777 / | world-writable / |
anything piped into sh, bash, zsh, ksh | piping a remote script into a shell |
:(){ :|:& };: | fork bomb |
sudo | privilege escalation |
nc/ncat/netcat with -e | reverse shell |
Rules are anchored to command position — the start of a line, or after ;, |,
&, $(, then, do. Without that anchor, grep -r "sudo" . and
echo "shutdown" >> notes.txt get refused, and a deny list that cries wolf is a deny
list users switch off.
Adding to it, and overriding it
guardrails:
denyCommands:
- 'git\s+(push|commit|reset\s+--hard)'
allowCommands:
- '^sudo apt-get install'allowCommands is checked first and wins over both your deny list and the built-in
one. An explicit allow entry is you saying you know better about your own machine, and
you do. An entry that is not valid regex falls back to a substring match rather than
throwing.
Output caps
Two independent caps, both defaulting to 256 KiB.
- Per exec.
OutputBufferkeeps the first 60% and the last 40% of each stream and elides the middle. The middle of a runaway build log is never the interesting part; the command that started it and the error that ended it are. - Per tool result.
limits.maxOutputBytesclamps a tool result before it re-enters the conversation, keeping the head and the tail.
Both mark the elision so neither the model nor you can mistake a truncated log for a complete one:
... [1048576 bytes elided by husk] ...The clamp runs before redaction, so redaction always operates over bounded input.
Timeouts
limits.execTimeoutSec defaults to 120 seconds per exec, and limits.timeoutSec to 300
seconds for the whole run. On a timeout the entire process group is killed —
process.kill(-pid, 'SIGKILL') on POSIX, taskkill /T /F on Windows — so a shell that
spawned a build does not leave the build running.
Which one to use
Use local for your own code on your own machine. Use docker or podman the moment
an agent will read anything you did not write.
Guardrails handle accidents and confusion. Only isolation handles prompt injection, because an injected model is not making a mistake — it is following instructions competently, toward someone else's goal.