bx-mac
Sandbox any macOS app — only your project directory stays accessible
📦 bx
Put your AI in a box. Launch VSCode, Claude Code, a terminal, or any command in a macOS sandbox — your tools can only see the project you're working on.
🤔 Why?
AI-powered coding tools like Claude Code, Copilot, or Cline run with broad file system access. A misguided tool call or hallucinated path could accidentally read your SSH keys, credentials, tax documents, or private photos.
bx wraps any application in a macOS sandbox (sandbox-exec) that blocks access to everything except the project directory you explicitly specify. No containers, no VMs, no setup — just one command.
bx ~/work/my-project
That's it. 🎉 VSCode opens with full access to ~/work/my-project and nothing else. Read the blog post for more background on the motivation behind bx.
Need multiple directories? No problem:
bx ~/work/my-project ~/work/shared-lib
✅ What it does
- 🔒 Blocks
~/Documents,~/Desktop,~/Downloads, and all other personal folders - 🚧 Blocks sibling projects — only the directory you specify is accessible
- 🛡️ Protects sensitive dotdirs like
~/.ssh,~/.gnupg,~/.docker,~/.cargo - 🏛️ Opinionated protection for
~/Library— blocks privacy-sensitive subdirectories (Mail, Messages, Photos, Safari, Contacts, …) and containers of password managers/finance apps, while keeping tooling-relevant paths accessible - ⚙️ Keeps VSCode, extensions, shell, Node.js, and other tooling fully functional
- 🔍 Generates sandbox rules dynamically based on your actual
$HOMEcontents - 📝 Supports
.bxignorefiles (searched recursively) to hide secrets like.envfiles within a project - 📂 Supports
rw:andro:prefixes in~/.bxignoreto grant read-write or read-only access to extra directories - 🗂️ Supports multiple working directories in a single sandbox
🚫 What it doesn't do
- No network restrictions — API calls, git push/pull, npm install all work normally
- No process isolation — this is file-level sandboxing, not a container
- No protection against root/sudo — the sandbox applies to the user-level process
- macOS only — relies on
sandbox-exec(Apple-specific) - Not dynamic — the sandbox profile is a snapshot of
$HOMEat launch time; directories or files created later are not automatically blocked - Not a vault —
sandbox-execis undocumented; treat this as a safety net, not a guarantee
📥 Install
# Homebrew
brew install holtwick/tap/bx
# npm
npm install -g bx-mac
# From source
git clone https://github.com/holtwick/bx-mac.git
cd bx-mac
pnpm install && pnpm build
pnpm link -g
Requirements: macOS (only tested on Tahoe 26.4, feedback welcome), Node.js >= 22
🚀 Modes
| Command | What it launches |
|---|---|
bx [workdir...] |
🖥️ VSCode (default) |
bx code [workdir...] |
🖥️ VSCode (explicit) |
bx xcode [workdir...] [-- project-or-workspace] |
🛠️ Xcode |
bx term [workdir...] |
💻 Sandboxed login shell ($SHELL -l) |
bx claude [workdir...] |
🤖 Claude Code CLI |
bx exec [workdir...] -- cmd |
⚡ Any command you want |
bx <app> [workdir...] [-- app-args...] |
🔌 Any app from ~/.bxconfig.toml |
If no directory is given, the current directory is used. All modes accept multiple directories.
For app modes, values before -- define the sandbox scope (workdir...). Values after -- are passed to the app as launch arguments.
For xcode, this distinction is important: the sandbox workdir is not passed as an Xcode open argument. Use -- if you want to open a specific .xcworkspace or .xcodeproj.
This behavior is configurable per app via passPaths in ~/.bxconfig.toml (default: true, built-in xcode default: false).
GUI app modes are activated in the foreground on launch (best effort), so the opened app should become the frontmost app.
Examples
# 🖥️ VSCode with sandbox protection
bx ~/work/my-project
# 📂 Multiple working directories
bx ~/work/my-project ~/work/shared-lib
# 💻 Work on a project in a sandboxed terminal
bx term ~/work/my-project
# 🤖 Let Claude Code work on a project — nothing else visible
bx claude ~/work/my-project
# 🛠️ Xcode (built-in) — sandbox only, open picker/restore state
bx xcode ~/work/my-ios-app
# 🛠️ Xcode with explicit project/workspace to open
bx xcode ~/work/my-ios-app -- MyApp.xcworkspace
# 🔌 Custom apps from ~/.bxconfig.toml
bx cursor ~/work/my-project
bx zed ~/work/my-project
# ⚡ Run a script in a sandbox
bx exec ~/work/my-project -- python train.py
# 🔀 Run in the background (terminal stays free)
bx --background code ~/work/my-project
# 🔍 Preview what will be protected (no launch)
bx --dry ~/work/my-project
# 🔍 See the generated sandbox profile
bx --verbose ~/work/my-project
⚙️ Options
| Option | Description |
|---|---|
--dry |
Show a tree of all protected, read-only, and accessible paths — don't launch anything |
--verbose |
Print the generated sandbox profile plus launch details (binary, arguments, cwd, focus command) |
--background |
Run the app detached in the background (like nohup &), output goes to /tmp/bx-<pid>.log |
--profile-sandbox |
Use an isolated VSCode profile (separate extensions/settings, code mode only) |
On normal runs, bx also prints a short policy summary (number of workdirs, blocked directories, hidden paths, and read-only directories).
📝 Configuration
~/.bxconfig.toml
App definitions in TOML format. Each [<name>] section becomes a CLI mode — use it as bx <name> [workdir...]. Built-in apps (code, xcode) are always available and can be overridden.
# Add Cursor (auto-discovered via macOS Spotlight)
[cursor]
bundle = "com.todesktop.230313mzl4w4u92"
binary = "Contents/MacOS/Cursor"
args = ["--no-sandbox"]
# Add Zed (explicit path, no discovery)
[zed]
path = "/Applications/Zed.app/Contents/MacOS/zed"
# Override built-in VSCode path
[code]
path = "/usr/local/bin/code"
| Field | Description |
|---|---|
mode |
Inherit from another app (e.g. "code", "cursor") — only paths / overrides needed |
bundle |
macOS bundle identifier — used with mdfind to find the app automatically |
binary |
Relative path to the executable inside the .app bundle |
path |
Absolute path to the executable or .app bundle (highest priority, skips discovery) |
fallback |
Absolute fallback path if mdfind discovery fails |
args |
Extra arguments always passed to the app |
passPaths |
Paths passed as app launch args (true/false/N/["~/p1", "~/p2"]) |
paths |
Default working directories when none are given on the CLI (supports ~/ paths) |
background |
Run the app detached in the background by default (true/false) |
Resolution order: path → mdfind by bundle + binary → fallback
passPaths controls launch argument behavior and is independent of sandbox scope. Even with passPaths = false, the provided workdir... still defines what the sandbox can access. Use passPaths = 1 to pass only the first path as a launch argument, or passPaths = ["~/specific/path"] to pass explicit paths instead of workdirs.
Workdir shortcuts with mode let you create named entries that inherit everything from an existing app — just set mode and paths:
# "bx myproject" opens VSCode with these directories
[myproject]
mode = "code"
paths = ["~/work/my-project", "~/work/shared-lib"]
# "bx ios" opens Xcode with this directory
[ios]
mode = "xcode"
paths = ["~/work/my-ios-app"]
Running bx myproject inherits VSCode's bundle, binary, args, and everything else — no need to repeat the full app configuration. Own fields override inherited ones, so you can still customize specific settings. Chaining is supported (e.g. myproject → cursor → code).
Preconfigured paths also work directly on app definitions:
[code]
paths = ["~/work/my-project", "~/work/shared-lib"]
Running bx code (without arguments) will then open VSCode with both directories sandboxed. CLI arguments always override configured paths.
When overriding a built-in app, only the specified fields are replaced — unset fields keep their defaults. See bxconfig.example.toml for a complete reference.
💡 Finding a bundle ID: Run
osascript -e 'id of app "AppName"'to get the bundle ID of any installed app. Usingbundleinstead ofpathis recommended — it survives app updates, relocations, and name changes.
~/.bxignore
Unified sandbox rules for your home directory. Paths relative to $HOME. Each line is either a deny rule (no prefix) or an access grant (rw: / ro: prefix, case-insensitive).
# Block additional sensitive paths (no prefix = deny)
.aws
.azure
.kube
.config/gcloud
# Allow read-write access to extra directories
rw:work/bin
rw:shared/libs
# Allow read-only access (can read but not modify)
ro:reference/docs
ro:shared/toolchain
Deny rules are applied in addition to the built-in protected lists:
🔒 Dotdirs:
.ssh.gnupg.docker.zsh_sessions.cargo.gradle.gem🏛️ Library (opinionated):
AccountsCalendarsContactsCookiesFinanceMessagesMobile DocumentsPhotosSafariand others (see full list) — plus containers of password managers & finance apps
<project>/.bxignore
Block paths within the working directory. Uses .gitignore-style pattern matching:
| Pattern | Matches | Why |
|---|---|---|
.env |
.env at any depth |
No / → recursive |
.env.* |
.env.local, sub/.env.production |
No / → recursive |
*.pem |
key.pem, sub/deep/cert.pem |
No / → recursive |
secrets/ |
secrets/ at any depth |
Trailing / is a dir marker, not a path separator |
/.env |
Only <workdir>/.env |
Leading / → anchored to root |
config/secrets |
Only <workdir>/config/secrets |
Contains / → relative to workdir |
bx searches for .bxignore files recursively through the entire project tree (skipping .-prefixed dirs and node_modules), so you can place them in subdirectories to hide secrets close to where they live.
.env
.env.*
secrets/
*.pem
*.key
For example, a monorepo might have:
my-project/.bxignore # top-level rules
my-project/services/api/.bxignore # API-specific secrets
my-project/deploy/.bxignore # deployment credentials
Each .bxignore resolves its patterns relative to its own directory.
Self-protecting directories
You can make any directory protect itself — no global configuration needed. There are two ways:
Option 1: .bxignore with / or .
Create a .bxignore file containing a bare / or .:
.
This blocks the entire directory and everything inside it. You can combine it with other patterns (they become redundant since the whole directory is blocked).
Option 2: .bxprotect marker file
Create an empty .bxprotect file:
touch ~/work/secret-project/.bxprotect
Both methods have the same effect:
- Inside a workdir: If a subdirectory is self-protected, it is completely blocked (deny) and bx does not recurse into it.
- As a workdir: If you try to open a self-protected directory with
bx, it refuses to launch with a clear error message.
🔧 How it works
bx generates a macOS sandbox profile at launch time:
- Scan
$HOMEfor non-hidden directories - Block each one individually with
(deny file* (subpath ...)) - Skip all working directories,
~/Library, dotfiles, andrw:/ro:paths from~/.bxignore - Descend into parent directories of allowed paths to block only siblings (because SBPL deny rules always override allow rules)
- Protect an opinionated set of
~/Librarysubdirectories (Mail, Messages, Photos, Safari, Contacts, Calendars, …) and app containers matching known password managers and finance apps (1Password, Bitwarden, MoneyMoney, …) - Append deny rules for protected dotdirs, plain entries in
~/.bxignore, and.bxignorefiles found recursively in each working directory - Apply
(deny file-write*)rules forro:directories (read allowed, write blocked) - Write the profile to
/tmp, launch the app viasandbox-exec, clean up on exit
Why not a simple deny-all + allow?
Apple's SBPL has a critical quirk: deny always wins over allow, regardless of rule order:
;; ❌ Does NOT work — the deny still blocks myproject
(deny file* (subpath "/Users/me/work"))
(allow file* (subpath "/Users/me/work/myproject"))
Additionally, a broad (deny file* (subpath HOME)) breaks kqueue/FSEvents file watchers and SQLite locks, causing VSCode errors.
bx avoids both issues by never denying a parent of an allowed path — it walks the directory tree and blocks only the specific siblings.
🛡️ Safety checks
bx detects and prevents problematic scenarios:
- 🔄 Sandbox nesting: If
CODEBOX_SANDBOX=1is set (auto-propagated), bx refuses to start — nested sandboxes cause silent failures. - 🔍 Unknown sandbox: On startup, bx probes
~/Documents,~/Desktop,~/Downloads. If any returnEPERM, another sandbox is active — bx aborts. - ⚠️ VSCode terminal: If
VSCODE_PIDis set, bx warns that it will launch a new instance, not sandbox the current one. - 🧩 App already sandboxed: For GUI app modes, bx inspects app entitlements (best effort) and warns if Apple App Sandbox is enabled, since nested sandboxing can cause startup/access issues.
- 🔁 App already running: If the target app is already running, bx warns that the new workspace would open in the existing (unsandboxed) instance and asks for confirmation. This is important because Electron apps like VSCode, Cursor, etc. always reuse the running process —
sandbox-exechas no effect on the already-running instance.
Single-instance apps
Most GUI editors (VSCode, Cursor, Xcode, Zed) are single-instance apps — launching them a second time just sends the path to the running process. This means you cannot run two separately sandboxed instances of the same app.
To work on multiple projects, specify all directories at launch:
bx code ~/work/project-a ~/work/project-b
Or preconfigure them in ~/.bxconfig.toml:
[code]
paths = ["~/work/project-a", "~/work/project-b"]
For VSCode specifically, --profile-sandbox forces a separate Electron process via an isolated --user-data-dir, but this means separate extensions and settings.
💡 Tips
Verify it works — try reading a blocked file from the sandboxed terminal:
cat ~/Documents/something.txt # ❌ Operation not permitted
cat ~/Desktop/file.txt # ❌ Operation not permitted
ls ~/work/other-project/ # ❌ Operation not permitted
cat ./src/index.ts # ✅ Works!
⚠️ Known limitations
- ⚠️ Sandbox profile is static: The sandbox rules are generated once at launch by scanning the current state of
$HOME. Directories or files created after the sandbox starts are not protected — for example, if a tool creates~/new-project/while the sandbox is running, that directory will be fully accessible. Similarly, project-level.bxignorepatterns only match files that exist at launch time; files matching a blocked pattern (e.g..env) that are created later will not be denied. Re-runbxto pick up changes. - File watcher warnings: VSCode may log
EPERMforfs.watch()on some paths — cosmetic only - SQLite warnings:
state.vscdberrors may appear in logs — extensions still work sandbox-execis undocumented: Apple could change behavior with OS updates
🤖 Built-in sandboxing in AI tools
Some AI coding tools ship with their own sandboxing. bx complements these by providing a uniform, tool-independent layer that works across all applications — including editors, shells, and custom commands:
- Claude Code — built-in sandbox for file and command restrictions
- Gemini CLI — sandbox mode for file system access control
- OpenAI Codex — containerized sandboxing for code execution
These are great when available, but they only protect within their own tool. bx wraps the entire process — so even if a tool's built-in sandbox is misconfigured, disabled, or absent, your files stay protected.
🔗 Alternatives
- Agent Safehouse — macOS kernel-level sandboxing for LLM coding agents via
sandbox-exec. Deny-first model that blocks write access outside the project directory.
📄 License
MIT — see LICENSE.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found