Skip to content
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Common wrapper options:
| `--new-session` | Force `bwrap --new-session` (stricter isolation; breaks SIGWINCH) |
| `--ro-path PATH`, `--rw-path PATH` | Mount an existing path read-only / read-write (may be repeated and nested) |
| `--github-tokens` | Forward `GH_TOKEN_*` variables so `gh` can authenticate inside the sandbox (off by default) |
| `--no-sandbox-context` | Do not tell the agent about the sandbox (by default a short description of the mounts is added to its instructions) |

`bwclaude` also supports `--debug`. `bwopencode2` has no `--init-auth` (sign-in
persists in its data directory), always runs OpenCode 2 with `--standalone`
Expand Down
51 changes: 51 additions & 0 deletions bin/bwclaude
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@
# accumulate multiple paths. Same blocked-path rules as --ro-path.
# A nested --ro-path/--rw-path overrides the one it sits under,
# so `--rw-path DIR --ro-path DIR/sub` protects DIR/sub.
# --no-sandbox-context
# Do not generate the sandbox context file (see below).
#
# Sandbox context: a short markdown file describing this sandbox (what is
# mounted read-only or read-write, what is lost at exit, which tools are
# disabled) is built from the final mounts and mounted read-only at
# ~/.sandbox/AGENTS.md, and BWRAP_SANDBOX / BWRAP_SANDBOX_CONTEXT are set.
# Claude Code loads it with --append-system-prompt-file, which adds to the
# default system prompt and leaves CLAUDE.md alone (verified with 2.1.310);
# skipped, with a warning, when the user passes --append-system-prompt[-file].
# Not added to a --exec command.

set -euo pipefail

Expand Down Expand Up @@ -149,6 +160,11 @@ parse_wrapper_args() {
add_n2c_token "$2"
shift 2
;;
--no-sandbox-context)
# shellcheck disable=SC2034 # consumed by sourced bwrap_sandbox_lib.sh
SANDBOX_CONTEXT_ENABLED=0
shift
;;
--ro-path)
if [[ $# -lt 2 ]]; then
echo "Error: --ro-path requires a path argument." >&2
Expand Down Expand Up @@ -231,6 +247,13 @@ Wrapper options (consumed before claude sees the command line):
Off by default. When set, use per-command token selection:
GH_TOKEN="$GH_TOKEN_NSLS2" gh pr list -R NSLS2/repo
GH_TOKEN="$GH_TOKEN_PERSONAL" gh issue list -R user/repo
--no-sandbox-context
Do not tell the agent about the sandbox. By default a short
description of what is mounted (read-only or read-write), what
is lost at exit and which tools are disabled is mounted at
~/.sandbox/AGENTS.md and is added to Claude Code's
system prompt (CLAUDE.md is kept).
Use --dry-run to see it.

All other arguments are passed through to claude unchanged.

Expand Down Expand Up @@ -532,6 +555,33 @@ build_claude_bin_mount() {
# Main
# ═══════════════════════════════════════════════════════════════════

# ── Sandbox context ──────────────────────────────────────────────
# Mount the context file (see build_sandbox_context) and make Claude Code load
# it. --append-system-prompt-file adds to the default system prompt and leaves
# CLAUDE.md alone. The user's own --append-system-prompt[-file] wins: two
# appends are not known to combine, so the file is then only mounted. Must run
# after build_claude_env (which may prepend --debug --verbose).
build_claude_context() {
# shellcheck disable=SC2034 # consumed by sourced bwrap_sandbox_lib.sh
SANDBOX_CONTEXT_NOTES=()
build_sandbox_context bwclaude
[[ -n "${SANDBOX_CONTEXT_PATH}" && -z "${TEST_CMD}" ]] || return 0

local _arg
for _arg in ${TOOL_ARGS[@]+"${TOOL_ARGS[@]}"}; do
case "${_arg}" in
--)
break
;;
--append-system-prompt | --append-system-prompt=* | --append-system-prompt-file | --append-system-prompt-file=*)
echo "Warning: bwclaude: ${_arg%%=*} was given, so Claude Code is not told to load ${SANDBOX_CONTEXT_PATH} (the file is still mounted)." >&2
return 0
;;
esac
done
TOOL_ARGS=(--append-system-prompt-file "${SANDBOX_CONTEXT_PATH}" ${TOOL_ARGS[@]+"${TOOL_ARGS[@]}"})
}

main() {
bwrap_check_account
parse_wrapper_args "$@"
Expand Down Expand Up @@ -580,6 +630,7 @@ main() {
# Environment variables
build_env_vars
build_claude_env
build_claude_context

# Mask dangerous binaries as the very last step: a mask is hidden by any
# later bind that covers it (e.g. --ro-path /usr or /usr/bin).
Expand Down
74 changes: 74 additions & 0 deletions bin/bwcodex
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@
# accumulate multiple paths. Same blocked-path rules as --ro-path.
# A nested --ro-path/--rw-path overrides the one it sits under,
# so `--rw-path DIR --ro-path DIR/sub` protects DIR/sub.
# --no-sandbox-context
# Do not generate the sandbox context file (see below).
#
# Sandbox context: a short markdown file describing this sandbox (what is
# mounted read-only or read-write, what is lost at exit, which tools are
# disabled) is built from the final mounts and mounted read-only at
# ~/.sandbox/AGENTS.md, and BWRAP_SANDBOX / BWRAP_SANDBOX_CONTEXT are set.
# Codex loads it through -c developer_instructions=..., which is sent as a
# separate message and leaves AGENTS.md alone (verified with 0.164.0); skipped,
# with a warning, when a config.toml or the command line already sets it.
# Not added to a --exec command.

set -euo pipefail

Expand Down Expand Up @@ -171,6 +182,11 @@ parse_wrapper_args() {
add_n2c_token "$2"
shift 2
;;
--no-sandbox-context)
# shellcheck disable=SC2034 # consumed by sourced bwrap_sandbox_lib.sh
SANDBOX_CONTEXT_ENABLED=0
shift
;;
--ro-path)
if [[ $# -lt 2 ]]; then
echo "Error: --ro-path requires a path argument." >&2
Expand Down Expand Up @@ -259,6 +275,13 @@ Wrapper options (consumed before codex sees the command line):
Off by default. When set, use per-command token selection:
GH_TOKEN="$GH_TOKEN_NSLS2" gh pr list -R NSLS2/repo
GH_TOKEN="$GH_TOKEN_PERSONAL" gh issue list -R user/repo
--no-sandbox-context
Do not tell the agent about the sandbox. By default a short
description of what is mounted (read-only or read-write), what
is lost at exit and which tools are disabled is mounted at
~/.sandbox/AGENTS.md and passed to Codex as
developer_instructions (AGENTS.md is kept).
Use --dry-run to see it.

All other arguments are passed through to codex unchanged.

Expand Down Expand Up @@ -536,6 +559,56 @@ build_codex_bin_mount() {
# Main
# ═══════════════════════════════════════════════════════════════════

# ── Sandbox context ──────────────────────────────────────────────
# Mount the context file (see build_sandbox_context) and make Codex load it.
# -c developer_instructions=... is sent as a separate message and leaves
# AGENTS.md alone. It is a single value, so it is not set when the user's
# config.toml (system, user or project) or command line already sets it: that
# would replace theirs. The file is then only mounted.
# _codex_sets_developer_instructions FILE
# Return 0 when the TOML FILE sets developer_instructions anywhere: as a key
# at the start of a line, a quoted or dotted key (profiles.x.developer_instructions)
# or inside an inline table. Comment lines are ignored.
_codex_sets_developer_instructions() {
[[ -r "$1" ]] || return 1
grep -vE '^[[:space:]]*#' "$1" | grep -qE "developer_instructions[\"']?[[:space:]]*="
}

build_codex_context() {
# shellcheck disable=SC2034 # consumed by sourced bwrap_sandbox_lib.sh
SANDBOX_CONTEXT_NOTES=()
build_sandbox_context bwcodex
[[ -n "${SANDBOX_CONTEXT_PATH}" && -z "${TEST_CMD}" ]] || return 0

# Project configs are read from the repository root down to the working
# directory, so check every directory on that path.
local _f _arg _toml _dir="${_PWD}"
local -a _files=("${SYSTEM_CODEX_CONFIG_FILE}" "${CODEX_CONFIG_FILE}")
while :; do
_files+=("${_dir}/.codex/config.toml")
[[ "${_dir}" == "${_GIT_ROOT:-${_PWD}}" || "${_dir}" == / || -z "${_dir}" ]] && break
_dir="${_dir%/*}"
done
for _f in "${_files[@]}"; do
if _codex_sets_developer_instructions "${_f}"; then
echo "Warning: bwcodex: ${_f} sets developer_instructions, so Codex is not told to load ${SANDBOX_CONTEXT_PATH} (the file is still mounted)." >&2
return 0
fi
done
for _arg in ${TOOL_ARGS[@]+"${TOOL_ARGS[@]}"}; do
[[ "${_arg}" == -- ]] && break
if [[ "${_arg}" == *developer_instructions* ]]; then
echo "Warning: bwcodex: developer_instructions was given on the command line, so Codex is not told to load ${SANDBOX_CONTEXT_PATH} (the file is still mounted)." >&2
return 0
fi
done
if ! _toml="$(_bwrap_toml_string "${_SANDBOX_CONTEXT_TEXT}")"; then
echo "Warning: bwcodex: cannot pass the sandbox context to Codex (unusual characters); the file is still mounted at ${SANDBOX_CONTEXT_PATH}." >&2
return 0
fi
TOOL_ARGS=(-c "developer_instructions=${_toml}" ${TOOL_ARGS[@]+"${TOOL_ARGS[@]}"})
}

main() {
bwrap_check_account
parse_wrapper_args "$@"
Expand Down Expand Up @@ -584,6 +657,7 @@ main() {
# Environment variables
build_env_vars
build_codex_env
build_codex_context

# Mask dangerous binaries as the very last step: a mask is hidden by any
# later bind that covers it (e.g. --ro-path /usr or /usr/bin).
Expand Down
37 changes: 37 additions & 0 deletions bin/bwcopilot
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@
# accumulate multiple paths. Same blocked-path rules as --ro-path.
# A nested --ro-path/--rw-path overrides the one it sits under,
# so `--rw-path DIR --ro-path DIR/sub` protects DIR/sub.
# --no-sandbox-context
# Do not generate the sandbox context file (see below).
#
# Sandbox context: a short markdown file describing this sandbox (what is
# mounted read-only or read-write, what is lost at exit, which tools are
# disabled) is built from the final mounts and mounted read-only at
# ~/.sandbox/AGENTS.md, and BWRAP_SANDBOX / BWRAP_SANDBOX_CONTEXT are set.
# Copilot loads it through COPILOT_CUSTOM_INSTRUCTIONS_DIRS=~/.sandbox, which
# adds that directory's AGENTS.md to the instruction files it already reads (a
# host value of the variable is not forwarded, so nothing is replaced).
# Not added to a --exec command.

set -euo pipefail

Expand Down Expand Up @@ -129,6 +140,11 @@ parse_wrapper_args() {
add_n2c_token "$2"
shift 2
;;
--no-sandbox-context)
# shellcheck disable=SC2034 # consumed by sourced bwrap_sandbox_lib.sh
SANDBOX_CONTEXT_ENABLED=0
shift
;;
--ro-path)
if [[ $# -lt 2 ]]; then
echo "Error: --ro-path requires a path argument." >&2
Expand Down Expand Up @@ -206,6 +222,13 @@ Wrapper options (consumed before copilot sees the command line):
Off by default. When set, use per-command token selection:
GH_TOKEN="$GH_TOKEN_NSLS2" gh pr list -R NSLS2/repo
GH_TOKEN="$GH_TOKEN_PERSONAL" gh issue list -R user/repo
--no-sandbox-context
Do not tell the agent about the sandbox. By default a short
description of what is mounted (read-only or read-write), what
is lost at exit and which tools are disabled is mounted at
~/.sandbox/AGENTS.md and added to Copilot's
instructions (your own instruction files are kept).
Use --dry-run to see it.

All other arguments are passed through to copilot unchanged.

Expand Down Expand Up @@ -431,6 +454,19 @@ build_copilot_env() {
# Main
# ═══════════════════════════════════════════════════════════════════

# ── Sandbox context ──────────────────────────────────────────────
# Mount the context file (see build_sandbox_context) and make Copilot load it:
# COPILOT_CUSTOM_INSTRUCTIONS_DIRS lists extra directories whose AGENTS.md is
# read in addition to the usual instruction files. A host value of the
# variable is not forwarded (it never was), so nothing is replaced.
build_copilot_context() {
# shellcheck disable=SC2034 # consumed by sourced bwrap_sandbox_lib.sh
SANDBOX_CONTEXT_NOTES=()
build_sandbox_context bwcopilot
[[ -n "${SANDBOX_CONTEXT_PATH}" && -z "${TEST_CMD}" ]] || return 0
SANDBOX_ENV+=("COPILOT_CUSTOM_INSTRUCTIONS_DIRS=${SANDBOX_CONTEXT_PATH%/*}")
}

main() {
bwrap_check_account
parse_wrapper_args "$@"
Expand Down Expand Up @@ -487,6 +523,7 @@ main() {
# Environment variables
build_env_vars
build_copilot_env
build_copilot_context

# Mask dangerous binaries as the very last step: a mask is hidden by any
# later bind that covers it (e.g. --ro-path /usr or /usr/bin).
Expand Down
42 changes: 42 additions & 0 deletions bin/bwopencode
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@
# accumulate multiple paths. Same blocked-path rules as --ro-path.
# A nested --ro-path/--rw-path overrides the one it sits under,
# so `--rw-path DIR --ro-path DIR/sub` protects DIR/sub.
# --no-sandbox-context
# Do not generate the sandbox context file (see below).
#
# Sandbox context: a short markdown file describing this sandbox (what is
# mounted read-only or read-write, what is lost at exit, which tools are
# disabled) is built from the final mounts and mounted read-only at
# ~/.sandbox/AGENTS.md, and BWRAP_SANDBOX / BWRAP_SANDBOX_CONTEXT are set.
# OpenCode loads it through OPENCODE_CONFIG_CONTENT, whose "instructions"
# list is added to the user's own instructions (verified with 1.19.4); no user
# file is overwritten. Not added to a --exec command.

set -euo pipefail

Expand Down Expand Up @@ -142,6 +152,11 @@ parse_wrapper_args() {
add_n2c_token "$2"
shift 2
;;
--no-sandbox-context)
# shellcheck disable=SC2034 # consumed by sourced bwrap_sandbox_lib.sh
SANDBOX_CONTEXT_ENABLED=0
shift
;;
--ro-path)
if [[ $# -lt 2 ]]; then
echo "Error: --ro-path requires a path argument." >&2
Expand Down Expand Up @@ -218,6 +233,12 @@ Wrapper options (consumed before opencode sees the command line):
Off by default. When set, use per-command token selection:
GH_TOKEN="$GH_TOKEN_NSLS2" gh pr list -R NSLS2/repo
GH_TOKEN="$GH_TOKEN_PERSONAL" gh issue list -R user/repo
--no-sandbox-context
Do not tell the agent about the sandbox. By default a short
description of what is mounted (read-only or read-write), what
is lost at exit and which tools are disabled is mounted at
~/.sandbox/AGENTS.md and added to OpenCode's instructions
(your own instructions are kept). Use --dry-run to see it.

All other arguments are passed through to opencode unchanged.

Expand Down Expand Up @@ -556,6 +577,26 @@ build_opencode_env() {
# Main
# ═══════════════════════════════════════════════════════════════════

# ── Sandbox context ──────────────────────────────────────────────
# Mount the context file (see build_sandbox_context) and make OpenCode load
# it. OPENCODE_CONFIG_CONTENT is an inline config layered over the user's:
# its "instructions" list is added to theirs, not substituted for it. Any
# host OPENCODE_CONFIG_CONTENT is dropped by build_opencode_env, so this is
# the only value the sandbox sees. Must run after build_opencode_env.
build_opencode_context() {
# shellcheck disable=SC2034 # consumed by sourced bwrap_sandbox_lib.sh
SANDBOX_CONTEXT_NOTES=()
build_sandbox_context bwopencode
[[ -n "${SANDBOX_CONTEXT_PATH}" && -z "${TEST_CMD}" ]] || return 0

local _path_json
if ! _path_json="$(_bwrap_json_quote "${SANDBOX_CONTEXT_PATH}")"; then
echo "Warning: bwopencode: cannot pass the sandbox context file to OpenCode (unusual characters in the path)." >&2
return 0
fi
SANDBOX_ENV+=("OPENCODE_CONFIG_CONTENT={\"instructions\":[${_path_json}]}")
}

main() {
bwrap_check_account
parse_wrapper_args "$@"
Expand Down Expand Up @@ -609,6 +650,7 @@ main() {
# Environment variables
build_env_vars
build_opencode_env
build_opencode_context

# Mask dangerous binaries as the very last step: a mask is hidden by any
# later bind that covers it (e.g. --ro-path /usr or /usr/bin).
Expand Down
Loading