bx-mac 0.11.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.
- package/README.md +17 -8
- package/dist/bx.js +49 -9
- 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
|
|
225
|
+
Deny rules are applied **in addition** to the built-in protected lists:
|
|
220
226
|
|
|
221
|
-
> 🔒 `.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. **
|
|
292
|
-
6. **
|
|
293
|
-
7. **
|
|
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
|
/**
|
|
@@ -1265,6 +1266,7 @@ function parseArgs(validModes) {
|
|
|
1265
1266
|
const verbose = rawArgs.includes("--verbose");
|
|
1266
1267
|
const dry = rawArgs.includes("--dry");
|
|
1267
1268
|
const profileSandbox = rawArgs.includes("--profile-sandbox");
|
|
1269
|
+
const background = rawArgs.includes("--background");
|
|
1268
1270
|
const positional = rawArgs.filter((a) => !a.startsWith("--"));
|
|
1269
1271
|
const doubleDashIdx = rawArgs.indexOf("--");
|
|
1270
1272
|
const appArgs = doubleDashIdx >= 0 ? rawArgs.slice(doubleDashIdx + 1) : [];
|
|
@@ -1291,6 +1293,7 @@ function parseArgs(validModes) {
|
|
|
1291
1293
|
verbose,
|
|
1292
1294
|
dry,
|
|
1293
1295
|
profileSandbox,
|
|
1296
|
+
background,
|
|
1294
1297
|
appArgs,
|
|
1295
1298
|
implicit: implicitWorkdirs
|
|
1296
1299
|
};
|
|
@@ -1300,8 +1303,10 @@ function parseArgs(validModes) {
|
|
|
1300
1303
|
function isBuiltinMode(mode) {
|
|
1301
1304
|
return BUILTIN_MODES.includes(mode);
|
|
1302
1305
|
}
|
|
1303
|
-
function
|
|
1304
|
-
|
|
1306
|
+
function getWorkdirsToPass(app, workDirs) {
|
|
1307
|
+
if (app.passWorkdirs === false) return [];
|
|
1308
|
+
if (app.passWorkdirs === "first") return workDirs.slice(0, 1);
|
|
1309
|
+
return workDirs;
|
|
1305
1310
|
}
|
|
1306
1311
|
function appBundleFromPath(path) {
|
|
1307
1312
|
if (path.endsWith(".app")) return path;
|
|
@@ -1372,7 +1377,7 @@ function buildAppCommand(mode, workDirs, home, profileSandbox, appArgs, apps) {
|
|
|
1372
1377
|
}
|
|
1373
1378
|
if (app.args) args.push(...app.args);
|
|
1374
1379
|
if (appArgs.length > 0) args.push(...appArgs);
|
|
1375
|
-
|
|
1380
|
+
args.push(...getWorkdirsToPass(app, workDirs));
|
|
1376
1381
|
return {
|
|
1377
1382
|
bin,
|
|
1378
1383
|
args
|
|
@@ -1462,6 +1467,7 @@ const OPTIONS_TEXT = `
|
|
|
1462
1467
|
Options:
|
|
1463
1468
|
--dry show what will be protected, don't launch
|
|
1464
1469
|
--verbose print the generated sandbox profile
|
|
1470
|
+
--background run in background, log output to /tmp/bx-<pid>.log
|
|
1465
1471
|
--profile-sandbox use an isolated VSCode profile (code mode only)
|
|
1466
1472
|
-v, --version show version
|
|
1467
1473
|
-h, --help show this help
|
|
@@ -1474,7 +1480,8 @@ Configuration:
|
|
|
1474
1480
|
binary = "..." relative path in .app bundle
|
|
1475
1481
|
path = "..." explicit executable path
|
|
1476
1482
|
args = ["..."] extra arguments
|
|
1477
|
-
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
|
|
1478
1485
|
built-in apps (code, xcode) can be overridden
|
|
1479
1486
|
~/.bxignore sandbox rules (one per line):
|
|
1480
1487
|
path block access (deny)
|
|
@@ -1555,7 +1562,7 @@ function printDryRunTree({ home, blockedDirs, ignoredPaths, readOnlyDirs, workDi
|
|
|
1555
1562
|
}
|
|
1556
1563
|
//#endregion
|
|
1557
1564
|
//#region package.json
|
|
1558
|
-
var version = "0.
|
|
1565
|
+
var version = "0.12.0";
|
|
1559
1566
|
//#endregion
|
|
1560
1567
|
//#region src/index.ts
|
|
1561
1568
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1575,7 +1582,7 @@ if (!process$1.env.HOME) {
|
|
|
1575
1582
|
const HOME = process$1.env.HOME;
|
|
1576
1583
|
async function main() {
|
|
1577
1584
|
const apps = getAvailableApps(loadConfig(HOME));
|
|
1578
|
-
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));
|
|
1579
1586
|
const app = apps[mode];
|
|
1580
1587
|
const workDirs = (implicit && app?.workdirs?.length ? app.workdirs : workArgs).map((a) => resolve(a.replace(/^~\//, HOME + "/")));
|
|
1581
1588
|
if (implicit && !app?.workdirs?.length) {
|
|
@@ -1621,10 +1628,43 @@ async function main() {
|
|
|
1621
1628
|
const profilePath = join("/tmp", `bx-${process$1.pid}.sb`);
|
|
1622
1629
|
writeFileSync(profilePath, profile);
|
|
1623
1630
|
const cmd = buildCommand(mode, workDirs, HOME, profileSandbox, appArgs, apps);
|
|
1631
|
+
const background = backgroundFlag || app?.background === true;
|
|
1624
1632
|
const nestedSandboxWarning = getNestedSandboxWarning(mode, apps);
|
|
1625
1633
|
if (nestedSandboxWarning) console.error(fmt.detail(nestedSandboxWarning));
|
|
1626
1634
|
if (verbose) printLaunchDetails(cmd, workDirs[0], getActivationCommand(mode, apps));
|
|
1627
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
|
+
}
|
|
1628
1668
|
const child = spawn("sandbox-exec", [
|
|
1629
1669
|
"-f",
|
|
1630
1670
|
profilePath,
|