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.
- package/README.md +8 -3
- package/dist/bx.js +96 -19
- 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
|
-
| `--
|
|
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, `--
|
|
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 --
|
|
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
|
|
1285
|
+
const vscodeUser = parseVscodeUserFlag(rawArgs);
|
|
1284
1286
|
const background = rawArgs.includes("--background");
|
|
1285
|
-
const positional = rawArgs
|
|
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)
|
|
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
|
-
|
|
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
|
|
1346
|
-
|
|
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
|
-
|
|
1352
|
-
|
|
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 (
|
|
1391
|
-
const dataDir =
|
|
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
|
-
--
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
51
|
+
"rolldown": "^1.0.0-rc.13",
|
|
52
52
|
"smol-toml": "^1.6.1",
|
|
53
53
|
"vitest": "^4.1.2"
|
|
54
54
|
},
|