The goal was simple:
Let Codex work on my GitHub repository without handing an AI agent the keys to my actual development environment.
Docker Sandboxes looked like the obvious answer.
In my head, the plan was three steps:
Give Codex a repository
↓
Let it work inside a container
↓
Keep the host protectedSimple, right? Yeah, no. This wasn’t one problem. It was about seven problems wearing a trench coat: Codex’s execution mode, the filesystem sandbox, Git, SSH agent forwarding, GitHub host verification, network policies, and reusable sandbox templates.
And at the end of it, I had to admit something annoying:
The most isolated setup wasn’t the one I actually wanted to use every day.
Here’s how I got there.
Docker had other plans for Codex
I wanted Codex running with sensible guardrails, so I expected to launch it with:
--sandbox workspace-write
--ask-for-approval on-request
Didn’t work. Docker’s built-in Codex setup was already launching it with:
--dangerously-bypass-approvals-and-sandbox
Read that flag again. It literally has “dangerously” in the name. I was trying to add safety rules to something that had been told to skip all safety rules. That’s like putting a lock on the front door while the back wall is missing.
So I made my own setup, where I control how Codex gets launched. I called it “Codex Safe.” Nothing fancy. It runs Codex with:
--sandbox workspace-write
--ask-for-approval on-request
The whole trick is making the entrypoint explicit:
entrypoint:
run:
- codex
- '--sandbox'
- 'workspace-write'
- '--ask-for-approval'
- 'on-request'
I felt pretty good about myself. Then git add started asking for permission.
Why the hell was git add asking for approval?
Codex tried to run:
git add .
And it wanted my approval. For git add. One of the most harmless commands in all of Git. My first thought:
How does staging files suddenly need a permission slip? What did I break?
Here’s the thing that took me way too long to get. Codex wasn’t treating git add as dangerous. The problem was one layer down.
In the Codex Safe setup, the repository’s Git metadata was effectively read-only as far as the filesystem sandbox was concerned. And git add has to update Git’s index, which means creating or modifying:
.git/index.lock
So the real chain of events was:
Think of a bouncer who lets you into the club but won’t let you near the bar. You’re technically in. You just can’t do anything. Codex was allowed to run the command. It just wasn’t allowed to write the file the command needed.
That’s when it clicked that I was dealing with two completely separate layers of control: what Codex can execute (the execution policy) and what that command can actually modify (the filesystem sandbox).
Loosening one does nothing for the other. Once you see that, half the “why is it asking me this?” moments stop being mysterious.
So instead of fighting those restrictions on my real repository, I started wondering if I was coming at this from the wrong direction.
Maybe Codex shouldn’t touch my repository at all
The real problem was never git add. Codex needed a repository it could fully own, .git included. I wanted it to be able to:
- create commits
- create branches
- modify the working tree
- modify
.git - run tests
- inspect its own history
But I still didn’t want it messing with the actual repository on my host. That’s where Docker’s --clone mode started making a lot of sense:
Now Codex had its own writable repository instead of banging its head against a read-only .git. The git add problem just vanished.
Better yet, this was the isolation model I’d wanted from the start. My host repository stays outside the sandbox’s writable workspace, and the sandbox gets its own copy to wreck however it likes.
Great. Problem solved. Except, obviously, it wasn’t.
Then GitHub became the problem
A private clone is useless if the agent can’t actually talk to GitHub.
I already use SSH for GitHub on my host. But copying my private SSH key into every sandbox? That’s building a vault and then taping the combination to the door. It defeats the whole point.
So the key stays on the host, and the sandbox gets SSH agent forwarding:
Host
|
| private SSH key
v
ssh-agent
|
| forwarded
v
Docker Sandbox
|
| SSH authentication
v
GitHubThe sandbox can use the agent to authenticate without ever seeing the private key. Nice and clean.
Then I created a fresh sandbox, and it couldn’t connect to GitHub. Of course it couldn’t.
The SSH problem I didn’t see coming
The SSH agent was there. Authentication worked on the host. The repository URL was right. And a brand new sandbox still choked on GitHub SSH and host verification errors.
The missing piece turned out to be this little file:
~/.ssh/known_hosts
Here’s what was actually happening:
SSH was politely asking “do you trust this server?” and there was nobody around to say yes. Fix one problem, find the next one. Classic.
What I needed was for every new sandbox to trust GitHub without turning setup into an interactive ritual.
Making GitHub access predictable
There was also the practical issue of SSH networking from inside the sandbox. GitHub supports SSH over port 443, so I added this to ~/.ssh/config on the host:
Host github.com
HostName ssh.github.com
Port 443
User git
The Git remote still looks completely normal:
git@github.com:roysup-code/personal.git
but SSH actually connects through:
ssh.github.com:443
I allowed that network path through the sandbox policy, and persisted GitHub’s published host key so new sandboxes don’t need an interactive trust prompt.
That last part mattered to me. The lazy fix is to auto-accept whatever host key shows up. That works right up until the day it really, really doesn’t. Pinning the published key was the whole point.
Now the path looked predictable:
Codex
|
v
Docker Sandbox
|
| SSH agent forwarding
|
| network policy
v
ssh.github.com:443
|
v
GitHubCodex working inside the sandbox, talking to GitHub, private key never leaving the host. Done, right?
Nope.
I was not doing this setup by hand every time
The nice thing about a sandbox is that it’s disposable. The annoying thing about a sandbox is also that it’s disposable.
I wasn’t going to redo the SSH config, host verification and everything else every time I spun up a new one. I wanted to type one command and get to work.
Docker’s sandbox templates were the obvious answer, so I built a reusable template with everything Codex and GitHub needed.
And that cleared up another thing I’d been muddling together.
The template sets up the environment. --clone decides how the Git workspace is structured. Two separate knobs. Once I stopped treating them as one thing, the whole setup got way easier to think about.
The command I ended up with:
sbx run --clone \
--template codex-github-template \
--name codex-personal-clone \
codex
And the full picture:
That’s a pretty damn reasonable setup. I was proud of it.
Then I actually used it.
The isolation worked. The workflow didn’t.
I used Codex to build this website, and that loop is painfully repetitive: make a change, refresh, look at it, change something else, refresh, repeat until it stops looking wrong.
With the cloned setup, every one of those changes lived inside the sandbox’s private Git repository. So to see them from my host repository, I had to fetch the sandbox’s Git remote:
git fetch sandbox-<name>
Every. Single. Change. Fetch, look, fetch, look.
For a big, isolated task, that tradeoff is completely fine. For tweaking CSS on a personal website? That’s wearing a hazmat suit to make toast.
The isolation was great. The workflow was miserable. And it turns out that matters a lot more than I expected.
So I changed the setup again
This is honestly the most useful part of the whole experiment.
I went back to running Codex in the normal Docker Sandbox workflow, without --clone.
Not because I stopped caring about safety. Because I finally understood what the sandbox was already protecting me from.
In my current setup, Docker’s sandbox automatically rejects the really dangerous stuff, like a direct git push. That took care of a big chunk of what I was worried about when I started.
I still have GitHub-side protections as another layer. But I don’t need to drag every tiny website change through a private clone just to feel comfortable.
Here’s the before and after:
The clone setup gives stronger separation, and for some workflows it’s the right call. For how I use Codex day to day, the simpler setup wins, easily.
And I never would have figured that out if I’d stopped at the first setup that technically worked.
What I actually learned
The most secure setup is worthless if you hate using it, because you’ll quietly stop using it. Build the safest thing you’ll actually run every day, not the safest thing you can brag about.
The funny part is I started this trying to make Codex “safe.” What I learned is there’s no magic switch for that. It’s layers:
- Docker provides the sandbox boundary.
- The filesystem model determines what the agent can modify.
- Git provides separation through branches and commits.
- SSH agent forwarding avoids copying the private key into the sandbox.
- Network policy controls outbound access.
- GitHub rules protect important repository operations.
- Pull requests provide another review boundary.
No single layer has to do everything. That’s the whole point. Think of a gym spotter: the agent still lifts the weight, but something is there to catch the bar when it slips.
The final setup is less about trusting the agent and more about limiting the damage when it does something I didn’t expect. That’s what I was actually trying to build all along.
Not a perfect AI agent. Not some magical “AI-safe” dev environment. Just a place where I can let an agent work without constantly wondering what happens if it makes the wrong move.
And, more importantly, one I actually want to use every day.