- Shell 99.6%
- Dockerfile 0.3%
- Makefile 0.1%
| .claude | ||
| .codex/agents | ||
| bin | ||
| clients | ||
| containers | ||
| docs | ||
| examples | ||
| scripts | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| COPYRIGHT | ||
| install.sh | ||
| Makefile | ||
| README.md | ||
azkaban
azkaban runs Codex CLI or Claude Code inside separate rootless Podman
containers while sharing one audited host launcher. The public commands remain
codex and claude; each keeps its own image, configuration, authentication,
and persistent state.
Security model
The native clients run with their own approval checks disabled because the rootless container is the intended containment boundary. Those bypass options are not safe to use on the host. A rootless container reduces host access, but it is not a virtual machine and is not a defense against every kernel or container-runtime vulnerability.
By default the client can access its workspace, its client-specific persistent
state, and the resources you explicitly expose. Extra mounts, SSH-agent
forwarding, networks, and policy files expand or alter that boundary; review
them as security-sensitive configuration. azkaban never mounts the Podman
socket.
The launcher parses environment files as data: it does not source them or use
eval. File-provided container variables are passed to Podman by name, so
their values do not appear in the generated process argument list. Trusted
adapters are loaded only from the installed runtime, never from a workspace,
PATH, or configuration file.
Prerequisites
- Bash 4.4 or newer
- Rootless Podman 4.3 or newer
- GNU or BSD userland tools used by the launcher
- Network access while building the selected client image
The installer can offer a package-manager command when Podman is missing. It builds locally and does not publish images to a registry.
Install
Choose one client, both clients, or the all selector:
./install.sh codex
./install.sh claude
./install.sh codex claude
./install.sh all
Use an unqualified image name when exactly one client is selected:
./install.sh --image private/codex codex
For multiple clients, qualify each custom image and repeat --image:
./install.sh \
--image codex=private/codex \
--image claude=private/claude \
codex claude
An image without a tag is built as :latest and, when native version
detection succeeds, receives an additional version tag. An explicit tag is
used as-is. --no-cache applies to every selected build, and --tz ZONE
sets the image timezone.
Installing one client later does not remove or replace the other client's public link, image, configuration, or state. The shared runtime release is updated and includes both trusted adapters, while only selected public links are published.
Add the user-local binary directory to PATH if it is not already present:
export PATH="$HOME/.local/bin:$PATH"
Put that line in your shell startup file to make it persistent.
Commands and dispatch
Use the preserved commands directly:
codex .
claude .
The first positional argument is the workspace and must precede native-client
arguments. Use -- to stop wrapper parsing explicitly:
codex ./project -- --native-option
claude ./project -- --native-option
The installer publishes a stable internal dispatcher at
~/.local/libexec/azkaban/azkaban. The ~/.local/bin/codex and
~/.local/bin/claude links both target it. It selects the trusted adapter from
the invoked basename after checking that name against clients/registry.sh,
the runtime's closed client list, so only codex and claude belong on
PATH; direct azkaban invocation is intentionally unsupported.
Adapters under the installed runtime's clients/ directory are trusted
installation code. They are not workspace plugins and cannot be supplied by
a project. See Adding a client for the extension
contract.
Common wrapper options
These options are accepted by both commands:
| Option | Purpose |
|---|---|
-h, --help |
Show command help. |
--with-ssh-agent |
Forward the available host SSH agent. |
--network NAME |
Attach to an existing Podman network. |
--mount SRC:DST[:OPTIONS] |
Add a bind mount; repeatable. |
--mcp-image REF |
Extract an MCP image entrypoint; repeatable. |
--ignore-file PATH |
Select an ignore file. |
--max-memory VALUE |
Set the container memory limit. |
--image NAME |
Override the client image. |
--container-workdir PATH |
Set the workspace path inside the container. |
--policy PATH |
Use an external policy environment file. |
--cidfile PATH |
Write the container ID to a host file outside the workspace. |
--no-tty |
Disable TTY allocation. |
--dry-run |
Print one shell-escaped Podman command without launching. |
-- |
Forward all remaining arguments to the native client. |
Claude additionally accepts --session ID and --all-sessions.
Environment configuration
Settings shared by every client go in one host file. Per-workspace settings go in a workspace file:
mkdir -p ~/.azkaban && cp examples/env.azkaban.example ~/.azkaban/env
cp examples/env.codex.example .env.codex
cp examples/env.claude.example .env.claude
Each client retains its original launcher namespace. The complete controls are
*_JAIL_IMAGE, *_JAIL_WORKSPACE, *_JAIL_NETWORK, *_JAIL_USE_SSH,
*_JAIL_MOUNTS, *_JAIL_MCP_IMAGES, *_JAIL_IGNORE, and
*_JAIL_NO_TTY, where * is CODEX or CLAUDE. Claude also retains
CLAUDE_JAIL_SESSION. CONTAINER_MAX_MEMORY remains common.
Seven of those controls also have a client-neutral spelling so one line can
serve every client: AZKABAN_WORKSPACE, AZKABAN_NETWORK,
AZKABAN_USE_SSH, AZKABAN_MOUNTS, AZKABAN_MCP_IMAGES, AZKABAN_IGNORE,
and AZKABAN_NO_TTY. Both spellings are accepted in every layer. The nearer
layer wins, and inside one layer the client-prefixed spelling wins. The image
and Claude's session have no neutral spelling. The whole AZKABAN_ prefix is
reserved for launcher controls and never reaches a container.
Keys belonging to another registered client are consumed and dropped in every
file layer: its *_JAIL_* controls and its API-key variable. A shared file may
therefore hold both OPENAI_API_KEY and ANTHROPIC_API_KEY, and each client
receives only its own.
Resolution order, highest priority first, is:
wrapper CLI > exported host environment > workspace environment file
> client env.default > shared ~/.azkaban/env > built-in default
The workspace files are .env.codex and .env.claude. Per-client defaults
live at ~/.codex-jail/env.default and ~/.claude-jail/env.default; Claude's
file is optional and user-created. Settings common to both clients belong in
the shared ~/.azkaban/env, which the launcher reads but never creates. The
first valid definition in a file wins. Values are literal except that one
matching outer pair of single or double quotes is removed. This is not shell
syntax.
Launcher controls are consumed by the wrapper and not forwarded. Other safe
keys are ordinary container variables. Exported host values take precedence,
including an explicitly exported empty value. API keys may be exported as
OPENAI_API_KEY or ANTHROPIC_API_KEY; do not commit credentials to a
workspace file.
Policy mode
--policy PATH selects one external environment file and skips the
workspace environment file, client env.default, and the shared
~/.azkaban/env:
codex ./project --policy /etc/azkaban/team.policy
claude ./project --policy /etc/azkaban/team.policy
The policy must be a readable regular file outside the workspace. The launcher checks every path component and fails closed on symlink or traversal attempts. The policy-selected ignore file is combined with the ordinary workspace ignore file: workspace rules may hide more paths but cannot unhide policy-hidden content.
Ignore rules
By default, Codex reads <workspace>/.codexignore and Claude reads
<workspace>/.claudeignore. Missing default files are harmless. Select a
different readable file with --ignore-file or the active client's
*_JAIL_IGNORE variable; a missing explicit file is an error.
Rules use the preserved glob-style syntax, including rooted paths, **,
directory-only patterns, character classes, comments, and blank lines. Matches
are hidden by overlays placed after the workspace mount. Symlinks are hidden,
not followed outside the workspace.
MCP binaries and images
Place manually managed executable MCP binaries in
~/.codex-jail/mcp/ or ~/.claude-jail/mcp/. The active directory is exposed
read-only at /opt/mcp.
--mcp-image REF or the comma-separated *_JAIL_MCP_IMAGES control pulls an
OCI image, inspects its entrypoint (falling back to Cmd), and copies that
single binary from an unstarted container. The MCP image itself is never run
or started. Cached binaries are keyed by image ID; manually managed files are
not overwritten.
Extra mounts, networks, and SSH
Extra mounts deliberately expand client access:
codex . --mount "$HOME/.gitconfig:/home/codex/.gitconfig:ro"
claude . --mount "$HOME/.gitconfig:/home/claude/.gitconfig:ro"
A destination of ~ or ~/... expands to the active client's container home,
so one shared line covers both clients:
# in ~/.azkaban/env
AZKABAN_MOUNTS=~/.gitconfig:~/.gitconfig:ro
--mount is repeatable and takes precedence over both comma-separated
*_JAIL_MOUNTS and AZKABAN_MOUNTS, following the same neutral-versus-prefixed
layer rule as other controls: the nearer layer wins, and inside one layer
*_JAIL_MOUNTS wins over AZKABAN_MOUNTS. A source must exist, and only a
leading ~, ~/, or a literal $HOME is expanded on the source side; $HOME
is not expanded on the destination side because it would be ambiguous. A
destination such as ~user/path is not expanded either and is rejected as a
non-absolute destination. Use :ro wherever write access is unnecessary.
Use --network NAME only when the client must reach services on that Podman
network. --with-ssh-agent forwards SSH_AUTH_SOCK when available and warns
without failing when it is absent. Neither option is enabled by default.
Dry run
Inspect the exact launch without starting Podman or creating persistent state:
codex . --dry-run
claude . --dry-run
Standard output contains one shell-escaped command. Diagnostics go to standard
error. A first-time Claude session dry-run warns that its printed bind sources
do not exist yet; run once without --dry-run to initialize them safely before
reusing the printed command.
Headless contract
Supervisors driving non-interactive runs (--no-tty, piped stdin) can rely on
these launcher guarantees:
- Exit-code propagation. After validation the launcher replaces itself
with
podman runviaexec, so the process a supervisor waits on exits with the native client's own status. Launcher-side failures are reported before that boundary with a nonzero status and no container. - Signal forwarding and cleanup. Every launch uses
--rm --init: the in-container init forwards termination signals to the native client, and the container is removed when it exits. Sendingpodman runa catchable signal terminates the workload. - Stderr-only diagnostics. Launcher diagnostics — workspace, policy,
cidfile, ignore, and MCP lines, plus warnings — go to standard error.
Standard output belongs to the native client or, under
--dry-run, carries exactly one shell-escaped Podman command. - SIGKILL and conmon orphaning.
podman runis a client attached to a container supervised byconmon. If a supervisor SIGKILLs the launcher process, the container keeps running detached.--cidfile PATHpasses through topodman runso the container ID lands inPATH, letting the supervisor recover withpodman stoporpodman rm -fon that ID.
--cidfile requires an existing parent directory and refuses any path inside
the workspace for the same reason as --policy: the workspace is
container-writable, and a container-rewritable cidfile could redirect a
supervisor's podman stop to an arbitrary container. Podman itself fails to
start when the cidfile already exists, so remove the previous file between
runs.
Separate image builds
The images share a base stage but remain independent final targets:
make build-codex
make build-claude
make build
Defaults are codex-cli:latest and claude-code:latest. Override them with
CODEX_IMAGE, CODEX_TAG, CLAUDE_IMAGE, and CLAUDE_TAG when invoking
make. Building one target installs only that native client.
The image entrypoints disable native approval checks only inside their rootless containers. Do not reuse those entrypoint flags for host execution.
Codex authentication and state
Codex reuses ~/.codex-jail across all workspaces and mounts it at
/home/codex/.codex. Authentication, configuration, history, and logs remain
where existing codex-jail installations stored them. On the first real run,
the launcher creates the root, env.default, and mcp/ if absent; it never
overwrites existing contents.
Authenticate interactively through the native client or export
OPENAI_API_KEY. The writable state mount is followed by protective masks for
env.default and the MCP storage subtree so the client cannot modify inputs
that control a later launch.
Claude authentication and sessions
Claude stores workspace sessions under ~/.claude-jail/sessions. Without an
explicit selection, the launcher derives a stable eight-character ID from the
canonical workspace path and reuses it on later runs. Select an existing
session or list sessions with:
claude . --session SESSION_ID
claude --all-sessions
CLAUDE_JAIL_SESSION supplies the same selection at lower precedence than the
CLI. Explicit session IDs must already exist. A first real run creates the
session's config/settings.json and .claude.json transactionally; listing is
read-only. Authentication may be interactive within that state or use an
exported ANTHROPIC_API_KEY.
Troubleshooting
command not found: add$HOME/.local/bintoPATHand start a new shell.- Podman version failure: install or upgrade rootless Podman to 4.3 or newer.
- No TTY or noisy terminal control: pass
--no-ttyor set the active*_JAIL_NO_TTY=1control. - Memory detection failure: pass
--max-memory 2gor exportCONTAINER_MAX_MEMORY. - Missing mount or ignore source: use an existing host path; explicit sources fail closed.
- Stale MCP lock: confirm no refresh is active before manually removing the exact lock named by the diagnostic.
- Installer lock or recovery warning: preserve the reported path and inspect it before cleanup; the installer intentionally avoids deleting state whose ownership changed.
Use codex --help, claude --help, or ./install.sh --help for the current
command surface.
Migration
There is no state conversion. Installing azkaban replaces only selected
legacy public wrappers with links to the shared runtime, while both original
state trees and configuration names remain in place. Review the intentional
security tightenings and rollback guidance in the
migration guide.
Development and verification
Run the dependency-free fast suite and syntax checks with:
make test
make lint
make lint always runs Bash syntax checks and runs ShellCheck when installed.
The networked, real-Podman release gate is separate:
make smoke-images
It builds unique temporary tags for both targets, verifies UID/GID, username,
home, executable, entrypoint, and native --version, then removes only those
tags. See the compatibility fixture inventory
for the source-to-current test map.
Historical provenance
This repository consolidates independently rooted projects at pinned commits:
- codex-jail,
48fa586bb7c1a841adc366bcf092ed7c040ec017 - claude-jail,
24dab3ad94975f754f851dfc7124b6fae8a5b183
Their histories are linked as provenance rather than grafted together. License and attribution details are in COPYRIGHT.