Run OpenAI Codex CLI/ Claude Code/ OpenCode safely inside separate rootless Podman containers while sharing one audited host launcher.
  • Shell 97.7%
  • Dockerfile 1.1%
  • JavaScript 0.8%
  • Makefile 0.4%
Find a file
Motoko Kusanagi 8d1f495f51
docs: keep custom image layers across refreshes
On Linux a launch rebuilds the client image whenever the nightly base
changes, and every layer of a workspace Dockerfile built on it rebuilds
with it. Show the multi-stage shape that keeps the heavy work in a
stage independent of the client image, and that podman image prune
drops that stage's cache, as a real Podman 5.7.0 build showed.

Refs #74
2026-11-11 15:53:14 +00:00
.agents
.claude
.codex
.forgejo
.opencode
bin refactor(launcher): expand arrays without the nounset guard 2026-11-11 13:41:53 +00:00
clients
containers build(images): clean npm cache in client layers 2026-11-11 13:41:53 +00:00
docs docs: keep custom image layers across refreshes 2026-11-11 15:53:14 +00:00
examples docs: align user documents with the code 2026-11-11 13:41:53 +00:00
lib
tests test: trim cases repeated inside tests 2026-11-11 13:45:17 +00:00
.gitignore
.prettierrc
.shellcheckrc
AGENTS.md
COPYRIGHT
install.sh
Makefile
README.md docs: align user documents with the code 2026-11-11 13:41:53 +00:00

azkaban

azkaban runs Codex CLI, Claude Code, or OpenCode inside separate containers, rootless Podman on Linux and Apple container on macOS, while sharing one audited host launcher. The public commands are codex, claude, and opencode; each keeps its own image, configuration, authentication, and persistent state.

An agent left to work without approval prompts can, on the host, read your keys, change files outside the project, and push as you. azkaban lets it work that way inside a container that sees the project folder, the client's own state, and only what you expose (Security model). It also keeps who acts apart: each launch runs as an identity with its own keys, home files, and instructions, and each client login lives in a state folder of its own, so personal, work, and bot sessions keep their credentials and history apart unless you configure them to share.

Every client image builds on Ubuntu 26.04 LTS with git, Node.js, ShellCheck, Prettier, the forge CLIs gh and fj, and common command-line tools. containers/Dockerfile, the installer's image definition, lists the rest; a project adds its own tools through a custom image.

Quick start

On Linux with Bash 4.4 or newer, get azkaban, install a client, put the launcher on PATH, and start it in a project folder that holds nothing the agent must not read:

git clone https://git.unitoo.it/claudiomaradonna/azkaban.git
cd azkaban
./install.sh claude
export PATH="$HOME/.local/bin:$PATH"
cd ~/projects/example
claude .

The installer installs rootless Podman when it is missing. Log in through the client on its first launch, or put its API key in ~/.azkaban/identities/default/env, as Set up the default identity describes. Installation covers the other clients, the install options, and updates, and macOS, not yet verified on a real Mac, the setup there.

Features

Usage

The first positional argument is the workspace when it is an existing folder, and must precede native-client arguments. Use -- to stop wrapper parsing explicitly:

codex .
claude ./project -- --native-option
opencode ./project -- run --auto "Explain the test suite"
opencode ./project -- --session SESSION_ID

Any other first argument goes to the client, and the workspace is then the current directory: a terminal asks you to confirm it, and a launch whose stdin is not a terminal uses it unasked. A mistyped folder name, as in claude ./projct, therefore mounts the whole current directory.

Every command accepts these options:

Option Purpose
-h, --help Show command help.
--network NAME Attach to an existing network.
--mount SRC:DST[:OPTIONS] Add a bind mount; repeatable (Extra mounts).
--max-memory VALUE Set the container memory limit; without one, Podman sets none; on macOS see macOS.
--image NAME Override the client image.
--skip-custom-images Run the image without building a workspace Dockerfile (Custom images).
--unmask-proc Lift the /proc masks for a nested sandbox; Linux only.
--cidfile PATH Write the container ID to a host file outside the workspace.
--identity NAME Run as an identity; default when omitted.
--account NAME Keep the client's state in an account instead of the identity's.
--no-tty Disable TTY allocation; without it, a TTY is allocated when standard input and output are terminals.
--dry-run Print one shell-escaped launch command without launching (Dry run).
-- Forward all remaining arguments to the native client.

Use codex --help, claude --help, opencode --help, or ./install.sh --help for the current command surface.

Security model

Codex and Claude run with their native approval checks disabled because the container is the intended containment boundary. Those bypass options are not safe to use on the host. OpenCode keeps its native permission rules by default; pass run --auto explicitly for unattended work. OpenCode's --auto approves requests while preserving explicit deny rules. On Linux a rootless container reduces host access, but it is not a virtual machine. On macOS each container runs in its own lightweight virtual machine without a user namespace, so root in the container is root in that machine. Neither is 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, and it runs at most 512 processes, a fixed limit. It sees the whole workspace, so keep any file it must not read outside that folder. Everything the launcher keeps on the host lives below ~/.azkaban, and the client's state directory, ~/.azkaban/state/<identity>/<client> for the identity the launch runs as, or ~/.azkaban/accounts/<account>/<client> for an account it names, is the only part of it the client can write. Extra mounts, an identity's SSH key or host agent, networks, and the /proc unmask expand or alter that boundary; review each of them as security-sensitive configuration. On Linux amd64 and arm64 the client image builds on your host on a base image the project's forge builds and publishes, which each launch pulls anew (Updates). A workspace Dockerfile.<client> or Dockerfile.azkaban changes the image only after you approve its content (see Custom images). azkaban never mounts the engine's socket. When agents need to sign and push, give them a bot account of their own, as described in Run agents as a bot account.

The launcher parses environment files as data: it does not source them or use eval. File-provided container variables reach the container through a private env file that the engine reads, so their values never enter the environment of a program the launcher runs on the host, nor any process argument list. The workspace .env.<client>, which the client can write, holds container variables only (see Environment configuration). Trusted adapters are loaded only from the installed runtime, never from a workspace, PATH, or configuration file.

A tool inside a Linux container can isolate folders with bwrap (Nested sandboxes), which keeps a cooperative command to its folders but cannot hold one crafted to escape it; the container stays the boundary toward the host.

Documentation

Beside the pages the features link:

License

COPYRIGHT holds the license and attribution details.