bx-mac 0.3.0 → 0.5.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 +45 -25
- package/dist/bx.js +67 -16
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -14,6 +14,12 @@ bx ~/work/my-project
|
|
|
14
14
|
|
|
15
15
|
That's it. 🎉 VSCode opens with full access to `~/work/my-project` and nothing else.
|
|
16
16
|
|
|
17
|
+
Need multiple directories? No problem:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bx ~/work/my-project ~/work/shared-lib
|
|
21
|
+
```
|
|
22
|
+
|
|
17
23
|
## ✅ What it does
|
|
18
24
|
|
|
19
25
|
- 🔒 Blocks `~/Documents`, `~/Desktop`, `~/Downloads`, and all other personal folders
|
|
@@ -21,8 +27,9 @@ That's it. 🎉 VSCode opens with full access to `~/work/my-project` and nothing
|
|
|
21
27
|
- 🛡️ Protects sensitive dotdirs like `~/.ssh`, `~/.gnupg`, `~/.docker`, `~/.cargo`
|
|
22
28
|
- ⚙️ Keeps VSCode, extensions, shell, Node.js, and other tooling fully functional
|
|
23
29
|
- 🔍 Generates sandbox rules dynamically based on your actual `$HOME` contents
|
|
24
|
-
- 📝 Supports `.bxignore` to hide secrets like `.env` files within a project
|
|
25
|
-
- 📂 Supports `~/.
|
|
30
|
+
- 📝 Supports `.bxignore` files (searched recursively) to hide secrets like `.env` files within a project
|
|
31
|
+
- 📂 Supports `rw:` and `ro:` prefixes in `~/.bxignore` to grant read-write or read-only access to extra directories
|
|
32
|
+
- 🗂️ Supports multiple working directories in a single sandbox
|
|
26
33
|
|
|
27
34
|
## 🚫 What it doesn't do
|
|
28
35
|
|
|
@@ -54,13 +61,13 @@ pnpm link -g
|
|
|
54
61
|
|
|
55
62
|
| Command | What it launches |
|
|
56
63
|
|---|---|
|
|
57
|
-
| `bx [workdir]` | 🖥️ VSCode (default) |
|
|
58
|
-
| `bx code [workdir]` | 🖥️ VSCode (explicit) |
|
|
59
|
-
| `bx term [workdir]` | 💻 Sandboxed login shell (`$SHELL -l`) |
|
|
60
|
-
| `bx claude [workdir]` | 🤖 Claude Code CLI |
|
|
61
|
-
| `bx exec [workdir] -- cmd` | ⚡ Any command you want |
|
|
64
|
+
| `bx [workdir...]` | 🖥️ VSCode (default) |
|
|
65
|
+
| `bx code [workdir...]` | 🖥️ VSCode (explicit) |
|
|
66
|
+
| `bx term [workdir...]` | 💻 Sandboxed login shell (`$SHELL -l`) |
|
|
67
|
+
| `bx claude [workdir...]` | 🤖 Claude Code CLI |
|
|
68
|
+
| `bx exec [workdir...] -- cmd` | ⚡ Any command you want |
|
|
62
69
|
|
|
63
|
-
If no directory is given, the current directory is used.
|
|
70
|
+
If no directory is given, the current directory is used. All modes accept multiple directories.
|
|
64
71
|
|
|
65
72
|
### Examples
|
|
66
73
|
|
|
@@ -68,6 +75,9 @@ If no directory is given, the current directory is used.
|
|
|
68
75
|
# 🖥️ VSCode with sandbox protection
|
|
69
76
|
bx ~/work/my-project
|
|
70
77
|
|
|
78
|
+
# 📂 Multiple working directories
|
|
79
|
+
bx ~/work/my-project ~/work/shared-lib
|
|
80
|
+
|
|
71
81
|
# 💻 Work on a project in a sandboxed terminal
|
|
72
82
|
bx term ~/work/my-project
|
|
73
83
|
|
|
@@ -90,36 +100,35 @@ bx --verbose ~/work/my-project
|
|
|
90
100
|
|
|
91
101
|
## 📝 Configuration
|
|
92
102
|
|
|
93
|
-
bx uses
|
|
94
|
-
|
|
95
|
-
### `~/.bxallow`
|
|
96
|
-
|
|
97
|
-
Allow extra directories beyond the working directory. Paths relative to `$HOME`.
|
|
98
|
-
|
|
99
|
-
```gitignore
|
|
100
|
-
# Shared shell scripts and utilities
|
|
101
|
-
work/bin
|
|
102
|
-
shared/libs
|
|
103
|
-
```
|
|
103
|
+
bx uses two optional config files — one entry per line, `#` for comments. Project `.bxignore` files are discovered recursively.
|
|
104
104
|
|
|
105
105
|
### `~/.bxignore`
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
Unified sandbox rules for your home directory. Paths relative to `$HOME`. Each line is either a deny rule (no prefix) or an access grant (`rw:` / `ro:` prefix, case-insensitive).
|
|
108
108
|
|
|
109
109
|
```gitignore
|
|
110
|
+
# Block additional sensitive paths (no prefix = deny)
|
|
110
111
|
.aws
|
|
111
112
|
.azure
|
|
112
113
|
.kube
|
|
113
114
|
.config/gcloud
|
|
115
|
+
|
|
116
|
+
# Allow read-write access to extra directories
|
|
117
|
+
rw:work/bin
|
|
118
|
+
rw:shared/libs
|
|
119
|
+
|
|
120
|
+
# Allow read-only access (can read but not modify)
|
|
121
|
+
ro:reference/docs
|
|
122
|
+
ro:shared/toolchain
|
|
114
123
|
```
|
|
115
124
|
|
|
116
|
-
|
|
125
|
+
Deny rules are applied **in addition** to the built-in protected list:
|
|
117
126
|
|
|
118
127
|
> 🔒 `.Trash` `.ssh` `.gnupg` `.docker` `.zsh_sessions` `.cargo` `.gradle` `.gem`
|
|
119
128
|
|
|
120
129
|
### `<project>/.bxignore`
|
|
121
130
|
|
|
122
|
-
Block paths within the working directory. Supports glob patterns.
|
|
131
|
+
Block paths within the working directory. Supports glob patterns. bx searches for `.bxignore` files **recursively** through the entire project tree (skipping `.`-prefixed dirs and `node_modules`), so you can place them in subdirectories to hide secrets close to where they live.
|
|
123
132
|
|
|
124
133
|
```gitignore
|
|
125
134
|
.env
|
|
@@ -129,16 +138,27 @@ secrets/
|
|
|
129
138
|
**/*.key
|
|
130
139
|
```
|
|
131
140
|
|
|
141
|
+
For example, a monorepo might have:
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
my-project/.bxignore # top-level rules
|
|
145
|
+
my-project/services/api/.bxignore # API-specific secrets
|
|
146
|
+
my-project/deploy/.bxignore # deployment credentials
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Each `.bxignore` resolves its patterns relative to its own directory.
|
|
150
|
+
|
|
132
151
|
## 🔧 How it works
|
|
133
152
|
|
|
134
153
|
bx generates a macOS sandbox profile at launch time:
|
|
135
154
|
|
|
136
155
|
1. **Scan** `$HOME` for non-hidden directories
|
|
137
156
|
2. **Block** each one individually with `(deny file* (subpath ...))`
|
|
138
|
-
3. **Skip**
|
|
157
|
+
3. **Skip** all working directories, `~/Library`, dotfiles, and `rw:`/`ro:` paths from `~/.bxignore`
|
|
139
158
|
4. **Descend** into parent directories of allowed paths to block only siblings (because SBPL deny rules always override allow rules)
|
|
140
|
-
5. **Append** deny rules for protected dotdirs and `.bxignore`
|
|
141
|
-
6. **
|
|
159
|
+
5. **Append** deny rules for protected dotdirs, plain entries in `~/.bxignore`, and `.bxignore` files found recursively in each working directory
|
|
160
|
+
6. **Apply** `(deny file-write*)` rules for `ro:` directories (read allowed, write blocked)
|
|
161
|
+
7. **Write** the profile to `/tmp`, launch the app via `sandbox-exec`, clean up on exit
|
|
142
162
|
|
|
143
163
|
### Why not a simple deny-all + allow?
|
|
144
164
|
|
package/dist/bx.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const __VERSION__ = "0.
|
|
2
|
+
const __VERSION__ = "0.5.0";
|
|
3
3
|
import { accessSync, constants, cpSync, existsSync, globSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -27,6 +27,23 @@ function checkVSCodeTerminal() {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
+
* Abort if any workdir IS $HOME or is not inside $HOME.
|
|
31
|
+
*/
|
|
32
|
+
function checkWorkDirs(workDirs, home) {
|
|
33
|
+
for (const dir of workDirs) {
|
|
34
|
+
if (dir === home) {
|
|
35
|
+
console.error("sandbox: ERROR — working directory cannot be $HOME itself.");
|
|
36
|
+
console.error("sandbox: Sandboxing your entire home directory is not supported. Aborting.");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
if (!dir.startsWith(home + "/")) {
|
|
40
|
+
console.error(`sandbox: ERROR — working directory is outside $HOME: ${dir}`);
|
|
41
|
+
console.error("sandbox: Only directories inside $HOME are supported. Aborting.");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
30
47
|
* Detect if we're inside an unknown sandbox by probing well-known
|
|
31
48
|
* directories that exist on every Mac but would be blocked.
|
|
32
49
|
*/
|
|
@@ -130,15 +147,39 @@ function collectIgnoreFilesRecursive(dir, ignored) {
|
|
|
130
147
|
}
|
|
131
148
|
}
|
|
132
149
|
/**
|
|
133
|
-
* Parse ~/.
|
|
150
|
+
* Parse ~/.bxignore for RW:/RO: prefixed lines and return allowed directories.
|
|
151
|
+
* Lines without prefix are ignored here (handled by collectIgnoredPaths).
|
|
152
|
+
* Also checks for deprecated ~/.bxallow and migrates its entries.
|
|
134
153
|
*/
|
|
135
|
-
function
|
|
154
|
+
function parseHomeConfig(home, workDirs) {
|
|
136
155
|
const allowed = new Set(workDirs);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
const readOnly = /* @__PURE__ */ new Set();
|
|
157
|
+
const bxallowPath = join(home, ".bxallow");
|
|
158
|
+
if (existsSync(bxallowPath)) {
|
|
159
|
+
console.error("sandbox: WARNING — ~/.bxallow is deprecated. Move entries to ~/.bxignore with RW: prefix.");
|
|
160
|
+
for (const line of parseLines(bxallowPath)) {
|
|
161
|
+
const absolute = resolve(home, line);
|
|
162
|
+
if (existsSync(absolute) && statSync(absolute).isDirectory()) allowed.add(absolute);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const line of parseLines(join(home, ".bxignore"))) {
|
|
166
|
+
let prefix = "";
|
|
167
|
+
let path = line;
|
|
168
|
+
const match = line.match(/^(RW|RO):(.+)$/i);
|
|
169
|
+
if (match) {
|
|
170
|
+
prefix = match[1].toUpperCase();
|
|
171
|
+
path = match[2].trim();
|
|
172
|
+
}
|
|
173
|
+
if (!prefix) continue;
|
|
174
|
+
const absolute = resolve(home, path);
|
|
175
|
+
if (!existsSync(absolute) || !statSync(absolute).isDirectory()) continue;
|
|
176
|
+
if (prefix === "RW") allowed.add(absolute);
|
|
177
|
+
else readOnly.add(absolute);
|
|
140
178
|
}
|
|
141
|
-
return
|
|
179
|
+
return {
|
|
180
|
+
allowed,
|
|
181
|
+
readOnly
|
|
182
|
+
};
|
|
142
183
|
}
|
|
143
184
|
/**
|
|
144
185
|
* Recursively collect directories to block under parentDir.
|
|
@@ -163,22 +204,27 @@ function collectBlockedDirs(parentDir, home, scriptDir, allowedDirs) {
|
|
|
163
204
|
}
|
|
164
205
|
/**
|
|
165
206
|
* Collect paths to deny from .bxignore files and built-in protected dotdirs.
|
|
166
|
-
* Searches ~/.bxignore and recursively through all workdirs.
|
|
207
|
+
* Searches ~/.bxignore (skipping RW:/RO: lines) and recursively through all workdirs.
|
|
167
208
|
*/
|
|
168
209
|
function collectIgnoredPaths(home, workDirs) {
|
|
169
210
|
const ignored = PROTECTED_DOTDIRS.map((d) => join(home, d));
|
|
170
|
-
|
|
211
|
+
const globalIgnore = join(home, ".bxignore");
|
|
212
|
+
if (existsSync(globalIgnore)) {
|
|
213
|
+
const denyLines = parseLines(globalIgnore).filter((l) => !l.match(/^(RW|RO):/i));
|
|
214
|
+
for (const line of denyLines) for (const match of globSync(line, { cwd: home })) ignored.push(resolve(home, match));
|
|
215
|
+
}
|
|
171
216
|
for (const workDir of workDirs) collectIgnoreFilesRecursive(workDir, ignored);
|
|
172
217
|
return ignored;
|
|
173
218
|
}
|
|
174
219
|
/**
|
|
175
220
|
* Generate the SBPL sandbox profile string.
|
|
176
221
|
*/
|
|
177
|
-
function generateProfile(workDirs, blockedDirs, ignoredPaths) {
|
|
222
|
+
function generateProfile(workDirs, blockedDirs, ignoredPaths, readOnlyDirs = []) {
|
|
178
223
|
const denyRules = blockedDirs.map((dir) => ` (subpath "${dir}")`).join("\n");
|
|
179
224
|
const ignoredRules = ignoredPaths.length > 0 ? `\n; Hidden paths from .bxignore\n(deny file*\n${ignoredPaths.map((p) => {
|
|
180
225
|
return existsSync(p) && statSync(p).isDirectory() ? ` (subpath "${p}")` : ` (literal "${p}")`;
|
|
181
226
|
}).join("\n")}\n)\n` : "";
|
|
227
|
+
const readOnlyRules = readOnlyDirs.length > 0 ? `\n; Read-only directories\n(deny file-write*\n${readOnlyDirs.map((dir) => ` (subpath "${dir}")`).join("\n")}\n)\n` : "";
|
|
182
228
|
return `; Auto-generated sandbox profile
|
|
183
229
|
; Working directories: ${workDirs.join(", ")}
|
|
184
230
|
|
|
@@ -189,7 +235,7 @@ function generateProfile(workDirs, blockedDirs, ignoredPaths) {
|
|
|
189
235
|
(deny file*
|
|
190
236
|
${denyRules}
|
|
191
237
|
)
|
|
192
|
-
${ignoredRules}
|
|
238
|
+
${ignoredRules}${readOnlyRules}
|
|
193
239
|
`;
|
|
194
240
|
}
|
|
195
241
|
//#endregion
|
|
@@ -232,7 +278,7 @@ function buildCommand(mode, workDirs, home, profileSandbox, execCmd) {
|
|
|
232
278
|
};
|
|
233
279
|
case "claude": return {
|
|
234
280
|
bin: "claude",
|
|
235
|
-
args: [
|
|
281
|
+
args: []
|
|
236
282
|
};
|
|
237
283
|
case "exec": return {
|
|
238
284
|
bin: execCmd[0],
|
|
@@ -265,8 +311,10 @@ Options:
|
|
|
265
311
|
-h, --help show this help
|
|
266
312
|
|
|
267
313
|
Configuration:
|
|
268
|
-
~/.
|
|
269
|
-
|
|
314
|
+
~/.bxignore sandbox rules (one per line):
|
|
315
|
+
path block access (deny)
|
|
316
|
+
rw:path allow read-write access
|
|
317
|
+
ro:path allow read-only access
|
|
270
318
|
<workdir>/.bxignore blocked paths in project (supports globs, searched recursively)
|
|
271
319
|
|
|
272
320
|
https://github.com/holtwick/bx-mac`);
|
|
@@ -278,12 +326,15 @@ checkExternalSandbox();
|
|
|
278
326
|
const { mode, workArgs, verbose, profileSandbox, execCmd } = parseArgs();
|
|
279
327
|
const HOME = process.env.HOME;
|
|
280
328
|
const WORK_DIRS = workArgs.map((a) => resolve(a));
|
|
329
|
+
checkWorkDirs(WORK_DIRS, HOME);
|
|
281
330
|
if (mode === "code" && profileSandbox) setupVSCodeProfile(HOME);
|
|
282
|
-
const
|
|
331
|
+
const { allowed, readOnly } = parseHomeConfig(HOME, WORK_DIRS);
|
|
332
|
+
const blockedDirs = collectBlockedDirs(HOME, HOME, __dirname, new Set([...allowed, ...readOnly]));
|
|
283
333
|
const ignoredPaths = collectIgnoredPaths(HOME, WORK_DIRS);
|
|
284
334
|
const extraIgnored = ignoredPaths.length - PROTECTED_DOTDIRS.length;
|
|
285
335
|
if (extraIgnored > 0) console.error(`sandbox: .bxignore hides ${extraIgnored} extra path(s)`);
|
|
286
|
-
|
|
336
|
+
if (readOnly.size > 0) console.error(`sandbox: ${readOnly.size} read-only director${readOnly.size === 1 ? "y" : "ies"}`);
|
|
337
|
+
const profile = generateProfile(WORK_DIRS, blockedDirs, ignoredPaths, [...readOnly]);
|
|
287
338
|
const profilePath = join("/tmp", `bx-${process.pid}.sb`);
|
|
288
339
|
writeFileSync(profilePath, profile);
|
|
289
340
|
const dirLabel = WORK_DIRS.length === 1 ? WORK_DIRS[0] : `${WORK_DIRS.length} directories`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bx-mac",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Launch apps in a macOS sandbox — only the project directory is accessible",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "rolldown -c",
|
|
14
|
+
"test": "vitest run",
|
|
14
15
|
"prepublishOnly": "npm run build",
|
|
15
16
|
"post:release": "./scripts/release.sh"
|
|
16
17
|
},
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
"license": "MIT",
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@types/node": "^25.5.0",
|
|
28
|
-
"rolldown": "^1.0.0-rc.12"
|
|
29
|
+
"rolldown": "^1.0.0-rc.12",
|
|
30
|
+
"vitest": "^4.1.2"
|
|
29
31
|
},
|
|
30
32
|
"engines": {
|
|
31
33
|
"node": ">=22"
|