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