No description
  • Shell 93.8%
  • Dockerfile 4.6%
  • Makefile 1.6%
Find a file
Claudio Maradonna d0c790e262
feat(claude.sh): derive session id from workspace path
Random 8-hex IDs meant the same project got a fresh empty session on
every invocation, and reusing prior context required juggling
CLAUDE_JAIL_SESSION or copy-pasting an --session flag. Most users
want the opposite: stable per-folder sessions, zero config.

Derive the default ID from the first 8 hex chars of sha256("$workspace")
so re-running claude in the same directory transparently reuses the
session. Create the on-disk state lazily on first use, and tighten
explicit --session / CLAUDE_JAIL_SESSION to require an existing
session (no more accidental creation from a typoed ID).

Update README and .env.example to document the new default and the
narrower role of CLAUDE_JAIL_SESSION.
2026-05-11 23:51:59 +02:00
.ai feat(ai): add system prompt and emacs 2026-05-03 02:17:05 +02:00
.claude chore: update .gitignore and add claude settings 2026-05-02 19:28:05 +02:00
.claudeignore chore(claude): rework little bug where env.claude > cli flags; fix README 2026-04-11 16:28:26 +02:00
.dir-locals.el feat(ai): add system prompt and emacs 2026-05-03 02:17:05 +02:00
.dockerignore
.env.example feat(claude.sh): derive session id from workspace path 2026-05-11 23:51:59 +02:00
.gitignore chore: update .gitignore and add claude settings 2026-05-02 19:28:05 +02:00
claude.sh feat(claude.sh): derive session id from workspace path 2026-05-11 23:51:59 +02:00
COPYRIGHT update license to bsd 3 clause 2026-04-06 23:46:30 +02:00
Dockerfile feat(image): add tooling for SSH commit signing in container 2026-05-11 23:51:44 +02:00
install.sh feat(image): bake configurable timezone into the container 2026-05-11 23:50:56 +02:00
Makefile feat(image): bake configurable timezone into the container 2026-05-11 23:50:56 +02:00
README.md feat(claude.sh): derive session id from workspace path 2026-05-11 23:51:59 +02:00

Claude Jail

Run Claude Code inside a rootless Podman container instead of directly on your host machine. Your project files are bind-mounted into the container, so Claude can read and edit them while everything else stays sandboxed.

Why

Claude Code needs broad filesystem access to be useful. Running it in a container gives you the convenience of a fully capable coding agent without exposing your entire home directory, system binaries, or credentials beyond what you explicitly mount.

Prerequisites

  • Podman (installed automatically by the install script, or bring your own)
  • A valid Claude Code account (you will authenticate on first run)

Quick start

git clone <repo-url> && cd claude-jail
./install.sh

The install script will:

  1. Install Podman if it is not already present (supports apt, dnf, pacman, brew)
  2. Build the claude-code container image from the included Dockerfile
  3. Place a claude wrapper script in ~/.local/bin/

If ~/.local/bin is not in your PATH, the script will tell you what to add. Example:

export PATH="${HOME}/.local/bin:${PATH}"

Add it to your shell rc file (.bashrc, .zshrc, etc.) to make it permanent.

Usage

claude <directory> [options]

If omitted, the wrapper prompts to use the current directory (or auto-accepts it when stdin is not a TTY, so non-interactive callers don't hang). All arguments that are not recognized by the wrapper are forwarded directly to the claude CLI inside the container, so every native flag works as expected.

Examples

# Interactive session on the current directory
claude .

# Work on a specific project
claude /path/to/project

# Pass native Claude Code flags
claude . --model sonnet
claude . --resume
claude . -p "explain this codebase"

# One-shot prompt mode
claude . -p "find and fix the memory leak in server.js"

# Forward your SSH agent (useful for git operations inside the container)
claude --with-ssh-agent .
claude --with-ssh-agent /path/to/project -p "push the fix"

# Use a custom container image
claude --image my-custom-claude .

# Use a custom working directory inside the container
claude --container-workdir /app .

# List all sessions
claude --all-sessions

# Resume a previous session
claude . --session a1b2c3d4

Wrapper-specific options

Flag Description
--with-ssh-agent Bind-mount the host SSH agent socket into the container so that git push, git clone, etc. work with your SSH keys. See note on commit signing below.
--session <id> Resume a previous session by its 8-character ID.
--all-sessions List all sessions with creation timestamps.
--mount <src:dst[:opt]> Additional bind mount, repeatable (e.g. :ro for read-only). Can also be set via CLAUDE_JAIL_MOUNTS env var.
--max-memory <value> Container memory limit (default: total host RAM). Can also be set via CONTAINER_MAX_MEMORY env var.
--image <name> Container image to use (default: claude-code). Can also be set via CLAUDE_JAIL_IMAGE env var.
--container-workdir <path> Working directory inside the container (default: /workspace). Can also be set via CLAUDE_JAIL_WORKSPACE env var.
--ignore-file <path> Path to ignore file (default: <workspace>/.claudeignore). Can also be set via CLAUDE_JAIL_IGNORE env var.
-h, --help Show help message and exit.

SSH commit signing

The image ships with openssh-client, so git push/clone over SSH and SSH-based commit signing (gpg.format = ssh) both work once --with-ssh-agent is on. Everything else is your responsibility: the wrapper does not inject a git identity, signing config, or public key into the container.

To sign commits inside the container, set up at least:

  • user.name, user.email, gpg.format = ssh, commit.gpgsign = true, user.signingkey
  • Make the signing key reachable. Either mount your gitconfig and public key explicitly, e.g.
    claude --with-ssh-agent \
      --mount ~/.gitconfig:/home/claude/.gitconfig:ro \
      --mount ~/.ssh/id_ed25519.pub:/home/claude/.ssh/id_ed25519.pub:ro \
      .
    
    or use the inline form user.signingkey = "key::ssh-ed25519 AAAA... you@host" and skip mounting any file.

If your host ~/.gitconfig uses ~ in paths (e.g. signingkey = ~/.ssh/id_ed25519.pub), remember that ~ resolves to /home/claude inside the container — the file must live there, or the path must be adjusted.

Environment variables

Variables can be set in three ways, listed by priority (highest first):

  1. CLI flags (--image, --session, etc.) — always win.
  2. Host environment (export ANTHROPIC_API_KEY=... in your shell) — overrides .env.claude.
  3. .env.claude file in the workspace directory — loaded automatically if present.

Variables are divided into two groups:

Script-level (configure how the container is launched)

These are consumed by claude.sh and are not forwarded into the container.

Variable Default CLI equivalent Description
CLAUDE_JAIL_IMAGE claude-code --image Container image name.
CLAUDE_JAIL_WORKSPACE /workspace --container-workdir Working directory inside the container.
CLAUDE_JAIL_USE_SSH 0 --with-ssh-agent Set to 1 to forward the host SSH agent.
CLAUDE_JAIL_SESSION (derived from workspace path) --session Override the deterministic session ID. Must point to an existing session.
CLAUDE_JAIL_MOUNTS (none) --mount Comma-separated extra bind mounts (e.g. /a:/b:ro,/c:/d).
CLAUDE_JAIL_IGNORE (none) --ignore-file Path to ignore file (default: <workspace>/.claudeignore).
CONTAINER_MAX_MEMORY (total RAM) --max-memory Container memory limit (e.g. 512m, 4g).

Container-level (injected inside the container)

Every variable in .env.claude that is not in the script-level group above gets forwarded into the container as a regular environment variable.

Variable Description
ANTHROPIC_API_KEY Anthropic API key. If also exported in your shell, the shell value wins.
(any other) Custom variables available to Claude Code and any process in the container.

Tip: copy the included .env.example to get started: cp .env.example .env.claude

Everything else is passed through to claude unchanged.

Sessions

The session ID is derived deterministically from the absolute workspace path (first 8 hex chars of its SHA-256). Re-invoking claude in the same directory transparently reuses the same session — no need to set CLAUDE_JAIL_SESSION in .env.claude or pass --session. Session data is stored under ~/.claude-jail/sessions/<id>/ and printed at startup:

Session: a1b2c3d4 (/home/user/.claude-jail/sessions/a1b2c3d4)

To pin a different ID (or share one across folders), pass --session <id> or set CLAUDE_JAIL_SESSION. In that case the session must already exist; if it does not, the wrapper prints the available sessions and exits.

When a session directory does not yet exist, an empty {} config is generated along with a config/ directory containing default settings. Each session has fully isolated state — configuration, credentials, and conversation history — so you can run multiple containers in parallel without conflicts.

~/.claude-jail/
  sessions/
    a1b2c3d4/              # session-specific state
      .claude.json          # credentials (initially empty {})
      config/               # mapped to /home/claude/.claude in container
        settings.json       # auto-trusts container workdir
    f9e8d7c6/
    ...

.env.claude

If a file named .env.claude exists in the workspace directory, its variables are automatically loaded. Variables prefixed with CLAUDE_JAIL_ and CONTAINER_MAX_MEMORY are used by the wrapper script itself. All other variables are forwarded into the container.

# .env.claude
ANTHROPIC_API_KEY=sk-ant-...
CLAUDE_JAIL_SESSION=a1b2c3d4
CLAUDE_JAIL_USE_SSH=1
CONTAINER_MAX_MEMORY=2g
# CLAUDE_JAIL_IMAGE=my-custom-image
# CLAUDE_JAIL_WORKSPACE=/app
MY_CUSTOM_VAR=hello
  • Blank lines and lines starting with # are ignored.
  • A single matching pair of surrounding "..." or '...' quotes is stripped from values; nothing inside is further expanded (no $VAR, no ~).
  • CLAUDE_JAIL_SESSION pins a specific session ID instead of the one derived from the workspace path. The CLI flag --session always takes precedence; both must reference an existing session.
  • CLAUDE_JAIL_* and CONTAINER_MAX_MEMORY variables are consumed by the script and not forwarded into the container.
  • Host environment variables (e.g. ANTHROPIC_API_KEY exported in your shell) override values from .env.claude.
  • In mount source paths (--mount / CLAUDE_JAIL_MOUNTS), a leading ~ or literal $HOME is expanded to the host user's home directory.

Tip: Add .env.claude to your .gitignore — it will typically contain secrets.

.claudeignore

If a file named .claudeignore exists in the workspace root, files and directories matching its patterns will be hidden inside the container. This lets you mount a project directory while keeping sensitive files (secrets, credentials, private configs) invisible to Claude.

# .claudeignore
.env.secret
credentials/
*.key
**/*.pem
config/.env.production

Format:

  • One glob pattern per line
  • # for comments, blank lines ignored
  • Supports simple globs (*.secret), recursive patterns (**/*.key), and directory-only matches (trailing /)
  • Leading / is stripped (patterns are always relative to the workspace root)
  • Negation patterns (!pattern) are not supported

How it works: The workspace directory is still bind-mounted as a whole, but each matched file gets /dev/null mounted over it (read-only), and each matched directory gets an empty tmpfs overlay. The original files on the host are never modified.

Custom ignore file: Use --ignore-file <path> or CLAUDE_JAIL_IGNORE=<path> to specify an alternative ignore file. If an explicitly-requested ignore file does not exist, the script exits with an error. The default .claudeignore is silently skipped if absent.

Tip: Add .claudeignore to your project if you keep secrets alongside your code (e.g. .env files, TLS certificates, API keys).

What gets mounted

Host path Container path Purpose
<directory> Container workdir (default /workspace, configurable via CLAUDE_JAIL_WORKSPACE) Your project files (read/write)
~/.claude-jail/sessions/<id>/config /home/claude/.claude Session-specific Claude Code state
~/.claude-jail/sessions/<id>/.claude.json /home/claude/.claude.json Session-specific credentials
$SSH_AUTH_SOCK /ssh-agent SSH agent socket (only with --with-ssh-agent)

Nothing else from the host is visible inside the container.

Note: The container runs Claude Code with --dangerously-skip-permissions, which disables its built-in confirmation prompts. This is safe because the container itself acts as the sandbox — Claude can only access the explicitly mounted workspace and session directories.

Uninstall

rm ~/.local/bin/claude
podman rmi claude-code    # or your custom image name if CLAUDE_JAIL_IMAGE was set

License

BSD 3-Clause (see COPYRIGHT)