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
feat(bwrap): tell sandboxed agents what is mounted - #31
The bwrap launchers (bwopencode, bwopencode2, bwclaude, bwcodex, bwcopilot) now generate a short description of the sandbox at start-up and
make the agent load it, so the agent knows it runs in a sandbox and what is
and is not mounted. Without it, agents guessed wrong, for example taking
read-only bind mounts of resolved symlinks for hard links.
Description
build_sandbox_context in lib/bwrap_sandbox_lib.sh builds the file from
the final BWRAP_ARGS, so it always matches the real mounts: $HOME is a
tmpfs; same-inode paths are bind mounts, not hard links; a table of the
mounts under $HOME (rw/ro/tmpfs) with system mounts collapsed; what
is not mounted; disabled tools; and the names of passed-in variables
(never values).
It is mounted read-only at ~/.sandbox/AGENTS.md with --ro-bind-data on a
file descriptor (nothing is written to the host). BWRAP_SANDBOX and BWRAP_SANDBOX_CONTEXT are set inside the sandbox.
Each tool loads it in addition to the user's own instructions; no user file
is overwritten:
bwopencode, bwopencode2: OPENCODE_CONFIG_CONTENT (its instructions
list is added to the user's; a host value is still dropped)
bwclaude: --append-system-prompt-file (skipped with a warning if the
user passes --append-system-prompt[-file])
bwcodex: -c developer_instructions=... (skipped with a warning if a config.toml or -c already sets it)
--no-sandbox-context turns it off; --dry-run prints the generated file; --exec mounts the file but adds no loader.
The masked binary list moves to file scope so the file can name it.
Docs: docs/bw-wrappers.md (new "Sandbox context" section) and README.md.
Testing
Run from a sandbox on a managed host, with real model runs (throwaway HOME, env -i, OpenCode 2 only with --standalone):
OpenCode 1.19.4 and 2.1.4: OPENCODE_CONFIG_CONTENT adds to a global and a
project instructions list rather than replacing them (global, project and
sandbox markers all reported); debug config lists the same files for v1.
bwopencode2, bwclaude (2.1.310) and bwcodex (0.164.0) real runs: the
agent names the launcher, says same-inode files are bind mounts and that ~/.ssh is not available, from the file alone.
bwcopilot instruction list shows the source with the feature on and none
with --no-sandbox-context.
Warning paths for a user-set --append-system-prompt and developer_instructions.
pre-commit on the changed files (shellcheck, shfmt, codespell,
markdownlint, gitleaks).
Not tested: bwopencode (OpenCode 1) model run (fails when nested inside a
v2 sandbox, with or without this change).
--dry-run for each launcher shows the generated file
bwopencode (OpenCode 1) session on a host answers a question about its mounts
pixi run -e dev lint
Checklist
Edit the sections above with relevant information
Update in-repo documentation, if applicable (docs/bw-wrappers.md, README.md)
Run pixi run -e dev lint locally (or wait for CI) — see .github/workflows/lint.yml
Test the changes on a representative managed host (or runansible deployment)
Adds shared sandbox context generation so agents launched through the bwrap wrappers receive information about filesystem access, persistence, and restrictions.
Changes:
Generates and mounts a read-only context file without writing it to the host.
Integrates tool-specific instruction loaders, an opt-out flag, and dry-run output.
Documents the context contents and loading behavior.
File
Description
README.md
Adds the context opt-out option.
lib/bwrap_sandbox_lib.sh
Generates, mounts, and displays sandbox context.
docs/bw-wrappers.md
Documents context contents and loader behavior.
bin/bwopencode2
Loads context through OpenCode configuration with v2-specific notes.
bin/bwopencode
Loads context through OpenCode configuration.
bin/bwcopilot
Registers the context directory as an instruction source.
bin/bwcodex
Injects context as developer instructions with conflict checks.
bin/bwclaude
Appends context to the system prompt with conflict checks.
Describe /tmp isolation based on its effective mount mode
lib/bwrap_sandbox_lib.sh:2976
With --rw-path /tmp, the final mount is a read-write bind of the host's /tmp, but this line still tells the agent that writes are private and discarded at exit (the /tmp row is also filtered out above). This can lead the agent to leave persistent or sensitive files on the host under a false isolation assumption. Derive this description from the effective /tmp mode, as is already done for the working directory.
Agents guessed wrong about the sandbox, for example taking a read-only
bind mount of a resolved symlink for a hard link.
build_sandbox_context writes a short markdown description of the session
from the final BWRAP_ARGS (mounts under $HOME with rw/ro/tmpfs, system
mounts collapsed, what is not mounted, disabled tools, names of passed-in
variables) and mounts it read-only at ~/.sandbox/AGENTS.md with
--ro-bind-data on a file descriptor, so nothing is written to the host.
BWRAP_SANDBOX and BWRAP_SANDBOX_CONTEXT identify the sandbox. --dry-run
prints the file and --no-sandbox-context turns it off.
bwopencode2 loads it through OPENCODE_CONFIG_CONTENT, whose instructions
list is added to the user's own (tested with 2.1.4), so no user file is
replaced. The masked binary list moves to file scope so the file can
name it.
Assisted-by: opencode:claude-sonnet-5-5
Same mechanism as bwopencode2: OPENCODE_CONFIG_CONTENT adds the
context file to the user's instructions (tested with 1.19.4).
Assisted-by: opencode:claude-sonnet-5-5
Claude Code gets the context file through --append-system-prompt-file,
which adds to the default system prompt and leaves CLAUDE.md alone
(tested with 2.1.310). A user-supplied --append-system-prompt[-file]
wins; the file is then only mounted and a warning is printed.
Assisted-by: opencode:claude-sonnet-5-5
Codex gets the context through -c developer_instructions, which is a
separate message and leaves AGENTS.md alone (tested with 0.164.0). It is
one value, so it is skipped with a warning when a config.toml (system,
user or project) or the command line already sets it.
Assisted-by: opencode:claude-sonnet-5-5
Copilot reads the AGENTS.md in ~/.sandbox through
COPILOT_CUSTOM_INSTRUCTIONS_DIRS, in addition to its usual instruction
files. A host value of the variable was never forwarded, so nothing is
replaced. Checked with `copilot instruction list`.
Assisted-by: opencode:claude-sonnet-5-5
Review feedback on the context file, each case checked with --dry-run:
- Record --file and --bind-data destinations as writable copies (mode
copy), so config files copied into an otherwise empty tmpfs are listed.
- A later mount over a parent now hides the child mounts recorded earlier
(for example --ro-path on the working directory covers .opencode).
- Clean control characters, | and backticks from every path once, so a
newline in a mount path no longer splits keys and aborts the launch
under set -u, and the table cannot break.
- Keep mount modes under /usr and /etc: a writable exception such as
--rw-path /usr/local or /etc/hosts is listed on its own, not folded
into the read-only summary.
- Take the working directory's access from the effective mounts.
- List every name in the home table; only system mounts are collapsed.
- Name credential and rc files as not mounted only when no mount exposes
them, so bwcopilot's gh hosts.yml and --ro-path ~/.zshrc are not denied.
Assisted-by: opencode:claude-sonnet-5-5
Second round of review feedback, each case checked with --dry-run:
- A read-only mount below a writable system mount (--rw-path /usr/local
with --ro-path /usr/local/bin) is listed as an exception instead of
being dropped with the other read-only entries under /usr and /etc.
- Describe /tmp from its effective mount: --rw-path /tmp now says it is
not private. Binds over /dev, /proc and /sys are listed too.
- bwcodex: detect developer_instructions in any form (spaces around =,
quoted, dotted or profile keys, inline tables, any project config from
the repository root down, any argument mentioning it), so a user's
instructions are not replaced by the context.
Assisted-by: opencode:claude-sonnet-5-5
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Config detection can overwrite user-defined developer instructions, while generated guidance can incorrectly describe explicitly mounted directories as inaccessible and persistent plans as temporary.
_t+=" sibling directories of the repository are not mounted."
else
_t+=" It is not a git repository, so only this directory is mounted."
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The bwrap launchers (
bwopencode,bwopencode2,bwclaude,bwcodex,bwcopilot) now generate a short description of the sandbox at start-up andmake the agent load it, so the agent knows it runs in a sandbox and what is
and is not mounted. Without it, agents guessed wrong, for example taking
read-only bind mounts of resolved symlinks for hard links.
Description
build_sandbox_contextinlib/bwrap_sandbox_lib.shbuilds the file fromthe final
BWRAP_ARGS, so it always matches the real mounts:$HOMEis atmpfs; same-inode paths are bind mounts, not hard links; a table of the
mounts under
$HOME(rw/ro/tmpfs) with system mounts collapsed; whatis not mounted; disabled tools; and the names of passed-in variables
(never values).
~/.sandbox/AGENTS.mdwith--ro-bind-dataon afile descriptor (nothing is written to the host).
BWRAP_SANDBOXandBWRAP_SANDBOX_CONTEXTare set inside the sandbox.is overwritten:
bwopencode,bwopencode2:OPENCODE_CONFIG_CONTENT(itsinstructionslist is added to the user's; a host value is still dropped)
bwclaude:--append-system-prompt-file(skipped with a warning if theuser passes
--append-system-prompt[-file])bwcodex:-c developer_instructions=...(skipped with a warning if aconfig.tomlor-calready sets it)bwcopilot:COPILOT_CUSTOM_INSTRUCTIONS_DIRS=~/.sandbox--no-sandbox-contextturns it off;--dry-runprints the generated file;--execmounts the file but adds no loader.docs/bw-wrappers.md(new "Sandbox context" section) andREADME.md.Testing
Run from a sandbox on a managed host, with real model runs (throwaway HOME,
env -i, OpenCode 2 only with--standalone):OpenCode 1.19.4 and 2.1.4:
OPENCODE_CONFIG_CONTENTadds to a global and aproject
instructionslist rather than replacing them (global, project andsandbox markers all reported);
debug configlists the same files for v1.bwopencode2,bwclaude(2.1.310) andbwcodex(0.164.0) real runs: theagent names the launcher, says same-inode files are bind mounts and that
~/.sshis not available, from the file alone.bwcopilot instruction listshows the source with the feature on and nonewith
--no-sandbox-context.Warning paths for a user-set
--append-system-promptanddeveloper_instructions.pre-commiton the changed files (shellcheck, shfmt, codespell,markdownlint, gitleaks).
Not tested:
bwopencode(OpenCode 1) model run (fails when nested inside av2 sandbox, with or without this change).
--dry-runfor each launcher shows the generated filebwopencode(OpenCode 1) session on a host answers a question about its mountspixi run -e dev lintChecklist
docs/bw-wrappers.md,README.md)pixi run -e dev lintlocally (or wait for CI) — see.github/workflows/lint.ymlrunansibledeployment)