safe-codex

Run Codex in a disposable Podman or Docker container with a small, explicit host surface.

What it does

Small host surface

Only the selected workspace and explicit extra mounts are exposed to the container.

Disposable runtime

The container root filesystem is read-only, capabilities are dropped, and containers are removed on exit.

Separate Codex state

Container Codex state lives in an engine named volume, not host ~/.codex.

Regulated network

Default egress goes through Squid, with local/private destinations blocked.

Rootless Podman is preferred when available. Docker works, but a user in the docker group should be treated as effectively root-capable on the host.

Repository files

Path Purpose
ContainerfileCodex runtime image.
Containerfile.proxySquid proxy image.
proxy/squid-nonlocal.confDefault egress policy: Internet allowed, local/private IP destinations blocked.
proxy/squid-openai.confStricter egress policy for OpenAI/ChatGPT domains.
bin/safe-codexShared launcher used by the Podman and Docker wrappers.
bin/install-native-codex-cliOptional host helper that installs the native Codex CLI payload without invoking npm, npx, corepack, nvm, or node.
bin/install-node-package-tool-deny-stubsOptional host helper that disables normal npm, npx, and corepack use while leaving node available.
bin/checkNon-invasive syntax and Dockerfile checks.
safe-codex.env.exampleOptional local defaults.

The launcher parses .env as plain KEY=VALUE. It does not source shell code, and it accepts only SAFE_CODEX_* keys plus OPENAI_API_KEY.

Build

Check what the launcher sees before building or running:

./bin/safe-codex-podman doctor

Build with Podman:

./bin/safe-codex-podman build

Build with Docker:

./bin/safe-codex-docker build

The build uses npm once inside the build container with --ignore-scripts, then removes npm, npx, and corepack from the runtime image.

Host npm guard

The container image removes package-manager entrypoints from the Codex runtime. If the host should also keep node and node-based tools available while avoiding accidental npm, npx, or corepack use, install the optional per-user deny stubs:

./bin/install-node-package-tool-deny-stubs --dry-run
./bin/install-node-package-tool-deny-stubs

This writes managed stubs and helper commands under ~/.local/bin:

~/.local/bin/npm
~/.local/bin/npx
~/.local/bin/corepack
~/.local/bin/strip-node-package-tools
~/.local/bin/check-node-package-tools-disabled

The helper also updates ~/.bashrc by default so ~/.local/bin stays first in PATH. If nvm has already been loaded by that startup file, the installed wrapper strips npm, npx, and corepack again after common nvm commands that can reintroduce them.

Existing ~/.local/bin entries with those names are moved under ~/.quarantine before replacement. Existing nvm package-manager files are also moved there by strip-node-package-tools. The script does not run npm, npx, corepack, or package install scripts.

Verify the host state at any time:

check-node-package-tools-disabled

Use --no-rc if you do not want the script to edit a shell startup file. Use --rc-file PATH to point it at a different Bash startup file.

This is a per-user guard against normal and accidental use, not a hard system policy. A process with your user privileges can still bypass it by invoking a hidden absolute path, changing PATH, installing another Node distribution, or restoring files from quarantine. Run untrusted package work inside containers.

To undo the guard, remove the marked safe-codex disable host npm/npx/corepack block from the shell startup file, remove the managed commands from ~/.local/bin, and restore files from ~/.quarantine only if you intentionally want those tools back.

Native Codex install

To restore or install host Codex without restoring host package-manager entrypoints, use the native installer:

./bin/install-native-codex-cli --dry-run --version 0.130.0
./bin/install-native-codex-cli --version 0.130.0

The installer downloads the platform-specific @openai/codex native tarball directly from the npm registry, requires an explicit version, verifies the registry-published sha512 integrity value, extracts only the native vendor payload, and writes a ~/.local/bin/codex launcher.

The default payload path is:

~/.local/share/codex-cli/VERSION

The script does not invoke npm, npx, corepack, nvm, or node. The generated launcher blocks codex update by default, so upgrades remain an explicit reinstall with a pinned version.

This still trusts the npm registry metadata, TLS, and the published native Codex tarball for the pinned version. It is meant to avoid package-manager execution on the host, not to prove the binary from source.

Run Codex

From the project you want Codex to work on:

/path/to/safe-codex/bin/safe-codex-podman run

Docker equivalent:

/path/to/safe-codex/bin/safe-codex-docker run

Default behavior

  • Profile: default.
  • Workspace: current directory mounted at /workspace read/write.
  • Persistent state: safe-codex-state-default mounted at /home/codex/.codex.
  • Nonpersistent home: /home/codex tmpfs with noexec.
  • Network: proxy, using safe-codex-proxy-nonlocal.
  • GPU devices: none.
  • Lifecycle: Codex containers are removed on exit; state persists in the named volume.

Common options

Option Use
--profile NAMEUse a separate persistent Codex state volume and container name.
--workspace PATHMount a specific project at /workspace.
--workspace-ro or --workspace-rwChoose whether the workspace bind mount is read-only or writable.
--network proxy, openai, or noneChoose the egress policy for the run.
--gpu nvidia or --gpu dri:/dev/dri/renderDNNNExpose GPU devices explicitly. The default is no GPU access.
--ro SRC[:DST] or --rw SRC[:DST]Add a narrow extra mount without exposing the host home or broad system paths.

For a read-only-by-default workflow, set SAFE_CODEX_WORKSPACE_MODE=ro in .env, then pass --workspace-rw only when a run should edit the workspace.

Detach and resume

Detach from a live container without stopping Codex:

Ctrl-p Ctrl-q

Attach again:

/path/to/safe-codex/bin/safe-codex-podman attach

If the machine restarted or Codex exited, run the same run command again. The process is gone, but Codex auth/session files persist in the profile volume.

Ask Codex to resume from the persistent volume:

/path/to/safe-codex/bin/safe-codex-podman run -- resume

Storage model

The container home is split deliberately.

Container path Backing storage Persistence
/home/codex/.codexEngine named volume safe-codex-state-PROFILEPersists
/home/codex/.cachetmpfs under /home/codexDeleted on exit
/home/codex/.localtmpfs under /home/codexDeleted on exit
/home/codex except .codextmpfs with nosuid,nodev,noexecDeleted on exit
/tmptmpfs with nosuid,nodev,noexecDeleted on exit
/runtmpfs with nosuid,nodev,noexecDeleted on exit
/workspaceSelected host workspace bind mountHost files

For the default Podman profile, the persistent volume is stored on the host at:

~/.local/share/containers/storage/volumes/safe-codex-state-default/_data

Inspect through Podman so rootless user-namespace ownership is displayed correctly:

podman volume inspect safe-codex-state-default
podman unshare ls -la ~/.local/share/containers/storage/volumes/safe-codex-state-default/_data

Do not mount or copy host ~/.codex into the container. The profile volume may contain Codex auth material, logs, sessions, plugins, skills, and local state.

Profiles

Use separate profiles for separate trust zones:

/path/to/safe-codex/bin/safe-codex-podman run \
  --profile id-overlay \
  --workspace /path/to/id-overlay

This creates container name safe-codex-id-overlay and state volume safe-codex-state-id-overlay. Profile names may contain only letters, numbers, dot, underscore, and dash.

List existing persistent profiles and any live Codex containers:

/path/to/safe-codex/bin/safe-codex-podman profiles

Multiple sessions

A profile controls Codex's persistent state. A workspace controls which host files are mounted at /workspace. Changing the profile while keeping the same workspace gives you a separate Codex state volume looking at the same host files.

/path/to/safe-codex/bin/safe-codex-podman run \
  --profile edit-1 \
  --workspace /path/to/project \
  --workspace-rw

/path/to/safe-codex/bin/safe-codex-podman run \
  --profile edit-2 \
  --workspace /path/to/project \
  --workspace-rw
Setting What changes What is shared
--profile edit-1 versus --profile edit-2 Separate containers and separate state volumes: safe-codex-state-edit-1 and safe-codex-state-edit-2. Nothing in ~/.codex is shared between those profiles.
Same --workspace /path/to/project The same host directory is mounted into each container at /workspace. Both sessions see the same files, git state, and uncommitted changes in that host directory.

If two sessions mount the same workspace read/write, they can edit the same files at the same time. The profile boundary separates Codex state; it does not isolate the shared workspace bind mount.

A safer pattern is one writable editor plus one or more read-only reviewers:

/path/to/safe-codex/bin/safe-codex-podman run \
  --profile edit \
  --workspace /path/to/project \
  --workspace-rw

/path/to/safe-codex/bin/safe-codex-podman run \
  --profile review \
  --workspace /path/to/project \
  --workspace-ro

If two sessions should edit independently, use separate git worktrees or clones and give each one its own profile.

Read-only and extra mounts

Inspect a repo read-only:

/path/to/safe-codex/bin/safe-codex-podman run \
  --workspace /path/to/suspicious \
  --workspace-ro

Override a read-only .env default for a normal editing session:

/path/to/safe-codex/bin/safe-codex-podman run \
  --workspace /path/to/project \
  --workspace-rw

Add one writable output folder:

/path/to/safe-codex/bin/safe-codex-podman run \
  --workspace /path/to/project \
  --rw /tmp/codex-output:/output

Add a read-only reference folder:

/path/to/safe-codex/bin/safe-codex-podman run \
  --workspace /path/to/project \
  --ro /path/to/docs:/mnt/docs

The launcher refuses risky mounts by default, including /run, /var/run, /dev, /proc, /sys, ~/.ssh, ~/.codex, and mount trees containing Unix sockets. Symlinks are resolved before this check. Paths containing commas or newlines are rejected because Docker and Podman --mount cannot represent them unambiguously.

Extra mount destinations are constrained to:

/mnt/rw...
/mnt/ro...
/workspace/...
/input...
/output...

Override these checks only with a concrete reason:

--allow-risky-mounts

GPU devices

GPU access is opt-in. The default run exposes no NVIDIA devices, no DRI render nodes, and no broad /dev mount.

Expose all NVIDIA GPUs:

/path/to/safe-codex/bin/safe-codex-podman run \
  --gpu nvidia

Expose one or more NVIDIA GPUs by index or UUID:

/path/to/safe-codex/bin/safe-codex-podman run \
  --gpu nvidia:0

/path/to/safe-codex/bin/safe-codex-podman run \
  --gpu nvidia:0,1

/path/to/safe-codex/bin/safe-codex-podman run \
  --gpu nvidia:GPU-01234567-89ab-cdef-0123-456789abcdef

With Podman, NVIDIA mode uses CDI devices such as nvidia.com/gpu=all. With Docker, it uses Docker's --gpus flag. The host still needs working NVIDIA container support.

Expose one explicit DRI render node for an AMD or Intel iGPU:

/path/to/safe-codex/bin/safe-codex-podman run \
  --gpu dri:/dev/dri/renderD128

/path/to/safe-codex/bin/safe-codex-podman run \
  --gpu dri:/dev/dri/by-path/pci-0000:00:02.0-render

Find stable render-node names on the host:

ls -l /dev/dri/by-path/*-render

Use the actual path from that listing for --gpu dri:... or SAFE_CODEX_AMD_IGPU_RENDER.

The DRI mode resolves symlinks and accepts only /dev/dri/renderD* character devices. The launcher also requires the current host user to be able to read and write the render node before starting the container. On many systems that means adding the user to the render group and starting a new login session.

If you want a named iGPU default in .env, set:

SAFE_CODEX_GPU=amd-igpu
SAFE_CODEX_AMD_IGPU_RENDER=/dev/dri/by-path/pci-0000:00:02.0-render

GPU mode weakens containment by exposing host device files. It only exposes devices; the runtime image still needs the userspace libraries, drivers, Python environment, or other stack required by the workload.

Network modes

Mode Behavior
--network proxy Default. Codex uses Squid on an internal network. The proxy blocks local/private destinations such as loopback, RFC1918 ranges, link-local, multicast, and IPv6 local ranges.
--network openai Stricter allowlist for OpenAI/ChatGPT-related domains. Useful for API-key use; often too strict for browser login, package registries, git remotes, or MCP servers.
--network none No network. Useful for offline inspection; Codex API calls will not work.
--network bridge --allow-bridge-network Unrestricted container bridge. This bypasses the proxy and is intentionally gated.

The proxy uses a dedicated egress network named safe-codex-egress-nonlocal unless SAFE_CODEX_EXTERNAL_NETWORK is set.

Authentication

Preferred: pass an API key from the environment.

export OPENAI_API_KEY=...
/path/to/safe-codex/bin/safe-codex-podman run

Do not mount host ~/.codex. That directory contains live auth/session material. If you use browser/login auth inside the container, it is stored in the profile's named Codex-state volume.

Environment variables passed with -e or --env-file are visible through container inspection while the container is running. The launcher removes the Codex container on exit so those values are not left in stopped container metadata.

Shell

Open a shell with the same containment settings:

/path/to/safe-codex/bin/safe-codex-podman shell \
  --workspace /path/to/project

Maintenance

Show readiness:

./bin/safe-codex-podman doctor

Show containers, volumes, and networks:

./bin/safe-codex-podman status

List persistent Codex profiles:

./bin/safe-codex-podman profiles

Inspect a specific proxy profile:

./bin/safe-codex-podman proxy status --mode openai

Stop a live Codex container:

./bin/safe-codex-podman stop --profile default

Remove a running container but keep persistent Codex state:

./bin/safe-codex-podman rm --profile default

Delete a profile's persistent Codex state only when intended:

podman volume rm safe-codex-state-default

Checks

Run local syntax checks without building images:

./bin/check

This validates the launcher scripts, the native Codex installer, the optional host npm guard, and Dockerfile syntax. If safe-codex-proxy:latest already exists, it also runs Squid's parser against both proxy profiles. If shellcheck is installed, it is used too.