No description
  • Shell 96.2%
  • Dockerfile 3%
  • Makefile 0.8%
Find a file
Claudio Maradonna ffb0c1ecc9
feat: name-only env forwarding, shared env.default, --dry-run, test suite
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
2026-07-28 14:30:29 +02:00
.claude init from claude-jail :) 2026-05-11 12:59:23 +02:00
docs/superpowers/specs docs: spec for name-only env forwarding, env.default, --dry-run, tests 2026-07-28 14:24:35 +02:00
tests feat: name-only env forwarding, shared env.default, --dry-run, test suite 2026-07-28 14:30:29 +02:00
.dir-locals.el init from claude-jail :) 2026-05-11 12:59:23 +02:00
.dockerignore feat: name-only env forwarding, shared env.default, --dry-run, test suite 2026-07-28 14:30:29 +02:00
.env.example feat: name-only env forwarding, shared env.default, --dry-run, test suite 2026-07-28 14:30:29 +02:00
.gitignore init from claude-jail :) 2026-05-11 12:59:23 +02:00
codex.sh feat: name-only env forwarding, shared env.default, --dry-run, test suite 2026-07-28 14:30:29 +02:00
COPYRIGHT init from claude-jail :) 2026-05-11 12:59:23 +02:00
Dockerfile feat(docker): add ssh client support 2026-05-20 16:41:19 +02:00
install.sh chore: add TZ argument for custom Timezone 2026-05-11 23:22:25 +02:00
Makefile feat: name-only env forwarding, shared env.default, --dry-run, test suite 2026-07-28 14:30:29 +02:00
README.md feat: name-only env forwarding, shared env.default, --dry-run, test suite 2026-07-28 14:30:29 +02:00

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:

  1. Installs Podman if it is not present (apt/dnf/pacman/brew).
  2. Builds the codex-cli:latest image and dual-tags it with the detected Codex CLI version (e.g. codex-cli:0.45.0).
  3. Drops the codex wrapper 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_KEY in your shell or set it in .env.codex. The wrapper forwards it into the container.
  • ChatGPT sign-in — run codex login inside the container; the resulting ~/.codex/auth.json is 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.codex or env.default collides 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), minus env.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 --mount or CODEX_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.