Run the OpenAI Codex CLI inside an isolated Docker container instead of directly on your host. The container mirrors your host environment (same paths, UID, shell), so file references, AGENTS.md discovery, sessions, and Codex auth all keep working with minimal friction.
This is an opinionated project tuned for the way I and my colleagues work day to day. The goal is to keep the daily Codex flow feeling exactly like running codex on the host — same paths, same git, same docker compose against your project's stack — while putting a soft blanket between Codex and the parts of your machine you'd rather it not touch by accident.
This is a safety net for bad prompts, not bad actors.
The threat model this project addresses is AI mistakes — the kind of footguns an LLM can stumble into when interpreting an ambiguous instruction, getting confused about state, or going overboard trying to be helpful. Concretely:
- You're checked out on
masterwithout realizing it and prompt "do the changes and push them". Codex triesgit push, the wrapper refuses pushes to protected branches, and you (the human) decide whether to push from the host. No accidental force-push tomasterbecause Codex didn't pause to ask. - You write "make REALLY sure that directory is gone" and Codex, in its enthusiasm, reaches for
sudo rm -rf /. The container is the blast radius — only your mounted project dirs are reachable, the host is untouched. Worst case you lose what you mounted, not your home directory. - A misread file path or runaway loop tries to write somewhere it shouldn't. The bind-mount allowlist confines damage to directories you explicitly opted in.
The threat model this project does not address is a deliberately adversarial Codex — an LLM actively crafting multi-step attacks to break out of the sandbox, exfiltrate credentials via sibling containers, or otherwise behave like a hostile insider. If that's your threat model, this isn't the right tool: don't give Codex the Docker socket at all, don't bind-mount ~/.codex, and consider air-gapped execution.
The friction trade-off goes one way on purpose: the sandbox must not get in our way. Standard docker compose up, docker compose exec, debugger attach, language servers, and the rest of the daily-driver workflow all work without per-project allowlists or extra config. If a hardening proposal would block a legitimate developer flow, it's out of scope for this project — even if it would close a theoretical attack path.
In short: paranoia calibrated to "AI mental breakdown", not to "nation-state in your chat window".
codex-docker is one of several sibling projects that apply the same sandboxing model to different AI coding agents. They share the security philosophy (filtered Docker socket, path mirroring, git-push wrapper, scope above) and most of the implementation, but each is adapted to its agent's config and auth model.
| Project | Wraps |
|---|---|
claude-docker |
Claude Code |
codex-docker (this project) |
OpenAI Codex CLI |
pi-docker |
pi |
Pick by which agent you actually use day to day. Running more than one in parallel is fine — the containers are independent.
- Isolated execution — codex runs in an Alpine container instead of directly on your host
- Docker socket proxy — filtered Docker API access via
wollomatic/socket-proxyplus an extra validation proxy - Path mirroring —
~/projectinside the container is the same path as on the host - Host identity mirroring — same username, UID, home path, and preferred shell
- Shared codex state — mounts your
~/.codex/directory, so auth, sessions, and config (auth.json,config.toml, history) are reused - Consistent tool PATHs — login shells keep image-installed tools like
codex,go, and custom wrappers onPATH - Session teardown for terminal and IDE callers — host watchdog plus in-container wrapper clean up orphaned codex processes even when the parent wrapper dies early
- Git safety rails — blocks pushes to protected branches from inside the container
- Optional GPG import — import signing keys into the container at startup
- Optional notifier hook — mount a custom
codex-notifierscript into the container for sound/desktop notifications - Optional beeper helper — host-side HTTP helper for simple sound notifications
Codex stores its state under ~/.codex/ (overridable via CODEX_HOME):
auth.json— ChatGPT login or API key tokensconfig.toml— user settingshistory.jsonl,log/, session rollouts
codex-docker mounts that directory directly at the same path inside the container, so the same Codex identity and configuration are visible inside the container.
If you use a custom config path, set CODEX_HOME on the host before running bin/codex-docker-ctrl start.
- macOS or Linux
- Docker Desktop, OrbStack, or Docker Engine
- Node/npm on the build host is not required; the image installs Codex itself
git clone https://github.com/hrubymar10/codex-docker.git
cd codex-dockerexport PATH="/path/to/codex-docker/bin:$PATH"This gives you:
codex-dockercodex-docker-ctrlcodex-docker-vscode-wrapper
You have two common options:
Export an OpenAI API key on the host:
export OPENAI_API_KEY=sk-...codex-docker-ctrl forwards OPENAI_API_KEY into the container if it's set on the host.
Codex stores OAuth/subscription auth in ~/.codex/auth.json. Since that directory is mounted into the container, you can:
- authenticate on the host once with
codex login(or use the existing browser flow), or - authenticate inside the container after startup
cp config/docker-compose.local.example.yml config/docker-compose.local.ymlEdit config/docker-compose.local.yml and add the directories you want Codex to access:
services:
codex:
volumes:
- ${HOST_HOME}/projects:${HOST_HOME}/projects
- ${HOST_HOME}/work:${HOST_HOME}/workPaths are mirrored exactly.
cp config/.env.example config/.envUseful for:
- pinning
CODEX_VERSION - selecting extra Alpine packages
- overriding
CODEX_HOME - setting protected branches
bin/codex-docker-ctrl startFrom any mounted project directory:
cd ~/projects/my-app
codex-dockerOr:
bin/codex-docker-ctrl execBoth run codex inside the container with the current working directory preserved.
For non-interactive / sandboxed runs, pass codex flags through:
codex-docker exec --dangerously-bypass-approvals-and-sandbox "summarize this repo"(The container itself is already a sandbox; the --dangerously-bypass-approvals-and-sandbox flag tells Codex to skip its own internal sandbox + approval prompts when you've intentionally run it inside one.)
Most VS Code Codex extensions launch a configured codex binary. Point the extension at the wrapper in this repo so it uses the container instead. For the official OpenAI Codex extension, set chatgpt.cliExecutable in settings.json:
{
"chatgpt.cliExecutable": "/path/to/codex-docker/bin/codex-docker-vscode-wrapper"
}Make sure the container is running before launching the extension.
bin/codex-docker-ctrl start # build image, start container
bin/codex-docker-ctrl stop # stop container
bin/codex-docker-ctrl status # show status
bin/codex-docker-ctrl shell # shell into the container
bin/codex-docker-ctrl exec # run codex in the container
bin/codex-docker-ctrl rebuild # rebuild image from scratch + restart
bin/codex-docker-ctrl beeper-start # start host beeper server (default 127.0.0.1:9999)
bin/codex-docker-ctrl beeper-stop # stop host beeper server
codex-docker # shortcut wrapper that runs codex in the containerAny codex arguments are forwarded:
codex-docker exec "summarize this repo"
codex-docker --model gpt-5
codex-docker --sandbox workspace-writeCodex inside the container can use your host SSH agent for git pushes, ssh connections, etc. — no private keys are copied into the container.
If SSH_AUTH_SOCK is set on the host (i.e. an ssh-agent is running), codex-docker-ctrl start automatically launches a socat TCP relay on 127.0.0.1:19922 that bridges the host's Unix-domain agent socket. Inside the container, SSH_AUTH_SOCK is configured to point at the relay over host.docker.internal, and ~/.ssh/known_hosts is bind-mounted in. Use ssh-add -l inside the container to confirm the agent is reachable.
- Install
socaton the host. On macOS:brew install socat. On Linux: install via your package manager. - Make sure your SSH agent is running on the host —
echo "$SSH_AUTH_SOCK"should print a path. - Add your key once:
ssh-add ~/.ssh/id_ed25519(or whichever key). - Start (or restart) the container:
bin/codex-docker-ctrl start.
The relay starts on codex-docker-ctrl start and stops on codex-docker-ctrl stop. If socat isn't available on the host, the relay is skipped with a warning and the rest of the container still starts normally.
Optional host-side HTTP server (beeper/main.go) that plays a sound when called. Started by codex-docker-ctrl beeper-start. Two env vars control access:
BEEPER_BIND—host:portto listen on. Default127.0.0.1:9999. Host must be an IP literal (no hostnames). Set to0.0.0.0:9999to expose on all interfaces.BEEPER_ALLOW— comma-separated list of source IPs / CIDRs that may call the beeper. Default127.0.0.0/8. Bare IPs are normalised to/32(v4) //128(v6). Requests from anywhere else get a403.
For container access via host.docker.internal, the defaults are sufficient on Docker Desktop / OrbStack (it forwards to host loopback). For VPN clients or other remote access, widen both:
export BEEPER_BIND=0.0.0.0:9999
export BEEPER_ALLOW=127.0.0.0/8,172.28.47.0/24Linux note: on Linux Docker Engine, host.docker.internal resolves to the Docker bridge gateway (typically in 172.17.0.0/16 or 172.16.0.0/12), not host loopback. The default BEEPER_ALLOW=127.0.0.0/8 will block those requests. Add the bridge subnet to the allowlist. Note: beeper/main.go calls afplay (macOS only) — sound playback does not work on Linux.
X-Forwarded-For is intentionally not honoured — this is a direct-connection service.
bash -n bin/codex-docker bin/codex-docker-ctrl bin/codex-docker-vscode-wrapper bin/lib/session-cleanup.sh scripts/*.sh test/*.sh
make testCurrent tests cover:
- mount boundary logic
- credential helper quoting
- session PID file naming
- wrapper behavior with mocked
docker - start-time preflight/override generation with mocked
docker - explicit compose project pinning (
COMPOSE_PROJECT_NAME) docker compose configrendering smoke test- VS Code wrapper forwarding
The container does not get direct access to /var/run/docker.sock.
Instead:
- Docker calls go through a filtering proxy
- dangerous container-create options are rejected
- Docker socket bind mounts are stripped from downstream create requests
- the in-container
dockerwrapper blocks dangerous subcommands likerun,build, andcp - the in-container
gitwrapper blocks pushes to protected branches (main,masterby default) and anygit pushthat would publish tags (--tags,--follow-tags,--mirror, arefs/tags/*refspec, or the<remote> tag <name>shorthand)
See SECURITY_ISSUES.md for known limitations.
- the notifier file is
config/codex-notifier, mounted as/usr/local/bin/codex-notifier - a compatibility symlink also exposes
/usr/local/bin/claude-notifierinside the container for older scripts - Codex reads
AGENTS.md;codex-docker-ctrlauto-mounts your global~/AGENTS.mdand~/CLAUDE.mdif present bin/codex-docker-ctrlpinsCOMPOSE_PROJECT_NAME=codex-dockerby default so docker resource names do not depend on the checkout directory name
MIT