bx-mac 0.12.0 → 1.0.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 +16 -12
- package/dist/bx.js +59 -32
- 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,5 +1,5 @@
|
|
|
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";
|
|
@@ -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;
|
|
@@ -1303,9 +1313,11 @@ function parseArgs(validModes) {
|
|
|
1303
1313
|
function isBuiltinMode(mode) {
|
|
1304
1314
|
return BUILTIN_MODES.includes(mode);
|
|
1305
1315
|
}
|
|
1306
|
-
function
|
|
1307
|
-
|
|
1308
|
-
if (
|
|
1316
|
+
function getPassPaths(app, workDirs, home) {
|
|
1317
|
+
const val = app.passPaths;
|
|
1318
|
+
if (val === false) return [];
|
|
1319
|
+
if (typeof val === "number") return workDirs.slice(0, val);
|
|
1320
|
+
if (Array.isArray(val)) return val.map((p) => p.replace(/^~\//, home + "/"));
|
|
1309
1321
|
return workDirs;
|
|
1310
1322
|
}
|
|
1311
1323
|
function appBundleFromPath(path) {
|
|
@@ -1377,7 +1389,7 @@ function buildAppCommand(mode, workDirs, home, profileSandbox, appArgs, apps) {
|
|
|
1377
1389
|
}
|
|
1378
1390
|
if (app.args) args.push(...app.args);
|
|
1379
1391
|
if (appArgs.length > 0) args.push(...appArgs);
|
|
1380
|
-
args.push(...
|
|
1392
|
+
args.push(...getPassPaths(app, workDirs, home));
|
|
1381
1393
|
return {
|
|
1382
1394
|
bin,
|
|
1383
1395
|
args
|
|
@@ -1480,7 +1492,7 @@ Configuration:
|
|
|
1480
1492
|
binary = "..." relative path in .app bundle
|
|
1481
1493
|
path = "..." explicit executable path
|
|
1482
1494
|
args = ["..."] extra arguments
|
|
1483
|
-
|
|
1495
|
+
passPaths = true|false|N|[...] paths passed as launch args
|
|
1484
1496
|
background = true run in background by default
|
|
1485
1497
|
built-in apps (code, xcode) can be overridden
|
|
1486
1498
|
~/.bxignore sandbox rules (one per line):
|
|
@@ -1562,7 +1574,7 @@ function printDryRunTree({ home, blockedDirs, ignoredPaths, readOnlyDirs, workDi
|
|
|
1562
1574
|
}
|
|
1563
1575
|
//#endregion
|
|
1564
1576
|
//#region package.json
|
|
1565
|
-
var version = "0.
|
|
1577
|
+
var version = "1.0.0";
|
|
1566
1578
|
//#endregion
|
|
1567
1579
|
//#region src/index.ts
|
|
1568
1580
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1584,14 +1596,14 @@ async function main() {
|
|
|
1584
1596
|
const apps = getAvailableApps(loadConfig(HOME));
|
|
1585
1597
|
const { mode, workArgs, verbose, dry, profileSandbox, background: backgroundFlag, appArgs, implicit } = parseArgs(getValidModes(apps));
|
|
1586
1598
|
const app = apps[mode];
|
|
1587
|
-
const workDirs = (implicit && app?.
|
|
1588
|
-
if (implicit && !app?.
|
|
1599
|
+
const workDirs = (implicit && app?.paths?.length ? app.paths : workArgs).map((a) => realpathSync(resolve(a.replace(/^~\//, HOME + "/"))));
|
|
1600
|
+
if (implicit && !app?.paths?.length) {
|
|
1589
1601
|
if (workDirs.some((d) => d === HOME)) {
|
|
1590
1602
|
console.error(`\n${fmt.error("no working directory specified and current directory is $HOME")}\n`);
|
|
1591
1603
|
console.error(fmt.detail(`Usage: bx ${mode} <workdir>`));
|
|
1592
|
-
console.error(fmt.detail(`Config: set default
|
|
1604
|
+
console.error(fmt.detail(`Config: set default paths in ~/.bxconfig.toml:\n`));
|
|
1593
1605
|
console.error(fmt.detail(`[${mode}]`));
|
|
1594
|
-
console.error(fmt.detail(`
|
|
1606
|
+
console.error(fmt.detail(`paths = ["~/work/my-project"]\n`));
|
|
1595
1607
|
process$1.exit(1);
|
|
1596
1608
|
}
|
|
1597
1609
|
if (!dry) await confirmLaunch(workDirs[0], mode);
|
|
@@ -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
|