- Shell 96.2%
- Dockerfile 3%
- Makefile 0.8%
Ported from claude-jail and adapted to this repo's sessionless model. Container-level variables now reach podman as `-e KEY` instead of `-e KEY=value`. Process arguments are world-readable on Linux, so every secret in .env.codex was visible to any local user via `ps` or /proc/<pid>/cmdline. Podman reads the values from the environment it inherits from the wrapper instead. A key that is not a real exported variable — because it collides with a script-internal name like `workspace` or `network` — is skipped with a warning rather than forwarded with the wrong value. Where a key is set in both the shell and .env.codex the container now receives the shell value, matching the documented precedence. Env loading moves into _load_env_file(), called for .env.codex then ~/.codex-jail/env.default, first definition winning. Since the whole jail directory is bind-mounted read-write, env.default is masked inside the container with a read-only /dev/null: Codex could otherwise append CODEX_JAIL_MOUNTS=/:/host and escape on the next run in any workspace. The mask is unconditional, and the file is seeded with a commented header on first use so both mount endpoints exist. --dry-run prints the assembled podman command and exits without running it or touching the filesystem. The .codexignore diagnostic moves to stderr so dry-run stdout is exactly the command; its path count was also reporting double, counting array elements rather than paths. tests/ runs against a stub podman that records argv and inherited env, so the argv/environment split above is directly asserted. 37 tests, no dependencies beyond bash: `make test`. Claude-Session: https://claude.ai/code/session_0134mePQ5EAQxnitf8Yst3ny |
||
|---|---|---|
| .claude | ||
| docs/superpowers/specs | ||
| tests | ||
| .dir-locals.el | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| codex.sh | ||
| COPYRIGHT | ||
| Dockerfile | ||
| install.sh | ||
| Makefile | ||
| README.md | ||
Codex Jail
Run the OpenAI Codex CLI inside a rootless Podman container.
Codex 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.
Why a jail?
Codex CLI ships with its own sandbox and approval system, but those primitives still run on top of your real user account. A misconfigured prompt or a sandbox bypass means the agent can touch anything you can. A container inverts the model: Codex sees an empty Debian system, plus only the paths you mount in. The host filesystem, your credentials, and unrelated projects stay out of reach.
Inside the container we pass --dangerously-bypass-approvals-and-sandbox
because the container itself is the sandbox — Codex can only act on the
explicitly mounted workspace and its own state directory.
Install
git clone https://git.unitoo.it/claudiomaradonna/codex-jail
cd codex-jail
./install.sh
The installer:
- Installs Podman if it is not present (apt/dnf/pacman/brew).
- Builds the
codex-cli:latestimage and dual-tags it with the detected Codex CLI version (e.g.codex-cli:0.45.0). - Drops the
codexwrapper into~/.local/bin/.
Make sure ~/.local/bin is in your PATH:
export PATH="${HOME}/.local/bin:${PATH}"
Installer options
./install.sh --image my-codex:dev # custom image name (skips version dual-tag)
./install.sh --no-cache # full rebuild bypassing layer cache
Usage
codex . # mount current directory
codex . --model gpt-5-codex # pass native codex flags
codex --with-ssh-agent . exec "fix" # SSH agent + non-interactive exec mode
codex --image my-image . # use a custom image
codex . --mount /data:/data:ro # mount /data as read-only
codex . --dry-run # print the podman command, run nothing
codex --help # full option list
Anything the wrapper does not recognise is forwarded verbatim to codex, so
all native subcommands (exec, resume, login, …) and flags (--model,
--profile, --config, --cd, …) work as usual.
Authentication
Codex CLI supports two auth modes:
- API key — export
OPENAI_API_KEYin your shell or set it in.env.codex. The wrapper forwards it into the container. - ChatGPT sign-in — run
codex logininside the container; the resulting~/.codex/auth.jsonis persisted on the host and reused on every run.
Unlike Claude Code, Codex CLI keeps a single login per host: splitting
state across multiple sessions would force you to log in again for each
one. The wrapper therefore mounts ~/.codex-jail/ directly as
/home/codex/.codex inside the container, so auth, config, conversation
history and logs are shared across every workspace — the same way the
native codex CLI behaves on your host.
Configuration: .env.codex
Drop a .env.codex file in your workspace and the wrapper loads it. Two
categories of variables are recognised:
Script-level (consumed by codex.sh, not forwarded into the container):
| Variable | Default | CLI override |
|---|---|---|
CODEX_JAIL_IMAGE |
codex-cli |
--image |
CODEX_JAIL_WORKSPACE |
/workspace |
--container-workdir |
CODEX_JAIL_NETWORK |
(none) | --network |
CODEX_JAIL_USE_SSH |
0 |
--with-ssh-agent |
CODEX_JAIL_MOUNTS |
(none) | --mount (repeats) |
CODEX_JAIL_IGNORE |
.codexignore |
--ignore-file |
CONTAINER_MAX_MEMORY |
host RAM | --max-memory |
Container-level (forwarded as env vars inside the container):
| Variable | Notes |
|---|---|
OPENAI_API_KEY |
Codex API authentication |
| anything else | Custom vars defined in .env.codex are forwarded |
Precedence order: CLI flag → host shell env → .env.codex →
~/.codex-jail/env.default → built-in default.
Copy .env.example as a starting point:
cp .env.example .env.codex
Values are passed by name, never by value
Container-level variables reach Podman as -e KEY, never as -e KEY=value.
Podman reads each value from the environment it inherits from the wrapper.
This matters because process arguments are world-readable on Linux: any user
on the host can run ps aux or read /proc/<pid>/cmdline. Passing
-e OPENAI_API_KEY=sk-… would put your key there for the lifetime of the
container. The wrapper's own environment is readable only by you and root.
Two consequences:
- A forwarded variable must be a real exported variable. If a key in
.env.codexorenv.defaultcollides with one of the wrapper's internal names (workspace,network,max_memory, …) it cannot be forwarded; the wrapper prints a warning and skips it. - When a variable is set in both your shell and
.env.codex, the container receives the shell value — matching the documented precedence.
This hides secrets from other users on the host. It does not encrypt
.env.codex at rest; keep that file out of version control.
Shared defaults: ~/.codex-jail/env.default
env.default in the jail directory holds settings shared across all
projects. Same format as .env.codex, same quoting rules, same script-level
vs. container-level split. The wrapper loads .env.codex first, then
env.default; for each key the first file to define it wins:
- Put what every project needs (preferred image, default mounts,
CODEX_JAIL_USE_SSH) in~/.codex-jail/env.default. - Keep only the per-project deltas in each workspace's
.env.codex. - Host environment variables still override both.
# ~/.codex-jail/env.default
CODEX_JAIL_USE_SSH=1
CODEX_JAIL_IMAGE=my-custom-image
CODEX_JAIL_MOUNTS=$HOME/.gitconfig:/home/codex/.gitconfig:ro
A commented template is created on first run. The file is entirely optional — without it the wrapper behaves exactly as before.
Because ~/.codex-jail/ is mounted read-write into the container, the wrapper
mounts /dev/null read-only over env.default inside the jail. Codex sees an
empty file and cannot rewrite the defaults that every future run in every
workspace depends on. The host file is untouched.
Inspecting the command: --dry-run
codex . --dry-run prints the exact podman run invocation to stdout and
exits without running it. Diagnostics go to stderr, so stdout is precisely the
command.
It is meant for inspection, not replay. The -e KEY arguments resolve against
whatever shell you paste them into, and a plain shell has not loaded
.env.codex — so variables like OPENAI_API_KEY would silently be missing
from the container.
.codexignore
To hide files or directories from the container, drop a .codexignore in
the workspace root. Patterns are one per line, glob-style; # starts a
comment.
# Secrets
*.pem
.env
**/*.key
# Whole directories (trailing slash)
secrets/
node_modules/
For each match the wrapper stacks a tmpfs (directories) or a read-only
/dev/null (files) on top of the workspace bind mount. The host files are
untouched; Codex just sees emptiness where they would be.
Mounts beyond your project
Only four host paths are exposed by default:
- the workspace itself (read/write)
~/.codex-jail/→/home/codex/.codex(read/write, UID-remapped), minusenv.default, which is masked with a read-only/dev/null- the SSH agent socket →
/ssh-agent(only with--with-ssh-agent) - anything you add via
--mountorCODEX_JAIL_MOUNTS
Nothing else from the host is reachable.
Build manually
make build # podman build -t codex-cli:latest .
podman build -t my-codex . # equivalent direct invocation
The CACHEBUST build arg forces npm to re-resolve @openai/codex, so a
plain make build always picks up the latest published Codex CLI.
Tests
make test # or: bash tests/run.sh
No dependencies beyond bash. A stub podman earlier on PATH records the
argv and environment the real one would have received, then exits without
starting a container — which is what lets the suite assert that a secret is
present in Podman's environment and absent from its argv.
License
BSD 3-Clause — see COPYRIGHT.