bx-mac 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +17 -8
  2. package/dist/bx.js +52 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,7 +17,7 @@ AI-powered coding tools like Claude Code, Copilot, or Cline run with **broad fil
17
17
  bx ~/work/my-project
18
18
  ```
19
19
 
20
- That's it. 🎉 VSCode opens with full access to `~/work/my-project` and nothing else.
20
+ That's it. 🎉 VSCode opens with full access to `~/work/my-project` and nothing else. Read [the blog post](https://holtwick.de/blog/bx-sandbox) for more background on the motivation behind bx.
21
21
 
22
22
  Need multiple directories? No problem:
23
23
 
@@ -30,6 +30,7 @@ bx ~/work/my-project ~/work/shared-lib
30
30
  - 🔒 Blocks `~/Documents`, `~/Desktop`, `~/Downloads`, and all other personal folders
31
31
  - 🚧 Blocks sibling projects — only the directory you specify is accessible
32
32
  - 🛡️ Protects sensitive dotdirs like `~/.ssh`, `~/.gnupg`, `~/.docker`, `~/.cargo`
33
+ - 🏛️ 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
33
34
  - ⚙️ Keeps VSCode, extensions, shell, Node.js, and other tooling fully functional
34
35
  - 🔍 Generates sandbox rules dynamically based on your actual `$HOME` contents
35
36
  - 📝 Supports `.bxignore` files (searched recursively) to hide secrets like `.env` files within a project
@@ -113,6 +114,9 @@ bx zed ~/work/my-project
113
114
  # ⚡ Run a script in a sandbox
114
115
  bx exec ~/work/my-project -- python train.py
115
116
 
117
+ # 🔀 Run in the background (terminal stays free)
118
+ bx --background code ~/work/my-project
119
+
116
120
  # 🔍 Preview what will be protected (no launch)
117
121
  bx --dry ~/work/my-project
118
122
 
@@ -126,6 +130,7 @@ bx --verbose ~/work/my-project
126
130
  | --- | --- |
127
131
  | `--dry` | Show a tree of all protected, read-only, and accessible paths — don't launch anything |
128
132
  | `--verbose` | Print the generated sandbox profile plus launch details (binary, arguments, cwd, focus command) |
133
+ | `--background` | Run the app detached in the background (like `nohup &`), output goes to `/tmp/bx-<pid>.log` |
129
134
  | `--profile-sandbox` | Use an isolated VSCode profile (separate extensions/settings, `code` mode only) |
130
135
 
131
136
  On normal runs, bx also prints a short policy summary (number of workdirs, blocked directories, hidden paths, and read-only directories).
@@ -160,12 +165,13 @@ path = "/usr/local/bin/code"
160
165
  | `path` | Absolute path to the executable **or** `.app` bundle (highest priority, skips discovery) |
161
166
  | `fallback` | Absolute fallback path if `mdfind` discovery fails |
162
167
  | `args` | Extra arguments always passed to the app |
163
- | `passWorkdirs` | Whether `workdir...` is forwarded as app launch args (`true`/`false`) |
168
+ | `passWorkdirs` | Whether `workdir...` is forwarded as app launch args (`true`/`false`/`"first"`) |
164
169
  | `workdirs` | Default working directories when none are given on the CLI (supports `~/` paths) |
170
+ | `background` | Run the app detached in the background by default (`true`/`false`) |
165
171
 
166
172
  **Resolution order:** `path` → `mdfind` by `bundle` + `binary` → `fallback`
167
173
 
168
- `passWorkdirs` controls launch argument behavior and is independent of sandbox scope. Even with `passWorkdirs = false`, the provided `workdir...` still defines what the sandbox can access.
174
+ `passWorkdirs` controls launch argument behavior and is independent of sandbox scope. Even with `passWorkdirs = false`, the provided `workdir...` still defines what the sandbox can access. Use `passWorkdirs = "first"` to pass only the first workdir as a launch argument — useful when the app should open one directory but the sandbox should grant access to multiple.
169
175
 
170
176
  **Workdir shortcuts with `mode`** let you create named entries that inherit everything from an existing app — just set `mode` and `workdirs`:
171
177
 
@@ -216,9 +222,11 @@ ro:reference/docs
216
222
  ro:shared/toolchain
217
223
  ```
218
224
 
219
- Deny rules are applied **in addition** to the built-in protected list:
225
+ Deny rules are applied **in addition** to the built-in protected lists:
220
226
 
221
- > 🔒 `.Trash` `.ssh` `.gnupg` `.docker` `.zsh_sessions` `.cargo` `.gradle` `.gem`
227
+ > 🔒 **Dotdirs:** `.ssh` `.gnupg` `.docker` `.zsh_sessions` `.cargo` `.gradle` `.gem`
228
+ >
229
+ > 🏛️ **Library (opinionated):** `Accounts` `Calendars` `Contacts` `Cookies` `Finance` `Mail` `Messages` `Mobile Documents` `Photos` `Safari` and [others (see full list)](src/profile.ts) — plus containers of password managers & finance apps
222
230
 
223
231
  ### `<project>/.bxignore`
224
232
 
@@ -288,9 +296,10 @@ bx generates a macOS sandbox profile at launch time:
288
296
  2. **Block** each one individually with `(deny file* (subpath ...))`
289
297
  3. **Skip** all working directories, `~/Library`, dotfiles, and `rw:`/`ro:` paths from `~/.bxignore`
290
298
  4. **Descend** into parent directories of allowed paths to block only siblings (because SBPL deny rules always override allow rules)
291
- 5. **Append** deny rules for protected dotdirs, plain entries in `~/.bxignore`, and `.bxignore` files found recursively in each working directory
292
- 6. **Apply** `(deny file-write*)` rules for `ro:` directories (read allowed, write blocked)
293
- 7. **Write** the profile to `/tmp`, launch the app via `sandbox-exec`, clean up on exit
299
+ 5. **Protect** an opinionated set of `~/Library` subdirectories (Mail, Messages, Photos, Safari, Contacts, Calendars, …) and app containers matching known password managers and finance apps (1Password, Bitwarden, MoneyMoney, …)
300
+ 6. **Append** deny rules for protected dotdirs, plain entries in `~/.bxignore`, and `.bxignore` files found recursively in each working directory
301
+ 7. **Apply** `(deny file-write*)` rules for `ro:` directories (read allowed, write blocked)
302
+ 8. **Write** the profile to `/tmp`, launch the app via `sandbox-exec`, clean up on exit
294
303
 
295
304
  ### Why not a simple deny-all + allow?
296
305
 
package/dist/bx.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { accessSync, constants, cpSync, existsSync, globSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { accessSync, constants, cpSync, existsSync, globSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { execFileSync, spawn } from "node:child_process";
5
5
  import { createInterface } from "node:readline";
@@ -814,8 +814,9 @@ function parseAppDef(def) {
814
814
  path: typeof def.path === "string" ? def.path : void 0,
815
815
  fallback: typeof def.fallback === "string" ? def.fallback : void 0,
816
816
  args: Array.isArray(def.args) ? def.args.filter((a) => typeof a === "string") : void 0,
817
- passWorkdirs: typeof def.passWorkdirs === "boolean" ? def.passWorkdirs : void 0,
818
- workdirs: Array.isArray(def.workdirs) ? def.workdirs.filter((a) => typeof a === "string") : void 0
817
+ passWorkdirs: typeof def.passWorkdirs === "boolean" || def.passWorkdirs === "first" ? def.passWorkdirs : void 0,
818
+ workdirs: Array.isArray(def.workdirs) ? def.workdirs.filter((a) => typeof a === "string") : void 0,
819
+ background: typeof def.background === "boolean" ? def.background : void 0
819
820
  };
820
821
  }
821
822
  /**
@@ -929,7 +930,6 @@ function resolveAppPath(app) {
929
930
  //#endregion
930
931
  //#region src/profile.ts
931
932
  const PROTECTED_DOTDIRS = [
932
- ".Trash",
933
933
  ".ssh",
934
934
  ".gnupg",
935
935
  ".docker",
@@ -1266,6 +1266,7 @@ function parseArgs(validModes) {
1266
1266
  const verbose = rawArgs.includes("--verbose");
1267
1267
  const dry = rawArgs.includes("--dry");
1268
1268
  const profileSandbox = rawArgs.includes("--profile-sandbox");
1269
+ const background = rawArgs.includes("--background");
1269
1270
  const positional = rawArgs.filter((a) => !a.startsWith("--"));
1270
1271
  const doubleDashIdx = rawArgs.indexOf("--");
1271
1272
  const appArgs = doubleDashIdx >= 0 ? rawArgs.slice(doubleDashIdx + 1) : [];
@@ -1292,6 +1293,7 @@ function parseArgs(validModes) {
1292
1293
  verbose,
1293
1294
  dry,
1294
1295
  profileSandbox,
1296
+ background,
1295
1297
  appArgs,
1296
1298
  implicit: implicitWorkdirs
1297
1299
  };
@@ -1301,8 +1303,10 @@ function parseArgs(validModes) {
1301
1303
  function isBuiltinMode(mode) {
1302
1304
  return BUILTIN_MODES.includes(mode);
1303
1305
  }
1304
- function shouldPassWorkdirs(app) {
1305
- return app.passWorkdirs !== false;
1306
+ function getWorkdirsToPass(app, workDirs) {
1307
+ if (app.passWorkdirs === false) return [];
1308
+ if (app.passWorkdirs === "first") return workDirs.slice(0, 1);
1309
+ return workDirs;
1306
1310
  }
1307
1311
  function appBundleFromPath(path) {
1308
1312
  if (path.endsWith(".app")) return path;
@@ -1373,7 +1377,7 @@ function buildAppCommand(mode, workDirs, home, profileSandbox, appArgs, apps) {
1373
1377
  }
1374
1378
  if (app.args) args.push(...app.args);
1375
1379
  if (appArgs.length > 0) args.push(...appArgs);
1376
- if (shouldPassWorkdirs(app)) args.push(...workDirs);
1380
+ args.push(...getWorkdirsToPass(app, workDirs));
1377
1381
  return {
1378
1382
  bin,
1379
1383
  args
@@ -1463,6 +1467,7 @@ const OPTIONS_TEXT = `
1463
1467
  Options:
1464
1468
  --dry show what will be protected, don't launch
1465
1469
  --verbose print the generated sandbox profile
1470
+ --background run in background, log output to /tmp/bx-<pid>.log
1466
1471
  --profile-sandbox use an isolated VSCode profile (code mode only)
1467
1472
  -v, --version show version
1468
1473
  -h, --help show this help
@@ -1475,7 +1480,8 @@ Configuration:
1475
1480
  binary = "..." relative path in .app bundle
1476
1481
  path = "..." explicit executable path
1477
1482
  args = ["..."] extra arguments
1478
- passWorkdirs = true|false pass workdirs as launch args
1483
+ passWorkdirs = true|false|"first" pass workdirs as launch args
1484
+ background = true run in background by default
1479
1485
  built-in apps (code, xcode) can be overridden
1480
1486
  ~/.bxignore sandbox rules (one per line):
1481
1487
  path block access (deny)
@@ -1555,9 +1561,12 @@ function printDryRunTree({ home, blockedDirs, ignoredPaths, readOnlyDirs, workDi
1555
1561
  console.log(`\n${RED}✖${RESET} = denied ${YELLOW}◉${RESET} = read-only ${GREEN}✔${RESET} = read-write\n`);
1556
1562
  }
1557
1563
  //#endregion
1564
+ //#region package.json
1565
+ var version = "0.12.0";
1566
+ //#endregion
1558
1567
  //#region src/index.ts
1559
1568
  const __dirname = dirname(fileURLToPath(import.meta.url));
1560
- const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "dev";
1569
+ const VERSION = version;
1561
1570
  if (process$1.argv.includes("--version") || process$1.argv.includes("-v")) {
1562
1571
  console.log(`bx ${VERSION}`);
1563
1572
  process$1.exit(0);
@@ -1573,7 +1582,7 @@ if (!process$1.env.HOME) {
1573
1582
  const HOME = process$1.env.HOME;
1574
1583
  async function main() {
1575
1584
  const apps = getAvailableApps(loadConfig(HOME));
1576
- const { mode, workArgs, verbose, dry, profileSandbox, appArgs, implicit } = parseArgs(getValidModes(apps));
1585
+ const { mode, workArgs, verbose, dry, profileSandbox, background: backgroundFlag, appArgs, implicit } = parseArgs(getValidModes(apps));
1577
1586
  const app = apps[mode];
1578
1587
  const workDirs = (implicit && app?.workdirs?.length ? app.workdirs : workArgs).map((a) => resolve(a.replace(/^~\//, HOME + "/")));
1579
1588
  if (implicit && !app?.workdirs?.length) {
@@ -1619,10 +1628,43 @@ async function main() {
1619
1628
  const profilePath = join("/tmp", `bx-${process$1.pid}.sb`);
1620
1629
  writeFileSync(profilePath, profile);
1621
1630
  const cmd = buildCommand(mode, workDirs, HOME, profileSandbox, appArgs, apps);
1631
+ const background = backgroundFlag || app?.background === true;
1622
1632
  const nestedSandboxWarning = getNestedSandboxWarning(mode, apps);
1623
1633
  if (nestedSandboxWarning) console.error(fmt.detail(nestedSandboxWarning));
1624
1634
  if (verbose) printLaunchDetails(cmd, workDirs[0], getActivationCommand(mode, apps));
1625
1635
  console.error("");
1636
+ if (background) {
1637
+ const logPath = join("/tmp", `bx-${process$1.pid}.log`);
1638
+ const logFd = openSync(logPath, "a");
1639
+ const child = spawn("sandbox-exec", [
1640
+ "-f",
1641
+ profilePath,
1642
+ "-D",
1643
+ `HOME=${HOME}`,
1644
+ "-D",
1645
+ `WORK=${workDirs[0]}`,
1646
+ cmd.bin,
1647
+ ...cmd.args
1648
+ ], {
1649
+ cwd: workDirs[0],
1650
+ stdio: [
1651
+ "ignore",
1652
+ logFd,
1653
+ logFd
1654
+ ],
1655
+ detached: true,
1656
+ env: {
1657
+ ...process$1.env,
1658
+ CODEBOX_SANDBOX: "1"
1659
+ }
1660
+ });
1661
+ child.unref();
1662
+ bringAppToFront(mode, apps);
1663
+ console.error(fmt.info(`running in background (pid ${child.pid})`));
1664
+ console.error(fmt.detail(`log: ${logPath}`));
1665
+ console.error(fmt.detail(`sandbox profile: ${profilePath} (kept until process exits)`));
1666
+ process$1.exit(0);
1667
+ }
1626
1668
  const child = spawn("sandbox-exec", [
1627
1669
  "-f",
1628
1670
  profilePath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bx-mac",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Sandbox any macOS app — only your project directory stays accessible",
5
5
  "type": "module",
6
6
  "bin": {