clabox 0.0.1 โ†’ 0.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Igor Suvorov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # ๐Ÿ“ฆ clabox
2
+
3
+ [![LSK.js](https://github.com/lskjs/presets/raw/main/docs/badge.svg)](https://github.com/lskjs)
4
+ [![NPM version](https://badgen.net/npm/v/clabox)](https://www.npmjs.com/package/clabox)
5
+ [![NPM downloads](https://badgen.net/npm/dt/clabox)](https://www.npmjs.com/package/clabox)
6
+ [![Have TypeScript types](https://badgen.net/npm/types/clabox)](https://www.npmjs.com/package/clabox)
7
+ [![Package size](https://img.shields.io/npm/unpacked-size/clabox?label=size&color=blue)](https://www.npmjs.com/package/clabox)
8
+ [![License](https://badgen.net/github/license/ycmds/clabox)](https://github.com/ycmds/clabox/blob/main/LICENSE)
9
+ [![Write us in Telegram](https://img.shields.io/badge/write%20us-0088CC?logo=telegram&logoColor=white)](https://t.me/isuvorov)
10
+
11
+ <div align="center">
12
+ <h3><p><strong>๐Ÿ›ก๏ธ Run Claude Code in a sandbox for super-safe YOLO mode ๐Ÿ›ก๏ธ</strong></p></h3>
13
+ </div>
14
+
15
+ <img src="./docs/logo.png" align="right" width="200" height="200" alt="clabox logo" />
16
+
17
+ **๐Ÿ›ก๏ธ Tight Seatbelt sandbox** โ€” the profile starts with `(deny default)` <br/>
18
+ **๐Ÿ“‚ Project-scoped access** โ€” only the CWD and explicitly allowed paths <br/>
19
+ **๐Ÿ”’ Secrets stay out of reach** โ€” SSH keys, `~/.aws`, `~/.ssh/id_*`, private dirs <br/>
20
+ **๐Ÿ“ฆ Declarative JS config** instead of sed surgery over a heredoc <br/>
21
+ **๐Ÿค– Bot identity** for git/ssh inside the sandbox <br/>
22
+ **๐Ÿงจ Fork-bomb guard** via `ulimit -u` <br/>
23
+ **โšก YOLO mode, safely** (`--dangerously-skip-permissions`) <br/>
24
+ **๐ŸŽ macOS only**, Node โ‰ฅ 18, no runtime deps beyond `yargs` <br/>
25
+
26
+ ---
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install -g clabox # exposes the global `clabox` command
32
+ # or run without installing:
33
+ bunx clabox โ€ฆ
34
+ npx clabox โ€ฆ
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```bash
40
+ # Default profile (~/.claude), YOLO mode
41
+ clabox run --dangerously-skip-permissions
42
+
43
+ # A different Claude profile
44
+ CLAUDE_CONFIG_DIR=~/.claude_work clabox run --dangerously-skip-permissions
45
+
46
+ # Debugging
47
+ clabox generate # build the profile, print the .sb path
48
+ clabox profile # just the path (no build)
49
+ CLABOX_DEBUG=1 clabox # print profile/config/dir on launch
50
+ clabox --help
51
+ ```
52
+
53
+ Unknown flags are passed straight through to `claude`, so anything after the
54
+ command (`--dangerously-skip-permissions`, `--model โ€ฆ`, etc.) just works.
55
+
56
+ ---
57
+
58
+ ## Configuration
59
+
60
+ Three layers, later wins: **defaults โ†’ environment variables โ†’ JS config file**.
61
+
62
+ The config file is looked up in this order: `--config /path` โ†’
63
+ `CLABOX_CONFIG=/path` โ†’ `./clabox.config.mjs` (project root) โ†’
64
+ `~/.config/clabox/config.mjs`.
65
+ See [`clabox.config.example.mjs`](clabox.config.example.mjs).
66
+
67
+ ```bash
68
+ clabox --config ./my.clabox.mjs run --dangerously-skip-permissions
69
+ ```
70
+
71
+ ```js
72
+ // clabox.config.mjs
73
+ export default {
74
+ configDir: '~/.claude_work',
75
+ bot: { name: 'workBOT', email: 'bot@work.dev', sshDir: '~/.ssh/workbot' },
76
+ network: true,
77
+ paths: {
78
+ readWrite: ['~/scratch'], // RW on top of project / configDir / tmp
79
+ readOnly: ['~/reference'], // RO
80
+ exec: ['/opt/tool/bin'], // process-exec
81
+ deny: ['~/secret'], // explicit deny (read + write)
82
+ },
83
+ };
84
+ ```
85
+
86
+ You may also export a function `(defaults) => config` for full control. `~` is
87
+ expanded to `$HOME`.
88
+
89
+ ### Environment variables
90
+
91
+ | Variable | Purpose | Default |
92
+ |---|---|---|
93
+ | `CLAUDE_CONFIG_DIR` | Claude config/profile dir (multi-account); passed through to `claude` | `~/.claude` |
94
+ | `CLABOX_CLAUDE_BIN` | path to the `claude` binary | `PATH`, then `~/.local/bin/claude` |
95
+ | `CLABOX_BOT_NAME` / `CLABOX_BOT_EMAIL` | git identity | `claudeBOT` / `bot@example.com` |
96
+ | `CLABOX_BOT_SSH_DIR` | bot key dir (`id_ed25519`, `config`) | `~/.ssh/claudebot` |
97
+ | `CLABOX_CONFIG` | path to the JS config file (the `--config` flag overrides it) | โ€” |
98
+ | `CLABOX_HOOKS_DIR` | hooks dir (RO + exec inside the sandbox) | โ€” (off) |
99
+ | `CLABOX_DEBUG` | print diagnostics on launch | โ€” |
100
+ | `TMPDIR` | where the generated profile is stored | `/tmp` |
101
+
102
+ ---
103
+
104
+ ## How it works
105
+
106
+ `sandbox-exec` runs a process inside a Seatbelt profile that starts with
107
+ `(deny default)` โ€” everything is forbidden unless explicitly allowed.
108
+
109
+ ```
110
+ clabox run โ†’ loadConfig() โ†’ buildProfile() โ†’ <TMPDIR>/โ€ฆsb
111
+ โ†’ sh -c 'ulimit -u N; exec sandbox-exec -f <sb> env โ€ฆ claude โ€ฆ'
112
+ ```
113
+
114
+ | Module | Responsibility |
115
+ |---|---|
116
+ | `src/utils/config.ts` | defaults, env, loading/merging the JS config, `~` expansion |
117
+ | `src/sandbox/profile.ts` | assembling the SBPL profile from config (typed helpers `subpath`/`literal`/`regex`/โ€ฆ) |
118
+ | `src/sandbox/run.ts` | locating `claude`/`sandbox-exec`, generating the profile, launching with bot env + `ulimit` |
119
+ | `src/cli.ts` | the CLI (`run` / `generate` / `profile`), built on yargs |
120
+
121
+ Profile path: `$TMPDIR/clabox-<dir-name>-<hash>.sb` (hash of the absolute
122
+ project path โ€” each project gets its own cached profile).
123
+
124
+ Package managers are autodetected (`src/sandbox/profile.ts`) and added to the
125
+ read/exec sections: Homebrew (`/opt/homebrew` or `/usr/local/Homebrew`),
126
+ `~/.local`, Nix (`/nix/store`).
127
+
128
+ ### What the profile allows and denies
129
+
130
+ **Read-only:** system dirs `/System`, `/usr`, `/bin`, `/sbin`,
131
+ `/Library/Frameworks`, Command Line Tools / Xcode, tzdata, system and user
132
+ `Library/Preferences`, detected package paths.
133
+
134
+ **Read-write:** the project dir (CWD), the Claude config dir (`configDir`),
135
+ `/tmp`, `/private/tmp`, `/private/var/folders/โ€ฆ`, `~/Library/Keychains` (for
136
+ OAuth refresh), plus `paths.readWrite` from your config.
137
+
138
+ **Network:** `(allow network*)` when `network: true` (the default).
139
+
140
+ **Explicit deny โ€” wins even over the allows above:**
141
+ - private dirs: `denyHome` (`~/Documents`, `~/Desktop`, `~/Downloads`,
142
+ `~/Pictures`, `~/Movies`, `~/Music`);
143
+ - secrets: `denyDotConfigs` (`~/.aws`, `~/.gnupg`, `~/.kube`, `~/.docker`,
144
+ `~/.config`) with a carve-out for `~/.config/git`;
145
+ - personal SSH keys `~/.ssh/id_*`, `*.pem`, `*.key` โ€” Claude physically cannot
146
+ read them. Only the bot key subdir (`bot.sshDir`) is readable.
147
+
148
+ ### Git/ssh bot identity
149
+
150
+ - `ulimit -u <ulimitProcs>` โ€” fork-bomb guard (`0` to disable);
151
+ - `GIT_AUTHOR_*` / `GIT_COMMITTER_*` โ€” bot name/email from config;
152
+ - if `bot.sshDir/id_ed25519` exists, `GIT_SSH_COMMAND` is pinned to it
153
+ (`IdentitiesOnly=yes`, `IdentityAgent=none`);
154
+ - gpg signing disabled, `NPM_CONFIG_USERCONFIG=/dev/null`, `DISABLE_AUTOUPDATER=1`.
155
+
156
+ ---
157
+
158
+ ## Tests
159
+
160
+ ```bash
161
+ bun test # unit + functional (bun:test)
162
+ bun run test # full gate: lint + types + unit + size
163
+ ```
164
+
165
+ The suite tests the wrapper, not `claude`:
166
+
167
+ - **Unit** โ€” the generated profile text: SBPL preamble, project RW/exec, network
168
+ toggle, config dir, ssh-key denials, the deny list, extra config paths, hooks.
169
+ - **Functional** โ€” runs real `sandbox-exec` against a generated profile and
170
+ asserts that reads/writes inside the project succeed while denied paths are
171
+ blocked. Auto-skipped off macOS or when running nested inside another sandbox.
172
+
173
+ ---
174
+
175
+ ## Limitations
176
+
177
+ - **macOS only** โ€” needs `sandbox-exec` (Seatbelt). Formally deprecated, still
178
+ works on macOS 14/15.
179
+ - **No nested sandbox** โ€” you cannot launch the sandbox from inside another
180
+ sandbox (`sandbox_apply: Operation not permitted`). Run from a bare host.
181
+ - **Keychain is writable** for OAuth refresh (otherwise tokens hit 401 after
182
+ ~24h). For a stricter setup, swap the RW Keychain block for RO in
183
+ `src/sandbox/profile.ts` (the "Keychain access" section).
184
+
185
+ ---
186
+
187
+ ## License
188
+
189
+ [MIT](LICENSE)
@@ -0,0 +1,46 @@
1
+ // Example clabox config. Copy to `clabox.config.mjs` (in your
2
+ // project root) or `~/.config/clabox/config.mjs`, then edit.
3
+ //
4
+ // Default-export either a plain object (merged over the built-in defaults) or
5
+ // a function `(defaults) => config` for full control. `~` is expanded to $HOME.
6
+
7
+ export default {
8
+ // Which Claude profile/account to use.
9
+ configDir: '~/.claude',
10
+
11
+ // Args always passed to `claude`, before any args from the CLI.
12
+ claudeArgs: ['--settings', '{"includeCoAuthoredBy": false}'],
13
+
14
+ // Identity forced onto git commits/pushes made from inside the sandbox.
15
+ bot: {
16
+ name: 'claudeBOT',
17
+ email: 'bot@example.com',
18
+ // If `${sshDir}/id_ed25519` exists, git ssh is pinned to it and your
19
+ // personal keys (~/.ssh/id_*, *.pem, *.key) stay denied either way.
20
+ sshDir: '~/.ssh/claudebot',
21
+ },
22
+
23
+ // Outbound network (set false to cut it off entirely).
24
+ network: true,
25
+
26
+ // Process-table cap inside the sandbox (fork-bomb guard); 0 to disable.
27
+ ulimitProcs: 1024,
28
+
29
+ // Extra directory granted read + execute (e.g. shared Claude hooks).
30
+ hooksDir: null, // '~/some/hooks'
31
+
32
+ // Extra rules layered on top of the base profile.
33
+ paths: {
34
+ readWrite: [], // e.g. ['~/scratch', '/Volumes/work']
35
+ readOnly: [], // e.g. ['~/reference-data']
36
+ exec: [], // e.g. ['/opt/some/tool/bin']
37
+ deny: [], // e.g. ['~/secret-project']
38
+ },
39
+
40
+ // Home subdirectories denied entirely (read + write).
41
+ denyHome: ['Documents', 'Desktop', 'Downloads', 'Pictures', 'Movies', 'Music'],
42
+
43
+ // Dotfile config dirs under $HOME denied entirely (.config/git is re-allowed
44
+ // read-only regardless, so git keeps working).
45
+ denyDotConfigs: ['aws', 'gnupg', 'kube', 'docker', 'config'],
46
+ };
@@ -0,0 +1,163 @@
1
+ # Project Guidelines
2
+
3
+ > **This file contains project documentation for developers.** For AI assistant instructions see [CLAUDE.md](../CLAUDE.md).
4
+
5
+ Guidelines for clabox โ€” run Claude Code in a sandbox for super-safe YOLO mode, configured in plain JavaScript.
6
+
7
+ **Important:**
8
+ - Update this file after large project changes
9
+ - Run `bun run fix` and `bun run test` after each code change
10
+
11
+ ## Stack
12
+
13
+ | Tool | Choice | Notes |
14
+ |---|---|---|
15
+ | Runtime (published) | Node.js โ‰ฅ 18 | `lib/` is plain ESM; macOS only (`sandbox-exec`) |
16
+ | Runtime (dev) | Bun | install / test / run TS source directly |
17
+ | Language | TypeScript (ESM) | strict `tsconfig`, `module`/`moduleResolution` nodenext |
18
+ | Build | tsdown (rolldown) | `src/**/*.ts` โ†’ `lib/` (ESM + `.d.ts` + sourcemaps) |
19
+ | Lint / Format | Biome | `recommended` preset, `.js`-import enforcement |
20
+ | Test | bun:test | `tests/` (configured in `bunfig.toml`) |
21
+ | Type-check | `tsc --noEmit` | strict, `src/` only |
22
+ | Size budget | size-limit | `@size-limit/preset-small-lib`, `lib/index.js` |
23
+ | Release | semantic-release | fully automatic on push to `main` |
24
+ | CI/CD | GitHub Actions | `macos-latest` (real `sandbox-exec`) |
25
+ | CLI | yargs ^17 | the only runtime dependency |
26
+ | Sandbox | macOS `sandbox-exec` (Seatbelt/SBPL) | profile generated as plain text |
27
+
28
+ ## Project Structure
29
+
30
+ **Rule:** Only entry-point files live in `src/` root โ€” `index.ts` (public API aggregator) and `cli.ts` (the CLI, built to `lib/cli.js` = the `clabox` bin). All other code lives in subdirectories. The build emits to `lib/`; `bin`/`main`/`exports` point at built `lib/*.js`.
31
+
32
+ ```
33
+ src/
34
+ โ”œโ”€โ”€ index.ts # public API aggregator โ€” re-exports config / profile / run
35
+ โ”œโ”€โ”€ cli.ts # CLI entry (yargs): run / generate / profile โ†’ lib/cli.js (bin)
36
+ โ”œโ”€โ”€ sandbox/
37
+ โ”‚ โ”œโ”€โ”€ profile.ts # pure SBPL builder: buildProfile, detectPackagePaths,
38
+ โ”‚ โ”‚ # subpath/literal/regex/globalName/ipcName/reEscape helpers
39
+ โ”‚ โ””โ”€โ”€ run.ts # I/O: profilePath, generateProfile, runClaude (sandbox-exec launch)
40
+ โ””โ”€โ”€ utils/
41
+ โ””โ”€โ”€ config.ts # defaultConfig, expandHome, mergeConfig, findConfigFile, loadConfig
42
+ tests/
43
+ โ””โ”€โ”€ profile.test.ts # bun:test โ€” unit (profile text) + functional (real sandbox-exec)
44
+ lib/ # build output (tsdown) โ€” gitignored
45
+ docs/
46
+ โ”œโ”€โ”€ guideline.md # this file
47
+ โ””โ”€โ”€ logo.png # README logo
48
+ .github/workflows/
49
+ โ”œโ”€โ”€ test.yml # PR โ†’ install + build + test (macos-latest)
50
+ โ””โ”€โ”€ release.yml # push main โ†’ semantic-release (macos-latest)
51
+ clabox.config.example.mjs # copyable user config (object or (defaults) => config)
52
+ ```
53
+
54
+ ## Commands
55
+
56
+ ```bash
57
+ # Build
58
+ bun run build # tsdown --out-dir lib (release build: ESM + .d.ts + maps)
59
+ bun run build:tsdown # tsdown โ†’ lib-tsdown (default outDir)
60
+ bun run build:tsdown:release # tsdown --out-dir lib
61
+ bun run dev # tsdown --watch
62
+
63
+ # Run
64
+ bun run cli # bun run src/cli.ts (pass args after `--`)
65
+ bun run generate # bun run src/cli.ts generate (build a profile, print its path)
66
+
67
+ # Testing
68
+ bun run test # lint + types + unit + size (the full gate)
69
+ bun run test:unit # bun test
70
+ bun run test:unit:coverage # bun test --coverage
71
+ bun run test:unit:watch # bun test --watch
72
+ bun run test:types # tsc --noEmit
73
+ bun run test:lint # biome lint
74
+ bun run test:size # size-limit
75
+
76
+ # Fixing
77
+ bun run fix # biome check --write
78
+ bun run fix:lint # biome check --write
79
+ bun run fix:lint:unsafe # biome check --write --unsafe
80
+
81
+ # Release (normally automatic in CI)
82
+ bun run version:release # semantic-release --no-ci --dry-run (preview next version)
83
+ bun run release # build + test + dry-run + npm publish (local fallback)
84
+ ```
85
+
86
+ ## Architecture
87
+
88
+ `sandbox-exec` runs a process inside a Seatbelt profile that starts with `(deny default)` โ€” everything is forbidden unless explicitly allowed.
89
+
90
+ ```
91
+ clabox run โ†’ loadConfig() โ†’ buildProfile() โ†’ <TMPDIR>/โ€ฆsb
92
+ โ†’ sh -c 'ulimit -u N; exec sandbox-exec -f <sb> env โ€ฆ claude โ€ฆ'
93
+ ```
94
+
95
+ ### `utils/config.ts`
96
+ Builds the effective config in three layers (later wins): `defaultConfig` โ†’ env vars โ†’ a JS config file. `findConfigFile(explicit?)` looks up the explicit path (the `--config` CLI flag, falling back to `CLABOX_CONFIG`) โ†’ `./clabox.config.mjs` / `./clabox.config.js` โ†’ `~/.config/clabox/config.mjs`; the `--config` flag wins over `CLABOX_CONFIG`. `loadConfig(explicit?)` forwards that path, dynamically imports the file and accepts either a plain object (merged via `mergeConfig`, a deep merge over the defaults) or a function `(defaults) => config`. `expandHome()` expands a leading `~`. Exports the `Config`/`BotConfig`/`PathRules`/`LoadedConfig` types.
97
+
98
+ ### `sandbox/profile.ts` (pure)
99
+ Assembles the SBPL profile text from typed helpers (`subpath`, `literal`, `regex`, `globalName`, `ipcName`, `reEscape`) โ€” no I/O beyond `fs.existsSync` for autodetection. `detectPackagePaths()` finds installed package managers (Homebrew `/opt/homebrew` or `/usr/local/Homebrew`, `~/.local`, Nix `/nix/store`) to grant read/exec. `buildProfile(config, { projectDir, detectedPaths })` returns the full profile and sanity-checks it carries `(version 1)`.
100
+
101
+ ### `sandbox/run.ts` (I/O)
102
+ `profilePath()` returns the deterministic `$TMPDIR/clabox-<dir>-<hash>.sb` path. `generateProfile()` requires `sandbox-exec`, builds the profile and writes it. `runClaude()` resolves the `claude` binary (config โ†’ PATH โ†’ `~/.local/bin/claude`), forces the bot git identity + hardening env (`buildEnvArgs`), sets the terminal title, and execs `sh -c 'ulimit -u N; exec sandbox-exec -f <sb> env โ€ฆ claude โ€ฆ'`, returning the exit code.
103
+
104
+ ### `cli.ts`
105
+ The yargs CLI (`scriptName('clabox')`). Commands: `run [claudeArgs..]` (default), `generate`, `profile`. `unknown-options-as-args` keeps unknown flags as positionals so they pass straight through to `claude` (e.g. `--dangerously-skip-permissions`). The one clabox-owned flag is `--config <path>` (a JS config file that overrides `CLABOX_CONFIG`), forwarded to `loadConfig()` by `run` and `generate`.
106
+
107
+ ### What the profile allows and denies
108
+ - **Read-only:** system dirs (`/System`, `/usr`, `/bin`, `/sbin`, `/Library/Frameworks`), Command Line Tools / Xcode, tzdata, system + user `Library/Preferences`, detected package paths.
109
+ - **Read-write:** the project dir (CWD), the Claude config dir (`configDir`), `/tmp`, `/private/tmp`, `/private/var/folders/โ€ฆ`, `~/Library/Keychains` (OAuth refresh), plus `paths.readWrite`.
110
+ - **Network:** `(allow network*)` when `network: true` (default).
111
+ - **Explicit deny โ€” wins over the allows:** `denyHome` (`~/Documents`, `~/Desktop`, โ€ฆ), `denyDotConfigs` (`~/.aws`, `~/.gnupg`, `~/.kube`, `~/.docker`, `~/.config`) with a carve-out for `~/.config/git`, and personal SSH keys `~/.ssh/id_*`, `*.pem`, `*.key`. Only the bot key subdir (`bot.sshDir`) is readable.
112
+
113
+ ### Git/ssh bot identity
114
+ `ulimit -u <ulimitProcs>` (fork-bomb guard, `0` to disable); `GIT_AUTHOR_*` / `GIT_COMMITTER_*` from `bot.name`/`bot.email`; if `bot.sshDir/id_ed25519` exists, `GIT_SSH_COMMAND` is pinned to it (`IdentitiesOnly=yes`, `IdentityAgent=none`); gpg signing disabled; `NPM_CONFIG_USERCONFIG=/dev/null`; `DISABLE_AUTOUPDATER=1`.
115
+
116
+ ## Lint
117
+
118
+ Biome (`biome.json`), scoped to `src/**/*.ts` + `tests/**/*.ts`:
119
+ - `recommended` preset; `noExplicitAny` and `noNonNullAssertion` off.
120
+ - `useImportExtensions` (error, `forceJsExtensions`) โ€” relative imports must use `.js` specifiers.
121
+ - Formatter: 2-space indent, line width 100, single quotes, always semicolons.
122
+
123
+ ## CI/CD
124
+
125
+ Both workflows run on `macos-latest` so the functional tests can exercise the real `sandbox-exec`.
126
+
127
+ - **`test.yml`** โ€” on PR to `main`: checkout โ†’ setup Bun + Node 20 โ†’ `bun install --frozen-lockfile` โ†’ `bun run build` โ†’ `bun run test`.
128
+ - **`release.yml`** โ€” on push to `main`: checkout (`fetch-depth: 0`) โ†’ setup Bun + Node 20 (`registry-url`) โ†’ install โ†’ build โ†’ test โ†’ `npx semantic-release`. Releases are **fully automatic**: semantic-release reads the Conventional Commits, decides the version, updates `CHANGELOG.md`, publishes to npm (provenance) + GitHub Releases, and commits the bump back with `[skip ci]`. Nobody bumps a version by hand.
129
+
130
+ ## Size Limits
131
+
132
+ | Entry | Limit | Note |
133
+ |---|---|---|
134
+ | `lib/index.js` | 10 kB | brotlied, `node:*` ignored (the CLI bin uses top-level await and is not size-budgeted) |
135
+
136
+ ## Package Exports
137
+
138
+ ```typescript
139
+ import { loadConfig, buildProfile, runClaude } from 'clabox'; // lib/index.js
140
+ import { loadConfig, defaultConfig, mergeConfig } from 'clabox/config'; // lib/utils/config.js
141
+ import { buildProfile, detectPackagePaths } from 'clabox/profile'; // lib/sandbox/profile.js
142
+ import { generateProfile, profilePath, runClaude } from 'clabox/run'; // lib/sandbox/run.js
143
+ // CLI entry: clabox/cli (lib/cli.js) โ€” also the `clabox` bin
144
+ ```
145
+
146
+ ## Environment Variables
147
+
148
+ | Variable | Purpose | Default |
149
+ |---|---|---|
150
+ | `CLAUDE_CONFIG_DIR` | Claude config/profile dir (multi-account); passed through to `claude` | `~/.claude` |
151
+ | `CLABOX_CLAUDE_BIN` | path to the `claude` binary | PATH, then `~/.local/bin/claude` |
152
+ | `CLABOX_BOT_NAME` / `CLABOX_BOT_EMAIL` | git identity inside the sandbox | `claudeBOT` / `bot@example.com` |
153
+ | `CLABOX_BOT_SSH_DIR` | bot key dir (`id_ed25519`, `config`) | `~/.ssh/claudebot` |
154
+ | `CLABOX_CONFIG` | path to the JS config file (the `--config` flag overrides it) | โ€” |
155
+ | `CLABOX_HOOKS_DIR` | hooks dir (RO + exec inside the sandbox) | โ€” (off) |
156
+ | `CLABOX_DEBUG` | print profile/config/dir diagnostics on launch | โ€” |
157
+ | `TMPDIR` | where the generated profile is stored | `/tmp` |
158
+
159
+ ## Limitations
160
+
161
+ - **macOS only** โ€” needs `sandbox-exec` (Seatbelt). Formally deprecated, still works on macOS 14/15.
162
+ - **No nested sandbox** โ€” you cannot launch the sandbox from inside another sandbox (`sandbox_apply: Operation not permitted`). Run from a bare host.
163
+ - **Keychain is writable** for OAuth refresh (else tokens hit 401 after ~24h). For a stricter setup, swap the RW Keychain block for RO in `src/sandbox/profile.ts` (the "Keychain access" section).
package/docs/logo.png ADDED
Binary file
package/lib/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/lib/cli.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { a as loadConfig } from "./config-DXTNeUhH.js";
3
+ import { n as profilePath, r as runClaude, t as generateProfile } from "./run-Dyp_hW97.js";
4
+ import yargs from "yargs";
5
+ import { hideBin } from "yargs/helpers";
6
+ //#region src/cli.ts
7
+ await yargs(hideBin(process.argv)).scriptName("clabox").parserConfiguration({ "unknown-options-as-args": true }).option("config", {
8
+ type: "string",
9
+ describe: "Path to a JS config file (overrides CLABOX_CONFIG)"
10
+ }).command(["run [claudeArgs..]", "$0 [claudeArgs..]"], "Generate the profile and run claude inside the sandbox (default)", (y) => y.positional("claudeArgs", {
11
+ describe: "Arguments passed through to claude",
12
+ array: true,
13
+ default: []
14
+ }), async (argv) => {
15
+ const { config, configFile } = await loadConfig(argv.config);
16
+ const code = runClaude(config, argv.claudeArgs ?? [], { configFile });
17
+ process.exit(code);
18
+ }).command("generate", "Build the sandbox profile only and print its path", {}, async (argv) => {
19
+ const { config } = await loadConfig(argv.config);
20
+ console.log(generateProfile(config));
21
+ }).command("profile", "Print the sandbox profile path (no build)", {}, () => {
22
+ console.log(profilePath());
23
+ }).example("$0 run --dangerously-skip-permissions", "YOLO mode inside the sandbox").example("$0 --config ./my.clabox.mjs run", "Use a specific JS config file").example("CLAUDE_CONFIG_DIR=~/.claude_work $0 run", "Use a different Claude profile").epilogue([
24
+ "Config (later wins): defaults -> env vars -> JS config file.",
25
+ "File: ./clabox.config.mjs or ~/.config/clabox/config.mjs",
26
+ "(or --config /path, or CLABOX_CONFIG=/path)."
27
+ ].join("\n")).version(false).help().alias("h", "help").fail((msg, err) => {
28
+ console.error(`Error: ${err?.message ?? msg}`);
29
+ process.exit(1);
30
+ }).parseAsync();
31
+ //#endregion
32
+ export {};
33
+
34
+ //# sourceMappingURL=cli.js.map
package/lib/cli.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n// clabox โ€” run Claude Code in a sandbox for super-safe YOLO mode.\n// SPDX-License-Identifier: MIT\n//\n// Configure in plain JS: clabox.config.mjs (CWD) or\n// ~/.config/clabox/config.mjs. See clabox.config.example.mjs.\n\nimport yargs from 'yargs';\nimport { hideBin } from 'yargs/helpers';\nimport { generateProfile, profilePath, runClaude } from './sandbox/run.js';\nimport { loadConfig } from './utils/config.js';\n\nawait yargs(hideBin(process.argv))\n .scriptName('clabox')\n // Keep unknown flags (e.g. --dangerously-skip-permissions) as positionals so\n // they pass straight through to claude instead of erroring out.\n .parserConfiguration({ 'unknown-options-as-args': true })\n // clabox-owned flag: a config-file path that wins over CLABOX_CONFIG.\n .option('config', {\n type: 'string',\n describe: 'Path to a JS config file (overrides CLABOX_CONFIG)',\n })\n .command(\n ['run [claudeArgs..]', '$0 [claudeArgs..]'],\n 'Generate the profile and run claude inside the sandbox (default)',\n (y) =>\n y.positional('claudeArgs', {\n describe: 'Arguments passed through to claude',\n array: true,\n default: [] as string[],\n }),\n async (argv) => {\n const { config, configFile } = await loadConfig(argv.config as string | undefined);\n const claudeArgs = (argv.claudeArgs ?? []) as string[];\n const code = runClaude(config, claudeArgs, { configFile });\n process.exit(code);\n },\n )\n .command('generate', 'Build the sandbox profile only and print its path', {}, async (argv) => {\n const { config } = await loadConfig(argv.config as string | undefined);\n console.log(generateProfile(config));\n })\n .command('profile', 'Print the sandbox profile path (no build)', {}, () => {\n console.log(profilePath());\n })\n .example('$0 run --dangerously-skip-permissions', 'YOLO mode inside the sandbox')\n .example('$0 --config ./my.clabox.mjs run', 'Use a specific JS config file')\n .example('CLAUDE_CONFIG_DIR=~/.claude_work $0 run', 'Use a different Claude profile')\n .epilogue(\n [\n 'Config (later wins): defaults -> env vars -> JS config file.',\n 'File: ./clabox.config.mjs or ~/.config/clabox/config.mjs',\n '(or --config /path, or CLABOX_CONFIG=/path).',\n ].join('\\n'),\n )\n .version(false)\n .help()\n .alias('h', 'help')\n .fail((msg, err) => {\n console.error(`Error: ${err?.message ?? msg}`);\n process.exit(1);\n })\n .parseAsync();\n"],"mappings":";;;;;;AAYA,MAAM,MAAM,QAAQ,QAAQ,IAAI,CAAC,CAAC,CAC/B,WAAW,QAAQ,CAAC,CAGpB,oBAAoB,EAAE,2BAA2B,KAAK,CAAC,CAAC,CAExD,OAAO,UAAU;CAChB,MAAM;CACN,UAAU;AACZ,CAAC,CAAC,CACD,QACC,CAAC,sBAAsB,mBAAmB,GAC1C,qEACC,MACC,EAAE,WAAW,cAAc;CACzB,UAAU;CACV,OAAO;CACP,SAAS,CAAC;AACZ,CAAC,GACH,OAAO,SAAS;CACd,MAAM,EAAE,QAAQ,eAAe,MAAM,WAAW,KAAK,MAA4B;CAEjF,MAAM,OAAO,UAAU,QADH,KAAK,cAAc,CAAC,GACG,EAAE,WAAW,CAAC;CACzD,QAAQ,KAAK,IAAI;AACnB,CACF,CAAC,CACA,QAAQ,YAAY,qDAAqD,CAAC,GAAG,OAAO,SAAS;CAC5F,MAAM,EAAE,WAAW,MAAM,WAAW,KAAK,MAA4B;CACrE,QAAQ,IAAI,gBAAgB,MAAM,CAAC;AACrC,CAAC,CAAC,CACD,QAAQ,WAAW,6CAA6C,CAAC,SAAS;CACzE,QAAQ,IAAI,YAAY,CAAC;AAC3B,CAAC,CAAC,CACD,QAAQ,yCAAyC,8BAA8B,CAAC,CAChF,QAAQ,mCAAmC,+BAA+B,CAAC,CAC3E,QAAQ,2CAA2C,gCAAgC,CAAC,CACpF,SACC;CACE;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI,CACb,CAAC,CACA,QAAQ,KAAK,CAAC,CACd,KAAK,CAAC,CACN,MAAM,KAAK,MAAM,CAAC,CAClB,MAAM,KAAK,QAAQ;CAClB,QAAQ,MAAM,UAAU,KAAK,WAAW,KAAK;CAC7C,QAAQ,KAAK,CAAC;AAChB,CAAC,CAAC,CACD,WAAW"}
@@ -0,0 +1,67 @@
1
+ //#region src/utils/config.d.ts
2
+ declare const HOME: string;
3
+ /** Expand a leading `~` / `~/` to the user's home directory. */
4
+ declare function expandHome(p: string): string;
5
+ /** Dedicated git/ssh identity for commits made from inside the sandbox. */
6
+ interface BotConfig {
7
+ name: string;
8
+ email: string;
9
+ /** If `${sshDir}/id_ed25519` exists, git ssh is pinned to it. */
10
+ sshDir: string;
11
+ }
12
+ /** Extra rules layered on top of the built-in base profile. */
13
+ interface PathRules {
14
+ /** RW subpaths (beyond project dir + configDir + /tmp). */
15
+ readWrite: string[];
16
+ /** RO subpaths. */
17
+ readOnly: string[];
18
+ /** process-exec subpaths. */
19
+ exec: string[];
20
+ /** explicit deny subpaths (read + write). */
21
+ deny: string[];
22
+ }
23
+ /** Effective clabox configuration. */
24
+ interface Config {
25
+ /** Path to the `claude` binary. null โ†’ autodetect (PATH, then ~/.local/bin). */
26
+ claudeBin: string | null;
27
+ /** Claude config/profile directory โ€” supports multiple accounts. */
28
+ configDir: string;
29
+ /** Extra args always passed to `claude`, before any args from the CLI. */
30
+ claudeArgs: string[];
31
+ bot: BotConfig;
32
+ /** Allow outbound network. `false` โ†’ no `(allow network*)` line. */
33
+ network: boolean;
34
+ /** Cap the process table inside the sandbox (fork-bomb guard). 0 โ†’ skip. */
35
+ ulimitProcs: number;
36
+ /** Extra directory granted read + execute inside the sandbox. null โ†’ disabled. */
37
+ hooksDir: string | null;
38
+ paths: PathRules;
39
+ /** Home subdirectories denied entirely (read + write). */
40
+ denyHome: string[];
41
+ /** Dotfile config dirs under $HOME denied entirely. */
42
+ denyDotConfigs: string[];
43
+ }
44
+ /** Built-in defaults. Everything here is meant to be overridable. */
45
+ declare const defaultConfig: Config;
46
+ /** Merge a (partial) override over a full config, returning a new config. */
47
+ declare function mergeConfig(base: Config, override: unknown): Config;
48
+ /**
49
+ * Locate a config file: explicit (CLI arg, then `CLABOX_CONFIG` env),
50
+ * then CWD, then ~/.config. The CLI arg wins over the env var.
51
+ */
52
+ declare function findConfigFile(explicit?: string | null): string | null;
53
+ /** Result of {@link loadConfig}: the effective config and the file it came from. */
54
+ interface LoadedConfig {
55
+ config: Config;
56
+ configFile: string | null;
57
+ }
58
+ /**
59
+ * Build the effective config: defaults โŠ• env โŠ• config file.
60
+ *
61
+ * @param explicitConfig optional config-file path (e.g. from `--config`);
62
+ * takes precedence over `CLABOX_CONFIG` and the default lookup locations.
63
+ */
64
+ declare function loadConfig(explicitConfig?: string | null): Promise<LoadedConfig>;
65
+ //#endregion
66
+ export { PathRules as a, findConfigFile as c, LoadedConfig as i, loadConfig as l, Config as n, defaultConfig as o, HOME as r, expandHome as s, BotConfig as t, mergeConfig as u };
67
+ //# sourceMappingURL=config-CUyriGxm.d.ts.map
@@ -0,0 +1,103 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { pathToFileURL } from "node:url";
5
+ //#region src/utils/config.ts
6
+ const HOME = os.homedir();
7
+ /** Expand a leading `~` / `~/` to the user's home directory. */
8
+ function expandHome(p) {
9
+ if (!p) return p;
10
+ if (p === "~") return HOME;
11
+ if (p.startsWith("~/")) return path.join(HOME, p.slice(2));
12
+ return p;
13
+ }
14
+ const env = process.env;
15
+ /** Built-in defaults. Everything here is meant to be overridable. */
16
+ const defaultConfig = {
17
+ claudeBin: env.CLABOX_CLAUDE_BIN ?? null,
18
+ configDir: env.CLAUDE_CONFIG_DIR ?? "~/.claude",
19
+ claudeArgs: ["--settings", "{\"includeCoAuthoredBy\": false}"],
20
+ bot: {
21
+ name: env.CLABOX_BOT_NAME ?? "claudeBOT",
22
+ email: env.CLABOX_BOT_EMAIL ?? "bot@example.com",
23
+ sshDir: env.CLABOX_BOT_SSH_DIR ?? "~/.ssh/claudebot"
24
+ },
25
+ network: true,
26
+ ulimitProcs: 1024,
27
+ hooksDir: env.CLABOX_HOOKS_DIR ?? null,
28
+ paths: {
29
+ readWrite: [],
30
+ readOnly: [],
31
+ exec: [],
32
+ deny: []
33
+ },
34
+ denyHome: [
35
+ "Documents",
36
+ "Desktop",
37
+ "Downloads",
38
+ "Pictures",
39
+ "Movies",
40
+ "Music"
41
+ ],
42
+ denyDotConfigs: [
43
+ "aws",
44
+ "gnupg",
45
+ "kube",
46
+ "docker",
47
+ "config"
48
+ ]
49
+ };
50
+ function isPlainObject(v) {
51
+ return v != null && typeof v === "object" && !Array.isArray(v);
52
+ }
53
+ /** Shallow-deep merge: nested plain objects merge, everything else replaces. */
54
+ function deepMerge(base, override) {
55
+ const out = { ...base };
56
+ for (const [key, val] of Object.entries(override)) {
57
+ if (val === void 0) continue;
58
+ const baseVal = base[key];
59
+ out[key] = isPlainObject(val) && isPlainObject(baseVal) ? deepMerge(baseVal, val) : val;
60
+ }
61
+ return out;
62
+ }
63
+ /** Merge a (partial) override over a full config, returning a new config. */
64
+ function mergeConfig(base, override) {
65
+ if (!isPlainObject(override)) return base;
66
+ return deepMerge(base, override);
67
+ }
68
+ /**
69
+ * Locate a config file: explicit (CLI arg, then `CLABOX_CONFIG` env),
70
+ * then CWD, then ~/.config. The CLI arg wins over the env var.
71
+ */
72
+ function findConfigFile(explicit) {
73
+ const chosen = explicit ?? env.CLABOX_CONFIG;
74
+ if (chosen) return expandHome(chosen);
75
+ return [
76
+ path.join(process.cwd(), "clabox.config.mjs"),
77
+ path.join(process.cwd(), "clabox.config.js"),
78
+ path.join(HOME, ".config", "clabox", "config.mjs")
79
+ ].find((c) => fs.existsSync(c)) ?? null;
80
+ }
81
+ /**
82
+ * Build the effective config: defaults โŠ• env โŠ• config file.
83
+ *
84
+ * @param explicitConfig optional config-file path (e.g. from `--config`);
85
+ * takes precedence over `CLABOX_CONFIG` and the default lookup locations.
86
+ */
87
+ async function loadConfig(explicitConfig) {
88
+ let cfg = defaultConfig;
89
+ const file = findConfigFile(explicitConfig);
90
+ if (file) {
91
+ const mod = await import(pathToFileURL(file).href);
92
+ const exported = mod.default ?? mod.config ?? mod;
93
+ cfg = mergeConfig(defaultConfig, typeof exported === "function" ? await exported(defaultConfig) : exported);
94
+ }
95
+ return {
96
+ config: cfg,
97
+ configFile: file
98
+ };
99
+ }
100
+ //#endregion
101
+ export { loadConfig as a, findConfigFile as i, defaultConfig as n, mergeConfig as o, expandHome as r, HOME as t };
102
+
103
+ //# sourceMappingURL=config-DXTNeUhH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-DXTNeUhH.js","names":[],"sources":["../src/utils/config.ts"],"sourcesContent":["// Configuration: sane defaults, env overrides, and an optional JS config file.\n//\n// Resolution order (later wins):\n// 1. defaultConfig (below)\n// 2. env vars (CLAUDE_CONFIG_DIR, CLABOX_*, โ€ฆ)\n// 3. a JS config file (see loadConfig)\n//\n// A config file default-exports either a plain object (merged over the defaults)\n// or a function `(defaults) => config` for full programmatic control.\n\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nexport const HOME = os.homedir();\n\n/** Expand a leading `~` / `~/` to the user's home directory. */\nexport function expandHome(p: string): string {\n if (!p) return p;\n if (p === '~') return HOME;\n if (p.startsWith('~/')) return path.join(HOME, p.slice(2));\n return p;\n}\n\nconst env = process.env;\n\n/** Dedicated git/ssh identity for commits made from inside the sandbox. */\nexport interface BotConfig {\n name: string;\n email: string;\n /** If `${sshDir}/id_ed25519` exists, git ssh is pinned to it. */\n sshDir: string;\n}\n\n/** Extra rules layered on top of the built-in base profile. */\nexport interface PathRules {\n /** RW subpaths (beyond project dir + configDir + /tmp). */\n readWrite: string[];\n /** RO subpaths. */\n readOnly: string[];\n /** process-exec subpaths. */\n exec: string[];\n /** explicit deny subpaths (read + write). */\n deny: string[];\n}\n\n/** Effective clabox configuration. */\nexport interface Config {\n /** Path to the `claude` binary. null โ†’ autodetect (PATH, then ~/.local/bin). */\n claudeBin: string | null;\n /** Claude config/profile directory โ€” supports multiple accounts. */\n configDir: string;\n /** Extra args always passed to `claude`, before any args from the CLI. */\n claudeArgs: string[];\n bot: BotConfig;\n /** Allow outbound network. `false` โ†’ no `(allow network*)` line. */\n network: boolean;\n /** Cap the process table inside the sandbox (fork-bomb guard). 0 โ†’ skip. */\n ulimitProcs: number;\n /** Extra directory granted read + execute inside the sandbox. null โ†’ disabled. */\n hooksDir: string | null;\n paths: PathRules;\n /** Home subdirectories denied entirely (read + write). */\n denyHome: string[];\n /** Dotfile config dirs under $HOME denied entirely. */\n denyDotConfigs: string[];\n}\n\n/** Built-in defaults. Everything here is meant to be overridable. */\nexport const defaultConfig: Config = {\n claudeBin: env.CLABOX_CLAUDE_BIN ?? null,\n configDir: env.CLAUDE_CONFIG_DIR ?? '~/.claude',\n claudeArgs: ['--settings', '{\"includeCoAuthoredBy\": false}'],\n bot: {\n name: env.CLABOX_BOT_NAME ?? 'claudeBOT',\n email: env.CLABOX_BOT_EMAIL ?? 'bot@example.com',\n sshDir: env.CLABOX_BOT_SSH_DIR ?? '~/.ssh/claudebot',\n },\n network: true,\n ulimitProcs: 1024,\n hooksDir: env.CLABOX_HOOKS_DIR ?? null,\n paths: {\n readWrite: [],\n readOnly: [],\n exec: [],\n deny: [],\n },\n denyHome: ['Documents', 'Desktop', 'Downloads', 'Pictures', 'Movies', 'Music'],\n // `.config/git` is always carved back out for git RO config in the profile.\n denyDotConfigs: ['aws', 'gnupg', 'kube', 'docker', 'config'],\n};\n\ntype Plain = Record<string, unknown>;\n\nfunction isPlainObject(v: unknown): v is Plain {\n return v != null && typeof v === 'object' && !Array.isArray(v);\n}\n\n/** Shallow-deep merge: nested plain objects merge, everything else replaces. */\nfunction deepMerge(base: Plain, override: Plain): Plain {\n const out: Plain = { ...base };\n for (const [key, val] of Object.entries(override)) {\n if (val === undefined) continue;\n const baseVal = base[key];\n out[key] = isPlainObject(val) && isPlainObject(baseVal) ? deepMerge(baseVal, val) : val;\n }\n return out;\n}\n\n/** Merge a (partial) override over a full config, returning a new config. */\nexport function mergeConfig(base: Config, override: unknown): Config {\n if (!isPlainObject(override)) return base;\n return deepMerge(base as unknown as Plain, override) as unknown as Config;\n}\n\n/**\n * Locate a config file: explicit (CLI arg, then `CLABOX_CONFIG` env),\n * then CWD, then ~/.config. The CLI arg wins over the env var.\n */\nexport function findConfigFile(explicit?: string | null): string | null {\n const chosen = explicit ?? env.CLABOX_CONFIG;\n if (chosen) return expandHome(chosen);\n const candidates = [\n path.join(process.cwd(), 'clabox.config.mjs'),\n path.join(process.cwd(), 'clabox.config.js'),\n path.join(HOME, '.config', 'clabox', 'config.mjs'),\n ];\n return candidates.find((c) => fs.existsSync(c)) ?? null;\n}\n\n/** Result of {@link loadConfig}: the effective config and the file it came from. */\nexport interface LoadedConfig {\n config: Config;\n configFile: string | null;\n}\n\n/**\n * Build the effective config: defaults โŠ• env โŠ• config file.\n *\n * @param explicitConfig optional config-file path (e.g. from `--config`);\n * takes precedence over `CLABOX_CONFIG` and the default lookup locations.\n */\nexport async function loadConfig(explicitConfig?: string | null): Promise<LoadedConfig> {\n let cfg: Config = defaultConfig;\n const file = findConfigFile(explicitConfig);\n if (file) {\n const mod = await import(pathToFileURL(file).href);\n const exported = mod.default ?? mod.config ?? mod;\n const resolved = typeof exported === 'function' ? await exported(defaultConfig) : exported;\n cfg = mergeConfig(defaultConfig, resolved);\n }\n return { config: cfg, configFile: file };\n}\n"],"mappings":";;;;;AAeA,MAAa,OAAO,GAAG,QAAQ;;AAG/B,SAAgB,WAAW,GAAmB;CAC5C,IAAI,CAAC,GAAG,OAAO;CACf,IAAI,MAAM,KAAK,OAAO;CACtB,IAAI,EAAE,WAAW,IAAI,GAAG,OAAO,KAAK,KAAK,MAAM,EAAE,MAAM,CAAC,CAAC;CACzD,OAAO;AACT;AAEA,MAAM,MAAM,QAAQ;;AA6CpB,MAAa,gBAAwB;CACnC,WAAW,IAAI,qBAAqB;CACpC,WAAW,IAAI,qBAAqB;CACpC,YAAY,CAAC,cAAc,kCAAgC;CAC3D,KAAK;EACH,MAAM,IAAI,mBAAmB;EAC7B,OAAO,IAAI,oBAAoB;EAC/B,QAAQ,IAAI,sBAAsB;CACpC;CACA,SAAS;CACT,aAAa;CACb,UAAU,IAAI,oBAAoB;CAClC,OAAO;EACL,WAAW,CAAC;EACZ,UAAU,CAAC;EACX,MAAM,CAAC;EACP,MAAM,CAAC;CACT;CACA,UAAU;EAAC;EAAa;EAAW;EAAa;EAAY;EAAU;CAAO;CAE7E,gBAAgB;EAAC;EAAO;EAAS;EAAQ;EAAU;CAAQ;AAC7D;AAIA,SAAS,cAAc,GAAwB;CAC7C,OAAO,KAAK,QAAQ,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;AAC/D;;AAGA,SAAS,UAAU,MAAa,UAAwB;CACtD,MAAM,MAAa,EAAE,GAAG,KAAK;CAC7B,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,QAAQ,GAAG;EACjD,IAAI,QAAQ,KAAA,GAAW;EACvB,MAAM,UAAU,KAAK;EACrB,IAAI,OAAO,cAAc,GAAG,KAAK,cAAc,OAAO,IAAI,UAAU,SAAS,GAAG,IAAI;CACtF;CACA,OAAO;AACT;;AAGA,SAAgB,YAAY,MAAc,UAA2B;CACnE,IAAI,CAAC,cAAc,QAAQ,GAAG,OAAO;CACrC,OAAO,UAAU,MAA0B,QAAQ;AACrD;;;;;AAMA,SAAgB,eAAe,UAAyC;CACtE,MAAM,SAAS,YAAY,IAAI;CAC/B,IAAI,QAAQ,OAAO,WAAW,MAAM;CAMpC,OAAO;EAJL,KAAK,KAAK,QAAQ,IAAI,GAAG,mBAAmB;EAC5C,KAAK,KAAK,QAAQ,IAAI,GAAG,kBAAkB;EAC3C,KAAK,KAAK,MAAM,WAAW,UAAU,YAAY;CAEnC,CAAC,CAAC,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,KAAK;AACrD;;;;;;;AAcA,eAAsB,WAAW,gBAAuD;CACtF,IAAI,MAAc;CAClB,MAAM,OAAO,eAAe,cAAc;CAC1C,IAAI,MAAM;EACR,MAAM,MAAM,MAAM,OAAO,cAAc,IAAI,CAAC,CAAC;EAC7C,MAAM,WAAW,IAAI,WAAW,IAAI,UAAU;EAE9C,MAAM,YAAY,eADD,OAAO,aAAa,aAAa,MAAM,SAAS,aAAa,IAAI,QACzC;CAC3C;CACA,OAAO;EAAE,QAAQ;EAAK,YAAY;CAAK;AACzC"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { a as PathRules, c as findConfigFile, i as LoadedConfig, l as loadConfig, n as Config, o as defaultConfig, r as HOME, s as expandHome, t as BotConfig, u as mergeConfig } from "./config-CUyriGxm.js";
2
+ import { a as ipcName, c as regex, i as globalName, l as subpath, n as buildProfile, o as literal, r as detectPackagePaths, s as reEscape, t as ProfileContext } from "./profile-Bw6L1MiV.js";
3
+ import { i as runClaude, n as generateProfile, r as profilePath, t as RunOptions } from "./run-BfF3Cwg7.js";
4
+ export { type BotConfig, type Config, HOME, type LoadedConfig, type PathRules, type ProfileContext, type RunOptions, buildProfile, defaultConfig, detectPackagePaths, expandHome, findConfigFile, generateProfile, globalName, ipcName, literal, loadConfig, mergeConfig, profilePath, reEscape, regex, runClaude, subpath };
package/lib/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import { a as loadConfig, i as findConfigFile, n as defaultConfig, o as mergeConfig, r as expandHome, t as HOME } from "./config-DXTNeUhH.js";
2
+ import { a as literal, c as subpath, i as ipcName, n as detectPackagePaths, o as reEscape, r as globalName, s as regex, t as buildProfile } from "./profile-CxqsgezL.js";
3
+ import { n as profilePath, r as runClaude, t as generateProfile } from "./run-Dyp_hW97.js";
4
+ export { HOME, buildProfile, defaultConfig, detectPackagePaths, expandHome, findConfigFile, generateProfile, globalName, ipcName, literal, loadConfig, mergeConfig, profilePath, reEscape, regex, runClaude, subpath };
@@ -0,0 +1,29 @@
1
+ import { n as Config } from "./config-CUyriGxm.js";
2
+
3
+ //#region src/sandbox/profile.d.ts
4
+ declare const subpath: (p: string) => string;
5
+ declare const literal: (p: string) => string;
6
+ declare const regex: (p: string) => string;
7
+ declare const globalName: (n: string) => string;
8
+ declare const ipcName: (n: string) => string;
9
+ /** Escape a path for safe embedding inside an SBPL regex. */
10
+ declare const reEscape: (s: string) => string;
11
+ /** Detect installed package managers whose paths must be readable/executable. */
12
+ declare function detectPackagePaths(): string[];
13
+ /** Context needed to assemble a profile for a specific project. */
14
+ interface ProfileContext {
15
+ projectDir: string;
16
+ detectedPaths?: string[];
17
+ }
18
+ /**
19
+ * Build the full SBPL profile text.
20
+ * @param config effective config (see config.ts)
21
+ * @param ctx { projectDir, detectedPaths }
22
+ */
23
+ declare function buildProfile(config: Config, {
24
+ projectDir,
25
+ detectedPaths
26
+ }: ProfileContext): string;
27
+ //#endregion
28
+ export { ipcName as a, regex as c, globalName as i, subpath as l, buildProfile as n, literal as o, detectPackagePaths as r, reEscape as s, ProfileContext as t };
29
+ //# sourceMappingURL=profile-Bw6L1MiV.d.ts.map
@@ -0,0 +1,103 @@
1
+ import { r as expandHome, t as HOME } from "./config-DXTNeUhH.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ //#region src/sandbox/profile.ts
5
+ const q = (s) => `"${String(s).replace(/"/g, "\\\"")}"`;
6
+ const subpath = (p) => `(subpath ${q(p)})`;
7
+ const literal = (p) => `(literal ${q(p)})`;
8
+ const regex = (p) => `(regex ${q(p)})`;
9
+ const globalName = (n) => `(global-name ${q(n)})`;
10
+ const ipcName = (n) => `(ipc-posix-name ${q(n)})`;
11
+ /** Escape a path for safe embedding inside an SBPL regex. */
12
+ const reEscape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+ function block(op, rules) {
14
+ return [
15
+ `(${op}`,
16
+ ...rules.map((r) => ` ${r}`),
17
+ ")"
18
+ ].join("\n");
19
+ }
20
+ const allow = (op, ...rules) => block(`allow ${op}`, rules);
21
+ const deny = (op, ...rules) => block(`deny ${op}`, rules);
22
+ /** Detect installed package managers whose paths must be readable/executable. */
23
+ function detectPackagePaths() {
24
+ const paths = [];
25
+ if (fs.existsSync("/opt/homebrew")) paths.push("/opt/homebrew");
26
+ else if (fs.existsSync("/usr/local/Homebrew")) paths.push("/usr/local/Homebrew");
27
+ const local = path.join(HOME, ".local");
28
+ if (fs.existsSync(local)) paths.push(local);
29
+ if (fs.existsSync("/nix/store")) paths.push("/nix/store");
30
+ return paths;
31
+ }
32
+ /**
33
+ * Build the full SBPL profile text.
34
+ * @param config effective config (see config.ts)
35
+ * @param ctx { projectDir, detectedPaths }
36
+ */
37
+ function buildProfile(config, { projectDir, detectedPaths = detectPackagePaths() }) {
38
+ const configDir = expandHome(config.configDir);
39
+ const sshDir = expandHome(config.bot.sshDir);
40
+ const hooksDir = config.hooksDir ? expandHome(config.hooksDir) : null;
41
+ const homeRe = reEscape(HOME);
42
+ const sections = [];
43
+ const add = (comment, body) => sections.push(`;; ---------- ${comment}\n${body}`);
44
+ sections.push([
45
+ ";; ------------------------------------------------------------------",
46
+ ";; Claude Code macOS sandbox profile (autogenerated)",
47
+ ";; ------------------------------------------------------------------",
48
+ "(version 1)",
49
+ "(deny default)"
50
+ ].join("\n"));
51
+ add("introspection & sysctl", "(allow file-read-metadata)\n(allow sysctl-read)");
52
+ add("basic dir traversal", [
53
+ allow("file-read*", literal("/")),
54
+ allow("file-read*", literal("/private")),
55
+ allow("file-read-data", literal("/Users")),
56
+ allow("file-read-data", literal(HOME))
57
+ ].join("\n"));
58
+ add("system runtime (read-only)", allow("file-read* file-map-executable", subpath("/System"), subpath("/usr"), subpath("/bin"), subpath("/sbin"), subpath("/Library/Frameworks"), subpath("/private/etc"), subpath("/var/db/dyld"), ...detectedPaths.map(subpath)));
59
+ add("Xcode / Command Line Tools (xcrun, git, etc.)", [allow("file-read* file-map-executable", subpath("/Library/Developer/CommandLineTools"), subpath("/Applications/Xcode.app")), allow("process-exec", subpath("/Library/Developer/CommandLineTools"), subpath("/Applications/Xcode.app"))].join("\n"));
60
+ const userPaths = detectedPaths.filter((p) => p.endsWith("/.local"));
61
+ add("global npm/pipx/cargo bins", userPaths.length ? userPaths.map((p) => allow("file-read*", subpath(p))).join("\n") : ";; No user package paths detected");
62
+ add("executable paths", allow("process-exec", subpath("/usr"), subpath("/System"), subpath("/bin"), subpath("/sbin"), literal("/usr/bin/env"), ...detectedPaths.map(subpath)));
63
+ add("temp dirs", allow("file-read* file-write*", subpath("/tmp"), subpath("/private/tmp"), regex("^/private/var/folders/")));
64
+ add("Claude config & token files", allow("file-read* file-write*", subpath(configDir)));
65
+ add("Claude auto-update (RO) -- suppress warnings", allow("file-read*", subpath(path.join(HOME, ".local/state/claude")), subpath(path.join(HOME, ".cache/claude"))));
66
+ add("time-zone & prefs (RO)", allow("file-read*", subpath("/private/var/db/timezone"), subpath("/Library/Preferences")));
67
+ add("/dev access (RO) + ioctl", [
68
+ allow("file-read*", literal("/dev")),
69
+ allow("file-read* file-write*", regex("^/dev/(tty.*|null|zero|dtracehelper)")),
70
+ allow("file-ioctl", literal("/dev/dtracehelper"), regex("^/dev/tty.*"))
71
+ ].join("\n"));
72
+ add("mach-lookup services", allow("mach-lookup", globalName("com.apple.system.opendirectoryd.libinfo"), globalName("com.apple.SystemConfiguration.DNSConfiguration"), globalName("com.apple.coreservices.launchservicesd"), globalName("com.apple.CoreServices.coreservicesd"), globalName("com.apple.system.notification_center"), globalName("com.apple.logd"), globalName("com.apple.diagnosticd"), globalName("com.apple.lsd.mapdb"), globalName("com.apple.lsd.modifydb"), globalName("com.apple.coreservices.quarantine-resolver"), globalName("com.apple.pasteboard.pboard"), globalName("com.apple.pasteboard.1")));
73
+ add("Launch Services needed by /usr/bin/open", allow("mach-lookup", regex("^com\\.apple\\.lsd(\\..*)?$")));
74
+ add("Developer Tools (xcrun / libxcrun)", allow("mach-lookup", globalName("com.apple.dt.xcsecurity"), regex("^com\\.apple\\.dt\\..*$")));
75
+ add("Audio (afplay)", allow("mach-lookup", globalName("com.apple.audio.audiohald"), globalName("com.apple.audio.AudioComponentRegistrar")));
76
+ add("Notification Center shared-memory (RO)", allow("ipc-posix-shm-read-data", ipcName("apple.shm.notification_center")));
77
+ add("User-level preference reads (RO)", allow("file-read*", subpath(path.join(HOME, "Library/Preferences"))));
78
+ add("Keychain access (for OAuth)", [allow("file-read* file-write*", subpath(path.join(HOME, "Library/Keychains"))), allow("mach-lookup", globalName("com.apple.SecurityServer"), globalName("com.apple.security.agent"), globalName("com.apple.securityd"), globalName("com.apple.secd"), globalName("com.apple.trustd"), globalName("com.apple.trustd.agent"), globalName("com.apple.CoreAuthentication.daemon"))].join("\n"));
79
+ add("git config (RO)", allow("file-read*", literal(path.join(HOME, ".gitconfig")), literal(path.join(HOME, ".gitignore_global")), subpath(path.join(HOME, ".config/git"))));
80
+ const denyRules = [...config.denyHome.map((d) => subpath(path.join(HOME, d)))];
81
+ if (config.denyDotConfigs.length) denyRules.push(regex(`^${homeRe}/\\.(${config.denyDotConfigs.join("|")})($|/)`));
82
+ denyRules.push(...config.paths.deny.map((p) => subpath(expandHome(p))));
83
+ add("explicit sensitive DENY list", deny("file-read* file-write*", ...denyRules));
84
+ add("SSH: bot key only, deny other keys", [
85
+ allow("file-read*", literal(path.join(HOME, ".ssh")), literal(path.join(HOME, ".ssh/known_hosts")), literal(path.join(HOME, ".ssh/known_hosts2")), literal(path.join(HOME, ".ssh/config")), subpath(sshDir)),
86
+ allow("file-write*", literal(path.join(HOME, ".ssh/known_hosts")), literal(path.join(HOME, ".ssh/known_hosts2"))),
87
+ deny("file-read* file-write*", regex(`^${homeRe}/\\.ssh/id_`), regex(`^${homeRe}/\\.ssh/.*\\.pem$`), regex(`^${homeRe}/\\.ssh/.*\\.key$`))
88
+ ].join("\n"));
89
+ add("claude hooks (RO + exec)", hooksDir && fs.existsSync(hooksDir) ? [allow("file-read* file-map-executable", subpath(hooksDir)), allow("process-exec", subpath(hooksDir))].join("\n") : ";; (no hooks dir; set config.hooksDir / CLABOX_HOOKS_DIR to enable)");
90
+ if (config.paths.readOnly.length) add("extra read-only paths", allow("file-read*", ...config.paths.readOnly.map((p) => subpath(expandHome(p)))));
91
+ if (config.paths.readWrite.length) add("extra read-write paths", allow("file-read* file-write*", ...config.paths.readWrite.map((p) => subpath(expandHome(p)))));
92
+ if (config.paths.exec.length) add("extra exec paths", allow("process-exec", ...config.paths.exec.map((p) => subpath(expandHome(p)))));
93
+ add("project workspace (RW)", [allow("file-read* file-write* file-map-executable", subpath(projectDir)), allow("process-exec", subpath(projectDir))].join("\n"));
94
+ if (config.network) add("networking", "(allow network*)");
95
+ sections.push("(allow process-fork)\n(allow lsopen)");
96
+ const text = `${sections.join("\n\n")}\n`;
97
+ if (!/^\(version 1\)/m.test(text)) throw new Error("generated sandbox profile is missing \"(version 1)\"");
98
+ return text;
99
+ }
100
+ //#endregion
101
+ export { literal as a, subpath as c, ipcName as i, detectPackagePaths as n, reEscape as o, globalName as r, regex as s, buildProfile as t };
102
+
103
+ //# sourceMappingURL=profile-CxqsgezL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile-CxqsgezL.js","names":[],"sources":["../src/sandbox/profile.ts"],"sourcesContent":["// Seatbelt (SBPL) profile generator.\n//\n// The old bash version baked the profile into a heredoc and patched it with\n// sed. Here the profile is assembled from small typed helpers, so the parts\n// you actually want to tweak live in config.ts as plain data.\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { type Config, expandHome, HOME } from '../utils/config.js';\n\n// ---- SBPL helpers ----------------------------------------------------------\n\n// Quote a literal string for SBPL. Only `\"` needs escaping; backslashes are\n// left as-is so regex patterns survive verbatim.\nconst q = (s: string): string => `\"${String(s).replace(/\"/g, '\\\\\"')}\"`;\n\nexport const subpath = (p: string): string => `(subpath ${q(p)})`;\nexport const literal = (p: string): string => `(literal ${q(p)})`;\nexport const regex = (p: string): string => `(regex ${q(p)})`;\nexport const globalName = (n: string): string => `(global-name ${q(n)})`;\nexport const ipcName = (n: string): string => `(ipc-posix-name ${q(n)})`;\n\n/** Escape a path for safe embedding inside an SBPL regex. */\nexport const reEscape = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nfunction block(op: string, rules: string[]): string {\n return [`(${op}`, ...rules.map((r) => ` ${r}`), ')'].join('\\n');\n}\nconst allow = (op: string, ...rules: string[]): string => block(`allow ${op}`, rules);\nconst deny = (op: string, ...rules: string[]): string => block(`deny ${op}`, rules);\n\n// ---- package-manager autodetection ----------------------------------------\n\n/** Detect installed package managers whose paths must be readable/executable. */\nexport function detectPackagePaths(): string[] {\n const paths: string[] = [];\n if (fs.existsSync('/opt/homebrew')) paths.push('/opt/homebrew');\n else if (fs.existsSync('/usr/local/Homebrew')) paths.push('/usr/local/Homebrew');\n const local = path.join(HOME, '.local');\n if (fs.existsSync(local)) paths.push(local);\n if (fs.existsSync('/nix/store')) paths.push('/nix/store');\n return paths;\n}\n\n/** Context needed to assemble a profile for a specific project. */\nexport interface ProfileContext {\n projectDir: string;\n detectedPaths?: string[];\n}\n\n/**\n * Build the full SBPL profile text.\n * @param config effective config (see config.ts)\n * @param ctx { projectDir, detectedPaths }\n */\nexport function buildProfile(\n config: Config,\n { projectDir, detectedPaths = detectPackagePaths() }: ProfileContext,\n): string {\n const configDir = expandHome(config.configDir);\n const sshDir = expandHome(config.bot.sshDir);\n const hooksDir = config.hooksDir ? expandHome(config.hooksDir) : null;\n const homeRe = reEscape(HOME);\n\n const sections: string[] = [];\n const add = (comment: string, body: string) => sections.push(`;; ---------- ${comment}\\n${body}`);\n\n sections.push(\n [\n ';; ------------------------------------------------------------------',\n ';; Claude Code macOS sandbox profile (autogenerated)',\n ';; ------------------------------------------------------------------',\n '(version 1)',\n '(deny default)',\n ].join('\\n'),\n );\n\n add('introspection & sysctl', '(allow file-read-metadata)\\n(allow sysctl-read)');\n\n add(\n 'basic dir traversal',\n [\n allow('file-read*', literal('/')),\n allow('file-read*', literal('/private')),\n allow('file-read-data', literal('/Users')),\n allow('file-read-data', literal(HOME)),\n ].join('\\n'),\n );\n\n add(\n 'system runtime (read-only)',\n allow(\n 'file-read* file-map-executable',\n subpath('/System'),\n subpath('/usr'),\n subpath('/bin'),\n subpath('/sbin'),\n subpath('/Library/Frameworks'),\n subpath('/private/etc'),\n subpath('/var/db/dyld'),\n ...detectedPaths.map(subpath),\n ),\n );\n\n add(\n 'Xcode / Command Line Tools (xcrun, git, etc.)',\n [\n allow(\n 'file-read* file-map-executable',\n subpath('/Library/Developer/CommandLineTools'),\n subpath('/Applications/Xcode.app'),\n ),\n allow(\n 'process-exec',\n subpath('/Library/Developer/CommandLineTools'),\n subpath('/Applications/Xcode.app'),\n ),\n ].join('\\n'),\n );\n\n // global npm/pipx/cargo bins (user-installed)\n const userPaths = detectedPaths.filter((p) => p.endsWith('/.local'));\n add(\n 'global npm/pipx/cargo bins',\n userPaths.length\n ? userPaths.map((p) => allow('file-read*', subpath(p))).join('\\n')\n : ';; No user package paths detected',\n );\n\n add(\n 'executable paths',\n allow(\n 'process-exec',\n subpath('/usr'),\n subpath('/System'),\n subpath('/bin'),\n subpath('/sbin'),\n literal('/usr/bin/env'),\n ...detectedPaths.map(subpath),\n ),\n );\n\n add(\n 'temp dirs',\n allow(\n 'file-read* file-write*',\n subpath('/tmp'),\n subpath('/private/tmp'),\n regex('^/private/var/folders/'),\n ),\n );\n\n add('Claude config & token files', allow('file-read* file-write*', subpath(configDir)));\n\n add(\n 'Claude auto-update (RO) -- suppress warnings',\n allow(\n 'file-read*',\n subpath(path.join(HOME, '.local/state/claude')),\n subpath(path.join(HOME, '.cache/claude')),\n ),\n );\n\n add(\n 'time-zone & prefs (RO)',\n allow('file-read*', subpath('/private/var/db/timezone'), subpath('/Library/Preferences')),\n );\n\n add(\n '/dev access (RO) + ioctl',\n [\n allow('file-read*', literal('/dev')),\n allow('file-read* file-write*', regex('^/dev/(tty.*|null|zero|dtracehelper)')),\n allow('file-ioctl', literal('/dev/dtracehelper'), regex('^/dev/tty.*')),\n ].join('\\n'),\n );\n\n add(\n 'mach-lookup services',\n allow(\n 'mach-lookup',\n globalName('com.apple.system.opendirectoryd.libinfo'),\n globalName('com.apple.SystemConfiguration.DNSConfiguration'),\n globalName('com.apple.coreservices.launchservicesd'),\n globalName('com.apple.CoreServices.coreservicesd'),\n globalName('com.apple.system.notification_center'),\n globalName('com.apple.logd'),\n globalName('com.apple.diagnosticd'),\n globalName('com.apple.lsd.mapdb'),\n globalName('com.apple.lsd.modifydb'),\n globalName('com.apple.coreservices.quarantine-resolver'),\n globalName('com.apple.pasteboard.pboard'),\n globalName('com.apple.pasteboard.1'),\n ),\n );\n\n add(\n 'Launch Services needed by /usr/bin/open',\n allow('mach-lookup', regex('^com\\\\.apple\\\\.lsd(\\\\..*)?$')),\n );\n\n add(\n 'Developer Tools (xcrun / libxcrun)',\n allow('mach-lookup', globalName('com.apple.dt.xcsecurity'), regex('^com\\\\.apple\\\\.dt\\\\..*$')),\n );\n\n add(\n 'Audio (afplay)',\n allow(\n 'mach-lookup',\n globalName('com.apple.audio.audiohald'),\n globalName('com.apple.audio.AudioComponentRegistrar'),\n ),\n );\n\n add(\n 'Notification Center shared-memory (RO)',\n allow('ipc-posix-shm-read-data', ipcName('apple.shm.notification_center')),\n );\n\n add(\n 'User-level preference reads (RO)',\n allow('file-read*', subpath(path.join(HOME, 'Library/Preferences'))),\n );\n\n // Keychain RW so Claude can persist refreshed OAuth tokens (else ~24h โ†’ 401).\n add(\n 'Keychain access (for OAuth)',\n [\n allow('file-read* file-write*', subpath(path.join(HOME, 'Library/Keychains'))),\n allow(\n 'mach-lookup',\n globalName('com.apple.SecurityServer'),\n globalName('com.apple.security.agent'),\n globalName('com.apple.securityd'),\n globalName('com.apple.secd'),\n globalName('com.apple.trustd'),\n globalName('com.apple.trustd.agent'),\n globalName('com.apple.CoreAuthentication.daemon'),\n ),\n ].join('\\n'),\n );\n\n add(\n 'git config (RO)',\n allow(\n 'file-read*',\n literal(path.join(HOME, '.gitconfig')),\n literal(path.join(HOME, '.gitignore_global')),\n subpath(path.join(HOME, '.config/git')),\n ),\n );\n\n // Explicit deny list โ€” wins even over allows above.\n const denyRules = [...config.denyHome.map((d) => subpath(path.join(HOME, d)))];\n if (config.denyDotConfigs.length) {\n denyRules.push(regex(`^${homeRe}/\\\\.(${config.denyDotConfigs.join('|')})($|/)`));\n }\n denyRules.push(...config.paths.deny.map((p) => subpath(expandHome(p))));\n add('explicit sensitive DENY list', deny('file-read* file-write*', ...denyRules));\n\n add(\n 'SSH: bot key only, deny other keys',\n [\n allow(\n 'file-read*',\n literal(path.join(HOME, '.ssh')),\n literal(path.join(HOME, '.ssh/known_hosts')),\n literal(path.join(HOME, '.ssh/known_hosts2')),\n literal(path.join(HOME, '.ssh/config')),\n subpath(sshDir),\n ),\n allow(\n 'file-write*',\n literal(path.join(HOME, '.ssh/known_hosts')),\n literal(path.join(HOME, '.ssh/known_hosts2')),\n ),\n deny(\n 'file-read* file-write*',\n regex(`^${homeRe}/\\\\.ssh/id_`),\n regex(`^${homeRe}/\\\\.ssh/.*\\\\.pem$`),\n regex(`^${homeRe}/\\\\.ssh/.*\\\\.key$`),\n ),\n ].join('\\n'),\n );\n\n add(\n 'claude hooks (RO + exec)',\n hooksDir && fs.existsSync(hooksDir)\n ? [\n allow('file-read* file-map-executable', subpath(hooksDir)),\n allow('process-exec', subpath(hooksDir)),\n ].join('\\n')\n : ';; (no hooks dir; set config.hooksDir / CLABOX_HOOKS_DIR to enable)',\n );\n\n // Extra user-supplied RO / RW / exec rules.\n if (config.paths.readOnly.length)\n add(\n 'extra read-only paths',\n allow('file-read*', ...config.paths.readOnly.map((p) => subpath(expandHome(p)))),\n );\n if (config.paths.readWrite.length)\n add(\n 'extra read-write paths',\n allow('file-read* file-write*', ...config.paths.readWrite.map((p) => subpath(expandHome(p)))),\n );\n if (config.paths.exec.length)\n add(\n 'extra exec paths',\n allow('process-exec', ...config.paths.exec.map((p) => subpath(expandHome(p)))),\n );\n\n add(\n 'project workspace (RW)',\n [\n allow('file-read* file-write* file-map-executable', subpath(projectDir)),\n allow('process-exec', subpath(projectDir)),\n ].join('\\n'),\n );\n\n if (config.network) add('networking', '(allow network*)');\n\n sections.push('(allow process-fork)\\n(allow lsopen)');\n\n const text = `${sections.join('\\n\\n')}\\n`;\n\n // Sanity-check before anyone feeds it to sandbox-exec.\n if (!/^\\(version 1\\)/m.test(text)) {\n throw new Error('generated sandbox profile is missing \"(version 1)\"');\n }\n return text;\n}\n"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { n as Config } from "./config-CUyriGxm.js";
2
+
3
+ //#region src/sandbox/run.d.ts
4
+ /** Deterministic per-project profile path under TMPDIR. */
5
+ declare function profilePath(projectDir?: string): string;
6
+ /** Generate the profile file for the current project, return its path. */
7
+ declare function generateProfile(config: Config, projectDir?: string): string;
8
+ /** Options accepted by {@link runClaude}. */
9
+ interface RunOptions {
10
+ configFile?: string | null;
11
+ }
12
+ /** Generate the profile and exec claude under sandbox-exec. Returns exit code. */
13
+ declare function runClaude(config: Config, claudeArgs: string[], {
14
+ configFile
15
+ }?: RunOptions): number;
16
+ //#endregion
17
+ export { runClaude as i, generateProfile as n, profilePath as r, RunOptions as t };
18
+ //# sourceMappingURL=run-BfF3Cwg7.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { r as expandHome, t as HOME } from "./config-DXTNeUhH.js";
2
+ import { n as detectPackagePaths, t as buildProfile } from "./profile-CxqsgezL.js";
3
+ import { execFileSync, spawnSync } from "node:child_process";
4
+ import crypto from "node:crypto";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ //#region src/sandbox/run.ts
8
+ const TMPDIR = (process.env.TMPDIR || "/tmp").replace(/\/$/, "");
9
+ /** Deterministic per-project profile path under TMPDIR. */
10
+ function profilePath(projectDir = process.cwd()) {
11
+ const hash = crypto.createHash("sha256").update(projectDir).digest("hex").slice(0, 8);
12
+ return path.join(TMPDIR, `clabox-${path.basename(projectDir)}-${hash}.sb`);
13
+ }
14
+ function which(bin) {
15
+ try {
16
+ return execFileSync("command", ["-v", bin], {
17
+ shell: "/bin/sh",
18
+ encoding: "utf8"
19
+ }).trim() || null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ function requireSandboxExec() {
25
+ if (!which("sandbox-exec")) throw new Error("sandbox-exec not found. This tool requires macOS with sandbox-exec.");
26
+ }
27
+ function resolveClaudeBin(config) {
28
+ const candidate = config.claudeBin || which("claude") || path.join(HOME, ".local/bin/claude");
29
+ if (!candidate || !fs.existsSync(candidate)) throw new Error(`claude not found at '${candidate}'`);
30
+ return candidate;
31
+ }
32
+ /** Generate the profile file for the current project, return its path. */
33
+ function generateProfile(config, projectDir = process.cwd()) {
34
+ requireSandboxExec();
35
+ const file = profilePath(projectDir);
36
+ const text = buildProfile(config, {
37
+ projectDir,
38
+ detectedPaths: detectPackagePaths()
39
+ });
40
+ fs.writeFileSync(file, text);
41
+ return file;
42
+ }
43
+ /** Build the `env KEY=VALUE โ€ฆ` argument list forced onto the sandboxed claude. */
44
+ function buildEnvArgs(config) {
45
+ const sshDir = expandHome(config.bot.sshDir);
46
+ const botKey = path.join(sshDir, "id_ed25519");
47
+ const botCfg = path.join(sshDir, "config");
48
+ const args = [
49
+ `PATH=${path.join(HOME, ".local/bin")}:${process.env.PATH || ""}`,
50
+ `CLAUDE_CONFIG_DIR=${expandHome(config.configDir)}`,
51
+ "DISABLE_AUTOUPDATER=1",
52
+ "NPM_CONFIG_USERCONFIG=/dev/null",
53
+ `GIT_AUTHOR_NAME=${config.bot.name}`,
54
+ `GIT_AUTHOR_EMAIL=${config.bot.email}`,
55
+ `GIT_COMMITTER_NAME=${config.bot.name}`,
56
+ `GIT_COMMITTER_EMAIL=${config.bot.email}`,
57
+ "GIT_CONFIG_COUNT=2",
58
+ "GIT_CONFIG_KEY_0=commit.gpgsign",
59
+ "GIT_CONFIG_VALUE_0=false",
60
+ "GIT_CONFIG_KEY_1=tag.gpgsign",
61
+ "GIT_CONFIG_VALUE_1=false"
62
+ ];
63
+ if (fs.existsSync(botKey)) args.push(`GIT_SSH_COMMAND=ssh -F ${botCfg} -i ${botKey} -o IdentitiesOnly=yes -o IdentityAgent=none`);
64
+ return args;
65
+ }
66
+ /** Generate the profile and exec claude under sandbox-exec. Returns exit code. */
67
+ function runClaude(config, claudeArgs, { configFile } = {}) {
68
+ const projectDir = process.cwd();
69
+ const claudeBin = resolveClaudeBin(config);
70
+ const profileFile = generateProfile(config, projectDir);
71
+ if (process.env.CLABOX_DEBUG) {
72
+ console.error(`โ†’ Running Claude Code sandboxed in: ${projectDir}`);
73
+ console.error(`โ†’ Profile: ${profileFile}`);
74
+ console.error(`โ†’ Config: ${expandHome(config.configDir)}`);
75
+ if (configFile) console.error(`โ†’ Config file: ${configFile}`);
76
+ }
77
+ const title = projectDir.startsWith(HOME) ? `~${projectDir.slice(HOME.length)}` : projectDir;
78
+ process.stdout.write(`\x1b]0;${title}\x07`);
79
+ const envArgs = buildEnvArgs(config);
80
+ const defaultArgs = Array.isArray(config.claudeArgs) ? config.claudeArgs : [];
81
+ const inner = [
82
+ "sandbox-exec",
83
+ "-f",
84
+ profileFile,
85
+ "env",
86
+ ...envArgs,
87
+ claudeBin,
88
+ ...defaultArgs,
89
+ ...claudeArgs
90
+ ];
91
+ const res = spawnSync("/bin/sh", [
92
+ "-c",
93
+ `${config.ulimitProcs > 0 ? `ulimit -u ${config.ulimitProcs} 2>/dev/null; ` : ""}exec "$@"`,
94
+ "sh",
95
+ ...inner
96
+ ], { stdio: "inherit" });
97
+ if (res.error) throw res.error;
98
+ if (res.signal) return 1;
99
+ return res.status ?? 0;
100
+ }
101
+ //#endregion
102
+ export { profilePath as n, runClaude as r, generateProfile as t };
103
+
104
+ //# sourceMappingURL=run-Dyp_hW97.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-Dyp_hW97.js","names":[],"sources":["../src/sandbox/run.ts"],"sourcesContent":["// Profile materialization + launching `claude` under sandbox-exec.\n\nimport { execFileSync, spawnSync } from 'node:child_process';\nimport crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { type Config, expandHome, HOME } from '../utils/config.js';\nimport { buildProfile, detectPackagePaths } from './profile.js';\n\nconst TMPDIR = (process.env.TMPDIR || '/tmp').replace(/\\/$/, '');\n\n/** Deterministic per-project profile path under TMPDIR. */\nexport function profilePath(projectDir: string = process.cwd()): string {\n const hash = crypto.createHash('sha256').update(projectDir).digest('hex').slice(0, 8);\n return path.join(TMPDIR, `clabox-${path.basename(projectDir)}-${hash}.sb`);\n}\n\nfunction which(bin: string): string | null {\n try {\n return (\n execFileSync('command', ['-v', bin], { shell: '/bin/sh', encoding: 'utf8' }).trim() || null\n );\n } catch {\n return null;\n }\n}\n\nfunction requireSandboxExec(): void {\n if (!which('sandbox-exec')) {\n throw new Error('sandbox-exec not found. This tool requires macOS with sandbox-exec.');\n }\n}\n\nfunction resolveClaudeBin(config: Config): string {\n const candidate = config.claudeBin || which('claude') || path.join(HOME, '.local/bin/claude');\n if (!candidate || !fs.existsSync(candidate)) {\n throw new Error(`claude not found at '${candidate}'`);\n }\n return candidate;\n}\n\n/** Generate the profile file for the current project, return its path. */\nexport function generateProfile(config: Config, projectDir: string = process.cwd()): string {\n requireSandboxExec();\n const file = profilePath(projectDir);\n const text = buildProfile(config, { projectDir, detectedPaths: detectPackagePaths() });\n fs.writeFileSync(file, text);\n return file;\n}\n\n/** Build the `env KEY=VALUE โ€ฆ` argument list forced onto the sandboxed claude. */\nfunction buildEnvArgs(config: Config): string[] {\n const sshDir = expandHome(config.bot.sshDir);\n const botKey = path.join(sshDir, 'id_ed25519');\n const botCfg = path.join(sshDir, 'config');\n const args = [\n `PATH=${path.join(HOME, '.local/bin')}:${process.env.PATH || ''}`,\n `CLAUDE_CONFIG_DIR=${expandHome(config.configDir)}`,\n 'DISABLE_AUTOUPDATER=1',\n 'NPM_CONFIG_USERCONFIG=/dev/null',\n `GIT_AUTHOR_NAME=${config.bot.name}`,\n `GIT_AUTHOR_EMAIL=${config.bot.email}`,\n `GIT_COMMITTER_NAME=${config.bot.name}`,\n `GIT_COMMITTER_EMAIL=${config.bot.email}`,\n 'GIT_CONFIG_COUNT=2',\n 'GIT_CONFIG_KEY_0=commit.gpgsign',\n 'GIT_CONFIG_VALUE_0=false',\n 'GIT_CONFIG_KEY_1=tag.gpgsign',\n 'GIT_CONFIG_VALUE_1=false',\n ];\n // Pin git ssh to the bot key only when it actually exists, so the sandbox\n // stays usable without a dedicated bot key configured.\n if (fs.existsSync(botKey)) {\n args.push(\n `GIT_SSH_COMMAND=ssh -F ${botCfg} -i ${botKey} -o IdentitiesOnly=yes -o IdentityAgent=none`,\n );\n }\n return args;\n}\n\n/** Options accepted by {@link runClaude}. */\nexport interface RunOptions {\n configFile?: string | null;\n}\n\n/** Generate the profile and exec claude under sandbox-exec. Returns exit code. */\nexport function runClaude(\n config: Config,\n claudeArgs: string[],\n { configFile }: RunOptions = {},\n): number {\n const projectDir = process.cwd();\n const claudeBin = resolveClaudeBin(config);\n const profileFile = generateProfile(config, projectDir);\n\n if (process.env.CLABOX_DEBUG) {\n console.error(`โ†’ Running Claude Code sandboxed in: ${projectDir}`);\n console.error(`โ†’ Profile: ${profileFile}`);\n console.error(`โ†’ Config: ${expandHome(config.configDir)}`);\n if (configFile) console.error(`โ†’ Config file: ${configFile}`);\n }\n\n // Terminal title = cwd (with ~ for $HOME), matching the bash version.\n const title = projectDir.startsWith(HOME) ? `~${projectDir.slice(HOME.length)}` : projectDir;\n process.stdout.write(`\\x1b]0;${title}\\x07`);\n\n const envArgs = buildEnvArgs(config);\n const defaultArgs = Array.isArray(config.claudeArgs) ? config.claudeArgs : [];\n const inner = [\n 'sandbox-exec',\n '-f',\n profileFile,\n 'env',\n ...envArgs,\n claudeBin,\n ...defaultArgs,\n ...claudeArgs,\n ];\n\n // `ulimit` is a shell builtin; run the whole thing under sh so we can set it.\n // `exec \"$@\"` keeps argv intact without re-quoting (args start after $0=sh).\n const ulimit = config.ulimitProcs > 0 ? `ulimit -u ${config.ulimitProcs} 2>/dev/null; ` : '';\n const res = spawnSync('/bin/sh', ['-c', `${ulimit}exec \"$@\"`, 'sh', ...inner], {\n stdio: 'inherit',\n });\n if (res.error) throw res.error;\n if (res.signal) return 1;\n return res.status ?? 0;\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { a as ipcName, c as regex, i as globalName, l as subpath, n as buildProfile, o as literal, r as detectPackagePaths, s as reEscape, t as ProfileContext } from "../profile-Bw6L1MiV.js";
2
+ export { ProfileContext, buildProfile, detectPackagePaths, globalName, ipcName, literal, reEscape, regex, subpath };
@@ -0,0 +1,2 @@
1
+ import { a as literal, c as subpath, i as ipcName, n as detectPackagePaths, o as reEscape, r as globalName, s as regex, t as buildProfile } from "../profile-CxqsgezL.js";
2
+ export { buildProfile, detectPackagePaths, globalName, ipcName, literal, reEscape, regex, subpath };
@@ -0,0 +1,2 @@
1
+ import { i as runClaude, n as generateProfile, r as profilePath, t as RunOptions } from "../run-BfF3Cwg7.js";
2
+ export { RunOptions, generateProfile, profilePath, runClaude };
@@ -0,0 +1,2 @@
1
+ import { n as profilePath, r as runClaude, t as generateProfile } from "../run-Dyp_hW97.js";
2
+ export { generateProfile, profilePath, runClaude };
@@ -0,0 +1,2 @@
1
+ import { a as PathRules, c as findConfigFile, i as LoadedConfig, l as loadConfig, n as Config, o as defaultConfig, r as HOME, s as expandHome, t as BotConfig, u as mergeConfig } from "../config-CUyriGxm.js";
2
+ export { BotConfig, Config, HOME, LoadedConfig, PathRules, defaultConfig, expandHome, findConfigFile, loadConfig, mergeConfig };
@@ -0,0 +1,2 @@
1
+ import { a as loadConfig, i as findConfigFile, n as defaultConfig, o as mergeConfig, r as expandHome, t as HOME } from "../config-DXTNeUhH.js";
2
+ export { HOME, defaultConfig, expandHome, findConfigFile, loadConfig, mergeConfig };
package/package.json CHANGED
@@ -1,12 +1,146 @@
1
1
  {
2
2
  "name": "clabox",
3
- "version": "0.0.1",
4
- "description": "",
5
- "main": "index.js",
3
+ "version": "0.0.2",
4
+ "description": "Run Claude Code in a sandbox for super-safe YOLO mode",
5
+ "type": "module",
6
+ "author": "Igor Suvorov",
7
+ "license": "MIT",
8
+ "bin": {
9
+ "clabox": "lib/cli.js"
10
+ },
11
+ "main": "lib/index.js",
12
+ "types": "lib/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib/index.js",
16
+ "types": "./lib/index.d.ts",
17
+ "default": "./lib/index.js"
18
+ },
19
+ "./cli": {
20
+ "import": "./lib/cli.js",
21
+ "types": "./lib/cli.d.ts",
22
+ "default": "./lib/cli.js"
23
+ },
24
+ "./config": {
25
+ "import": "./lib/utils/config.js",
26
+ "types": "./lib/utils/config.d.ts",
27
+ "default": "./lib/utils/config.js"
28
+ },
29
+ "./profile": {
30
+ "import": "./lib/sandbox/profile.js",
31
+ "types": "./lib/sandbox/profile.d.ts",
32
+ "default": "./lib/sandbox/profile.js"
33
+ },
34
+ "./run": {
35
+ "import": "./lib/sandbox/run.js",
36
+ "types": "./lib/sandbox/run.d.ts",
37
+ "default": "./lib/sandbox/run.js"
38
+ },
39
+ "./*": "./lib/*"
40
+ },
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "files": [
45
+ "lib",
46
+ "docs",
47
+ "clabox.config.example.mjs",
48
+ "README.md",
49
+ "LICENSE"
50
+ ],
6
51
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
52
+ "build:tsdown": "tsdown",
53
+ "build:tsdown:release": "tsdown --out-dir lib",
54
+ "build": "bun run build:tsdown:release",
55
+ "dev": "tsdown --watch",
56
+ "cli": "bun run src/cli.ts",
57
+ "generate": "bun run src/cli.ts generate",
58
+ "test:unit": "bun test",
59
+ "test:unit:coverage": "bun test --coverage",
60
+ "test:unit:watch": "bun test --watch",
61
+ "test:types": "tsc --noEmit",
62
+ "test:lint": "biome lint",
63
+ "test:size": "size-limit",
64
+ "test": "bun run test:lint && bun run test:types && bun run test:unit && bun run test:size",
65
+ "fix:lint": "biome check --write",
66
+ "fix:lint:unsafe": "biome check --write --unsafe",
67
+ "fix": "bun run fix:lint",
68
+ "version:release": "semantic-release --no-ci --dry-run",
69
+ "release": "bun run build && bun run test && bun run version:release && npm publish"
70
+ },
71
+ "size-limit": [
72
+ {
73
+ "path": "lib/index.js",
74
+ "limit": "10 kb",
75
+ "ignore": [
76
+ "node:*"
77
+ ]
78
+ }
79
+ ],
80
+ "dependencies": {
81
+ "yargs": "^17.7.2"
82
+ },
83
+ "release": {
84
+ "branches": [
85
+ "main"
86
+ ],
87
+ "plugins": [
88
+ "@semantic-release/commit-analyzer",
89
+ "@semantic-release/release-notes-generator",
90
+ "@semantic-release/changelog",
91
+ [
92
+ "@semantic-release/npm",
93
+ {
94
+ "npmPublish": true
95
+ }
96
+ ],
97
+ "@semantic-release/github",
98
+ [
99
+ "@semantic-release/git",
100
+ {
101
+ "assets": [
102
+ "package.json",
103
+ "CHANGELOG.md",
104
+ "bun.lock"
105
+ ],
106
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
107
+ }
108
+ ]
109
+ ]
110
+ },
111
+ "publishConfig": {
112
+ "access": "public",
113
+ "provenance": true
114
+ },
115
+ "keywords": [
116
+ "claude",
117
+ "claude-code",
118
+ "sandbox",
119
+ "seatbelt",
120
+ "sandbox-exec",
121
+ "macos",
122
+ "security",
123
+ "cli"
124
+ ],
125
+ "repository": {
126
+ "type": "git",
127
+ "url": "git+https://github.com/ycmds/clabox.git"
128
+ },
129
+ "homepage": "https://github.com/ycmds/clabox#readme",
130
+ "bugs": {
131
+ "url": "https://github.com/ycmds/clabox/issues"
8
132
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "ISC"
133
+ "devDependencies": {
134
+ "@biomejs/biome": "^2.5.0",
135
+ "@semantic-release/changelog": "^6.0.3",
136
+ "@semantic-release/git": "^10.0.1",
137
+ "@size-limit/preset-small-lib": "^12.1.0",
138
+ "@types/bun": "^1.3.14",
139
+ "@types/node": "^26.0.0",
140
+ "@types/yargs": "^17.0.35",
141
+ "semantic-release": "^25.0.5",
142
+ "size-limit": "^12.1.0",
143
+ "tsdown": "^0.22.3",
144
+ "typescript": "^6.0.3"
145
+ }
12
146
  }