Network isolation, traffic inspection, and HTTP filtering.
Proxy mode routes all HTTP/HTTPS traffic through a local MITM (Man-in-the-Middle) proxy for inspection and logging. The sections below describe bwrap backend behavior by default. The Docker backend achieves the same goal with different mechanisms - see Backend-Specific Behavior for details.
- Audit AI agent activity - See exactly what API calls your AI coding assistant makes
- Debug network issues - Inspect request/response headers and bodies
- Security monitoring - Detect unexpected network connections
- Compliance - Log all external communications for review
| Enable proxy when | Skip proxy when |
|---|---|
| You want to monitor AI agent network activity | You trust the code and don't need traffic visibility |
| You need credential injection (GitHub tokens) | Tools use certificate pinning that breaks MITM |
| You need port forwarding (requires network isolation) | You want the fastest possible startup |
| You need content redaction (secret scanning) | You only need filesystem isolation |
| You need HTTP filtering (domain whitelist/blacklist) |
Proxy mode on the bwrap backend requires passt/pasta for network namespace creation. This is the only feature that requires passt-basic sandboxing works without it.
devsandbox includes an embedded pasta binary - no system packages required. To use a system-installed pasta instead, set use_embedded = false in configuration.
Docker backend: The Docker backend does NOT require pasta. It uses per-session Docker networks for isolation instead. See Backend-Specific Behavior.
Optionally install the system package as a fallback:
# Arch Linux
sudo pacman -S passt
# Debian/Ubuntu
sudo apt install passt
# Fedora
sudo dnf install passtVerify installation:
devsandbox doctorThe doctor output shows whether each binary is embedded or system-installed.
# Enable proxy mode for this session
devsandbox --proxy
# With custom port
devsandbox --proxy --proxy-port 9090
# Run a command with proxy
devsandbox --proxy npm installEnable proxy mode by default in ~/.config/devsandbox/config.toml:
[proxy]
enabled = true
port = 8080By default, the proxy performs MITM (Man-in-the-Middle) interception on HTTPS connections, using a generated CA certificate. This enables full traffic inspection, credential injection, and content redaction for HTTPS.
If you don't need HTTPS inspection - for example, when tools have certificate pinning or you only need network isolation with HTTP logging — you can disable MITM:
devsandbox --proxy --no-mitm[proxy]
enabled = true
mitm = false| Feature | MITM enabled (default) | MITM disabled |
|---|---|---|
| HTTP filtering/logging | Full | Full |
| HTTPS body/header inspection | Full | None |
| HTTPS credential injection | Works | Does not work |
| HTTPS content redaction | Works | Does not work |
| HTTPS request logging | Full request/response | CONNECT hostname only |
| CA certificate injection | Yes | Skipped |
| Network isolation | Yes | Yes |
When MITM is disabled, the proxy logs warnings at startup if credential injectors, redaction rules, or filter rules are configured - since these features cannot inspect encrypted HTTPS traffic.
When to disable MITM:
- Tools with certificate pinning that reject the proxy CA
- You only need network isolation and HTTP (not HTTPS) logging
- You don't need credential injection, content redaction, or HTTPS filtering
If you're running AI coding assistants (Claude Code, aider, etc.), keep MITM enabled - it's required for credential injection and secret scanning.
The proxy achieves the same goal on both backends - intercept and log HTTP/HTTPS traffic — but the underlying mechanisms differ.
- Network isolation: pasta creates a new network namespace with its own network stack
- Gateway address: Traffic is routed through
10.0.2.2(pasta virtual gateway) - CA certificate path (inside sandbox):
/tmp/devsandbox-ca.crt - Requirement: passt/pasta must be installed
- Network isolation: A per-session Docker network is created (no pasta required)
- Gateway address:
host.docker.internal(Docker's built-in host access) - CA certificate path (inside container):
/etc/ssl/certs/devsandbox-ca.crt - Requirement: Docker daemon running (no additional dependencies)
| Aspect | bwrap | Docker |
|---|---|---|
| Network isolation | pasta namespace | Per-session Docker network |
| Gateway IP | 10.0.2.2 |
host.docker.internal |
| CA cert location | /tmp/devsandbox-ca.crt |
/etc/ssl/certs/devsandbox-ca.crt |
| Extra dependency | passt/pasta | None (Docker only) |
- Network Isolation - pasta creates a new network namespace with its own network stack
- Gateway Setup - Traffic is routed through a virtual gateway (10.0.2.2)
- Proxy Server - A local HTTP/HTTPS proxy runs on the host
- Traffic Enforcement - All HTTP(S) traffic must go through the proxy
- TLS Interception - A generated CA certificate enables HTTPS inspection
┌─────────────────────────────────────────────────────────────┐
│ Host System │
│ ┌─────────────────┐ │
│ │ Proxy Server │◄─────┐ │
│ │ 127.0.0.1:8080 │ │ │
│ └────────┬────────┘ │ │
│ │ │ │
│ ▼ │ │
│ Internet │ pasta NAT │
│ │ (10.0.2.2 → 127.0.0.1) │
├───────────────────────────┼─────────────────────────────────┤
│ │ Sandbox (netns) │
│ ┌──────┴──────┐ │
│ │ Gateway │ │
│ │ 10.0.2.2 │ │
│ └──────▲──────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ Application │ │
│ │ HTTP_PROXY= │ │
│ │ 10.0.2.2: │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
A CA certificate is automatically generated for HTTPS interception and stored at:
~/.local/share/devsandbox/<project>/.ca/ca.crt
Inside the sandbox, the certificate is available at /tmp/devsandbox-ca.crt (bwrap backend) or /etc/ssl/certs/devsandbox-ca.crt (Docker backend) and automatically configured via
environment variables:
| Variable | Purpose |
|---|---|
NODE_EXTRA_CA_CERTS |
Node.js |
REQUESTS_CA_BUNDLE |
Python requests |
CURL_CA_BUNDLE |
curl |
GIT_SSL_CAINFO |
Git HTTPS |
SSL_CERT_FILE |
General SSL/TLS |
Some tools implement certificate pinning and won't work with the MITM proxy:
- Mobile app backends
- Some cloud SDKs
- Security-focused applications
View HTTP/HTTPS traffic captured in proxy mode:
# View all proxy logs for current project
devsandbox logs proxy
# View last 50 requests
devsandbox logs proxy --last 50
# Follow/tail logs in real-time
devsandbox logs proxy -f# Filter by time
devsandbox logs proxy --since 1h # Last hour
devsandbox logs proxy --since today # Since midnight
devsandbox logs proxy --since 2024-01-15 # Since specific date
devsandbox logs proxy --until 2024-01-15T12:00:00
# Filter by content
devsandbox logs proxy --url /api # URL contains "/api"
devsandbox logs proxy --method POST # Only POST requests
devsandbox logs proxy --status 200 # Specific status code
devsandbox logs proxy --status 400-599 # Status code range
devsandbox logs proxy --status ">=400" # Comparison
devsandbox logs proxy --errors # All errors (status >= 400)
# Combine filters
devsandbox logs proxy --method POST --url /api --since 1h# Table format (default)
devsandbox logs proxy
# Compact one-line format
devsandbox logs proxy --compact
# JSON output (for scripting)
devsandbox logs proxy --json
# Include request/response bodies
devsandbox logs proxy --body
# Show summary statistics
devsandbox logs proxy --stats
# Disable colors (for piping)
devsandbox logs proxy --no-colorTable format:
┌──────────┬────────┬────────┬──────────┬────────────────────────┐
│ TIME │ METHOD │ STATUS │ DURATION │ URL │
├──────────┼────────┼────────┼──────────┼────────────────────────┤
│ 10:30:05 │ GET │ 200 │ 150ms │ https://api.example.com│
│ 10:30:06 │ POST │ 201 │ 89ms │ https://api.example.com│
└──────────┴────────┴────────┴──────────┴────────────────────────┘
Compact format:
10:30:05 GET 200 150ms https://api.example.com/users
10:30:06 POST 201 89ms https://api.example.com/orders
Stats output:
Summary:
Total requests: 150
Success (2xx): 120 (80.0%)
Redirect (3xx): 10 (6.7%)
Client err (4xx): 15 (10.0%)
Server err (5xx): 5 (3.3%)
Avg duration: 245ms
Logs are stored as gzip-compressed JSONL files:
~/.local/share/devsandbox/<project>/logs/
├── proxy/
│ ├── requests_20240115_0000.jsonl.gz
│ ├── requests_20240115_0001.jsonl.gz
│ └── ...
└── internal/
├── proxy_20240115_0000.log.gz
└── logging-errors.log
- Files rotate when they reach 50MB
- Maximum 5 files kept per type
- Older files are automatically pruned
Each log entry contains:
{
"ts": "2024-01-15T10:30:05.123Z",
"method": "POST",
"url": "https://api.example.com/users",
"req_headers": {
"Content-Type": [
"application/json"
],
"Authorization": [
"Bearer ..."
]
},
"req_body": "eyJ1c2VyIjogImpvaG4ifQ==",
"status": 201,
"resp_headers": {
"Content-Type": [
"application/json"
]
},
"resp_body": "eyJpZCI6IDEyM30=",
"duration_ns": 89000000,
"error": ""
}Note: Request/response bodies are base64-encoded.
View proxy server errors and warnings:
# View all internal logs
devsandbox logs internal
# Filter by log type
devsandbox logs internal --type proxy # Proxy server logs
devsandbox logs internal --type logging # Remote logging errors
# Follow internal logs
devsandbox logs internal -f
# Show last N lines
devsandbox logs internal --last 100Proxy logs can be forwarded to remote destinations. See Configuration - Remote Logging for setup instructions.
HTTP filtering allows you to control which requests are allowed, blocked, or require user approval.
Filtering is enabled by setting default_action which determines what happens to requests that don't match any rule:
| Default Action | Behavior |
|---|---|
block |
Block unmatched requests (whitelist behavior) |
allow |
Allow unmatched requests (blacklist behavior) |
ask |
Prompt user for each unmatched request |
# Whitelist behavior - only allow specific domains, block everything else
devsandbox --proxy --filter-default=block \
--allow-domain="*.github.com" \
--allow-domain="api.anthropic.com"
# Blacklist behavior - block specific domains, allow everything else
devsandbox --proxy --filter-default=allow \
--block-domain="*.tracking.io" \
--block-domain="ads.example.com"
# Ask mode - interactive approval for unmatched requests
devsandbox --proxy --filter-default=askAdd filter rules to ~/.config/devsandbox/config.toml:
[proxy.filter]
# Enable filtering with default action for unmatched requests
default_action = "block" # whitelist behavior
ask_timeout = 30
cache_decisions = true
[[proxy.filter.rules]]
pattern = "*.github.com"
action = "allow"
scope = "host"
[[proxy.filter.rules]]
pattern = "api.anthropic.com"
action = "allow"
scope = "host"
[[proxy.filter.rules]]
pattern = "*.internal.corp"
action = "block"
scope = "host"
reason = "Internal network blocked"Lock down an AI coding assistant to only communicate with known services:
[proxy.filter]
default_action = "block"
[[proxy.filter.rules]]
pattern = "api.anthropic.com"
action = "allow"
scope = "host"
[[proxy.filter.rules]]
pattern = "*.github.com"
action = "allow"
scope = "host"
[[proxy.filter.rules]]
pattern = "registry.npmjs.org"
action = "allow"
scope = "host"You can generate filter rules from a "known good" session using devsandbox proxy filter generate (see Generate Filter Rules).
Default is glob. Patterns containing regex characters (^$|()[]{}\+) are auto-detected as regex.
| Type | Example | Description |
|---|---|---|
glob |
*.example.com |
Glob patterns (* and ?) - default |
exact |
api.example.com |
Exact string match |
regex |
^api\.(dev|prod)\.com$ |
Regular expressions |
Default is host.
| Scope | Description | Example Match |
|---|---|---|
host |
Request host only - default | api.example.com |
path |
Request path only | /api/v1/users |
url |
Full URL | https://api.example.com/v1/users |
In ask mode, unmatched requests require user approval via a separate monitor terminal. This is particularly useful when running AI agents autonomously -- you can approve or block each network request the agent makes, giving you real-time control over what data leaves your machine.
Step 1: Start the sandbox with ask mode:
devsandbox --proxy --filter-default=askThe sandbox will display:
Filter: ask mode (default action for unmatched requests)
Run in another terminal to approve/deny requests:
devsandbox proxy monitor
Requests without response within 30s will be rejected.
Step 2: Open another terminal (in the same project directory) and run the monitor:
devsandbox proxy monitorThe socket path is auto-detected from the current directory's sandbox. You can also specify it explicitly:
devsandbox proxy monitor /path/to/ask.sockThe monitor displays incoming requests:
┌──────────────────────────────────────────────────────────────────┐
│ Request #1 │
├──────────────────────────────────────────────────────────────────┤
│ Method: GET │
│ Host: api.example.com │
│ Path: /v1/users │
├──────────────────────────────────────────────────────────────────┤
│ [A]llow [B]lock Allow [S]ession Block [N]ever │
└──────────────────────────────────────────────────────────────────┘
Decision:
Keys (instant response, no Enter needed):
a- Allow this requestb- Block this requests- Allow and remember for sessionn- Block and remember for session
Timeout: Requests that don't receive a response within 30 seconds are automatically rejected and logged to internal logs as unanswered.
Analyze existing proxy logs to generate filter configuration:
# Generate whitelist rules from current project's logs (default: block unmatched)
devsandbox proxy filter generate
# Generate from specific log directory
devsandbox proxy filter generate --from-logs ~/.local/share/devsandbox/myproject/logs/proxy/
# Generate blacklist rules (allow unmatched)
devsandbox proxy filter generate --default-action allow
# Save to file
devsandbox proxy filter generate -o filter-rules.toml
# Only include domains with 5+ requests
devsandbox proxy filter generate --min-requests 5devsandbox proxy filter showFilter decisions are logged with requests:
{
"ts": "2024-01-15T10:30:05Z",
"method": "GET",
"url": "https://blocked.example.com/",
"status": 403,
"filter_action": "block",
"filter_reason": "matched rule: *.blocked.com"
}The proxy can inject authentication credentials into requests for specific domains, keeping tokens completely out of the sandbox environment. The sandboxed process never sees the token - it stays on the host side.
- Intercept - The proxy intercepts outgoing requests from the sandbox.
- Match - For each configured credential injector, it checks whether the request host matches the injector's
host(exact match or glob). - Inject - If matched, the injector sets the configured
headerto the renderedvalue_formatwith{token}substituted from the resolvedsource. The existing header is preserved unlessoverwrite = true. - Isolate - The sandbox process never sees the token. It is read from the host environment and added transparently.
Every injector is defined by the same set of fields under [proxy.credentials.<name>]:
| Field | Purpose |
|---|---|
enabled |
Master switch. Injector is inert unless enabled = true. |
host |
Hostname to match. Exact (api.github.com) or glob (*.example.com). |
header |
HTTP header to set on matching requests. Canonicalized at load (authorization → Authorization). |
value_format |
Template for the header value. {token} is replaced with the resolved source value. Defaults to "{token}". |
overwrite |
When true, replaces any existing value for the configured header. Default false. |
preset |
Optional name of a built-in preset whose defaults are used as the base for this injector. |
[...source] sub-table |
Where the token comes from: env, file, or value. |
A custom non-GitHub injector - no Go code required:
[proxy.credentials.gitlab]
enabled = true
host = "gitlab.com"
header = "PRIVATE-TOKEN"
value_format = "{token}"
[proxy.credentials.gitlab.source]
env = "GITLAB_TOKEN"Built-in preset names are reserved - using one as the section name (e.g. [proxy.credentials.github]) automatically applies the preset's defaults. User fields override preset defaults; [...source] overrides the preset's default source.
| Preset | host |
header |
value_format |
Default source |
|---|---|---|---|---|
github |
api.github.com |
Authorization |
Bearer {token} |
env = "GITHUB_TOKEN" (with GH_TOKEN fallback when no explicit source is set) |
Minimal GitHub configuration - the preset supplies everything else:
[proxy.credentials.github]
enabled = true| Field | Description | Example |
|---|---|---|
env |
Read from an environment variable | env = "DEVSANDBOX_GITHUB_TOKEN" |
file |
Read from a file (supports ~ expansion, whitespace trimmed) |
file = "~/.config/devsandbox/github-token" |
value |
Static value in config | value = "github_pat_..." |
When multiple fields are set, priority is: value > env > file. Set exactly one for clarity.
When more than one configured injector could match the same request host, the most-specific one wins:
- Exact host beats any glob.
- Among globs, the longer literal portion (
len(host) - count('*')) wins. - Ties are broken by injector name in alphabetical order.
So an exact api.github.com injector wins over a *.github.com injector for api.github.com, and the proxy injects exactly one credential per request.
A glob that doesn't match any actual request is not an error - host coverage is enforced lazily at request time, not at config load.
By default the injector never replaces an existing value for its configured header - the sandboxed tool wins. That's safe, but breaks the pattern where a CLI inside the sandbox needs a token set in its environment to start (e.g. gh CLI refuses to run without GH_TOKEN).
To handle this, set overwrite = true and inject a placeholder env var into the sandbox so the CLI starts:
[sandbox.environment.GH_TOKEN]
value = "placeholder"
[proxy.credentials.github]
enabled = true
overwrite = true
[proxy.credentials.github.source]
env = "GH_RO_TOKEN" # real read-only token on the hostExport GH_RO_TOKEN on the host only. The sandbox sees GH_TOKEN=placeholder; gh adds Authorization: Bearer placeholder to its requests; the proxy replaces the header with the real token from GH_RO_TOKEN before forwarding to api.github.com.
Security trade-off: the sandbox sees a non-functional placeholder, not the real token - leaking the placeholder is harmless. This preserves the core guarantee: the real credential never enters the sandbox.
AI agent workflow: Credential injection is particularly useful for AI coding assistants like Claude Code that need GitHub API access. The token stays on the host - the AI agent never sees it, but its API requests to github.com are automatically authenticated.
Notes:
- Credential injection requires proxy mode (
--proxy) with MITM enabled (the default). - Injectors are only active when explicitly
enabled = trueand the credential source resolves to a non-empty value. An empty source silently disables the injector - it is not a config error. - By default the injector never overwrites an existing value for its configured header. Set
overwrite = trueto change this. - Invalid configuration fails fast at load time: unknown
preset, missinghost/headerwhenenabled = true, invalid glob, or unreadable sourcefile.
See Configuration: Proxy Credentials for the complete TOML reference.
Content redaction scans outgoing requests for secrets before they leave your machine. It checks request bodies, headers, and URLs against configured rules.
| Action | What happens |
|---|---|
| Block | Request rejected with HTTP 403. Secret never leaves your machine. |
| Redact | Secret replaced with [REDACTED:<rule-name>] in body, headers, and URL. Modified request forwarded to destination. |
| Log | Request forwarded unmodified. Match recorded in proxy logs as a warning. |
# Block requests containing your API key
devsandbox --proxy# ~/.config/devsandbox/config.toml or .devsandbox.toml
[proxy.redaction]
enabled = true
default_action = "block"
[[proxy.redaction.rules]]
name = "api-key"
[proxy.redaction.rules.source]
env = "API_SECRET_KEY"Any outgoing request containing the value of $API_SECRET_KEY is blocked with HTTP 403.
Rules detect secrets using either a source (exact value lookup) or a pattern (regex match).
| Field | Description | Example |
|---|---|---|
env |
Environment variable on the host | env = "API_SECRET_KEY" |
file |
File path (supports ~, whitespace trimmed) |
file = "~/.secrets/token" |
env_file_key |
Key in project .env file |
env_file_key = "DB_PASSWORD" |
value |
Static value in config | value = "literal-secret" |
Choosing an action:
- Block when the secret must never leave your machine (most secure, may break the tool's request)
- Redact when the request should proceed but without the secret (destination sees
[REDACTED:rule-name]) - Log when you want visibility without enforcement (monitoring only)
Redaction events appear in proxy logs with additional fields:
{
"ts": "2026-02-23T10:30:05Z",
"method": "POST",
"url": "https://api.example.com/v1/chat",
"status": 403,
"redaction_action": "block",
"redaction_matches": ["api-key"]
}For the redact action, the logged URL and body contain the replacement placeholders - the original secret never appears in logs.
View redaction events:
devsandbox logs proxy --json | jq 'select(.redaction_action != null)'- Content redaction requires proxy mode (
--proxy) with MITM enabled. - All source values must resolve at startup. If an environment variable is missing or a file is unreadable, devsandbox exits with an error (fail-closed).
- Log entries for blocked and redacted requests have secrets replaced - secrets never appear in proxy logs.
- Redaction rules are always additive when merging configs. The default action uses most-restrictive-wins.
- Redaction rules must not match values used by credential injectors. If a redaction rule (source or pattern) would match an injected credential, devsandbox exits with an error at startup. This prevents the confusing situation where credential injection adds a token and redaction immediately blocks it.
- When multiple rules match, the most severe action wins: block > redact > log.
See Configuration: Content Redaction for the full TOML reference, pattern rules, and merge behavior.
Log-skip rules drop matching requests from the proxy log entirely. This is for noise reduction, not for security: matched requests still pass through (filtering, redaction, and credential injection still apply); they simply never appear in logs/proxy/requests.jsonl and are never forwarded to remote log dispatchers (syslog/OTLP).
The intended use case is endpoints you'd otherwise see hundreds of times per session and don't care about — for example, telemetry traffic that an agent sends to your own observability infrastructure, where the data is already captured upstream.
Each rule has a pattern and an optional scope (default host) and type (default glob, regex auto-detected on metacharacters). Matching reuses the same engine as filter rules: host matches the request hostname (port stripped), path matches the URL path, url matches the full URL string. Rules are evaluated in order, first match wins. Skip is absolute — a matched entry is never logged, even if the request errored, was blocked by the security filter, or triggered a redaction rule.
[[proxy.log_skip.rules]]
pattern = "telemetry.example.com"
# scope defaults to "host", type auto-detects glob
[[proxy.log_skip.rules]]
pattern = "*/v1/traces"
scope = "url"
type = "glob"
[[proxy.log_skip.rules]]
pattern = "/v1/metrics"
scope = "path"
type = "exact"- Filter (allow/block): independent. A request blocked by the filter is still skipped from logs if it also matches a
log_skiprule. If you want to keep a record of blocked attempts, do not log-skip the same hosts you block. - Redaction: independent. Redaction still scrubs request/response bodies in flight; log-skip just decides whether the (already-redacted) entry gets persisted.
- Credential injection: independent. Tokens are still injected on outbound requests; log-skip only affects the local log artifact.
See Configuration: Log Skip for the full TOML reference.
devsandbox includes an embedded pasta binary. If you see this error, extraction failed and no system package is installed:
# Check doctor for details (shows embedded vs system source)
devsandbox doctor
# Install system package as fallback (see Requirements above)- Check if the target allows proxy connections
- Some services block known proxy IPs
- Try accessing the URL directly to verify it's reachable
- Ensure the CA environment variables are set correctly
- Some tools require manual CA configuration
- Certificate pinning may prevent interception
- Verify proxy mode is enabled:
devsandbox --proxy --info - Check the log directory exists
- Make HTTP requests (not just TCP connections)
- Sandboxing -- filesystem and process isolation, security model
- Configuration: Remote Logging -- send proxy logs to syslog or OTLP
- Configuration: Credential Injection -- inject tokens into requests without exposing them to the sandbox
- Configuration: Port Forwarding -- forward ports between host and sandbox (requires proxy mode)
- Use Cases: Security Monitoring -- real-time monitoring and post-session audit scripts