Skip to content
Merged
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
63 changes: 45 additions & 18 deletions sandbox-runtime/.bashrc.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,18 @@ _ccx_run() {
mkdir -p "$td"
chmod 700 "$td"

# The /**/*.pem deny glob blocks /etc/ssl/cert.pem, breaking every TLS
# stack that reads a CA bundle from disk (cargo, system curl, git-over-
# HTTPS, python/certifi). Stage the public root store at a .crt path the
# glob doesn't match. This copy runs OUTSIDE the sandbox; $td is readable
# inside. Public certs only — no weakening of the private-key globs. If
# network.tlsTerminate is ever enabled, srt's MITM CA must be appended
# to this bundle.
local cabundle="$td/ca-bundle.crt"
cp /etc/ssl/cert.pem "$cabundle"
# No CA-bundle staging: with network.tlsTerminate in the configs, srt
# builds its own trust bundle (MITM CA + host roots) at a path the
# /**/*.pem deny glob doesn't cover, and sets SSL_CERT_FILE /
# CURL_CA_BUNDLE / CARGO_HTTP_CAINFO / GIT_SSL_CAINFO /
# REQUESTS_CA_BUNDLE itself. Don't export those here — wrapper exports
# would shadow srt's and break TLS inside.

# gh stores its token in the macOS keychain, which the sandbox denies. Pull
# it out here so gh inside the sandbox can authenticate via $GH_TOKEN.
# The configs mask it (credentials.envVars): the sandbox only ever sees a
# fake token; the TLS-terminating proxy swaps in this real one on egress
# to github.com hosts only.
local gh_token=""
if command -v gh >/dev/null 2>&1; then
gh_token=$(gh auth token 2>/dev/null)
Expand All @@ -81,9 +81,40 @@ _ccx_run() {
fi
fi

# SSL_CERT_FILE/CURL_CA_BUNDLE cover curl, python, openssl-cli, etc.
# CARGO_HTTP_CAINFO is required separately — cargo's statically-linked
# libcurl ignores SSL_CERT_FILE. GIT_SSL_CAINFO likewise for git.
# Persistent MITM CA: srt mints per-host leaf certs from this CA instead
# of an ephemeral per-session one, so it can be trusted ONCE in the login
# keychain (security add-trusted-cert, see README) — required for tools
# that verify via trustd (gh and other Go binaries) rather than a PEM
# bundle. Generated on first launch; the key never leaves this machine
# and stays sandbox-unreadable (mode 600 + the /**/*.key deny glob).
local cadir="$HOME/.config/srt"
if [[ ! -f "$cadir/mitm-ca.crt" || ! -f "$cadir/mitm-ca.key" ]]; then
mkdir -p "$cadir"
openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
-keyout "$cadir/mitm-ca.key" -out "$cadir/mitm-ca.crt" \
-subj "/CN=srt MITM CA ($USER)" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" 2>/dev/null
chmod 600 "$cadir/mitm-ca.key"
fi

# Per-launch config copy, for the two settings that can't be static JSON:
# - SSH agent forwarding: SSH inside authenticates through the agent's
# signing socket while raw keys stay unreadable (/**/id_* globs).
# Seatbelt gates Unix sockets via network.allowUnixSockets, and the
# launchd socket path (/var/run/com.apple.launchd.*/Listeners) is
# random per login session. srt realpaths it (/var → /private/var).
# Trade-off: sandboxed code can authenticate as you while the session
# runs — mitigate with ssh-add -c or hardware-backed keys.
# - The CA paths above (tlsTerminate does no tilde expansion, so the
# $HOME-absolute paths can't live in the shared template).
local cfg="$td/settings.json"
jq --arg sock "$SSH_AUTH_SOCK" --arg cadir "$cadir" '
(if $sock != "" then .network.allowUnixSockets = [$sock] else . end)
| .network.tlsTerminate.caCertPath = ($cadir + "/mitm-ca.crt")
| .network.tlsTerminate.caKeyPath = ($cadir + "/mitm-ca.key")
' "$settings" > "$cfg"

# pnpm_config_store_dir pins pnpm's global store: its store-selection
# heuristic misfires inside the sandbox and silently falls back to a
# per-project .pnpm-store/ even when the global store is writable
Expand All @@ -92,13 +123,9 @@ _ccx_run() {
DISABLE_FEEDBACK_COMMAND=1 \
CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY=1 \
GH_TOKEN="$gh_token" \
SSL_CERT_FILE="$cabundle" \
CURL_CA_BUNDLE="$cabundle" \
CARGO_HTTP_CAINFO="$cabundle" \
GIT_SSL_CAINFO="$cabundle" \
REQUESTS_CA_BUNDLE="$cabundle" \
SSH_AUTH_SOCK="$SSH_AUTH_SOCK" \
pnpm_config_store_dir="$HOME/Library/pnpm/store" \
srt --settings "$settings" -- claude "$@"
srt --settings "$cfg" -- claude "$@"
}

ccx() { _ccx_run ~/.srt-claude-denyall.json "$@"; }
Expand Down
33 changes: 30 additions & 3 deletions sandbox-runtime/.srt-claude-allowall.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,29 @@
"network": {
"allowedDomains": [],
"deniedDomains": [
"gist.github.com"
"gist.github.com",
"*:25",
"*:587",
"*:465"
],
"deniedDomainReasons": {
"gist.github.com": "Creating gists is blocked by sandbox policy (exfiltration risk). Write to a file in the repo instead.",
"*:25": "Direct SMTP is blocked by sandbox policy.",
"*:587": "Direct SMTP is blocked by sandbox policy.",
"*:465": "Direct SMTP is blocked by sandbox policy."
},
"allowAllDomains": true,
"allowLocalBinding": false
"allowLocalBinding": false,
"tlsTerminate": {}
},
"credentials": {
"envVars": [
{
"name": "GH_TOKEN",
"mode": "mask",
"injectHosts": ["github.com", "*.github.com"]
}
]
},
"filesystem": {
"denyRead": [
Expand Down Expand Up @@ -50,7 +69,7 @@

"~/.cisco",
"~/.vpn",
"~/OneDrive - UBC/",
"~/OneDrive - UBC",
"~/Documents",
"~/Desktop",
"~/Downloads",
Expand Down Expand Up @@ -84,6 +103,14 @@
"~/.claude/CLAUDE.md"
]
},
"ignoreViolations": {
"*": [
"sysctl-read kern.",
"mach-lookup com.apple.SystemConfiguration.configd",
".GlobalPreferences",
"Library/Preferences/ByHost"
]
},
"enableWeakerNetworkIsolation": true,
"allowPty": true
}
31 changes: 29 additions & 2 deletions sandbox-runtime/.srt-claude-denyall.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,29 @@
"network": {
"allowedDomains": [],
"deniedDomains": [
"gist.github.com"
"gist.github.com",
"*:25",
"*:587",
"*:465"
],
"deniedDomainReasons": {
"gist.github.com": "Creating gists is blocked by sandbox policy (exfiltration risk). Write to a file in the repo instead.",
"*:25": "Direct SMTP is blocked by sandbox policy.",
"*:587": "Direct SMTP is blocked by sandbox policy.",
"*:465": "Direct SMTP is blocked by sandbox policy."
},
"allowAllDomains": true,
"allowLocalBinding": false
"allowLocalBinding": true,
"tlsTerminate": {}
},
"credentials": {
"envVars": [
{
"name": "GH_TOKEN",
"mode": "mask",
"injectHosts": ["github.com", "*.github.com"]
}
]
},
"filesystem": {
"denyRead": [
Expand Down Expand Up @@ -74,6 +93,14 @@
"~/.claude/CLAUDE.md"
]
},
"ignoreViolations": {
"*": [
"sysctl-read kern.",
"mach-lookup com.apple.SystemConfiguration.configd",
".GlobalPreferences",
"Library/Preferences/ByHost"
]
},
"enableWeakerNetworkIsolation": true,
"allowPty": true
}
63 changes: 45 additions & 18 deletions sandbox-runtime/.zshrc.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ function _ccx_run {
mkdir -p "$td"
chmod 700 "$td"

# The /**/*.pem deny glob blocks /etc/ssl/cert.pem, breaking every TLS
# stack that reads a CA bundle from disk (cargo, system curl, git-over-
# HTTPS, python/certifi). Stage the public root store at a .crt path the
# glob doesn't match. This copy runs OUTSIDE the sandbox; $td is readable
# inside. Public certs only — no weakening of the private-key globs. If
# network.tlsTerminate is ever enabled, srt's MITM CA must be appended
# to this bundle.
local cabundle="$td/ca-bundle.crt"
cp /etc/ssl/cert.pem "$cabundle"
# No CA-bundle staging: with network.tlsTerminate in the configs, srt
# builds its own trust bundle (MITM CA + host roots) at a path the
# /**/*.pem deny glob doesn't cover, and sets SSL_CERT_FILE /
# CURL_CA_BUNDLE / CARGO_HTTP_CAINFO / GIT_SSL_CAINFO /
# REQUESTS_CA_BUNDLE itself. Don't export those here — wrapper exports
# would shadow srt's and break TLS inside.

# gh stores its token in the macOS keychain, which the sandbox denies. Pull
# it out here so gh inside the sandbox can authenticate via $GH_TOKEN.
# The configs mask it (credentials.envVars): the sandbox only ever sees a
# fake token; the TLS-terminating proxy swaps in this real one on egress
# to github.com hosts only.
local gh_token=""
if command -v gh >/dev/null 2>&1; then
gh_token=$(gh auth token 2>/dev/null)
Expand All @@ -77,9 +77,40 @@ function _ccx_run {
fi
fi

# SSL_CERT_FILE/CURL_CA_BUNDLE cover curl, python, openssl-cli, etc.
# CARGO_HTTP_CAINFO is required separately — cargo's statically-linked
# libcurl ignores SSL_CERT_FILE. GIT_SSL_CAINFO likewise for git.
# Persistent MITM CA: srt mints per-host leaf certs from this CA instead
# of an ephemeral per-session one, so it can be trusted ONCE in the login
# keychain (security add-trusted-cert, see README) — required for tools
# that verify via trustd (gh and other Go binaries) rather than a PEM
# bundle. Generated on first launch; the key never leaves this machine
# and stays sandbox-unreadable (mode 600 + the /**/*.key deny glob).
local cadir="$HOME/.config/srt"
if [[ ! -f "$cadir/mitm-ca.crt" || ! -f "$cadir/mitm-ca.key" ]]; then
mkdir -p "$cadir"
openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
-keyout "$cadir/mitm-ca.key" -out "$cadir/mitm-ca.crt" \
-subj "/CN=srt MITM CA ($USER)" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" 2>/dev/null
chmod 600 "$cadir/mitm-ca.key"
fi

# Per-launch config copy, for the two settings that can't be static JSON:
# - SSH agent forwarding: SSH inside authenticates through the agent's
# signing socket while raw keys stay unreadable (/**/id_* globs).
# Seatbelt gates Unix sockets via network.allowUnixSockets, and the
# launchd socket path (/var/run/com.apple.launchd.*/Listeners) is
# random per login session. srt realpaths it (/var → /private/var).
# Trade-off: sandboxed code can authenticate as you while the session
# runs — mitigate with ssh-add -c or hardware-backed keys.
# - The CA paths above (tlsTerminate does no tilde expansion, so the
# $HOME-absolute paths can't live in the shared template).
local cfg="$td/settings.json"
jq --arg sock "$SSH_AUTH_SOCK" --arg cadir "$cadir" '
(if $sock != "" then .network.allowUnixSockets = [$sock] else . end)
| .network.tlsTerminate.caCertPath = ($cadir + "/mitm-ca.crt")
| .network.tlsTerminate.caKeyPath = ($cadir + "/mitm-ca.key")
' "$settings" > "$cfg"

# pnpm_config_store_dir pins pnpm's global store: its store-selection
# heuristic misfires inside the sandbox and silently falls back to a
# per-project .pnpm-store/ even when the global store is writable
Expand All @@ -88,13 +119,9 @@ function _ccx_run {
DISABLE_FEEDBACK_COMMAND=1 \
CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY=1 \
GH_TOKEN="$gh_token" \
SSL_CERT_FILE="$cabundle" \
CURL_CA_BUNDLE="$cabundle" \
CARGO_HTTP_CAINFO="$cabundle" \
GIT_SSL_CAINFO="$cabundle" \
REQUESTS_CA_BUNDLE="$cabundle" \
SSH_AUTH_SOCK="$SSH_AUTH_SOCK" \
pnpm_config_store_dir="$HOME/Library/pnpm/store" \
srt --settings "$settings" -- claude "$@"
srt --settings "$cfg" -- claude "$@"
}

function ccx { _ccx_run ~/.srt-claude-denyall.json "$@" }
Expand Down
Loading