bx-mac 1.0.2 → 1.1.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 +8 -3
  2. package/dist/bx.js +96 -19
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -122,6 +122,9 @@ bx --dry ~/work/my-project
122
122
 
123
123
  # 🔍 See the generated sandbox profile
124
124
  bx --verbose ~/work/my-project
125
+
126
+ # 🔄 Use an isolated app profile
127
+ bx --vscode-user code ~/work/my-project
125
128
  ```
126
129
 
127
130
  ## ⚙️ Options
@@ -131,7 +134,7 @@ bx --verbose ~/work/my-project
131
134
  | `--dry` | Show a tree of all protected, read-only, and accessible paths — don't launch anything |
132
135
  | `--verbose` | Print the generated sandbox profile plus launch details (binary, arguments, cwd, focus command) |
133
136
  | `--background` | Run the app detached in the background (like `nohup &`), output goes to `/tmp/bx-<pid>.log` |
134
- | `--profile-sandbox` | Use an isolated VSCode profile (separate extensions/settings, `code` mode only) |
137
+ | `--vscode-user [path]` | Use an isolated app profile (default: `~/.vscode-sandbox`, or specify a custom path) |
135
138
 
136
139
  On normal runs, bx also prints a short policy summary (number of workdirs, blocked directories, hidden paths, and read-only directories).
137
140
 
@@ -166,8 +169,9 @@ path = "/usr/local/bin/code"
166
169
  | `fallback` | Absolute fallback path if `mdfind` discovery fails |
167
170
  | `args` | Extra arguments always passed to the app |
168
171
  | `passPaths` | Paths passed as app launch args (`true`/`false`/`N`/`["~/p1", "~/p2"]`) |
169
- | `paths` | Default working directories when none are given on the CLI (supports `~/` paths) |
172
+ | `paths` | Default working directories when none are given on the CLI (supports `~/` paths and `*` globs) |
170
173
  | `background` | Run the app detached in the background by default (`true`/`false`) |
174
+ | `profile` | Use an isolated app profile (`true` = `~/.vscode-sandbox`, `"path"` = custom path) |
171
175
 
172
176
  **Resolution order:** `path` → `mdfind` by `bundle` + `binary` → `fallback`
173
177
 
@@ -342,7 +346,7 @@ Or preconfigure them in `~/.bxconfig.toml`:
342
346
  paths = ["~/work/project-a", "~/work/project-b"]
343
347
  ```
344
348
 
345
- For VSCode specifically, `--profile-sandbox` forces a separate Electron process via an isolated `--user-data-dir`, but this means separate extensions and settings.
349
+ For VSCode specifically, `--vscode-user` forces a separate Electron process via an isolated `--user-data-dir`, but this means separate extensions and settings. You can specify a custom path (`--vscode-user ~/my-profile`) or use the default (`--vscode-user` alone uses `~/.vscode-sandbox`). This can also be configured per app in `~/.bxconfig.toml` via the `profile` field.
346
350
 
347
351
  ## 💡 Tips
348
352
 
@@ -369,6 +373,7 @@ Some AI coding tools ship with their own sandboxing. bx complements these by pro
369
373
  - [Claude Code](https://code.claude.com/docs/en/sandboxing) — built-in sandbox for file and command restrictions
370
374
  - [Gemini CLI](https://geminicli.com/docs/cli/sandbox/) — sandbox mode for file system access control
371
375
  - [OpenAI Codex](https://developers.openai.com/codex/concepts/sandboxing) — containerized sandboxing for code execution
376
+ - [VS Code Copilot](https://code.visualstudio.com/docs/copilot/agents/agent-tools#_sandbox-agent-commands) — agent sandbox mode (preview) that restricts write access to the working directory and blocks network access for terminal commands (`chat.agent.sandbox` setting)
372
377
 
373
378
  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.
374
379
 
package/dist/bx.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { accessSync, constants, cpSync, existsSync, globSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
- import { execFileSync, spawn } from "node:child_process";
4
+ import { execFileSync, execSync, spawn } from "node:child_process";
5
5
  import { createInterface } from "node:readline";
6
6
  import process$1 from "node:process";
7
7
  import { fileURLToPath } from "node:url";
@@ -830,7 +830,8 @@ function parseAppDef(def) {
830
830
  args: parseStringArray(def.args),
831
831
  passPaths: parsePassPaths(def.passPaths ?? def.passWorkPaths ?? def.passWorkdirs),
832
832
  paths: parseStringArray(def.paths ?? def.workdirs),
833
- background: typeof def.background === "boolean" ? def.background : void 0
833
+ background: typeof def.background === "boolean" ? def.background : void 0,
834
+ profile: typeof def.profile === "boolean" || typeof def.profile === "string" ? def.profile : void 0
834
835
  };
835
836
  }
836
837
  /**
@@ -855,7 +856,8 @@ function loadConfig(home) {
855
856
  "passWorkdirs",
856
857
  "paths",
857
858
  "workdirs",
858
- "background"
859
+ "background",
860
+ "profile"
859
861
  ]);
860
862
  for (const [key, val] of Object.entries(doc)) {
861
863
  if (key === "apps") continue;
@@ -1236,7 +1238,7 @@ async function checkAppAlreadyRunning(mode, apps) {
1236
1238
  }
1237
1239
  console.error(`\n${fmt.warn(`"${appName}" is already running`)}`);
1238
1240
  console.error(fmt.detail("the workspace will open in the EXISTING instance — sandbox will NOT apply"));
1239
- if (mode === "code") console.error(fmt.detail("quit the app first, or use --profile-sandbox for an isolated instance"));
1241
+ if (mode === "code") console.error(fmt.detail("quit the app first, or use --vscode-user for an isolated instance"));
1240
1242
  else console.error(fmt.detail("quit the app first to ensure sandbox protection"));
1241
1243
  const rl = createInterface({
1242
1244
  input: process$1.stdin,
@@ -1280,12 +1282,12 @@ function parseArgs(validModes) {
1280
1282
  const rawArgs = process$1.argv.slice(2);
1281
1283
  const verbose = rawArgs.includes("--verbose");
1282
1284
  const dry = rawArgs.includes("--dry");
1283
- const profileSandbox = rawArgs.includes("--profile-sandbox");
1285
+ const vscodeUser = parseVscodeUserFlag(rawArgs);
1284
1286
  const background = rawArgs.includes("--background");
1285
- const positional = rawArgs.filter((a) => !a.startsWith("--"));
1287
+ const positional = collectPositional(rawArgs);
1286
1288
  const doubleDashIdx = rawArgs.indexOf("--");
1287
1289
  const appArgs = doubleDashIdx >= 0 ? rawArgs.slice(doubleDashIdx + 1) : [];
1288
- const beforeDash = doubleDashIdx >= 0 ? rawArgs.slice(0, doubleDashIdx).filter((a) => !a.startsWith("--")) : positional;
1290
+ const beforeDash = doubleDashIdx >= 0 ? collectPositional(rawArgs.slice(0, doubleDashIdx)) : positional;
1289
1291
  let mode = "code";
1290
1292
  let workArgs;
1291
1293
  let implicitWorkdirs = false;
@@ -1307,12 +1309,42 @@ function parseArgs(validModes) {
1307
1309
  workArgs,
1308
1310
  verbose,
1309
1311
  dry,
1310
- profileSandbox,
1312
+ vscodeUser,
1311
1313
  background,
1312
1314
  appArgs,
1313
1315
  implicit: implicitWorkdirs
1314
1316
  };
1315
1317
  }
1318
+ function looksLikePath(val) {
1319
+ return val.includes("/") || val.startsWith("~") || val.startsWith(".");
1320
+ }
1321
+ /** Filter positional args, skipping the path value after --vscode-user */
1322
+ function collectPositional(args) {
1323
+ const result = [];
1324
+ for (let i = 0; i < args.length; i++) {
1325
+ if (args[i] === "--vscode-user" || args[i] === "--vscode-user-data") {
1326
+ const next = args[i + 1];
1327
+ if (next && looksLikePath(next)) i++;
1328
+ continue;
1329
+ }
1330
+ if (args[i].startsWith("--")) continue;
1331
+ result.push(args[i]);
1332
+ }
1333
+ return result;
1334
+ }
1335
+ function parseVscodeUserFlag(args) {
1336
+ for (let i = 0; i < args.length; i++) {
1337
+ if (args[i] === "--vscode-user" || args[i] === "--vscode-user-data") {
1338
+ const next = args[i + 1];
1339
+ if (next && looksLikePath(next)) return next;
1340
+ return true;
1341
+ }
1342
+ if (args[i] === "--vscode-user=false" || args[i] === "--vscode-user-data=false") return false;
1343
+ const eqMatch = args[i].match(/^--vscode-user=(.+)$/);
1344
+ if (eqMatch) return eqMatch[1];
1345
+ }
1346
+ return false;
1347
+ }
1316
1348
  //#endregion
1317
1349
  //#region src/modes.ts
1318
1350
  function isBuiltinMode(mode) {
@@ -1342,15 +1374,30 @@ function hasAppSandboxEntitlement(entitlements) {
1342
1374
  if (new RegExp(`<key>\\s*${SANDBOX_KEY.replace(/\./g, "\\.")}\\s*</key>\\s*<false\\s*/>`, "i").test(entitlements)) return false;
1343
1375
  return new RegExp(`${SANDBOX_KEY.replace(/\./g, "\\.")}\\s*[=:]\\s*(1|true)`, "i").test(entitlements);
1344
1376
  }
1345
- function setupVSCodeProfile(home) {
1346
- const dataDir = join(home, ".vscode-sandbox");
1377
+ function resolveProfileDir(home, profile) {
1378
+ if (typeof profile === "string") return resolve(profile.replace(/^~\//, home + "/"));
1379
+ return join(home, ".vscode-sandbox");
1380
+ }
1381
+ async function setupVSCodeProfile(home, profile) {
1382
+ const dataDir = resolveProfileDir(home, profile);
1347
1383
  const globalExt = join(home, ".vscode", "extensions");
1348
1384
  const localExt = join(dataDir, "extensions");
1349
1385
  mkdirSync(dataDir, { recursive: true });
1350
1386
  if (!existsSync(localExt) && existsSync(globalExt)) {
1351
- console.error(fmt.detail("copying extensions from global install..."));
1352
- cpSync(globalExt, localExt, { recursive: true });
1387
+ const rl = createInterface({
1388
+ input: process$1.stdin,
1389
+ output: process$1.stderr
1390
+ });
1391
+ const answer = await new Promise((res) => {
1392
+ rl.question(`${fmt.info(`copy extensions to ${dataDir}?`)} [Y/n] `, res);
1393
+ });
1394
+ rl.close();
1395
+ if (!answer || answer.match(/^y(es)?$/i)) {
1396
+ console.error(fmt.detail("copying extensions from global install..."));
1397
+ cpSync(globalExt, localExt, { recursive: true });
1398
+ }
1353
1399
  }
1400
+ console.error(fmt.detail(`profile: ${dataDir}`));
1354
1401
  }
1355
1402
  function buildCommand(mode, workDirs, home, profileSandbox, appArgs, apps) {
1356
1403
  if (isBuiltinMode(mode)) return buildBuiltinCommand(mode, appArgs);
@@ -1387,8 +1434,8 @@ function buildAppCommand(mode, workDirs, home, profileSandbox, appArgs, apps) {
1387
1434
  }
1388
1435
  const bin = executableFromBundle(resolvedPath, app);
1389
1436
  const args = [];
1390
- if (mode === "code" && profileSandbox) {
1391
- const dataDir = join(home, ".vscode-sandbox");
1437
+ if (profileSandbox) {
1438
+ const dataDir = resolveProfileDir(home, profileSandbox);
1392
1439
  args.push("--user-data-dir", join(dataDir, "data"));
1393
1440
  args.push("--extensions-dir", join(dataDir, "extensions"));
1394
1441
  }
@@ -1454,6 +1501,26 @@ function bringAppToFront(mode, apps) {
1454
1501
  }, 250);
1455
1502
  }
1456
1503
  //#endregion
1504
+ //#region src/paths.ts
1505
+ /** Expand glob patterns (e.g. ~/work/*) in path lists. Only directories are matched. */
1506
+ function expandGlobs(paths, home) {
1507
+ const result = [];
1508
+ for (const p of paths) {
1509
+ const resolved = p.replace(/^~(\/|$)/, home + "/");
1510
+ if (!resolved.includes("*")) {
1511
+ result.push(resolved);
1512
+ continue;
1513
+ }
1514
+ const dir = dirname(resolved);
1515
+ const pattern = basename(resolved);
1516
+ const regex = new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
1517
+ try {
1518
+ for (const entry of readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory() && regex.test(entry.name)) result.push(join(dir, entry.name));
1519
+ } catch {}
1520
+ }
1521
+ return result;
1522
+ }
1523
+ //#endregion
1457
1524
  //#region src/help.ts
1458
1525
  function printHelp(version) {
1459
1526
  const HOME = process.env.HOME;
@@ -1485,9 +1552,10 @@ Options:
1485
1552
  --dry show what will be protected, don't launch
1486
1553
  --verbose print the generated sandbox profile
1487
1554
  --background run in background, log output to /tmp/bx-<pid>.log
1488
- --profile-sandbox use an isolated VSCode profile (code mode only)
1555
+ --vscode-user [path] use an isolated app profile (default: ~/.vscode-sandbox)
1489
1556
  -v, --version show version
1490
1557
  -h, --help show this help
1558
+ --docs open documentation in browser
1491
1559
 
1492
1560
  Configuration:
1493
1561
  ~/.bxconfig.toml app definitions (TOML):
@@ -1498,6 +1566,8 @@ Configuration:
1498
1566
  path = "..." explicit executable path
1499
1567
  args = ["..."] extra arguments
1500
1568
  passPaths = true|false|N|[...] paths passed as launch args
1569
+ profile = true|"path" use an isolated app profile
1570
+ paths = ["~/work/*"] default workdirs (globs supported)
1501
1571
  background = true run in background by default
1502
1572
  built-in apps (code, xcode) can be overridden
1503
1573
  ~/.bxignore sandbox rules (one per line):
@@ -1579,7 +1649,7 @@ function printDryRunTree({ home, blockedDirs, ignoredPaths, readOnlyDirs, workDi
1579
1649
  }
1580
1650
  //#endregion
1581
1651
  //#region src/index.ts
1582
- const VERSION = "1.0.2";
1652
+ const VERSION = "1.1.0";
1583
1653
  const __dirname = dirname(fileURLToPath(import.meta.url));
1584
1654
  if (process$1.argv.includes("--version") || process$1.argv.includes("-v")) {
1585
1655
  console.log(`bx ${VERSION}`);
@@ -1589,6 +1659,10 @@ if (process$1.argv.includes("--help") || process$1.argv.includes("-h")) {
1589
1659
  printHelp(VERSION);
1590
1660
  process$1.exit(0);
1591
1661
  }
1662
+ if (process$1.argv.includes("--docs")) {
1663
+ execSync("open https://github.com/holtwick/bx-mac");
1664
+ process$1.exit(0);
1665
+ }
1592
1666
  if (!process$1.env.HOME) {
1593
1667
  console.error(`\n${fmt.error("$HOME environment variable is not set")}\n`);
1594
1668
  process$1.exit(1);
@@ -1598,9 +1672,9 @@ async function main() {
1598
1672
  checkOwnSandbox();
1599
1673
  checkExternalSandbox();
1600
1674
  const apps = getAvailableApps(loadConfig(HOME));
1601
- const { mode, workArgs, verbose, dry, profileSandbox, background: backgroundFlag, appArgs, implicit } = parseArgs(getValidModes(apps));
1675
+ const { mode, workArgs, verbose, dry, vscodeUser: vscodeUserFlag, background: backgroundFlag, appArgs, implicit } = parseArgs(getValidModes(apps));
1602
1676
  const app = apps[mode];
1603
- const workDirs = (implicit && app?.paths?.length ? app.paths : workArgs).map((a) => realpathSync(resolve(a.replace(/^~\//, HOME + "/"))));
1677
+ const workDirs = expandGlobs(implicit && app?.paths?.length ? app.paths : workArgs, HOME).map((a) => realpathSync(resolve(a)));
1604
1678
  if (implicit && !app?.paths?.length) {
1605
1679
  if (workDirs.some((d) => d === HOME)) {
1606
1680
  console.error(`\n${fmt.error("no working directory specified and current directory is $HOME")}\n`);
@@ -1608,6 +1682,8 @@ async function main() {
1608
1682
  console.error(fmt.detail(`Config: set default paths in ~/.bxconfig.toml:\n`));
1609
1683
  console.error(fmt.detail(`[${mode}]`));
1610
1684
  console.error(fmt.detail(`paths = ["~/work/my-project"]\n`));
1685
+ console.error(fmt.detail(`Run bx --help for more info.`));
1686
+ console.error(fmt.detail(`Docs: https://github.com/holtwick/bx-mac\n`));
1611
1687
  process$1.exit(1);
1612
1688
  }
1613
1689
  if (!dry) await confirmLaunch(workDirs[0], mode);
@@ -1615,7 +1691,8 @@ async function main() {
1615
1691
  if (!dry) checkVSCodeTerminal();
1616
1692
  checkWorkDirs(workDirs, HOME);
1617
1693
  await checkAppAlreadyRunning(mode, apps);
1618
- if (mode === "code" && profileSandbox) setupVSCodeProfile(HOME);
1694
+ const profileSandbox = vscodeUserFlag !== false ? vscodeUserFlag : app?.profile ?? false;
1695
+ if (profileSandbox) await setupVSCodeProfile(HOME, profileSandbox);
1619
1696
  const { allowed, readOnly } = parseHomeConfig(HOME, workDirs);
1620
1697
  const blockedDirs = collectBlockedDirs(HOME, HOME, __dirname, new Set([...allowed, ...readOnly]));
1621
1698
  const ignoredPaths = collectIgnoredPaths(HOME, workDirs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bx-mac",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Sandbox any macOS app — only your project directory stays accessible",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,7 +48,7 @@
48
48
  ],
49
49
  "devDependencies": {
50
50
  "@types/node": "^25.5.0",
51
- "rolldown": "^1.0.0-rc.12",
51
+ "rolldown": "^1.0.0-rc.13",
52
52
  "smol-toml": "^1.6.1",
53
53
  "vitest": "^4.1.2"
54
54
  },