bx-mac 1.2.0 → 1.3.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 +19 -3
- package/dist/bx.js +42 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/holtwick/bx-mac/blob/master/LICENSE)
|
|
6
6
|
[](https://github.com/holtwick/bx-mac)
|
|
7
7
|
|
|
8
|
-
> **Put your AI in a box.** Launch VSCode, Claude Code, a terminal, or any command in a macOS sandbox — your tools can only see the project you're working on.
|
|
8
|
+
> **Put your AI in a box.** Launch VSCode, Claude Code, a terminal, or any command in a macOS sandbox — your tools can only see the project you're working on. Not a vault, but a reasonable safety net.
|
|
9
9
|
|
|
10
10
|
## 🤔 Why?
|
|
11
11
|
|
|
@@ -45,7 +45,23 @@ bx ~/work/my-project ~/work/shared-lib
|
|
|
45
45
|
- **macOS only** — relies on `sandbox-exec` (Apple-specific)
|
|
46
46
|
- **Not dynamic** — the sandbox profile is a snapshot of `$HOME` at launch time; directories or files created later are **not** automatically blocked
|
|
47
47
|
- **File names visible** — blocked files cannot be read or written, but their names still appear in directory listings (a kernel-level `readdir` constraint, same as `chmod 000`)
|
|
48
|
-
- **Not a vault** —
|
|
48
|
+
- **Not a vault** — this is a safety net, not airtight isolation (see [Security model](#-security-model-allow-first))
|
|
49
|
+
|
|
50
|
+
## 🧱 Security model: allow-first
|
|
51
|
+
|
|
52
|
+
bx uses an **allow-first / blocklist** approach: everything is accessible by default, and only sensitive paths are explicitly blocked. This is the opposite of a deny-first / allowlist model where everything is blocked and only specific paths are opened up.
|
|
53
|
+
|
|
54
|
+
**Why allow-first?** Developer tools require access to an enormous and ever-changing set of paths -- dotfiles, `~/Library`, runtimes, caches, toolchains. A deny-first model would require new allow rules for every tool or framework update, breaking silently when a path is missing. The allow-first model works out of the box without per-tool tuning.
|
|
55
|
+
|
|
56
|
+
**What this means in practice:**
|
|
57
|
+
|
|
58
|
+
- bx provides **reasonable protection** against accidental or misguided file access -- not airtight isolation
|
|
59
|
+
- Sensitive paths (credentials, personal data, other projects) are explicitly blocked
|
|
60
|
+
- Paths that are not on the blocklist remain accessible -- including parts of `~/Library` and most dotfiles
|
|
61
|
+
- The sandbox profile is a **snapshot at launch time** -- files created later are not protected
|
|
62
|
+
- `sandbox-exec` itself is undocumented Apple API that could change with OS updates
|
|
63
|
+
|
|
64
|
+
If you need stricter, deny-first isolation, consider [Agent Safehouse](https://agent-safehouse.dev/) or a Docker/VM-based approach (see [Alternatives](#-alternatives)). bx is designed for the common case: keep AI tools and editors functional while blocking access to things they should never touch.
|
|
49
65
|
|
|
50
66
|
## 📥 Install
|
|
51
67
|
|
|
@@ -380,7 +396,7 @@ These are great when available, but they only protect within their own tool. bx
|
|
|
380
396
|
|
|
381
397
|
## 🔗 Alternatives
|
|
382
398
|
|
|
383
|
-
- [Agent Safehouse](https://agent-safehouse.dev/) — macOS kernel-level sandboxing for LLM coding agents via `sandbox-exec`.
|
|
399
|
+
- [Agent Safehouse](https://agent-safehouse.dev/) — macOS kernel-level sandboxing for LLM coding agents via `sandbox-exec`. Uses a **deny-first model**: everything is blocked by default and only explicitly listed paths are opened up. This gives you theoretically stricter control (e.g. `~/Library` is fully blocked and only specific subdirs are allowed), but requires more configuration — tools and runtimes that need paths you haven't whitelisted will break silently. If you need that level of precision and are willing to tune profiles per tool, Agent Safehouse may be the better fit. bx uses the opposite **allow-first model** (only sensitive paths are blocked), which works out of the box for VSCode, shells, Claude Code, and other tools without any per-tool configuration.
|
|
384
400
|
- **Docker / VMs** — for stronger isolation, run AI tools in a virtualized environment (containers, VMs). Full process and network isolation at the cost of setup overhead.
|
|
385
401
|
- **Web sandboxes** — browser-based approaches for running AI agents. See Simon Willison's [Living dangerously with Claude](https://simonwillison.net/2025/Oct/22/living-dangerously-with-claude/) for an overview.
|
|
386
402
|
|
package/dist/bx.js
CHANGED
|
@@ -953,13 +953,43 @@ function resolveAppPath(app) {
|
|
|
953
953
|
//#endregion
|
|
954
954
|
//#region src/profile.ts
|
|
955
955
|
const PROTECTED_DOTDIRS = [
|
|
956
|
+
".Trash",
|
|
956
957
|
".ssh",
|
|
957
958
|
".gnupg",
|
|
958
959
|
".docker",
|
|
959
960
|
".zsh_sessions",
|
|
960
961
|
".cargo",
|
|
961
962
|
".gradle",
|
|
962
|
-
".gem"
|
|
963
|
+
".gem",
|
|
964
|
+
".aws",
|
|
965
|
+
".azure",
|
|
966
|
+
".azd",
|
|
967
|
+
".kube",
|
|
968
|
+
".config/gcloud"
|
|
969
|
+
];
|
|
970
|
+
const PROTECTED_HOME_DOTFILES = [
|
|
971
|
+
".zsh_history",
|
|
972
|
+
".bash_history",
|
|
973
|
+
".sh_history",
|
|
974
|
+
".node_repl_history",
|
|
975
|
+
".python_history",
|
|
976
|
+
".netrc",
|
|
977
|
+
".git-credentials",
|
|
978
|
+
".npmrc",
|
|
979
|
+
".pypirc",
|
|
980
|
+
".extra"
|
|
981
|
+
];
|
|
982
|
+
const PROTECTED_HOME_DOTFILES_RO = [
|
|
983
|
+
".zshrc",
|
|
984
|
+
".zprofile",
|
|
985
|
+
".zshenv",
|
|
986
|
+
".zlogin",
|
|
987
|
+
".zlogout",
|
|
988
|
+
".bashrc",
|
|
989
|
+
".bash_profile",
|
|
990
|
+
".bash_login",
|
|
991
|
+
".profile",
|
|
992
|
+
".config/fish/config.fish"
|
|
963
993
|
];
|
|
964
994
|
const PROTECTED_LIBRARY_DIRS = [
|
|
965
995
|
"Accounts",
|
|
@@ -1113,9 +1143,13 @@ function collectProtectedContainers(home) {
|
|
|
1113
1143
|
}
|
|
1114
1144
|
return matched;
|
|
1115
1145
|
}
|
|
1146
|
+
function collectReadOnlyDotfiles(home) {
|
|
1147
|
+
return PROTECTED_HOME_DOTFILES_RO.map((f) => join(home, f));
|
|
1148
|
+
}
|
|
1116
1149
|
function collectIgnoredPaths(home, workDirs) {
|
|
1117
1150
|
const ignored = [
|
|
1118
1151
|
...PROTECTED_DOTDIRS.map((d) => join(home, d)),
|
|
1152
|
+
...PROTECTED_HOME_DOTFILES.map((f) => join(home, f)),
|
|
1119
1153
|
...PROTECTED_LIBRARY_DIRS.map((d) => join(home, "Library", d)),
|
|
1120
1154
|
...new Set(collectProtectedContainers(home))
|
|
1121
1155
|
];
|
|
@@ -1162,17 +1196,18 @@ function collectSystemDenyPaths(home) {
|
|
|
1162
1196
|
} catch {}
|
|
1163
1197
|
return paths;
|
|
1164
1198
|
}
|
|
1165
|
-
function generateProfile(workDirs, blockedDirs, ignoredPaths, readOnlyDirs = [], home = "") {
|
|
1199
|
+
function generateProfile(workDirs, blockedDirs, ignoredPaths, readOnlyDirs = [], home = "", readOnlyFiles = []) {
|
|
1166
1200
|
const blockedRules = sbplDenyBlock("Blocked paths (auto-generated from $HOME contents)", "file*", blockedDirs.map(sbplPathRule));
|
|
1167
1201
|
const ignoredRules = sbplDenyBlock("Hidden paths from .bxignore", "file*", ignoredPaths.map(sbplPathRule));
|
|
1168
1202
|
const readOnlyRules = sbplDenyBlock("Read-only directories", "file-write*", readOnlyDirs.map(sbplSubpath));
|
|
1203
|
+
const readOnlyFileRules = sbplDenyBlock("Read-only home dotfiles (shell init - write-protected against injection)", "file-write*", readOnlyFiles.map(sbplLiteral));
|
|
1169
1204
|
const systemRules = home ? sbplDenyBlock("System-level restrictions", "file*", collectSystemDenyPaths(home).map(sbplSubpath)) : "";
|
|
1170
1205
|
return `; Auto-generated sandbox profile
|
|
1171
1206
|
; Working directories: ${workDirs.join(", ")}
|
|
1172
1207
|
|
|
1173
1208
|
(version 1)
|
|
1174
1209
|
(allow default)
|
|
1175
|
-
${blockedRules}${ignoredRules}${readOnlyRules}${systemRules}
|
|
1210
|
+
${blockedRules}${ignoredRules}${readOnlyRules}${readOnlyFileRules}${systemRules}
|
|
1176
1211
|
`;
|
|
1177
1212
|
}
|
|
1178
1213
|
//#endregion
|
|
@@ -1664,7 +1699,7 @@ function printDryRunTree({ home, blockedDirs, ignoredPaths, readOnlyDirs, workDi
|
|
|
1664
1699
|
}
|
|
1665
1700
|
//#endregion
|
|
1666
1701
|
//#region src/index.ts
|
|
1667
|
-
const VERSION = "1.
|
|
1702
|
+
const VERSION = "1.3.0";
|
|
1668
1703
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1669
1704
|
if (process$1.argv.includes("--version") || process$1.argv.includes("-v")) {
|
|
1670
1705
|
console.log(`bx ${VERSION}`);
|
|
@@ -1715,8 +1750,9 @@ async function main() {
|
|
|
1715
1750
|
const { allowed, readOnly } = parseHomeConfig(HOME, workDirs);
|
|
1716
1751
|
const blockedDirs = collectBlockedDirs(HOME, HOME, __dirname, new Set([...allowed, ...readOnly]));
|
|
1717
1752
|
const ignoredPaths = collectIgnoredPaths(HOME, workDirs);
|
|
1753
|
+
const readOnlyDotfiles = collectReadOnlyDotfiles(HOME);
|
|
1718
1754
|
printPolicySummary(mode, workDirs, blockedDirs, ignoredPaths, readOnly);
|
|
1719
|
-
const profile = generateProfile(workDirs, blockedDirs, ignoredPaths, [...readOnly], HOME);
|
|
1755
|
+
const profile = generateProfile(workDirs, blockedDirs, ignoredPaths, [...readOnly], HOME, readOnlyDotfiles);
|
|
1720
1756
|
if (verbose) {
|
|
1721
1757
|
console.error("\n--- Generated sandbox profile ---");
|
|
1722
1758
|
console.error(profile);
|
|
@@ -1837,7 +1873,7 @@ function printPolicySummary(mode, workDirs, blockedDirs, ignoredPaths, readOnly)
|
|
|
1837
1873
|
console.error(`\n${fmt.info(`${mode} → ${dirLabel}`)}`);
|
|
1838
1874
|
const parts = [`${blockedDirs.length} blocked`, `${ignoredPaths.length} hidden`];
|
|
1839
1875
|
if (readOnly.size > 0) parts.push(`${readOnly.size} read-only`);
|
|
1840
|
-
const extraIgnored = ignoredPaths.length - PROTECTED_DOTDIRS.length;
|
|
1876
|
+
const extraIgnored = ignoredPaths.length - PROTECTED_DOTDIRS.length - PROTECTED_HOME_DOTFILES.length;
|
|
1841
1877
|
if (extraIgnored > 0) parts.push(`${extraIgnored} from .bxignore`);
|
|
1842
1878
|
console.error(fmt.detail(parts.join(" · ")));
|
|
1843
1879
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bx-mac",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Sandbox any macOS app — only your project directory stays accessible",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"darwin"
|
|
48
48
|
],
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@types/node": "^25.5.
|
|
50
|
+
"@types/node": "^25.5.2",
|
|
51
51
|
"rolldown": "^1.0.0-rc.13",
|
|
52
52
|
"smol-toml": "^1.6.1",
|
|
53
53
|
"vitest": "^4.1.2"
|