You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 4fb6558
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: docs/api/_media/configuration.md
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -52,7 +52,8 @@ Default log path: `~/.local/share/opencode/forge/logs/forge.log` or `$XDG_DATA_H
52
52
|`loop.cleanupWorktree`|`false`| Auto-remove worktree on cancel. |
53
53
|`loop.stallTimeoutMs`|`60000`| Stall watchdog timeout in milliseconds. |
54
54
|`loop.maxConsecutiveStalls`|`5`| Consecutive stalls before terminating with `stall_timeout`. `0` disables stall termination. |
55
-
|`loop.allowExternalDirectories`| unset | Absolute host directories that loop, audit, and post-action sessions may read despite worktree isolation. |
55
+
|`loop.busyStallTimeoutMs`|`900000`| How long a session may stay busy with no sign of progress before the watchdog aborts the wedged message and sends a continue prompt. Both tool activity and streamed content (including reasoning/thinking deltas) count as progress, in the loop session or any of its subagent sessions, so a long thinking stretch is not mistaken for a wedged stream. `0` disables nudging. |
56
+
|`loop.allowExternalDirectories`| unset | Absolute host directories that loop, audit, and post-action sessions may read despite worktree isolation. In sandboxed loops each entry is also bind-mounted read-only, so in-container `bash`/`glob`/`grep` see the same tree as host `read`. |
56
57
|`loop.permissions`| unset | Per-tool `deny` overrides for loop, audit, and post-action sessions. See [Loop Permissions](#loop-permissions). |
57
58
|`loop.worktreeOpencodeConfig`| unset | Inline [opencode config](https://opencode.ai/config.json) written as `opencode.jsonc` into each freshly created loop worktree. Enables per-loop customization (MCP servers, model overrides, etc.). Skip-if-exists — never overwrites a committed `opencode.json`/`opencode.jsonc`. The written file is git-excluded to keep it out of loop commits. |
58
59
@@ -74,7 +75,7 @@ Configured rules are layered into the ruleset in this order:
74
75
75
76
1. Blanket allow-all (worktree/audit isolation).
76
77
2. Blanket `external_directory` deny.
77
-
3.`external_directory` allows (opencode's tool-output directory, then `loop.allowExternalDirectories`).
78
+
3.`external_directory` allows (opencode's tool-output and temp directories, then `loop.allowExternalDirectories`).
78
79
4. Configured `deny` rules.
79
80
5. Forge structural denies.
80
81
@@ -246,6 +247,7 @@ See [Sandbox](sandbox.md) for detailed behavior and security notes.
246
247
|`sandbox.enabled`|`true`| Enable sandboxed execution when the `sbx` daemon is available. |
247
248
|`sandbox.mode`|`"sbx"`| Sandbox mode. `sbx` is currently the only supported mode. |
248
249
|`sandbox.image`|`"oc-forge-sandbox:latest"`| sbx template tag used for sandboxed execution. |
250
+
|`sandbox.imageFeatures.browserControl`|`false`| Include Chromium, the Browser Control CLI/MCP server, and its extension when building the bundled sandbox image. Rebuild the template after changing it. |
Copy file name to clipboardExpand all lines: docs/api/_media/sandbox.md
+74-5Lines changed: 74 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,6 +11,8 @@ See also: [Configuration](configuration.md), [Tools](tools.md), [Loop System](lo
11
11
- A platform the `sbx` daemon supports: macOS 14+ on Apple silicon, Windows 11 with Hypervisor Platform, or Ubuntu 24.04+ with KVM.
12
12
- Docker, used only to build the sandbox template (see below).
13
13
14
+
The daemon serializes its work behind in-flight sandbox commands, so `sbx daemon status` and `sbx ls` can take seconds while several loops are running. Forge bounds those queries at 30s and treats a query that does not answer as *indeterminate* rather than "daemon down": it logs and continues, letting the actual sandbox operation report the authoritative error. Only a daemon that answers definitively (or a missing CLI) fails a loop launch with remediation advice.
15
+
14
16
Build and load the bundled template:
15
17
16
18
```bash
@@ -19,16 +21,59 @@ docker save oc-forge-sandbox:latest -o forge-sandbox.tar
19
21
sbx template load forge-sandbox.tar
20
22
```
21
23
22
-
The image includes Node.js 24, pnpm, Bun, Python 3 + uv, ripgrep, git, jq, and a native Docker daemon inside each sandbox.
24
+
The default image includes Node.js 24, pnpm, Bun, Python 3 + uv, ripgrep, git, jq, and a native Docker daemon inside each sandbox.
25
+
26
+
The sandbox image grants the `agent` user passwordless sudo, so loops can install whatever software they need at runtime. `sbx` commands stay unprivileged as `agent` (keeping host-mapped worktree files owned by the host user), so system-wide installs use an explicit `sudo` prefix, for example `sudo apt-get install ruby`.
27
+
28
+
### Browser Control (opt-in)
29
+
30
+
Chromium and Browser Control add a substantial browser payload, so they are excluded by default. Enable them for future image builds:
31
+
32
+
```jsonc
33
+
{
34
+
"sandbox": {
35
+
"imageFeatures": {
36
+
"browserControl":true
37
+
}
38
+
}
39
+
}
40
+
```
41
+
42
+
Then run `Build sandbox template` from the command palette. Changing the option does not modify an already-loaded template; rebuild it explicitly. The equivalent manual Docker build is:
43
+
44
+
```bash
45
+
docker build \
46
+
--build-arg INSTALL_BROWSER_CONTROL=true \
47
+
-t oc-forge-sandbox:latest \
48
+
container/
49
+
docker save oc-forge-sandbox:latest -o forge-sandbox.tar
50
+
sbx template load forge-sandbox.tar
51
+
```
52
+
53
+
The resulting image provides `browser-control`, `browser-control-mcp`, Chromium as `chromium`, and the unpacked extension at `/opt/browser-control-extension`.
54
+
55
+
Browser Control connects to a browser in the same sandbox. Launch Chromium with the packaged extension path resolved to its installation directory:
It cannot control a browser running on the host because sandbox networking cannot reach the host loopback interface.
23
68
24
69
## How It Works
25
70
26
71
1. A sandbox loop uses its isolated git worktree. A host-session sandbox instead uses the project root selected from the TUI.
27
72
2. Forge creates one sandbox per loop, or one project-scoped host-session sandbox shared by plugin instances in the process.
28
-
3. The active directory and the read-only source project (when `sandbox.mountProjectReadonly` is enabled) are mounted at their identical host paths, so absolute paths resolve the same on both sides. There is no `/workspace` or `/project` container path.
73
+
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.
29
74
4. Shell commands and search tools execute inside the sandbox; file tools stay on the host, so LSP and editor integration continue to work.
30
75
31
-
The read-only project mount is dropped whenever the worktree's git directories live inside the source project (the default forge layout), so `sandbox.mountProjectReadonly` is effectively inert there.
76
+
The read-only project mount is dropped whenever it would nest over the writable worktree — which is the default forge layout, where the worktree lives inside the source project — so `sandbox.mountProjectReadonly` is effectively inert there. 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.
32
77
33
78
## Shell Routing
34
79
@@ -93,7 +138,16 @@ By default, Forge mounts the source project directory read-only at its identical
93
138
|---|---:|---|
94
139
|`sandbox.mountProjectReadonly`|`true`| Enable the read-only source project mount. |
95
140
96
-
The loop worktree remains writable.
141
+
The loop worktree remains writable. When the read-only project mount would nest over the writable worktree (the default forge layout), it is dropped instead: inside the sandbox the outermost mount's read-only flag applies to the whole subtree, so a read-only ancestor would silently make the worktree read-only. The worktree and the shared git metadata directory are mounted read-write in that case.
142
+
143
+
## Git Metadata and Hooks
144
+
145
+
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:
146
+
147
+
-`<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.
148
+
- 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 `sbx` workspaces are directories, not files.
149
+
150
+
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.
97
151
98
152
## Custom Bind Mounts
99
153
@@ -123,6 +177,10 @@ Security note: read-write custom mounts give the sandbox write access to host pa
123
177
124
178
Each sbx sandbox has its own Docker daemon natively, so loops can build and run containers (for example end-to-end tests) without touching the host Docker daemon. Every sandbox gets isolated image and container storage.
125
179
180
+
## Sandbox Lifecycle
181
+
182
+
`sbx` auto-stops a sandbox roughly 35 seconds after the last exec session ends, and `sbx exec` auto-starts a stopped sandbox, 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 (Docker images, containers, and files) persists across it. Cold starts are roughly 0.9s for the first command after a stop, vs ~0.16s warm. Forge relies on auto-resume instead of holding a separate keep-alive exec open.
183
+
126
184
## Large Command Output
127
185
128
186
Shell output truncation is handled by opencode's native bash 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.
@@ -132,7 +190,18 @@ Shell output truncation is handled by opencode's native bash tool: when output e
132
190
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:
133
191
134
192
-**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.
135
-
-**Host file tools** (`read`): the directory is granted an `external_directory` allow rule in the loop/audit permission ruleset (layered after the blanket external-directory deny), so reads succeed without prompting in the unattended loop. All other external directories remain denied unless added via `loop.allowExternalDirectories`.
193
+
-**Host file tools** (`read`): the directory is granted an `external_directory` allow rule in the loop/audit permission ruleset (layered after the blanket external-directory deny), so reads succeed without prompting in the unattended loop — the ruleset's blanket allow covers the `read` permission itself, but a `loop.permissions` rule that denies or asks for `read` is layered after these grants and still applies. All other external directories remain denied unless added via `loop.allowExternalDirectories`.
194
+
195
+
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 for writes: it is granted an `external_directory` allow rule for host file tools **and** bind-mounted read-write at the identical sandbox path, so scratch files an agent writes at that path 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.
196
+
197
+
## External Directory Access
198
+
199
+
`loop.allowExternalDirectories` entries are granted the same two ways, so host and container agree on what exists:
200
+
201
+
-**Host file tools** (`read`, `write`, `edit`): an `external_directory` allow rule layered after the blanket deny.
202
+
-**Sandbox tools** (`bash`, `glob`, `grep`): a **read-only** bind mount at the identical sandbox path, added automatically. Entries that do not exist on the host are skipped with a log line.
203
+
204
+
The mount is read-only because the setting exists to grant read access. To make an external directory writable from inside the sandbox, add it to `sandbox.mounts` with `"readonly": false`; explicit `sandbox.mounts` entries are resolved first, so they win for any path listed in both.
Copy file name to clipboardExpand all lines: docs/configuration.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -53,7 +53,7 @@ Default log path: `~/.local/share/opencode/forge/logs/forge.log` or `$XDG_DATA_H
53
53
|`loop.stallTimeoutMs`|`60000`| Stall watchdog timeout in milliseconds. |
54
54
|`loop.maxConsecutiveStalls`|`5`| Consecutive stalls before terminating with `stall_timeout`. `0` disables stall termination. |
55
55
|`loop.busyStallTimeoutMs`|`900000`| How long a session may stay busy with no sign of progress before the watchdog aborts the wedged message and sends a continue prompt. Both tool activity and streamed content (including reasoning/thinking deltas) count as progress, in the loop session or any of its subagent sessions, so a long thinking stretch is not mistaken for a wedged stream. `0` disables nudging. |
56
-
|`loop.allowExternalDirectories`| unset | Absolute host directories that loop, audit, and post-action sessions may read despite worktree isolation. |
56
+
|`loop.allowExternalDirectories`| unset | Absolute host directories that loop, audit, and post-action sessions may read despite worktree isolation. In sandboxed loops each entry is also bind-mounted read-only, so in-container `bash`/`glob`/`grep` see the same tree as host `read`. |
57
57
|`loop.permissions`| unset | Per-tool `deny` overrides for loop, audit, and post-action sessions. See [Loop Permissions](#loop-permissions). |
58
58
|`loop.worktreeOpencodeConfig`| unset | Inline [opencode config](https://opencode.ai/config.json) written as `opencode.jsonc` into each freshly created loop worktree. Enables per-loop customization (MCP servers, model overrides, etc.). Skip-if-exists — never overwrites a committed `opencode.json`/`opencode.jsonc`. The written file is git-excluded to keep it out of loop commits. |
Copy file name to clipboardExpand all lines: docs/sandbox.md
+9Lines changed: 9 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -194,6 +194,15 @@ opencode spills large tool outputs to its truncation directory (`<opencode-data>
194
194
195
195
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 for writes: it is granted an `external_directory` allow rule for host file tools **and** bind-mounted read-write at the identical sandbox path, so scratch files an agent writes at that path 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.
196
196
197
+
## External Directory Access
198
+
199
+
`loop.allowExternalDirectories` entries are granted the same two ways, so host and container agree on what exists:
200
+
201
+
-**Host file tools** (`read`, `write`, `edit`): an `external_directory` allow rule layered after the blanket deny.
202
+
-**Sandbox tools** (`bash`, `glob`, `grep`): a **read-only** bind mount at the identical sandbox path, added automatically. Entries that do not exist on the host are skipped with a log line.
203
+
204
+
The mount is read-only because the setting exists to grant read access. To make an external directory writable from inside the sandbox, add it to `sandbox.mounts` with `"readonly": false`; explicit `sandbox.mounts` entries are resolved first, so they win for any path listed in both.
0 commit comments