Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -81,6 +81,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 @@ -138,6 +149,11 @@ parse_wrapper_args() {
FORCE_NEW_SESSION=1
shift
;;
--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 @@ -212,6 +228,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 @@ -513,6 +536,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 @@ -560,6 +610,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 @@ -98,6 +98,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 @@ -160,6 +171,11 @@ parse_wrapper_args() {
FORCE_NEW_SESSION=1
shift
;;
--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 @@ -240,6 +256,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 @@ -517,6 +540,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 @@ -564,6 +637,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 @@ -66,6 +66,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 @@ -118,6 +129,11 @@ parse_wrapper_args() {
FORCE_NEW_SESSION=1
shift
;;
--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 @@ -187,6 +203,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 @@ -412,6 +435,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 @@ -467,6 +503,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 @@ -75,6 +75,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 @@ -128,6 +138,11 @@ parse_wrapper_args() {
FORCE_NEW_SESSION=1
shift
;;
--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 @@ -196,6 +211,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 @@ -532,6 +553,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 @@ -584,6 +625,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