# Sandbox

HakoRun wraps the runtime in an OS-native sandbox on every run: Bubblewrap on Linux, seatbelt on macOS.

## Linux: systemd-run + bwrap

`internal/sandbox/command_linux.go` builds `systemd-run --scope --user --quiet --slice=hakorun.slice -- bwrap ... -- <runtime> /wrapper.<ext>`.

| Aspect | Setting |
|--------|---------|
| Namespaces | `--unshare-all`, `--unshare-net` (no network) |
| Privileges | `--cap-drop ALL`, `--new-session`, `--die-with-parent` |
| Filesystem | Read-only `/usr`, `/lib`, `/lib64`, and the wrapper; tmpfs `/tmp` and `/home/sandbox`; fresh `/proc` and `/dev` |
| Working directory | `/tmp` |
| Environment | `HOME=/home/sandbox`, `PATH=/usr/local/bin:/usr/bin:/bin`, `TMPDIR=/tmp`, `LANG=C.UTF-8`; unsets `LD_PRELOAD`, `LD_LIBRARY_PATH` |
| TypeScript extra | Mounts the project root read-only and points `NODE_PATH` at its `node_modules` |

Because only `/usr` is mounted, the global `tsx` must live under `/usr` (npm's default `/usr/local` prefix works); `bwrap` fails to start on systems without `/lib64`.

## macOS: sandbox-exec

The command is `sandbox-exec -p <profile> <runtime> <wrapper>`, with the profile from `seatbeltProfile()`:

```scheme
(version 1)
(deny default)
(allow process-exec)
(allow process-fork)
(allow sysctl-read)
(allow mach-lookup)
(allow signal)
(allow ipc-posix*)
(allow file-read*)
(allow file-write* (subpath "<HOME>"))
(allow network*)
```

## Platform Comparison

| Aspect | Linux | macOS |
|--------|-------|-------|
| Network | Disabled | Allowed |
| Writes | tmpfs only | `$HOME` only |
| Reads | Mounted paths only | Everywhere |
| Resource caps | `hakorun.slice` | None |
| Environment | Reset | Inherits the server's |

The macOS boundary is much wider and suits development; serve production traffic from Linux.

## Related Pages

- [Resource Limits](/resource-limits)
- [Script Runtime](/script-runtime)
