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.
Files changed (3) hide show
  1. package/README.md +19 -3
  2. package/dist/bx.js +42 -6
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![license](https://img.shields.io/github/license/holtwick/bx-mac)](https://github.com/holtwick/bx-mac/blob/master/LICENSE)
6
6
  [![macOS](https://img.shields.io/badge/platform-macOS-lightgrey)](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** — `sandbox-exec` is undocumented; treat this as a safety net, not a guarantee
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`. Deny-first model that blocks write access outside the project directory.
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.2.0";
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.2.0",
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.0",
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"