Run Claude with separate sessions in isolated sandboxes. The end-goal is to provide enough safety to make it comfortable (for me) to run with permissions bypassed.
  • Go 99.1%
  • Shell 0.5%
  • Just 0.4%
Find a file
Viktor Varland 3dc5905359
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
docs: changelog for v4.1.0
2026-06-24 13:59:28 +02:00
.woodpecker fix: depend on published gout + teach CI to fetch it 2026-06-24 08:55:37 +02:00
cmd/claudia-shim feat: hold secrets in an out-of-sandbox broker instead of an on-disk snapshot 2026-06-10 10:31:26 +02:00
docs docs: migration 3.x to 4.x 2026-06-11 18:46:42 +02:00
internal refactor: delegate the cli framework to gout/cli 2026-06-24 12:56:37 +02:00
scripts refactor: prep for landlock 2026-05-11 10:14:39 +02:00
.gitignore feat: initial commit 2026-03-31 09:03:00 +02:00
CHANGELOG.md docs: changelog for v4.1.0 2026-06-24 13:59:28 +02:00
go.mod refactor: delegate the cli framework to gout/cli 2026-06-24 12:56:37 +02:00
go.sum refactor: delegate the cli framework to gout/cli 2026-06-24 12:56:37 +02:00
justfile chore: idempotency guard for release 2026-05-03 07:51:36 +02:00
main.go feat: conform to CLI output contract (ps --json, exit 2) 2026-06-23 18:58:31 +02:00
README.md docs: migration 3.x to 4.x 2026-06-11 18:46:42 +02:00

claudia

Run Claude Code in an OS-native sandbox using Landlock 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. Upgrading from 3.x? See the 3.x → 4.0 migration guide.

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 add <path>             # add a read-only path (default)
claudia mount add <path>:rw          # add a read-write path
claudia mount rm <path>              # remove a mounted path

claudia secret add K V               # set a secret (hidden from agent)
claudia secret add K                 # read value from stdin if piped, else prompt (no echo)
claudia secret add K -               # read secret value from stdin
claudia secret rm K                  # remove a secret

claudia config                       # show current profile config
claudia config set env.K V          # set a profile env var
claudia config rm  env.K            # remove a profile env var
claudia config add --scope seed env-host GOFLAGS   # seed a default for new profiles

claudia edit config                  # open profile config in $EDITOR
claudia edit prompt                  # edit profile CLAUDE.md
claudia edit statusline              # edit statusline script
claudia edit --scope seed prompt     # edit the seed CLAUDE.md (default for new profiles)

claudia resume                       # resume most recent conversation for cwd
claudia resume --list                # list all conversations
claudia resume --search "query"      # list conversations containing query (case-insensitive)

claudia audit                        # pretty-print audit log
claudia audit -n 50                  # print only the last 50 entries
claudia audit -f                     # print last 10 entries, then stream new ones (--follow)
claudia audit -f -n 50               # stream with 50 entries of backlog
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 get                  # list profiles
claudia profile init                 # check prerequisites, install claude, set up profile
claudia profile use <n>              # switch active profile
claudia profile rm <n>               # delete a profile (--all for everything)

claudia plugin get                   # list this profile's plugins
claudia plugin add -- install <p@m>  # install a plugin into this profile
claudia plugin enable <name>         # enable plugin for this profile
claudia plugin disable <name>        # disable plugin for this profile
claudia plugin rm <name>             # remove plugin from this profile
claudia plugin seed -- <args...>     # populate the seed set for new profiles

claudia memory                       # show the memory subcommands
claudia memory list                  # list current project + profile-global memories
claudia memory show <name>           # print memory body
claudia memory add --scope project   # draft a new project memory in $EDITOR
claudia memory add --scope profile   # draft a new profile-global memory
claudia memory add --scope seed      # draft a memory every new profile inherits
claudia memory edit <name>           # open memory in $EDITOR
claudia memory rm --scope project <name>...  # delete project memory, or ignore a profile-global here
claudia memory rm --scope profile <name>     # delete a profile-global memory outright
claudia memory query <text>          # FTS search (default: this project + profile-globals)

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 get       # 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

Each profile owns its plugins, fully isolated like everything else — no shared live mount. A seed directory at ~/.local/share/claudia/seed/plugins/ holds the default set that new profiles are created with; from then on each profile manages its own copy. (Pre-v2.1 installs at ~/.local/share/claudia/plugins/ are migrated in place on the next claudia profile init.)

Use claudia plugin seed to populate the seed set. 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

A new profile copies the seed set in at claudia profile init, disabled by default. (Adding to the seed afterwards only affects new profiles; re-run claudia -p <name> profile init to pull new seed plugins into an existing profile.)

To install directly into the current profile instead of the seed, use claudia plugin add (same claude plugin passthrough, targeting this profile):

claudia plugin add -- marketplace add your-org/plugins
claudia plugin add -- install my-tool@your-plugins

Either way plugins arrive disabled. Enable one for the current profile with:

claudia plugin enable my-tool@your-plugins

Disable or remove plugins for the current profile:

claudia plugin disable my-tool@your-plugins   # disable in this profile
claudia plugin rm my-tool@your-plugins        # remove from this profile

Plugin content lives under each profile's writable plugin dir (CLAUDE_CODE_PLUGIN_CACHE_DIR).

claudia plugin get        # list this profile's plugins with version and status
claudia plugin path       # print the seed directory path

Memories

Each profile keeps a 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.

Memories have two scopes:

  • Project-local — under <profile>/claude/projects/<encoded-cwd>/memory/. Apply only to the current project.
  • Profile-global — under <profile>/memory/. Apply to every project in the profile. New profile-globals can be seeded into every new profile via ~/.local/share/claudia/seed/memories/ (see Seeding profiles).

Preload merges both at session start; on filename collision, project-local wins. A per-project .ignored list (gitignore-style, in the project memory dir) suppresses specific profile-globals for that project without deleting them.

claudia memory                # show the memory subcommands
claudia memory list           # list memories (project + profile, with scope tags)
claudia memory add --scope project  # draft a new project-local memory
claudia memory add --scope profile  # draft a new profile-global memory
claudia memory show <n>       # print one memory's body
claudia memory edit <n>       # open in $EDITOR (resolves project then profile)
claudia memory rm --scope project <n>  # delete project memory; ignore profile-global here
claudia memory rm --scope profile <n>  # delete a profile-global outright
claudia memory query <text>   # FTS search (default: this project + profile-globals)

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 add NPM_TOKEN npm_abc123
claudia secret add GITHUB_TOKEN ghp_...
claudia secret get                     # list secret names (never values)
claudia secret rm NPM_TOKEN

Read a value from stdin instead of argv (avoids shell history capture):

pass show npm/token | claudia secret add NPM_TOKEN -

How it works

  1. Values live in ~/.local/share/claudia/secrets/<profile>.json (mode 0600), outside any directory that's bind-mounted into the sandbox. The agent cannot read this file.
  2. At launch, claudia writes a per-instance snapshot to ~/.local/share/claudia/runtime/<profile>/<pid>-<rand>/secrets.json and symlinks shims/<tool>/usr/local/bin/claudia-shim for each wrapped tool.
  3. The sandbox env gets a placeholder like NPM_TOKEN=__claudia_secret_NPM_TOKEN__. PATH is prefixed with the shims directory.
  4. When the agent runs npm, the shim intercepts, reads the snapshot, substitutes the real value into its own env, strips itself from PATH, and syscall.Execs the real npm. 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 plugins + content (CLAUDE_CODE_PLUGIN_CACHE_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 set 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.

Cross-platform defaults

Both backends apply the same policy: deny-default on $HOME, with explicit allow rules per profile. On Linux this is enforced by Landlock (kernel ≥ 5.13); on macOS by Seatbelt.

Path Access Purpose
/bin, /sbin, /lib, /lib64, /usr, /nix read+exec system tools and libraries
/etc, /proc, /sys, /run, /var read-only system config and runtime
/dev read+write PTY, /dev/null, /dev/urandom
/tmp read+write+exec scratch dir (real /tmp, observable on host)
$HOME (default) denied only explicit mounts and dotfiles allowed
$HOME/.gitconfig, $HOME/.ssh/config, ... read-only dotfiles tools need

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 add 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 set 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)
seed/                       # user-owned source-of-truth for tunable defaults
  plugins/                  # default plugin set, copied into new profiles on init
  memories/                 # profile-global memory seed (*.md, copied on init)
  profile/
    settings.json           # deep-merged into every profile's claude/settings.json
    CLAUDE.md               # copied to every new profile's CLAUDE.md (if absent)
    env-host                # gitignore-style list, merged into config.env_host
    env-cache-vars          # gitignore-style list, merged into config.env_cache_vars
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
  memory/                   # profile-global memories for this profile
  memory.db                 # SQLite FTS5 index of project memories
audit/<name>/log.jsonl      # per-profile audit log (mounted; sibling dirs not reachable)
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). seed/ is the user-owned source-of-truth for tunable defaults — claudia profile init materializes starter files there from Go constants the first time around, then treats anything you've edited as authoritative on every subsequent init. The secrets/ and runtime/ siblings are deliberately outside the bind-mounted profile 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.

See Seeding profiles for what each seed file controls and how to back up / restore your tunable defaults.

Config

Profile config lives at <profile>/config.json. Edit with claudia edit 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"
  ],
  "path_dirs": [
    "~/.cargo/bin",
    "/opt/homebrew/bin"
  ],
  "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.

path_dirs extends the sandbox PATH with extra directories and grants them read+exec, so tools installed outside the default system paths (~/.cargo/bin, /opt/homebrew/bin, …) are both findable and runnable. They sit after claudia's managed bin/shim dirs but before the host PATH, so they can override problematic host entries while never shadowing the managed claude. PATH itself is protected and cannot be overridden via env.