- Go 97.9%
- Shell 1.4%
- Just 0.7%
| .woodpecker | ||
| cmd/claudia-shim | ||
| docs | ||
| internal | ||
| scripts | ||
| .gitignore | ||
| CHANGELOG.md | ||
| go.mod | ||
| justfile | ||
| main.go | ||
| main_test.go | ||
| README.md | ||
claudia
Run Claude Code in an
OS-native sandbox using bubblewrap
on Linux and Seatbelt (sandbox-exec) on macOS.
Each profile gets its own isolated Claude config, cache, and conversation history. The host filesystem is read-only except for explicitly mounted directories.
See Getting started for install instructions, requirements, and first-run setup.
Usage
claudia # launch claude in sandbox with cwd writable
claudia status # show profile config
claudia shell # open bash in sandbox
claudia update # download latest claude binary
claudia version # show version info
claudia mount <path> # add a read-only path (default)
claudia mount <path>:rw # add a read-write path
claudia mount --delete <path> # remove a mounted path
claudia secret K=V # set a secret (hidden from agent)
claudia secret K - # read secret value from stdin
claudia secret --delete K # remove a secret
claudia config # show current profile config
claudia config env K=V # set a profile env var
claudia config env --delete K # remove a profile env var
claudia edit config # open profile config in $EDITOR
claudia edit prompt # edit profile CLAUDE.md
claudia edit statusline # edit statusline script
claudia resume # resume most recent conversation for cwd
claudia resume --list # list all conversations
claudia audit # pretty-print audit log
claudia audit path # print audit log file path
claudia audit clear # truncate audit log
claudia reset # reset all config to defaults
claudia reset mount # reset just mounts (also: secret, env, prompt, ...)
claudia profile # list profiles
claudia profile init # check prerequisites, install claude, set up profile
claudia profile use <n> # switch active profile
claudia profile --delete # delete current profile
claudia plugin # list seeded plugins
claudia plugin enable <name> # enable plugin for this profile
claudia plugin disable <name> # disable plugin for this profile
claudia plugin -d <name> # remove plugin from seed
claudia plugin seed -- <args...> # install shared plugins
claudia memory # list memories for current project
claudia memory show <name> # print memory body
claudia memory add # draft a new memory in $EDITOR
claudia memory add <path> # file an existing draft (e.g. checked-in template)
claudia memory edit <name> # open memory in $EDITOR
claudia memory rm <name>... # delete memory and strip from MEMORY.md
Running claudia with no arguments is the main workflow: it ensures the
profile is set up, adds your current directory as writable, and launches
claude inside the sandbox.
Profiles
Each profile is fully isolated — its own Claude config, conversations, cache, and writable paths. Profiles share the claude binary but nothing else.
claudia -p work profile init # create a profile named "work"
claudia -p work # run claude in the "work" profile
claudia profile # list all profiles
claudia profile use work # set "work" as active profile
direnv integration
Add to ~/.config/direnv/direnvrc:
use_claudia() { eval "$(claudia profile load "$@")"; }
Then in any project's .envrc:
use claudia myproject
Plugins
All profiles share a single plugin seed directory at
~/.local/share/claudia/plugins/. Plugins installed here are available
read-only in every profile via CLAUDE_CODE_PLUGIN_SEED_DIR.
Use claudia plugin seed to install plugins into the shared directory.
Arguments after -- are passed to claude plugin inside a sandbox:
claudia plugin seed -- marketplace add your-org/plugins
claudia plugin seed -- install my-tool@your-plugins
Seeded plugins are synced into all profiles on startup but disabled by default. Enable a plugin for the current profile with:
claudia plugin enable my-tool@your-plugins
Disable or remove plugins:
claudia plugin disable my-tool@your-plugins # disable for this profile
claudia plugin -d my-tool@your-plugins # remove from seed entirely
Each profile also has its own writable plugin cache
(CLAUDE_CODE_PLUGIN_CACHE_DIR) for profile-specific plugin state.
claudia plugin # list seeded plugins with version and status
claudia plugin path # print seed directory path
Memories
Each profile keeps a per-project, file-backed memory store that Claude
reads and writes across sessions. Memories survive /clear, --resume,
and new conversations — they're how the agent remembers user preferences,
project decisions, and prior corrections instead of relearning them every
session.
claudia memory # list memories for the current project
claudia memory add # draft a new memory in $EDITOR
claudia memory show <n> # print one memory's body
claudia memory edit <n> # open an existing memory in $EDITOR
claudia memory rm <n> # delete and strip from MEMORY.md
A SessionStart hook (claudia memory preload) injects the full body
of every memory file into context at the start of each session, so
recall doesn't depend on the agent guessing relevance from a one-line
index hook.
See Memories workflow for the file format, the four memory types (user / feedback / project / reference), the pre-written / git-tracked draft workflow, and tips for writing memories Claude actually retrieves.
Secrets
For tokens, API keys, and credentials, use claudia secret. Unlike
profile config env values (which the agent can read directly), secret
values are kept out of the agent's environment entirely. The agent sees
a placeholder (NPM_TOKEN=__claudia_secret_NPM_TOKEN__); the real value
is substituted at execve time, only for a curated set of wrapped tools.
claudia secret NPM_TOKEN=npm_abc123
claudia secret GITHUB_TOKEN=ghp_...
claudia secret # list secret names (never values)
claudia secret --delete NPM_TOKEN
Read a value from stdin instead of argv (avoids shell history capture):
pass show npm/token | claudia secret NPM_TOKEN -
How it works
- Values live in
~/.local/share/claudia/secrets/<profile>.json(mode0600), outside any directory that's bind-mounted into the sandbox. The agent cannot read this file. - At launch, claudia writes a per-instance snapshot to
~/.local/share/claudia/runtime/<profile>/<pid>-<rand>/secrets.jsonand symlinksshims/<tool>→/usr/local/bin/claudia-shimfor each wrapped tool. - The sandbox env gets a placeholder like
NPM_TOKEN=__claudia_secret_NPM_TOKEN__.PATHis prefixed with the shims directory. - When the agent runs
npm, the shim intercepts, reads the snapshot, substitutes the real value into its own env, strips itself fromPATH, andsyscall.Execs the realnpm. From that point npm sees the real token; nothing else does.
Wrapped tools: npm, npx, yarn, pnpm, bun, git, gh, aws,
s4cmd, gcloud, gsutil, bq, curl. Shells (bash, sh, zsh) and
interpreters (python, node, ruby, perl, java, go) are
deliberately not wrapped — wrapping a shell would leak the real
values into any command the agent runs through that shell, including
printenv.
Trust model
This is a soft threat model: it prevents accidental leakage into
transcripts, agent self-inspection (process.env, /proc/self/environ,
printenv), and normal tool output. A determined adversarial agent
that knows the design could read the runtime snapshot file from inside
the sandbox (the path is discoverable via CLAUDIA_SHIM_DIR). Every
shim invocation is recorded in the audit log (claudia audit).
For the current threat model — "Claude is curious, not adversarial; don't let secrets show up in transcripts by accident" — this is sufficient. If you need hard guarantees against extraction, the only robust approach is out-of-sandbox tool execution via IPC, which would be a significant architectural change.
How isolation works
Cross-platform
| Path | Access | Purpose |
|---|---|---|
/dev |
read-write | device nodes (/dev/null, /dev/tty, etc.) |
$PATH dirs under $HOME |
read-only | user bin directories |
~/.local/share/claudia/bin/ |
read-only | managed claude binary |
~/.gitconfig, ~/.gitignore, ~/.npmrc, ~/.yarnrc, ~/.curlrc, ~/.wgetrc |
read-only | tool config dotfiles |
<profile>/claude/ |
read-write | per-profile Claude config (CLAUDE_CONFIG_DIR) |
<profile>/cache/ |
read-write | per-profile Claude cache |
<profile>/plugins/ |
read-write | per-profile plugin cache (CLAUDE_CODE_PLUGIN_CACHE_DIR) |
~/.local/share/claudia/plugins/ |
read-only | shared plugin seed (CLAUDE_CODE_PLUGIN_SEED_DIR) |
<profile>/ |
read-write | lockfiles |
| cwd and mounted paths | configurable | your project files |
Everything else under $HOME — including ~/.ssh, ~/.aws, ~/.gnupg,
and all other dotfiles and directories — is not accessible.
Environment variables are filtered to a safe allowlist (system, locale,
toolchains, proxy settings). Host env vars like AWS_SECRET_ACCESS_KEY,
GITHUB_TOKEN, etc. are not passed into the sandbox.
For non-secret env vars, use the profile config env key
(claudia config env K=V). Values set this way are visible to the agent.
For actual secrets (tokens, API keys, credentials), use claudia secret.
The real value never enters the agent's environment — the agent sees a
placeholder __claudia_secret_<NAME>__, and a small claudia-shim helper
substitutes the real value at execve time, only for a curated list of
wrapped tools (npm, npx, yarn, pnpm, bun, git, gh, aws, s4cmd, gcloud, gsutil, bq, curl). See the Secrets
section below.
Linux only
Uses bubblewrap to create a new mount namespace.
| Path | Access | Purpose |
|---|---|---|
/bin, /etc, /lib, /lib64, /nix, /run, /sbin, /usr, /var |
read-only | system tools and libraries |
/proc |
read-only | process info |
~/.cache |
read-write | tool caches (go, npm, etc.) |
/tmp |
tmpfs | temporary files (ephemeral) |
$HOME |
tmpfs | empty by default — dotfiles bind-mounted individually |
macOS only
Uses Seatbelt (sandbox-exec) to apply a deny-default policy.
| Path | Access | Purpose |
|---|---|---|
Everything outside $HOME |
read-only | system tools and libraries |
~/Library/Caches |
read-write | tool caches (go, npm, etc.) |
/private/tmp |
read-write | temporary files |
/private/var/folders |
read-write | system temporary directories |
$HOME |
denied | reads blocked except for allowlisted paths above |
Dotfile security
The dotfiles listed above are passed into the sandbox read-only. Do not store secrets directly in these files. Instead, use environment variable references and pass the values through the profile config.
npm — use a token variable in ~/.npmrc:
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
Then set the token as a secret:
claudia secret NPM_TOKEN=npm_abc123...
npm is a wrapped tool, so it sees the real token at execve time via
the shim. The agent itself only ever sees the placeholder
NPM_TOKEN=__claudia_secret_NPM_TOKEN__, so transcripts never leak the
token — even if the agent calls printenv in a Bash tool.
git — if your ~/.gitconfig includes credentials or tokens, move them
to a credential helper or environment variable. The sandbox can read your
gitconfig, so anything hardcoded there is visible inside the sandbox.
curl / wget — avoid storing credentials in ~/.curlrc or ~/.wgetrc.
Use environment variables or pass auth headers explicitly. Note that
curl is not currently a wrapped tool, so a secret value in its env
would appear as the placeholder, not the real token. If you need curl
to authenticate, either call it from within npm run/yarn run scripts,
or pass the header explicitly from a script that reads the secret at
runtime from a mounted file.
As a general rule: if a dotfile supports ${VAR} substitution, prefer
that over hardcoded secrets, and set the variable with claudia secret
rather than claudia config env so the value is never visible to the
agent.
Data
Everything lives in ~/.local/share/claudia/ (respects XDG_DATA_HOME):
bin/
claude # managed claude binary (shared)
plugins/ # shared plugin seed directory (read-only in sandbox)
profiles.json # profile registry
profiles/<name>/ # bind-mounted into the sandbox (agent-reachable)
config.json # writable paths, non-secret env vars, cache dirs
claude/ # CLAUDE_CONFIG_DIR (per-profile)
cache/ # claude cache (per-profile)
plugins/ # per-profile plugin cache
secrets/<name>.json # per-profile secret store (NOT mounted; mode 0600)
runtime/<name>/<pid>-<rand>/ # per-instance secret snapshot + shim symlinks
secrets.json # snapshot of the store taken at launch
shims/ # symlinks to claudia-shim, one per wrapped tool
The profiles/<name>/ subtree is bind-mounted into the sandbox (the
agent can read and write within it). The secrets/ and runtime/
siblings are deliberately outside that subtree: secrets/ is never
mounted, and runtime/<name>/<pid>-<rand>/ is bind-mounted read-only
via a separate explicit mount so only the shim — not the agent's other
code paths — has a natural reason to reach it.
Config
Profile config lives at <profile>/config.json. Edit with claudia config.
{
"paths": [
"/home/user/dev/project:rw",
"/home/user/data:ro"
],
"cache_dirs": [
"~/.cache/go-build",
"~/.cache/gopls",
"~/.cache/npm",
"~/.cache/bun",
"~/.cache/uv",
"~/.cache/pip",
"~/.cache/nix",
"~/.cache/node",
"~/.cache/direnv"
],
"env": {
"NODE_ENV": "development"
}
}
The env map is for non-secret environment variables — things
visible to the agent, like NODE_ENV or AWS_PROFILE. For tokens and
credentials, use claudia secret instead so values never appear in the
agent's view. See the Secrets section.