Skip to content

ycmds/clabox

Repository files navigation

πŸ“¦ clabox

LSK.js NPM version NPM downloads Have TypeScript types Package size License Write us in Telegram

πŸ›‘οΈ Run Claude Code in a sandbox for super-safe YOLO mode πŸ›‘οΈ

clabox logo

πŸ›‘οΈ Tight Seatbelt sandbox β€” the profile starts with (deny default)
πŸ“‚ Project-scoped access β€” only the CWD and explicitly allowed paths
πŸ”’ Secrets stay out of reach β€” SSH keys, ~/.aws, ~/.ssh/id_*, private dirs
πŸ“¦ Declarative JS config instead of sed surgery over a heredoc
πŸ€– Bot identity for git/ssh inside the sandbox
🧨 Fork-bomb guard via ulimit -u
⚑ YOLO mode, safely (--dangerously-skip-permissions)
🍎 macOS only, Node β‰₯ 18, no runtime deps beyond yargs


Install

npm install -g clabox      # exposes the global `clabox` command
# or run without installing:
bunx clabox …
npx clabox …

Usage

# Default profile (~/.claude), YOLO mode
clabox run --dangerously-skip-permissions

# A different Claude profile
CLAUDE_CONFIG_DIR=~/.claude_work clabox run --dangerously-skip-permissions

# A named box from ~/.config/clabox/configs/<name>.config.mjs
clabox -b ax-root --dangerously-skip-permissions

# Debugging
clabox generate            # build the profile, print the .sb path
clabox profile             # just the path (no build)
CLABOX_DEBUG=1 clabox      # print profile/config/dir on launch
clabox --help

Unknown flags are passed straight through to claude, so anything after the command (--dangerously-skip-permissions, --model …, etc.) just works.


Configuration

Three layers, later wins: defaults β†’ environment variables β†’ JS config file.

The config file is looked up in this order: --config /path β†’ CLABOX_CONFIG=/path β†’ ./clabox.config.mjs (project root) β†’ ~/.config/clabox/config.mjs. See clabox.config.example.mjs.

clabox --config ./my.clabox.mjs run --dangerously-skip-permissions
// clabox.config.mjs
export default {
  configDir: '~/.claude_work',
  bot: { name: 'workBOT', email: 'bot@work.dev', sshDir: '~/.ssh/workbot' },
  network: true,
  paths: {
    readWrite: ['~/scratch'],    // RW on top of project / configDir / tmp
    readOnly:  ['~/reference'],   // RO
    exec:      ['/opt/tool/bin'], // process-exec
    deny:      ['~/secret'],      // explicit deny (read + write)
  },
};

You may also export a function (defaults) => config for full control. ~ is expanded to $HOME.

Named boxes (-b / --box)

Keep a directory of named configs and switch between them by name from anywhere:

# ~/.config/clabox/configs/ax-root.config.mjs
clabox -b ax-root --dangerously-skip-permissions

-b <name> resolves ~/.config/clabox/configs/<name>.config.mjs (falling back to a bare <name>.mjs) and loads it like --config β€” so it wins over --config / CLABOX_CONFIG. Override the dir with CLABOX_CONFIGS_DIR. Files named _*.mjs are treated as shared partials (e.g. _presets.mjs), not boxes.

A box can pin its own cwd so it always targets one project, no matter where you run clabox from:

// ~/.config/clabox/configs/ax-root.config.mjs
export default {
  cwd: '~/projects/my-app', // claude runs here; this dir is the RW project dir
  configDir: '~/.claude_axiomus',
};

Environment variables

Variable Purpose Default
CLAUDE_CONFIG_DIR Claude config/profile dir (multi-account); passed through to claude ~/.claude
CLABOX_CLAUDE_BIN path to the claude binary PATH, then ~/.local/bin/claude
CLABOX_BOT_NAME / CLABOX_BOT_EMAIL git identity claudeBOT / bot@example.com
CLABOX_BOT_SSH_DIR bot key dir (id_ed25519, config) ~/.ssh/claudebot
CLABOX_CONFIG path to the JS config file (the --config flag overrides it) β€”
CLABOX_CONFIGS_DIR global dir of named boxes for -b/--box <name> (<name>.config.mjs) ~/.config/clabox/configs
CLABOX_CWD working dir to run claude in (also the RW project dir); ~ expanded β€” (the shell CWD)
CLABOX_HOOKS_DIR hooks dir (RO + exec inside the sandbox) β€” (off)
CLABOX_DEBUG print diagnostics on launch β€”
TMPDIR where the generated profile is stored /tmp

How it works

sandbox-exec runs a process inside a Seatbelt profile that starts with (deny default) β€” everything is forbidden unless explicitly allowed.

clabox run  β†’  loadConfig()  β†’  buildProfile()  β†’  <TMPDIR>/…sb
            β†’  sh -c 'ulimit -u N; exec sandbox-exec -f <sb> env … claude …'
Module Responsibility
src/utils/config.ts defaults, env, loading/merging the JS config, ~ expansion
src/sandbox/profile.ts assembling the SBPL profile from config (typed helpers subpath/literal/regex/…)
src/sandbox/run.ts locating claude/sandbox-exec, generating the profile, launching with bot env + ulimit
src/cli.ts the CLI (run / generate / profile), built on yargs

Profile path: $TMPDIR/clabox-<dir-name>-<hash>.sb (hash of the absolute project path β€” each project gets its own cached profile).

Package managers are autodetected (src/sandbox/profile.ts) and added to the read/exec sections: Homebrew (/opt/homebrew or /usr/local/Homebrew), ~/.local, Nix (/nix/store).

What the profile allows and denies

Read-only: system dirs /System, /usr, /bin, /sbin, /Library/Frameworks, Command Line Tools / Xcode, tzdata, system and user Library/Preferences, detected package paths.

Read-write: the project dir (CWD), the Claude config dir (configDir), /tmp, /private/tmp, /private/var/folders/…, ~/Library/Keychains (for OAuth refresh), plus paths.readWrite from your config.

Network: (allow network*) when network: true (the default).

Explicit deny β€” wins even over the allows above:

  • private dirs: denyHome (~/Documents, ~/Desktop, ~/Downloads, ~/Pictures, ~/Movies, ~/Music);
  • secrets: denyDotConfigs (~/.aws, ~/.gnupg, ~/.kube, ~/.docker, ~/.config) with a carve-out for ~/.config/git;
  • personal SSH keys ~/.ssh/id_*, *.pem, *.key β€” Claude physically cannot read them. Only the bot key subdir (bot.sshDir) is readable.

Git/ssh bot identity

  • ulimit -u <ulimitProcs> β€” fork-bomb guard (0 to disable);
  • GIT_AUTHOR_* / GIT_COMMITTER_* β€” bot name/email from config;
  • 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.

Tests

bun test            # unit + functional (bun:test)
bun run test        # full gate: lint + types + unit + size

The suite tests the wrapper, not claude:

  • Unit β€” the generated profile text: SBPL preamble, project RW/exec, network toggle, config dir, ssh-key denials, the deny list, extra config paths, hooks.
  • Functional β€” runs real sandbox-exec against a generated profile and asserts that reads/writes inside the project succeed while denied paths are blocked. Auto-skipped off macOS or when running nested inside another sandbox.

Limitations

  • macOS only β€” needs sandbox-exec (Seatbelt). Formally deprecated, still works on macOS 14/15.
  • No nested sandbox β€” you cannot launch the sandbox from inside another sandbox (sandbox_apply: Operation not permitted). Run from a bare host.
  • Keychain is writable for OAuth refresh (otherwise 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).

License

MIT

About

Claude code in safe Sandbox

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors