Skip to content

Latest commit

 

History

History
300 lines (194 loc) · 32.2 KB

File metadata and controls

300 lines (194 loc) · 32.2 KB

Sandbox

Forge can run loop iterations or one selected host session inside an isolated msb sandbox — a microVM booted by the microsandbox CLI — while keeping the active project directory mounted at its identical host path for fast host/sandbox file sharing.

See also: Configuration, Tools, Loop System.

Prerequisites

  • The msb CLI installed — there is no account and no login step. Install with:
    curl -fsSL https://install.microsandbox.dev | sh
    Verify the host is ready with msb doctor (an alias of msb self doctor), which checks the hypervisor prerequisites. The interactive installer (bunx opencode-forge, or pnpm run setup from a checkout) offers to run this command for you when it cannot find msb on PATH.
  • A host that can run microVMs: Linux with KVM or macOS on Apple silicon. msb advertises Windows 11 with Windows Hypervisor Platform, but Forge's sandbox shell routing relies on a POSIX shell shim that is unavailable on win32, so with the sandbox enabled Forge refuses to start there rather than silently running on the host — set sandbox.enabled: false on Windows.
  • Docker on the host, used only to build the sandbox image (see below) — the msb runtime itself does not need it.

Forge probes availability with msb doctor bounded at 30s. A probe that does not answer is treated as indeterminate rather than "daemon down": Forge logs and continues, letting the actual sandbox operation report the authoritative error. Only a host that answers definitively (or a missing CLI) fails a loop launch with remediation advice.

Build and load the bundled image:

docker build -t oc-forge-sandbox:latest container/
docker save oc-forge-sandbox:latest -o forge-sandbox.tar
msb load --input forge-sandbox.tar --tag oc-forge-sandbox:latest

msb load registers the archive under the tag Forge looks up (sandbox.image, default oc-forge-sandbox:latest); list loaded images with msb images --format json.

container/Dockerfile ships with the plugin package. If the image is missing when OpenCode starts, Forge shows a warning toast with a Build sandbox template command in the palette. Trigger it at any time by searching for Build sandbox template, which opens a confirmation dialog and runs the build/save/load sequence automatically. The dialog stays open for the duration and shows a live progress bar, the current Docker step, elapsed time, and streamed build output; on failure it keeps the last lines of Docker output so the cause is visible. A first build takes several minutes, and closing the dialog does not cancel it — it finishes in the background and reports with a toast. Restart OpenCode after changing sandbox configuration.

The default image includes Node.js (NodeSource current channel), pnpm, Bun, Python 3 + uv, ripgrep, git with Git LFS, jq, rsync, sqlite3, the GitHub CLI (gh), the Hugging Face CLI (hf), Chromium, and Docker Engine (see Nested Docker).

The sandbox image grants the agent user passwordless sudo, so loops can install whatever software they need at runtime. Commands arrive via msb exec without -u, so they run as the image's USER agent (keeping host-mapped worktree files owned by the host user); system-wide installs use an explicit sudo prefix, for example sudo apt-get install ruby.

Chromium

The image ships Playwright's Chromium build as chromium — the architecture-matched build for the host — and install --with-deps pulls the runtime libraries it needs. Launch it headless with the usual sandbox flags:

chromium --no-sandbox --disable-dev-shm-usage --headless

The browser lives at /opt/forge/.local/share/ms-playwright (PLAYWRIGHT_BROWSERS_PATH), deliberately off the cache disk so the image-shipped build is not hidden by the cache-disk mount (see Tool Caches and Disk Space).

How It Works

  1. A sandbox loop uses its isolated git worktree. A host-session sandbox instead uses the project root selected from the TUI.
  2. Forge creates one sandbox per loop, or one project-scoped host-session sandbox shared by plugin instances in the process.
  3. The active directory, the read-only source project (when sandbox.mountProjectReadonly is enabled), and the worktree's git metadata directory are mounted at their identical host paths, so absolute paths resolve the same on both sides. There is no /workspace or /project container path.
  4. Shell commands and search tools execute inside the sandbox; file tools stay on the host, so LSP and editor integration continue to work, but are fenced to the sandbox mounts (see File-Tool Boundary).

Loop worktrees are created under <dataDir>/worktrees/<slug>, outside the source project. The read-only project mount is dropped whenever it overlaps an accepted read-write mount with conflicting permissions (a read-only ancestor would silently make its whole subtree read-only). For a loop, the worktree's git metadata directory is the source repository's .git — inside the source project — and is mounted read-write, so the read-only project mount is dropped in the default layout and sandbox.mountProjectReadonly is effectively inert there. When the repository's git directory lives outside the source project the read-only mount can survive instead. The worktree stays writable and the git metadata directory is mounted read-write alongside it, so in-sandbox git works and multiple loops in the same project each mount their worktree plus the shared git metadata independently.

Shell Routing

Sandbox loops use opencode's native shell tool — streaming output, truncation with spill-to-file, timeouts, and abort all behave exactly as in a normal session. Routing happens underneath the tool:

  1. Forge wraps the built-in shell tool. For a call from a session that should be sandboxed, the wrapper prefixes the command with a one-off forge-sandbox-required-<uuid> && marker and remembers the sandbox for that marker.
  2. The shell create.before hook has no session ID. It recognizes the marker, strips it, points event.shell at the generated shim (<dataDir>/forge-shell), and sets FORGE_SANDBOX_CONTAINER. Without a marker it routes by location instead: a shell spawn in a loop worktree location resolves to that loop's sandbox.
  3. The shim runs the command via msb exec --quiet "$FORGE_SANDBOX_CONTAINER" --no-tty -w "$PWD" -- bash "$@".
  4. Shell calls from sessions with no expected sandbox are left alone: the shim is not used and OpenCode's own shell runs the command. Active loop routing always takes precedence over host-session preference. When no loop is active and no host sandbox is on, the wrapper performs no session lookups.

The marker makes the routing fail closed: if it is not stripped, the command fails with "command not found" rather than running on the host. The shim itself also fails closed — if the sandbox is expected but msb exec fails (or the loop sandbox cannot be restored), the command errors and never silently runs on the host. msb exec propagates the guest command's exit code verbatim, so the shell tool keeps seeing real exit statuses.

Commands the user runs directly — ! commands and terminals — do not pass through the tool wrapper and stay on the host.

Permission Auto-Approval

When the host sandbox is turned on for a session (the Host sandbox menu in the TUI, which writes a desired state through the Forge server RPC and waits for the server's applied acknowledgement), Forge resolves that session's permission prompts automatically, and those of its Task subagents. It does this through OpenCode's permission.evaluate hook: a decision that would ask is resolved to allow or deny rather than shown. It is denied when it matches an autoApprove.deny rule; otherwise it is allowed, including requests that match an explicit OpenCode ask rule. OpenCode deny rules still deny. This applies only while the session's shell calls actually route into a running sandbox. A request that arrives during initial sandbox startup waits for that resolution: if the sandbox becomes active it is auto-approved under the same policy; otherwise the prompt is shown as usual. When the sandbox is off or failed to start, prompts are shown as usual unless a per-session auto-approve is separately enabled. The attached server's sandbox.enabled decides whether the toggle is available at all.

OpenCode deny rules still apply: they settle before the hook runs. Loop sessions are unaffected because their permission ruleset already allows everything it doesn't deny.

The same allow/deny policy also applies to the per-session Toggle auto-approve, which works whether or not a sandbox is on — see TUI → Sidebar.

File tools (read, write, edit, patch) run on the host, not in the sandbox, but the file-tool boundary refuses any path the sandbox cannot see, so auto-approval never reaches host files outside the mounts. To keep prompting while sandboxed, disable it. This does not turn off a per-session Toggle auto-approve:

{
  "sandbox": {
    "autoApprovePermissions": false
  }
}

Tool Behavior

Tool category Behavior in a sandboxed session
Shell Native shell tool, executed inside the loop sandbox via the shell shim.
Search tools glob and grep route through the msb exec execution hooks.
File tools read, write, edit, and patch operate on the host filesystem, restricted to the sandbox mounts; write/edit/patch are refused in read-only mounts.
Git operations managed by Forge Worktree commits, cleanup, and branch management are handled on the host.

Network Access

Public egress is allowed by default: when no host restrictions or secret destinations are configured — or sandbox.network.allow contains "*" or "**" — Forge passes no network flags with the default allowLan: false. msb then allows public egress and blocks private ranges. With allowLan: true, Forge instead passes --net public,private; see LAN Access. A wildcard entry overrides narrower entries in the same list. Listing concrete hosts enables restricted public egress:

{
  "sandbox": {
    "network": {
      "allow": ["registry.npmjs.org"]
    }
  }
}

When any concrete host is configured (including secret destination hosts, which are unioned into the same allow list) and no */** entry is present in sandbox.network.allow, Forge creates the sandbox with --net-default deny plus one --net-rule allow@<host> per validated host. Each entry is validated before use; invalid entries are skipped and logged. Verified rejections: a comma, any colon (both example.com:443 and example.com:tcp:443 are rejected; use example.com), an @, a wildcard suffix with fewer than two labels (*.example.com is valid, *.com is rejected), and a bare single-label hostname (use domain=myhost). The domain= and suffix= forms pass through. A wildcard in a secret's destination hosts stays invalid — those hosts declare where that secret may be sent, not global egress policy. If every configured host is invalid, Forge retains deny-by-default without host allow rules rather than falling back to allow-all. An explicit allowLan: true still adds the private-address group.

With sandbox.network.allowLan off (the default), the host's loopback interface remains unreachable from inside the sandbox: the private range is not part of msb's public egress group, so a host listener on e.g. 0.0.0.0:18923 stays unreachable via host.microsandbox.internal or the gateway IP even when no net flags are passed. See LAN Access for the allowLan: true posture.

Forge applies the rules at sandbox create time only. msb rules are per sandbox (not daemon-global) and msb modify has no --net-rule flag, so egress rules cannot be changed on a live sandbox: a newly configured host requires the sandbox to be recreated.

LAN Access

msb's default egress is deny with an implicit allow@public, so private (LAN) address ranges are blocked even when public egress is open, including with the * wildcard. Set sandbox.network.allowLan: true (default false) to let the sandbox reach them. Forge then creates the sandbox with --net public,private, or appends --net-rule allow@private when an allow-list is active. Public-host restrictions remain, but the entire private-address group is allowed independently of the host list, even if every configured host is invalid. This exposes every service on your LAN (routers, NAS, other machines) to commands the agent runs, so keep it off unless a task needs it.

The loop settings dialog and the Host sandbox menu override it per sandbox. Like the other egress rules it is fixed at create time: changing it on a running host sandbox removes and recreates the sandbox, after confirmation. Host bind mounts survive (they are host directories), but the VM root filesystem, the Docker and cache disks, and all running processes are lost — the removal deletes the sandbox's named Docker and cache volumes along with it. Forge reads the current setting back from msb inspect rather than assuming it, and refuses to apply overrides when that read fails, so a LAN restriction is never assumed to hold.

Environment Passthrough

Select host environment variables can be injected into the sandbox at create time:

{
  "sandbox": {
    "network": {
      "env": ["DATABASE_URL", "API_KEY"]
    }
  }
}

Forge passes each name as the bare -e <NAME> form, so msb resolves the value from its own environment and the value never appears on forge's command line (or in ps output). Only names that are set in the host process are injected; unset names are skipped and logged.

Secrets

Host-held credentials are bound with sandbox.network.secrets instead of env. A secret never enters the guest: msb keeps a host-side source reference to the environment variable, exposes a $MSB_<ENV> placeholder inside the sandbox, and substitutes the real value only for the listed hosts at the network boundary.

{
  "sandbox": {
    "network": {
      "secrets": [
        { "env": "GITHUB_TOKEN", "hosts": ["api.github.com"] }
      ]
    }
  }
}

Each entry maps to msb create --secret <env>@<hosts>. Once a sandbox has a secret bound, every msb exec fails unless the named host environment variable is present in the environment of the process invoking msb — msb reports error: invalid config: secret X: host environment variable X is not set. Because the shell shim inherits opencode's process environment, a variable missing there breaks every sandboxed shell command. The variables named in sandbox.network.secrets must therefore be exported in the environment that launches opencode, not merely present in an interactive shell. Forge logs an explicit warning naming the variable when a configured secret's host variable is unset.

Adopting an existing sandbox (for example after a plugin restart) converges the bound secrets with msb modify: --secret <env>@<hosts> refreshes the current value of every configured entry, and --secret-rm <env> drops entries that are no longer configured. Convergence runs once per sandbox adoption per plugin instance, not on every liveness check. A refresh failure blocks adoption without marking the sandbox converged, so a later startup can retry.

One placeholder caveat: a secret introduced by msb modify on an already-existing sandbox gets a $<ENV> placeholder instead of the $MSB_<ENV> form, so a newly added secret is most reliable on a freshly created sandbox.

Security notes:

  • Only pass variables you are willing to expose to the sandbox. Plain variables listed in network.env are readable inside the guest.
  • The previous per-sandbox plaintext env file under <dataDir>/sandbox-env/ is gone: nothing is written to disk, and the real secret value is stored only on the host.

Read-Only Project Mount

When enabled, Forge mounts the source project directory read-only at its identical host path. The mount is dropped when it overlaps a read-write mount with conflicting permissions (see below), so it is not guaranteed for every loop.

Option Default Description
sandbox.mountProjectReadonly true Enable the read-only source project mount.

The loop worktree remains writable. The read-only project mount is dropped whenever it overlaps an accepted read-write mount with conflicting permissions: inside the sandbox the outermost mount's read-only flag applies to the whole subtree, so a read-only ancestor would silently make a read-write descendant read-only. Loop worktrees live under <dataDir>/worktrees/<slug>, outside the source project, so the worktree itself does not nest under the project mount; the usual conflict is the worktree's git metadata directory — the source repository's .git, inside the source project — which is mounted read-write. When the git directory lives outside the source project, the read-only mount can be kept. The worktree and the shared git metadata directory are mounted read-write.

Git Metadata and Hooks

The worktree's git metadata directory is mounted read-write so git works inside the sandbox (status, log, diff, and commits all resolve against the real repository). Two guards keep that from becoming a path out of the sandbox:

  • <git-common-dir>/hooks is mounted read-only, so a sandboxed agent cannot plant a hook that the user's own git would later execute on the host.
  • Every git command Forge itself runs is invoked with core.hooksPath disabled, so no repository hook runs on the host — including one reached through a core.hooksPath entry in the repo-local config, which cannot be mounted read-only because msb workspaces are directories, not files.

Consequences: tools that install hooks into .git/hooks (for example pre-commit install, or Husky v4) fail inside the sandbox — hook managers that keep hooks in the working tree and point core.hooksPath at them still work. Forge's own scratch-branch commits never run repository hooks.

Custom Bind Mounts

Configure additional bind mounts with sandbox.mounts. Each entry is a host directory mounted at its identical host path — there is no container field, because the path is the same on both sides:

{
  "sandbox": {
    "mounts": [
      { "host": "/abs/host/reference" },
      { "host": "/abs/host/cache", "readonly": false }
    ]
  }
}

Rules:

  • host must be an absolute path.
  • Mounts default to read-only.
  • Invalid entries are skipped and logged.
  • Mounts cannot equal or nest inside reserved paths such as the worktree, the project mount, git metadata, or earlier custom mounts.

Security note: read-write custom mounts give the sandbox write access to host paths. Use them only for trusted directories.

Nested Docker

The sandbox image ships an in-VM Docker Engine, enabled by default. container/Dockerfile installs it from Docker's official apt repository — docker-ce, docker-ce-cli, containerd.io, docker-buildx-plugin, docker-compose-plugin — and the agent user is in the docker group.

msb runs its own agentd as PID 1 and ignores the image's ENTRYPOINT/CMD, so the daemon cannot start at boot. Instead the image ships /usr/local/bin/forge-dockerd-start: idempotent and safe to run concurrently (an flock serializes starts), self-elevating via the passwordless sudo rule so agent can call it bare, it starts dockerd detached with setsid and waits up to 60s for readiness, exiting non-zero with the daemon log tail on failure.

/var/lib/docker is backed by a real block device, because Docker's overlayfs driver cannot run on a virtiofs workspace mount. Forge passes --mount-named <sandbox>-docker-data:/var/lib/docker:kind=disk,size=<size>, configurable via sandbox.resources.dockerDisk (default 16g). The disk is sparse, so the default costs no real disk up front.

Named volumes survive msb rm, so Forge explicitly removes the sandbox's docker data and cache data volumes when it removes the sandbox (and during orphan cleanup) — otherwise a multi-gigabyte volume would leak per loop.

Verified working inside the sandbox: docker info reports server 29.7.2 with storage=overlayfs, docker run --rm hello-world succeeds, docker compose version works, and docker build works. The daemon survives across separate msb exec calls.

Registry pulls work with the default allow-public egress posture and need no extra configuration. Under an opt-in restriction, pulling from Docker Hub requires allow-listing registry-1.docker.io, auth.docker.io, production.cloudfront.docker.com, and the CloudFront blob host (*.cloudfront.net).

The image derives from a plain OCI base, keeps the final USER agent, and declares no ENTRYPOINT/CMD. Docker remains required on the host to build the image. The built image is roughly 1.65 GB, up from about 1 GB, because Docker Engine is heavy.

Tool Caches and Disk Space

The sandbox root filesystem is a small overlay (about 4 GB), while package-manager caches grow unbounded: real loops accumulated multi-gigabyte pnpm stores, uv wheel caches, and Rust sdist bootstrap caches (puccinialin) until the root filesystem hit 100%. The image therefore pins every tool cache under /opt/forge/cache (XDG_CACHE_HOME), and Forge mounts that directory as a dedicated sparse block device at create time — the same mechanism as the Docker data disk: --mount-named <sandbox>-cache-data:/opt/forge/cache:kind=disk,size=<size>, configurable via sandbox.resources.cacheDisk (default 16g).

msb formats a fresh named disk as root-owned 0755 while msb exec runs as agent, so Forge makes the mount root agent-owned and world-writable (chown agent:agent plus chmod 0777, non-recursive) in a single exec before the sandbox is registered. World-writable matches the image's build-time posture for /opt/forge, which the runtime mount would otherwise shadow, and keeps the tree usable when a command is run under sudo. The step is memoized per sandbox and re-applied on adoption, so a sandbox whose preparation failed — or one created by an older Forge build — is healed on the next start instead of being adopted with an unwritable cache.

Caches that do not honor XDG_CACHE_HOME are routed into that directory by image environment variables:

Cache Variable
pnpm content store (pnpm 11 / pnpm 10) PNPM_CONFIG_STORE_DIR / npm_config_store_dir = /opt/forge/cache/pnpm/store
npm cache npm_config_cache=/opt/forge/cache/npm
uv-managed Pythons UV_PYTHON_INSTALL_DIR=/opt/forge/cache/uv-python
uv-installed tools UV_TOOL_DIR=/opt/forge/cache/uv-tools
uv tool executables UV_TOOL_BIN_DIR=/opt/forge/cache/uv-bin (on PATH)
cargo and rustup CARGO_HOME / RUSTUP_HOME under /opt/forge/cache
Go modules GOPATH=/opt/forge/cache/go

CLIs installed globally at image-build time (fallow, playwright-core) are a deliberate exception: they are installed with an explicit --store-dir under /opt/forge/.local/share/pnpm/store. pnpm does not copy a global package into PNPM_HOME — the global node_modules entry is a symlink chain into the store — so a global built against the cache-disk store would resolve to a dangling symlink the moment the disk is mounted over that path, and the CLI would fail with Cannot find module. The build-time store therefore stays on a path no mount shadows, while the environment keeps agent installs on the cache disk.

uv, pip, puccinialin, and pnpm's own cache resolve under XDG_CACHE_HOME unchanged. Playwright is deliberately pinned off it with PLAYWRIGHT_BROWSERS_PATH=/opt/forge/.local/share/ms-playwright: the image ships Chromium there at build time, and a browser under the cache disk would be hidden by the mount. The uv-managed interpreters deliberately sit at uv-python, outside uv's own cache directory ($XDG_CACHE_HOME/uv), because uv cache clean clears that directory entirely and would otherwise delete interpreters that project virtualenvs link against. Because the store sits on a different filesystem than the mounted project, pnpm copies packages into node_modules instead of hard-linking — the same trade the container-internal store already made against the virtiofs project mount.

The image ships forge-cache-prune, safe to run as agent whenever the sandbox is idle. It clears re-downloadable caches — the pnpm store, npm and uv caches, cargo/registry, cargo/git, go/pkg/mod, and any unrecognized entry — while preserving installed toolchains: rustup, the uv-managed Pythons, uv tool environments and executable links (uv-tools and uv-bin), and the cargo/bin and go/bin binaries. It then runs apt-get clean and fstrim, so the freed space is returned to the host's sparse disk image rather than only to the guest. The sandbox context note tells agents about it, so a full cache disk is reclaimed by running it instead of hand-hunting du.

The cache volume keeps its contents across msb's --replace recovery (the retry Forge uses when msb create reports an already-existing orphaned sandbox), because the named volume is reused. It does not survive the remove-and-recreate path a LAN-access change takes, which deletes the sandbox's named Docker and cache volumes. Sandboxes created before this feature keep their caches on the root filesystem; as a stopgap their root disk can be grown in place (msb modify <sandbox> --root-disk <size>, grow-only), but the cache disk itself requires recreating the sandbox (msb rm <sandbox>). Changing sandbox.resources.cacheDisk does not resize an existing sandbox — see Resource Defaults.

Sandbox Lifecycle

msb sandboxes are reusable: msb exec resolves a stopped or crashed sandbox by starting it in place, so a stop is never a correctness problem — only a restart. A stop is a full VM reboot that destroys in-memory state while on-disk state persists. Forge adopts an existing running or stopped sandbox without recreating it (reusing the same forge-<worktree> name), and relies on msb's start-in-place resolution instead of holding a separate keep-alive exec open.

Large Command Output

Shell output truncation is handled by opencode's native shell tool: when output exceeds the tool limit, the full output is spilled to opencode's tool-output directory on the host (readable from loop sessions, see below). The worktree .forge/ scratch directory is added to git exclude so forge-written files are not committed.

Tool-Output Access

opencode spills large tool outputs to its truncation directory (<opencode-data>/tool-output, e.g. ~/.local/share/opencode/tool-output) and references the saved file by absolute host path. Forge makes those overflow files readable from loop and audit sessions in two complementary ways:

  • Sandbox tools (bash, glob, grep): the directory is bind-mounted read-only at the identical sandbox path, so the same absolute path opencode reports resolves inside the sandbox. The mount is added automatically when the directory exists; it is skipped when missing or already covered by the workspace mount.
  • Host file tools (read): the same mount is what the file-tool boundary checks, so read of an overflow file succeeds, and loop permission rulesets never ask about external directories.

opencode's temp directory (<os-tmp>/opencode — the path opencode's bash tool advertises to agents as pre-approved scratch space) is handled the same way, but bind-mounted read-write at the identical sandbox path, so scratch files an agent writes at that path, from the shell or with write, resolve identically on the host and inside the sandbox. It is opencode's own directory — Forge provides no separate scratch directory, and agents can use the advertised OS temp path without issue.

File-Tool Boundary

In a sandboxed session (a sandbox loop, its subagents and auditor, or a host session with the sandbox toggled on), the sandbox mounts are the boundary for every tool, including the file tools that run on the host. Before read, write, edit, or patch runs, Forge resolves each target path (relative paths against the workspace, ~ against the home directory, symlinks followed to their real location) and:

  • refuses it when it lies outside every sandbox mount, so a symlink in the worktree pointing at ~/.ssh is refused like the direct path;
  • refuses write, edit, and patch when the path lies in a read-only mount;
  • fails closed, like the shell, when the session's sandbox cannot be resolved or restored.

Because the mounts are the boundary, loop and audit permission rulesets contain no external_directory rules: external directories fall under the blanket allow, so an unattended loop never waits on an approval. A loop with the sandbox disabled (sandbox.enabled: false) has no boundary and full host file access.

External Directory Access

loop.allowExternalDirectories entries become read-only bind mounts at their identical host paths, added automatically, so in-container bash/glob/grep and the host file tools see the same tree. Entries that do not exist on the host are skipped with a log line.

The mount is read-only because the setting exists to grant read access. To make an external directory writable, add it to sandbox.mounts with "readonly": false; explicit sandbox.mounts entries are resolved first, so they win for any path listed in both.

Resource Defaults

Option Default msb flag
sandbox.resources.memory "8g" msb create -m
sandbox.resources.cpus "4" msb create -c (integer-only)
sandbox.resources.dockerDisk "16g" msb create --mount-named <sandbox>-docker-data:/var/lib/docker:kind=disk,size=<size>
sandbox.resources.cacheDisk "16g" msb create --mount-named <sandbox>-cache-data:/opt/forge/cache:kind=disk,size=<size>

The TUI execution dialog can override these for a single loop when launching in Loop mode, and can turn the sandbox off for that loop. The overrides are persisted on the loop row and read back whenever its sandbox is (re)created, so a restarted loop keeps its resources and a loop launched with the sandbox off stays off. They apply when the loop's sandbox is created, and a per-loop setting can turn the sandbox off, never on when the server has sandbox.enabled: false. See TUI → Loop Settings.

The Host sandbox menu overrides cpus and memory (and LAN access) for the project's host sandbox. The overrides are stored with the host sandbox's desired state, so they survive restarts. Unlike a loop's, they are also applied to a host sandbox that already exists: a CPU or memory change runs msb modify <sandbox> --cpus <n> --memory <size> --restart. msb cannot resize a running microVM live, so the sandbox restarts. Its root filesystem and the Docker and cache disks are kept; running processes, including the Docker daemon and its containers, are not. See TUI → Host Sandbox Menu. A failed resize is handled like a failed start: the sandbox is removed and the session is blocked until the sandbox is turned on again.

Every sandboxed request's context note states the sandbox's CPUs, memory, and LAN access. After the sandbox a session was using restarts (a resize) or is recreated (a LAN change, or turned off and on between two requests), the next request's note says so once and names what was lost, so the agent restarts services or reinstalls tooling instead of assuming they are still there.

memory and cpus are exactly what the guest gets. There is no autoscaling: nothing observes memory pressure, so a build needing more than memory is OOM-killed rather than given more. Size memory for the peak of the heaviest command the sandbox will run.

msb's --max-memory/--max-cpus ceilings are deliberately not exposed. They only reserve hotplug capacity that must be claimed explicitly with msb modify --memory <size> from the host; agents run inside the sandbox and cannot call msb, so a ceiling never rescues a failing in-sandbox build.

Forge reuses existing running or stopped sandboxes rather than recreating them. Changing these config values does not resize an existing loop sandbox; remove it (msb rm <sandbox>) so the next run creates it with the new values, or resize it in place with msb modify. The host sandbox converges to its overrides over config the next time it is validated after a restart.