bx-mac 0.12.0 → 1.0.1
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 +16 -12
- package/dist/bx.js +74 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ For app modes, values before `--` define the sandbox scope (`workdir...`). Value
|
|
|
82
82
|
|
|
83
83
|
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`.
|
|
84
84
|
|
|
85
|
-
This behavior is configurable per app via `
|
|
85
|
+
This behavior is configurable per app via `passPaths` in `~/.bxconfig.toml` (default: `true`, built-in `xcode` default: `false`).
|
|
86
86
|
|
|
87
87
|
GUI app modes are activated in the foreground on launch (best effort), so the opened app should become the frontmost app.
|
|
88
88
|
|
|
@@ -159,44 +159,44 @@ path = "/usr/local/bin/code"
|
|
|
159
159
|
|
|
160
160
|
| Field | Description |
|
|
161
161
|
| --- | --- |
|
|
162
|
-
| `mode` | Inherit from another app (e.g. `"code"`, `"cursor"`) — only `
|
|
162
|
+
| `mode` | Inherit from another app (e.g. `"code"`, `"cursor"`) — only `paths` / overrides needed |
|
|
163
163
|
| `bundle` | macOS bundle identifier — used with `mdfind` to find the app automatically |
|
|
164
164
|
| `binary` | Relative path to the executable inside the `.app` bundle |
|
|
165
165
|
| `path` | Absolute path to the executable **or** `.app` bundle (highest priority, skips discovery) |
|
|
166
166
|
| `fallback` | Absolute fallback path if `mdfind` discovery fails |
|
|
167
167
|
| `args` | Extra arguments always passed to the app |
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
168
|
+
| `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) |
|
|
170
170
|
| `background` | Run the app detached in the background by default (`true`/`false`) |
|
|
171
171
|
|
|
172
172
|
**Resolution order:** `path` → `mdfind` by `bundle` + `binary` → `fallback`
|
|
173
173
|
|
|
174
|
-
`
|
|
174
|
+
`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.
|
|
175
175
|
|
|
176
|
-
**Workdir shortcuts with `mode`** let you create named entries that inherit everything from an existing app — just set `mode` and `
|
|
176
|
+
**Workdir shortcuts with `mode`** let you create named entries that inherit everything from an existing app — just set `mode` and `paths`:
|
|
177
177
|
|
|
178
178
|
```toml
|
|
179
179
|
# "bx myproject" opens VSCode with these directories
|
|
180
180
|
[myproject]
|
|
181
181
|
mode = "code"
|
|
182
|
-
|
|
182
|
+
paths = ["~/work/my-project", "~/work/shared-lib"]
|
|
183
183
|
|
|
184
184
|
# "bx ios" opens Xcode with this directory
|
|
185
185
|
[ios]
|
|
186
186
|
mode = "xcode"
|
|
187
|
-
|
|
187
|
+
paths = ["~/work/my-ios-app"]
|
|
188
188
|
```
|
|
189
189
|
|
|
190
190
|
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`).
|
|
191
191
|
|
|
192
|
-
**Preconfigured
|
|
192
|
+
**Preconfigured paths** also work directly on app definitions:
|
|
193
193
|
|
|
194
194
|
```toml
|
|
195
195
|
[code]
|
|
196
|
-
|
|
196
|
+
paths = ["~/work/my-project", "~/work/shared-lib"]
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
-
Running `bx code` (without arguments) will then open VSCode with both directories sandboxed. CLI arguments always override configured
|
|
199
|
+
Running `bx code` (without arguments) will then open VSCode with both directories sandboxed. CLI arguments always override configured paths.
|
|
200
200
|
|
|
201
201
|
When overriding a built-in app, only the specified fields are replaced — unset fields keep their defaults. See [`bxconfig.example.toml`](bxconfig.example.toml) for a complete reference.
|
|
202
202
|
|
|
@@ -339,7 +339,7 @@ Or preconfigure them in `~/.bxconfig.toml`:
|
|
|
339
339
|
|
|
340
340
|
```toml
|
|
341
341
|
[code]
|
|
342
|
-
|
|
342
|
+
paths = ["~/work/project-a", "~/work/project-b"]
|
|
343
343
|
```
|
|
344
344
|
|
|
345
345
|
For VSCode specifically, `--profile-sandbox` forces a separate Electron process via an isolated `--user-data-dir`, but this means separate extensions and settings.
|
|
@@ -372,6 +372,10 @@ Some AI coding tools ship with their own sandboxing. bx complements these by pro
|
|
|
372
372
|
|
|
373
373
|
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
374
|
|
|
375
|
+
## 🔗 Alternatives
|
|
376
|
+
|
|
377
|
+
- [Agent Safehouse](https://agent-safehouse.dev/) — macOS kernel-level sandboxing for LLM coding agents via `sandbox-exec`. Deny-first model that blocks write access outside the project directory.
|
|
378
|
+
|
|
375
379
|
## 📄 License
|
|
376
380
|
|
|
377
381
|
MIT — see [LICENSE](LICENSE).
|
package/dist/bx.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { accessSync, constants, cpSync, existsSync, globSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
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
4
|
import { execFileSync, spawn } from "node:child_process";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
6
|
import process$1 from "node:process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
8
|
//#region node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/error.js
|
|
9
9
|
/*!
|
|
10
10
|
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
@@ -797,7 +797,7 @@ const BUILTIN_APPS = {
|
|
|
797
797
|
bundle: "com.apple.dt.Xcode",
|
|
798
798
|
binary: "Contents/MacOS/Xcode",
|
|
799
799
|
fallback: "/Applications/Xcode.app/Contents/MacOS/Xcode",
|
|
800
|
-
|
|
800
|
+
passPaths: false
|
|
801
801
|
}
|
|
802
802
|
};
|
|
803
803
|
/** Shell-only built-in modes that are not app definitions */
|
|
@@ -806,6 +806,20 @@ const BUILTIN_MODES = [
|
|
|
806
806
|
"claude",
|
|
807
807
|
"exec"
|
|
808
808
|
];
|
|
809
|
+
function parsePassPaths(val) {
|
|
810
|
+
if (typeof val === "boolean") return val;
|
|
811
|
+
if (typeof val === "number" && Number.isInteger(val) && val > 0) return val;
|
|
812
|
+
if (Array.isArray(val)) {
|
|
813
|
+
const paths = val.filter((a) => typeof a === "string");
|
|
814
|
+
if (paths.length > 0) return paths;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function parseStringArray(val) {
|
|
818
|
+
if (Array.isArray(val)) {
|
|
819
|
+
const items = val.filter((a) => typeof a === "string");
|
|
820
|
+
if (items.length > 0) return items;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
809
823
|
function parseAppDef(def) {
|
|
810
824
|
return {
|
|
811
825
|
mode: typeof def.mode === "string" ? def.mode : void 0,
|
|
@@ -813,9 +827,9 @@ function parseAppDef(def) {
|
|
|
813
827
|
binary: typeof def.binary === "string" ? def.binary : void 0,
|
|
814
828
|
path: typeof def.path === "string" ? def.path : void 0,
|
|
815
829
|
fallback: typeof def.fallback === "string" ? def.fallback : void 0,
|
|
816
|
-
args:
|
|
817
|
-
|
|
818
|
-
|
|
830
|
+
args: parseStringArray(def.args),
|
|
831
|
+
passPaths: parsePassPaths(def.passPaths ?? def.passWorkPaths ?? def.passWorkdirs),
|
|
832
|
+
paths: parseStringArray(def.paths ?? def.workdirs),
|
|
819
833
|
background: typeof def.background === "boolean" ? def.background : void 0
|
|
820
834
|
};
|
|
821
835
|
}
|
|
@@ -836,8 +850,12 @@ function loadConfig(home) {
|
|
|
836
850
|
"path",
|
|
837
851
|
"fallback",
|
|
838
852
|
"args",
|
|
853
|
+
"passPaths",
|
|
854
|
+
"passWorkPaths",
|
|
839
855
|
"passWorkdirs",
|
|
840
|
-
"
|
|
856
|
+
"paths",
|
|
857
|
+
"workdirs",
|
|
858
|
+
"background"
|
|
841
859
|
]);
|
|
842
860
|
for (const [key, val] of Object.entries(doc)) {
|
|
843
861
|
if (key === "apps") continue;
|
|
@@ -1033,14 +1051,6 @@ const ACCESS_PREFIX_RE = /^(RW|RO):(.+)$/i;
|
|
|
1033
1051
|
function parseHomeConfig(home, workDirs) {
|
|
1034
1052
|
const allowed = new Set(workDirs);
|
|
1035
1053
|
const readOnly = /* @__PURE__ */ new Set();
|
|
1036
|
-
const bxallowPath = join(home, ".bxallow");
|
|
1037
|
-
if (existsSync(bxallowPath)) {
|
|
1038
|
-
console.error("sandbox: WARNING — ~/.bxallow is deprecated. Move entries to ~/.bxignore with RW: prefix.");
|
|
1039
|
-
for (const line of parseLines(bxallowPath)) {
|
|
1040
|
-
const absolute = resolve(home, line);
|
|
1041
|
-
if (existsSync(absolute) && statSync(absolute).isDirectory()) allowed.add(absolute);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
1054
|
for (const line of parseLines(join(home, ".bxignore"))) {
|
|
1045
1055
|
const match = line.match(ACCESS_PREFIX_RE);
|
|
1046
1056
|
if (!match) continue;
|
|
@@ -1209,17 +1219,22 @@ async function checkAppAlreadyRunning(mode, apps) {
|
|
|
1209
1219
|
if (BUILTIN_MODES.includes(mode)) return;
|
|
1210
1220
|
const app = apps[mode];
|
|
1211
1221
|
if (!app?.bundle) return;
|
|
1212
|
-
let
|
|
1222
|
+
let appName = mode;
|
|
1213
1223
|
try {
|
|
1214
|
-
|
|
1224
|
+
const list = execFileSync("lsappinfo", ["list"], {
|
|
1215
1225
|
encoding: "utf-8",
|
|
1216
1226
|
timeout: 3e3
|
|
1217
|
-
})
|
|
1227
|
+
});
|
|
1228
|
+
if (!list.includes(`bundleID="${app.bundle}"`)) return;
|
|
1229
|
+
const idx = list.indexOf(`bundleID="${app.bundle}"`);
|
|
1230
|
+
const bundleLine = list.lastIndexOf("\n", idx);
|
|
1231
|
+
const prevLine = list.lastIndexOf("\n", bundleLine - 1);
|
|
1232
|
+
const nameMatch = list.slice(prevLine === -1 ? 0 : prevLine, bundleLine).match(/"([^"]+)"/);
|
|
1233
|
+
if (nameMatch) appName = nameMatch[1];
|
|
1218
1234
|
} catch {
|
|
1219
1235
|
return;
|
|
1220
1236
|
}
|
|
1221
|
-
|
|
1222
|
-
console.error(`\n${fmt.warn(`"${mode}" is already running`)}`);
|
|
1237
|
+
console.error(`\n${fmt.warn(`"${appName}" is already running`)}`);
|
|
1223
1238
|
console.error(fmt.detail("the workspace will open in the EXISTING instance — sandbox will NOT apply"));
|
|
1224
1239
|
if (mode === "code") console.error(fmt.detail("quit the app first, or use --profile-sandbox for an isolated instance"));
|
|
1225
1240
|
else console.error(fmt.detail("quit the app first to ensure sandbox protection"));
|
|
@@ -1228,7 +1243,7 @@ async function checkAppAlreadyRunning(mode, apps) {
|
|
|
1228
1243
|
output: process$1.stderr
|
|
1229
1244
|
});
|
|
1230
1245
|
const answer = await new Promise((res) => {
|
|
1231
|
-
rl.question(` continue
|
|
1246
|
+
rl.question(` continue with existing instance? [y/N]`, res);
|
|
1232
1247
|
});
|
|
1233
1248
|
rl.close();
|
|
1234
1249
|
if (!answer.match(/^y(es)?$/i)) process$1.exit(0);
|
|
@@ -1303,9 +1318,11 @@ function parseArgs(validModes) {
|
|
|
1303
1318
|
function isBuiltinMode(mode) {
|
|
1304
1319
|
return BUILTIN_MODES.includes(mode);
|
|
1305
1320
|
}
|
|
1306
|
-
function
|
|
1307
|
-
|
|
1308
|
-
if (
|
|
1321
|
+
function getPassPaths(app, workDirs, home) {
|
|
1322
|
+
const val = app.passPaths;
|
|
1323
|
+
if (val === false) return [];
|
|
1324
|
+
if (typeof val === "number") return workDirs.slice(0, val);
|
|
1325
|
+
if (Array.isArray(val)) return val.map((p) => p.replace(/^~\//, home + "/"));
|
|
1309
1326
|
return workDirs;
|
|
1310
1327
|
}
|
|
1311
1328
|
function appBundleFromPath(path) {
|
|
@@ -1377,7 +1394,7 @@ function buildAppCommand(mode, workDirs, home, profileSandbox, appArgs, apps) {
|
|
|
1377
1394
|
}
|
|
1378
1395
|
if (app.args) args.push(...app.args);
|
|
1379
1396
|
if (appArgs.length > 0) args.push(...appArgs);
|
|
1380
|
-
args.push(...
|
|
1397
|
+
args.push(...getPassPaths(app, workDirs, home));
|
|
1381
1398
|
return {
|
|
1382
1399
|
bin,
|
|
1383
1400
|
args
|
|
@@ -1480,7 +1497,7 @@ Configuration:
|
|
|
1480
1497
|
binary = "..." relative path in .app bundle
|
|
1481
1498
|
path = "..." explicit executable path
|
|
1482
1499
|
args = ["..."] extra arguments
|
|
1483
|
-
|
|
1500
|
+
passPaths = true|false|N|[...] paths passed as launch args
|
|
1484
1501
|
background = true run in background by default
|
|
1485
1502
|
built-in apps (code, xcode) can be overridden
|
|
1486
1503
|
~/.bxignore sandbox rules (one per line):
|
|
@@ -1561,12 +1578,9 @@ function printDryRunTree({ home, blockedDirs, ignoredPaths, readOnlyDirs, workDi
|
|
|
1561
1578
|
console.log(`\n${RED}✖${RESET} = denied ${YELLOW}◉${RESET} = read-only ${GREEN}✔${RESET} = read-write\n`);
|
|
1562
1579
|
}
|
|
1563
1580
|
//#endregion
|
|
1564
|
-
//#region package.json
|
|
1565
|
-
var version = "0.12.0";
|
|
1566
|
-
//#endregion
|
|
1567
1581
|
//#region src/index.ts
|
|
1582
|
+
const VERSION = "1.0.1";
|
|
1568
1583
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1569
|
-
const VERSION = version;
|
|
1570
1584
|
if (process$1.argv.includes("--version") || process$1.argv.includes("-v")) {
|
|
1571
1585
|
console.log(`bx ${VERSION}`);
|
|
1572
1586
|
process$1.exit(0);
|
|
@@ -1581,26 +1595,24 @@ if (!process$1.env.HOME) {
|
|
|
1581
1595
|
}
|
|
1582
1596
|
const HOME = process$1.env.HOME;
|
|
1583
1597
|
async function main() {
|
|
1598
|
+
checkOwnSandbox();
|
|
1599
|
+
checkExternalSandbox();
|
|
1584
1600
|
const apps = getAvailableApps(loadConfig(HOME));
|
|
1585
1601
|
const { mode, workArgs, verbose, dry, profileSandbox, background: backgroundFlag, appArgs, implicit } = parseArgs(getValidModes(apps));
|
|
1586
1602
|
const app = apps[mode];
|
|
1587
|
-
const workDirs = (implicit && app?.
|
|
1588
|
-
if (implicit && !app?.
|
|
1603
|
+
const workDirs = (implicit && app?.paths?.length ? app.paths : workArgs).map((a) => realpathSync(resolve(a.replace(/^~\//, HOME + "/"))));
|
|
1604
|
+
if (implicit && !app?.paths?.length) {
|
|
1589
1605
|
if (workDirs.some((d) => d === HOME)) {
|
|
1590
1606
|
console.error(`\n${fmt.error("no working directory specified and current directory is $HOME")}\n`);
|
|
1591
1607
|
console.error(fmt.detail(`Usage: bx ${mode} <workdir>`));
|
|
1592
|
-
console.error(fmt.detail(`Config: set default
|
|
1608
|
+
console.error(fmt.detail(`Config: set default paths in ~/.bxconfig.toml:\n`));
|
|
1593
1609
|
console.error(fmt.detail(`[${mode}]`));
|
|
1594
|
-
console.error(fmt.detail(`
|
|
1610
|
+
console.error(fmt.detail(`paths = ["~/work/my-project"]\n`));
|
|
1595
1611
|
process$1.exit(1);
|
|
1596
1612
|
}
|
|
1597
1613
|
if (!dry) await confirmLaunch(workDirs[0], mode);
|
|
1598
1614
|
}
|
|
1599
|
-
if (!dry)
|
|
1600
|
-
checkOwnSandbox();
|
|
1601
|
-
checkVSCodeTerminal();
|
|
1602
|
-
checkExternalSandbox();
|
|
1603
|
-
}
|
|
1615
|
+
if (!dry) checkVSCodeTerminal();
|
|
1604
1616
|
checkWorkDirs(workDirs, HOME);
|
|
1605
1617
|
await checkAppAlreadyRunning(mode, apps);
|
|
1606
1618
|
if (mode === "code" && profileSandbox) setupVSCodeProfile(HOME);
|
|
@@ -1625,17 +1637,25 @@ async function main() {
|
|
|
1625
1637
|
});
|
|
1626
1638
|
process$1.exit(0);
|
|
1627
1639
|
}
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1640
|
+
const tmpDir = mkdtempSync(join("/tmp", "bx-"));
|
|
1641
|
+
const profilePath = join(tmpDir, "profile.sb");
|
|
1642
|
+
writeFileSync(profilePath, profile, { mode: 384 });
|
|
1630
1643
|
const cmd = buildCommand(mode, workDirs, HOME, profileSandbox, appArgs, apps);
|
|
1631
1644
|
const background = backgroundFlag || app?.background === true;
|
|
1632
1645
|
const nestedSandboxWarning = getNestedSandboxWarning(mode, apps);
|
|
1633
1646
|
if (nestedSandboxWarning) console.error(fmt.detail(nestedSandboxWarning));
|
|
1634
|
-
|
|
1647
|
+
printLaunchDetails(cmd, workDirs[0]);
|
|
1648
|
+
if (verbose) {
|
|
1649
|
+
const activationCmd = getActivationCommand(mode, apps);
|
|
1650
|
+
if (activationCmd) {
|
|
1651
|
+
const quote = (a) => JSON.stringify(a);
|
|
1652
|
+
console.error(fmt.detail(`focus: ${activationCmd.bin} ${activationCmd.args.map(quote).join(" ")}`));
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1635
1655
|
console.error("");
|
|
1636
1656
|
if (background) {
|
|
1637
|
-
const logPath = join(
|
|
1638
|
-
const logFd = openSync(logPath, "a");
|
|
1657
|
+
const logPath = join(tmpDir, "bx.log");
|
|
1658
|
+
const logFd = openSync(logPath, "a", 384);
|
|
1639
1659
|
const child = spawn("sandbox-exec", [
|
|
1640
1660
|
"-f",
|
|
1641
1661
|
profilePath,
|
|
@@ -1683,8 +1703,16 @@ async function main() {
|
|
|
1683
1703
|
}
|
|
1684
1704
|
});
|
|
1685
1705
|
bringAppToFront(mode, apps);
|
|
1706
|
+
const cleanup = () => {
|
|
1707
|
+
try {
|
|
1708
|
+
rmSync(tmpDir, {
|
|
1709
|
+
recursive: true,
|
|
1710
|
+
force: true
|
|
1711
|
+
});
|
|
1712
|
+
} catch {}
|
|
1713
|
+
};
|
|
1714
|
+
process$1.on("exit", cleanup);
|
|
1686
1715
|
child.on("close", (code) => {
|
|
1687
|
-
rmSync(profilePath, { force: true });
|
|
1688
1716
|
process$1.exit(code ?? 0);
|
|
1689
1717
|
});
|
|
1690
1718
|
}
|
|
@@ -1708,12 +1736,11 @@ function printPolicySummary(mode, workDirs, blockedDirs, ignoredPaths, readOnly)
|
|
|
1708
1736
|
if (extraIgnored > 0) parts.push(`${extraIgnored} from .bxignore`);
|
|
1709
1737
|
console.error(fmt.detail(parts.join(" · ")));
|
|
1710
1738
|
}
|
|
1711
|
-
function printLaunchDetails(cmd, cwd
|
|
1739
|
+
function printLaunchDetails(cmd, cwd) {
|
|
1712
1740
|
const quote = (a) => JSON.stringify(a);
|
|
1713
1741
|
console.error(fmt.detail(`bin: ${cmd.bin}`));
|
|
1714
1742
|
console.error(fmt.detail(`args: ${cmd.args.map(quote).join(" ") || "(none)"}`));
|
|
1715
1743
|
console.error(fmt.detail(`cwd: ${cwd}`));
|
|
1716
|
-
if (activationCmd) console.error(fmt.detail(`focus: ${activationCmd.bin} ${activationCmd.args.map(quote).join(" ")}`));
|
|
1717
1744
|
}
|
|
1718
1745
|
main();
|
|
1719
1746
|
//#endregion
|