Sandboxing Coding Agents Micah Lee, a security researcher, published a technical guide on sandboxing coding agents by creating an isolated SSH agent that loads a dedicated agent-only signing key, preventing agents from accessing the developer's full SSH credentials. The setup uses Docker Sandboxes and a script that sources an isolated SSH agent, ensuring commits are signed with a key that is only used by agents and clearly identifies AI-authored commits. Sandboxing coding agents At the beginning of the month I wrote https://micahflee.com/agentic-coding-techniques/ about how I've been using coding agents to write high quality code securely. In this post I'll show the details of setting up isolated sandboxes for agents, where they can only access a single isolated GitHub repo, and where they sign commits with a dedicated agent-only key. As an aside: After writing my last post, I went to DEF CON. I spent the whole time in the AI Village playing HalCTF https://aivillage.org/blog/halctf/ using a lot of these same agentic techniques actually . Then I came home and immediately tested positive for Covid. And even though it's been three weeks, I'm still sick . It sucks and I've barely been able to work. I have hope that I'll get better soon. Create the agent signing key I'm increasingly of the opinion that everyone should be transparent about their AI use, and this includes agentic coding. Because of this, I think it makes sense for commits made by LLMs to 1 use an author name that makes it clear it's not a human, and 2 sign the commit with an SSH key that's only used by agents. To get started, generate a new SSH key, and save it as ~/.ssh/agent-signing-key : ssh-keygen -t ed25519 This will ask you where to save the file, and what the passphrase should be. Give it a passphrase. When you're done, you should have two new files: ~/.ssh/agent-signing-key : your agent's secret key ~/.ssh/agent-signing-key.pub : your agent's public key Next, go to your GitHub account settings and edit your SSH keys. You can access it at https://github.com/settings/keys https://github.com/settings/keys . Here, you can define what SSH public keys are included in your GitHub account, and which are for authentication being able to git clone with SSH git URLs , and signing being listed as "verified" when you use it to sign a commit . Add your agent's new public key as a signing key . Make sure it's not an authentication key. Otherwise, the agent will be able to access all of the repos your GitHub account can access. Here's what my SSH keys in my GitHub settings currently look like: In my case, I have an SSH key where the secret key is stored on a Yubikey. I can use that for authentication or signing. And then, I have an SSH key called " agent git signing key" that's only used for commit signing. Script for creating an isolated SSH agent As I mentioned in my last blog post, I use Docker Sandboxes https://docs.docker.com/ai/sandboxes/ as my sandbox technology. Docker Sandboxes supports forwarding your host SSH agent into the sandbox so that it can sign commits with your SSH key. See the commit signing https://docs.docker.com/ai/sandboxes/workflows/git/ commit-signing docs. However, I don't want to forward my normal SSH agent into the sandbox, because then the agent running in the sandbox will have access to my SSH key, and by extension everything that my SSH key can access. So instead, I wrote a little script that creates an isolated SSH agent, and only loads the agent's SSH key into it. Here's my current start-isolated-ssh.sh : bash /bin/sh This file must be sourced so SSH AUTH SOCK is updated in the current shell: . ~/.local/bin/start-isolated-ssh.sh if return 0 2 /dev/null ; then echo "Source this script instead of executing it:" &2 echo " . ~/.local/bin/start-isolated-ssh.sh" &2 exit 1 fi SIGNING KEY="${HOME}/.ssh/agent-signing-key" if -f "$SIGNING KEY" ; then echo "Signing key not found: $SIGNING KEY" &2 return 1 fi if command -v sbx /dev/null 2 &1; then echo "sbx was not found on PATH." &2 return 1 fi if -n "${ISOLATED SSH AGENT PID:-}" ; then echo "An isolated SSH agent is already active in this shell." &2 echo "Run stop isolated ssh before starting another one." &2 return 1 fi ISOLATED SSH PREVIOUS AUTH SOCK="${SSH AUTH SOCK-}" ISOLATED SSH PREVIOUS AGENT PID="${SSH AGENT PID-}" eval "$ ssh-agent -s " /dev/null ISOLATED SSH AGENT PID="$SSH AGENT PID" stop isolated ssh { sbx daemon stop /dev/null 2 &1 || true if -n "${ISOLATED SSH AGENT PID:-}" ; then SSH AGENT PID="$ISOLATED SSH AGENT PID" ssh-agent -k /dev/null 2 &1 || true fi if -n "${ISOLATED SSH PREVIOUS AUTH SOCK:-}" ; then SSH AUTH SOCK="$ISOLATED SSH PREVIOUS AUTH SOCK" export SSH AUTH SOCK else unset SSH AUTH SOCK fi if -n "${ISOLATED SSH PREVIOUS AGENT PID:-}" ; then SSH AGENT PID="$ISOLATED SSH PREVIOUS AGENT PID" export SSH AGENT PID else unset SSH AGENT PID fi unset ISOLATED SSH AGENT PID unset ISOLATED SSH PREVIOUS AUTH SOCK unset ISOLATED SSH PREVIOUS AGENT PID echo "Stopped the isolated SSH agent and restored the previous agent." } if ssh-add "$SIGNING KEY"; then echo "Failed to load the signing key." &2 stop isolated ssh return 1 fi echo "Isolated SSH agent identities:" ssh-add -l -E sha256 if sbx daemon stop; then echo "Failed to stop the sbx daemon." &2 stop isolated ssh return 1 fi if sbx daemon start -d; then echo "Failed to start the sbx daemon with the isolated SSH agent." &2 stop isolated ssh return 1 fi echo echo "The sbx daemon is now using the isolated SSH agent." echo "Run stop isolated ssh when you are finished." If you want to follow along, save this in ~/code/start-isolated-ssh.sh . I'll show you how I actually use it soon. But first, it's time to create a GitHub fine-grained personal access token PAT . GitHub PATs for limiting what repos agents can access For this next part, I'm gonna make a new test repo on GitHub called micahflee/sandbox-test . Now, go to GitHub Settings Developer Settings Personal access tokens Fine-grained personal access tokens . You can access this directly at https://github.com/settings/personal-access-tokens https://github.com/settings/personal-access-tokens . Generate a new token. Here are the fields to fill out. Token name: I'm calling mine " agent sandbox sandbox-test". Description: If you want, give it a description. Resource owner: For resource owner, you'll choose your own GitHub account if the repo is in your account. Note that if the repo belongs to a GitHub organization, the resource owner will need to be the organization. An organization admin will have to approve you generating a PAT. Expiration: Choose an expiration date. It's fine for this to be short-lived – if it expires and you still need it, just generate a new one. Repository access: Choose "Only select repositories", and then select the limited repo s that the agent should have access to. For permissions, give it: Contents: read and write Issues: read and write Pull requests: read and write Actions: read-only Commit statuses: read-only Metadata: read-only always required So far this is the only access I've needed to give any agents. Click Generate token . Your token will start with github path . Save it somewhere safe. Set up a few Docker Sandboxes I'm gonna set up two separate Docker Sandboxes, using Claude, for my micahflee/sandbox-test repo. First, make sure you have the GitHub CLI tool https://cli.github.com gh installed and logged in to your GitHub account. Then, make sure to run gh auth setup-git . This allows you to git clone private repos over HTTPS. See the docs https://cli.github.com/manual/gh auth setup-git . Since the agent won't have an authentication SSH key, you'll need to clone your repos over HTTPS, not SSH. Clone the repo a few times Next, I'm gonna make two clones of my test repo. I'm making two so that I can run two separate agents simultaneously, so they won't clobber each other's files. You can make as many as you need. cd ~/code git clone https://github.com/micahflee/sandbox-test.git sandbox-test-1 git clone https://github.com/micahflee/sandbox-test.git sandbox-test-2 Create sbx sandboxes Next, I'm going to create two sandboxes, one for each of these folders. Note that each has a unique name sandbox-test-1 and sandbox-test-2 . In my case, I'm using claude as the agent, but Docker Sandboxes supports https://docs.docker.com/ai/sandboxes/agents/ several different agents. sbx create --name sandbox-test-1 --no-share-skills claude ~/code/sandbox-test-1 sbx create --name sandbox-test-2 --no-share-skills claude ~/code/sandbox-test-2 The first time you create a sandbox might take some time, as sbx needs to download the sandbox image. Authenticate them with the GitHub PAT Next, give your sandboxes their GitHub credentials. You can set global credentials by running sbx secret set github – the docs https://docs.docker.com/ai/sandboxes/configuration/credentials/ github-token tell you to do this – but you shouldn't actually do that, as that will give all of your sandboxes access to your whole GitHub account. Instead, you can give each sandbox its own GitHub secret by using the --sandbox arg, and passing in the name of the sandbox. When it asks you to enter the secret, paste in the GitHub PAT you created earlier. Here's what it should look like: ❯ sbx secret set github --sandbox sandbox-test-1 Enter secret: Saved secret for service "github" in scope "sandbox-test-1" Applied secret for sandbox "sandbox-test-1" ❯ sbx secret set github --sandbox sandbox-test-2 Enter secret: Saved secret for service "github" in scope "sandbox-test-2" Applied secret for sandbox "sandbox-test-2" We'll confirm that this works in a minute. Start your isolated SSH agent Remember that start-isolated-ssh.sh script? It's time to make use of it. Run source ~/code/start-isolated-ssh.sh . This will start a new SSH agent. You'll need to enter the passphrase of the agent signing key you created earlier, to unlock the key. This will then stop the sbx daemon and start a new one, this time forwarding your new isolated SSH agent. Here's what it looks like when I run it on my Mac: ❯ source ~/code/start-isolated-ssh.sh Enter passphrase for /Users/user/.ssh/agent-signing-key: Identity added: /Users/user/.ssh/agent-signing-key /Users/user/.ssh/agent-signing-key Isolated SSH agent identities: 256 SHA256:QnFfu3bc6WLkGy80Pvze391+5I/HptI+Emrwnx9lEl8 /Users/user/.ssh/agent-signing-key ED25519 Stopping daemon at /Users/user/Library/Application Support/com.docker.sandboxes/sandboxes/sandboxd/sandboxd.sock... ✓ Daemon stopped successfully Daemon started PID: 21534, socket: /Users/user/Library/Application Support/com.docker.sandboxes/sandboxes/sandboxd/sandboxd.sock Logs: /Users/user/Library/Application Support/com.docker.sandboxes/sandboxes/sandboxd/daemon.log The sbx daemon is now using the isolated SSH agent. Run stop isolated ssh when you are finished. If you just run sbx , Docker Sandboxes will give you a nice terminal UI for viewing all of your sandboxes, opening shells in them, or opening your coding agent in them. Here's what it looks like when I run sbx : Finish configuring the sandboxes I'm going to finish configuring sandbox-test-1 . You'll want to follow the same steps for each sandbox you're creating. In the sbx terminal UI, select the sandbox you want to configure and press x to open a shell. Since the sandbox is just a Docker container based on an image, there's a good chance that your coding agent is already out-of-date. Update it now. Here's what it looks like when I update claude : bash agent@sandbox-test-1:/Users/user/code/sandbox-test-1$ claude update Current version: 2.1.246 Checking for updates to latest version... Warning: Running native installation but config install method is 'unknown' Fix: Run claude install to update configuration Updating to 2.1.247... Successfully updated from 2.1.246 to version 2.1.247 Run gh auth status to see if it's authenticated to GitHub successfully with your PAT. It should look like this: bash agent@sandbox-test-1:/Users/user/code/sandbox-test-1$ gh auth status github.com ✓ Logged in to github.com account micahflee GH TOKEN - Active account: true - Git operations protocol: https - Token: gho - Token scopes: none Run ssh-add -L to see what SSH keys your sandbox has access to. It should only have access to ~/.ssh/agent-signing-key . If it has access to more than this, you probably need to go back and start your isolated SSH agent again. It should look something like this: bash agent@sandbox-test-1:/Users/user/code/sandbox-test-1$ ssh-add -L ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIgUKcZtNswt4iwzYmbzcJWxD86HJlce9s8/zinFjifM /Users/user/.ssh/agent-signing-key Next, configure your sandbox's ~/.gitconfig file, to control how it creates git commits. Personally, I set the author name to my name, but with " agent " after it. If you see a commit by "Micah Lee agent ", that means that one of my coding agents made it, not me directly. git config --global user.name "Micah Lee agent " git config --global user.email micah@micahflee.com And then configure it to sign commits with the agent signing key: git config --global gpg.format ssh git config --global user.signingkey "key::$ ssh-add -L | head -n 1 " git config --global commit.gpgsign true git config --global tag.gpgSign true When you're done, if you run cat ~/.gitconfig , it should look something like this: bash agent@sandbox-test-1:/Users/user/code/sandbox-test-1$ cat ~/.gitconfig safe directory = /Users/user/code/sandbox-test-1 core checkStat = minimal excludesFile = /home/agent/.gitignore global user name = Micah Lee agent email = micah@micahflee.com signingkey = key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIgUKcZtNswt4iwzYmbzcJWxD86HJlce9s8/zinFjifM /Users/user/.ssh/agent-signing-key gpg format = ssh commit gpgsign = true tag gpgSign = true You might need to do other configuration here too if you want. For example, this is the time when I might run claude plugins install mattpocock-skills to install the mattpocock/skills Claude plugin. Finally, type exit to quit the shell. Then press Enter to actually open the coding agent – in my case, Claude. Making sure it works Just to make sure it works, I'm going to have my agent create a commit and make a PR. My prompt was: sup, Claude? you're running in a sandbox. I want to make sure everything works right. add something clever to the readme, and then create a new commit in its own branch and create a PR for it. It took 30 seconds, didn't ask for any permissions, and successfully created a signed commit and opened a PR: https://github.com/micahflee/sandbox-test/pull/1 https://github.com/micahflee/sandbox-test/pull/1 I reviewed it and merged it. See micahflee/sandbox-test https://github.com/micahflee/sandbox-test . If you inspect the commits https://github.com/micahflee/sandbox-test/commits/main/ , and click "Verified" next to the one the agent made, it should show you that it was signed by the dedicated agent signing key: The GitHub user interface doesn't make it easy to see, but if you run git log , you can see that that commit also is authored by "Micah Lee agent " instead of just "Micah Lee": commit 19e02adbc0ef631d401258f4a6bf924843c9e8ab origin/readme-from-inside-the-box, readme-from-inside-the-box Author: Micah Lee agent