{"slug": "sandboxing-coding-agents", "title": "Sandboxing Coding Agents", "summary": "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.", "body_md": "# Sandboxing coding agents\n\nAt 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.\n\nAs 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.\n\n## Create the agent signing key\n\nI'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.\n\nTo get started, generate a new SSH key, and save it as `~/.ssh/agent-signing-key`\n\n:\n\n```\nssh-keygen -t ed25519\n```\n\nThis 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:\n\n`~/.ssh/agent-signing-key`\n\n: your agent's secret key`~/.ssh/agent-signing-key.pub`\n\n: your agent's public key\n\nNext, 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).\n\nAdd 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:\n\nIn 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.\n\n## Script for creating an isolated SSH agent\n\nAs 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.\n\nHowever, 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.\n\nHere's my current `start-isolated-ssh.sh`\n\n:\n\n``` bash\n#!/bin/sh\n\n# This file must be sourced so SSH_AUTH_SOCK is updated in the current shell:\n#   . ~/.local/bin/start-isolated-ssh.sh\n\nif ! (return 0 2>/dev/null); then\n  echo \"Source this script instead of executing it:\" >&2\n  echo \"  . ~/.local/bin/start-isolated-ssh.sh\" >&2\n  exit 1\nfi\n\nSIGNING_KEY=\"${HOME}/.ssh/agent-signing-key\"\n\nif [ ! -f \"$SIGNING_KEY\" ]; then\n  echo \"Signing key not found: $SIGNING_KEY\" >&2\n  return 1\nfi\n\nif ! command -v sbx >/dev/null 2>&1; then\n  echo \"sbx was not found on PATH.\" >&2\n  return 1\nfi\n\nif [ -n \"${ISOLATED_SSH_AGENT_PID:-}\" ]; then\n  echo \"An isolated SSH agent is already active in this shell.\" >&2\n  echo \"Run stop_isolated_ssh before starting another one.\" >&2\n  return 1\nfi\n\nISOLATED_SSH_PREVIOUS_AUTH_SOCK=\"${SSH_AUTH_SOCK-}\"\nISOLATED_SSH_PREVIOUS_AGENT_PID=\"${SSH_AGENT_PID-}\"\n\neval \"$(ssh-agent -s)\" >/dev/null\nISOLATED_SSH_AGENT_PID=\"$SSH_AGENT_PID\"\n\nstop_isolated_ssh() {\n  sbx daemon stop >/dev/null 2>&1 || true\n\n  if [ -n \"${ISOLATED_SSH_AGENT_PID:-}\" ]; then\n    SSH_AGENT_PID=\"$ISOLATED_SSH_AGENT_PID\" ssh-agent -k >/dev/null 2>&1 || true\n  fi\n\n  if [ -n \"${ISOLATED_SSH_PREVIOUS_AUTH_SOCK:-}\" ]; then\n    SSH_AUTH_SOCK=\"$ISOLATED_SSH_PREVIOUS_AUTH_SOCK\"\n    export SSH_AUTH_SOCK\n  else\n    unset SSH_AUTH_SOCK\n  fi\n\n  if [ -n \"${ISOLATED_SSH_PREVIOUS_AGENT_PID:-}\" ]; then\n    SSH_AGENT_PID=\"$ISOLATED_SSH_PREVIOUS_AGENT_PID\"\n    export SSH_AGENT_PID\n  else\n    unset SSH_AGENT_PID\n  fi\n\n  unset ISOLATED_SSH_AGENT_PID\n  unset ISOLATED_SSH_PREVIOUS_AUTH_SOCK\n  unset ISOLATED_SSH_PREVIOUS_AGENT_PID\n\n  echo \"Stopped the isolated SSH agent and restored the previous agent.\"\n}\n\nif ! ssh-add \"$SIGNING_KEY\"; then\n  echo \"Failed to load the signing key.\" >&2\n  stop_isolated_ssh\n  return 1\nfi\n\necho \"Isolated SSH agent identities:\"\nssh-add -l -E sha256\n\nif ! sbx daemon stop; then\n  echo \"Failed to stop the sbx daemon.\" >&2\n  stop_isolated_ssh\n  return 1\nfi\n\nif ! sbx daemon start -d; then\n  echo \"Failed to start the sbx daemon with the isolated SSH agent.\" >&2\n  stop_isolated_ssh\n  return 1\nfi\n\necho\necho \"The sbx daemon is now using the isolated SSH agent.\"\necho \"Run stop_isolated_ssh when you are finished.\"\n```\n\nIf you want to follow along, save this in `~/code/start-isolated-ssh.sh`\n\n. 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).\n\n## GitHub PATs for limiting what repos agents can access\n\nFor this next part, I'm gonna make a new test repo on GitHub called `micahflee/sandbox-test`\n\n.\n\nNow, 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).\n\nGenerate a new token. Here are the fields to fill out.\n\n**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.\n\nFor permissions, give it:\n\n**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)\n\nSo far this is the only access I've needed to give any agents.\n\nClick **Generate token**. Your token will start with `github_path_`\n\n. Save it somewhere safe.\n\n## Set up a few Docker Sandboxes\n\nI'm gonna set up two separate Docker Sandboxes, using Claude, for my `micahflee/sandbox-test`\n\nrepo. First, make sure you have the [GitHub CLI tool](https://cli.github.com) (`gh`\n\n) installed and logged in to your GitHub account.\n\nThen, make sure to run `gh auth setup-git`\n\n. 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.\n\n### Clone the repo a few times\n\nNext, 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.\n\n```\ncd ~/code\ngit clone https://github.com/micahflee/sandbox-test.git sandbox-test-1\ngit clone https://github.com/micahflee/sandbox-test.git sandbox-test-2\n```\n\n### Create `sbx`\n\nsandboxes\n\nNext, I'm going to create two sandboxes, one for each of these folders. Note that each has a unique name (`sandbox-test-1`\n\nand `sandbox-test-2`\n\n.) In my case, I'm using `claude`\n\nas the agent, but Docker Sandboxes [supports](https://docs.docker.com/ai/sandboxes/agents/) several different agents.\n\n```\nsbx create --name sandbox-test-1 --no-share-skills claude ~/code/sandbox-test-1\nsbx create --name sandbox-test-2 --no-share-skills claude ~/code/sandbox-test-2\n```\n\nThe first time you create a sandbox might take some time, as `sbx`\n\nneeds to download the sandbox image.\n\n### Authenticate them with the GitHub PAT\n\nNext, give your sandboxes their GitHub credentials. You can set global credentials by running `sbx secret set github`\n\n– 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.\n\nInstead, you can give each sandbox its own GitHub secret by using the `--sandbox`\n\narg, 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:\n\n```\n❯ sbx secret set github --sandbox sandbox-test-1\nEnter secret: \nSaved secret for service \"github\" in scope \"sandbox-test-1\"\nApplied secret for sandbox \"sandbox-test-1\"\n\n❯ sbx secret set github --sandbox sandbox-test-2\nEnter secret: \nSaved secret for service \"github\" in scope \"sandbox-test-2\"\nApplied secret for sandbox \"sandbox-test-2\"\n```\n\nWe'll confirm that this works in a minute.\n\n### Start your isolated SSH agent\n\nRemember that `start-isolated-ssh.sh`\n\nscript? It's time to make use of it. Run `source ~/code/start-isolated-ssh.sh`\n\n.\n\nThis 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`\n\ndaemon 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:\n\n```\n❯ source ~/code/start-isolated-ssh.sh \nEnter passphrase for /Users/user/.ssh/agent-signing-key: \nIdentity added: /Users/user/.ssh/agent-signing-key (/Users/user/.ssh/agent-signing-key)\nIsolated SSH agent identities:\n256 SHA256:QnFfu3bc6WLkGy80Pvze391+5I/HptI+Emrwnx9lEl8 /Users/user/.ssh/agent-signing-key (ED25519)\nStopping daemon at /Users/user/Library/Application Support/com.docker.sandboxes/sandboxes/sandboxd/sandboxd.sock...\n✓ Daemon stopped successfully\nDaemon started (PID: 21534, socket: /Users/user/Library/Application Support/com.docker.sandboxes/sandboxes/sandboxd/sandboxd.sock)\nLogs: /Users/user/Library/Application Support/com.docker.sandboxes/sandboxes/sandboxd/daemon.log\n\nThe sbx daemon is now using the isolated SSH agent.\nRun stop_isolated_ssh when you are finished.\n```\n\nIf you just run `sbx`\n\n, 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`\n\n:\n\n### Finish configuring the sandboxes\n\nI'm going to finish configuring `sandbox-test-1`\n\n. You'll want to follow the same steps for each sandbox you're creating.\n\nIn the `sbx`\n\nterminal UI, select the sandbox you want to configure and press `x`\n\nto open a shell.\n\nSince 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`\n\n:\n\n``` bash\nagent@sandbox-test-1:/Users/user/code/sandbox-test-1$ claude update\nCurrent version: 2.1.246\nChecking for updates to latest version...\n\nWarning: Running native installation but config install method is 'unknown'\nFix: Run claude install to update configuration\nUpdating to 2.1.247...\nSuccessfully updated from 2.1.246 to version 2.1.247\n```\n\nRun `gh auth status`\n\nto see if it's authenticated to GitHub successfully with your PAT. It should look like this:\n\n``` bash\nagent@sandbox-test-1:/Users/user/code/sandbox-test-1$ gh auth status\ngithub.com\n  ✓ Logged in to github.com account micahflee (GH_TOKEN)\n  - Active account: true\n  - Git operations protocol: https\n  - Token: gho_************************************\n  - Token scopes: none\n```\n\nRun `ssh-add -L`\n\nto see what SSH keys your sandbox has access to. It should **only** have access to `~/.ssh/agent-signing-key`\n\n. *If it has access to more than this, you probably need to go back and start your isolated SSH agent again.*\n\nIt should look something like this:\n\n``` bash\nagent@sandbox-test-1:/Users/user/code/sandbox-test-1$ ssh-add -L \nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIgUKcZtNswt4iwzYmbzcJWxD86HJlce9s8/zinFjifM /Users/user/.ssh/agent-signing-key\n```\n\nNext, configure your sandbox's `~/.gitconfig`\n\nfile, 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.\n\n```\ngit config --global user.name \"Micah Lee (agent)\"\ngit config --global user.email micah@micahflee.com\n```\n\nAnd then configure it to sign commits with the agent signing key:\n\n```\ngit config --global gpg.format ssh\ngit config --global user.signingkey \"key::$(ssh-add -L | head -n 1)\"\ngit config --global commit.gpgsign true\ngit config --global tag.gpgSign true\n```\n\nWhen you're done, if you run `cat ~/.gitconfig`\n\n, it should look something like this:\n\n``` bash\nagent@sandbox-test-1:/Users/user/code/sandbox-test-1$ cat ~/.gitconfig \n[safe]\n\tdirectory = /Users/user/code/sandbox-test-1\n[core]\n\tcheckStat = minimal\n\texcludesFile = /home/agent/.gitignore_global\n[user]\n\tname = Micah Lee (agent)\n\temail = micah@micahflee.com\n\tsigningkey = key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIgUKcZtNswt4iwzYmbzcJWxD86HJlce9s8/zinFjifM /Users/user/.ssh/agent-signing-key\n[gpg]\n\tformat = ssh\n[commit]\n\tgpgsign = true\n[tag]\n\tgpgSign = true\n```\n\nYou 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`\n\nto install the `mattpocock/skills`\n\nClaude plugin.\n\nFinally, type `exit`\n\nto quit the shell. Then press **Enter** to actually open the coding agent – in my case, Claude.\n\n## Making sure it works\n\nJust to make sure it works, I'm going to have my agent create a commit and make a PR. My prompt was:\n\nsup, 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.\n\nIt 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)\n\nI reviewed it and merged it. See [ micahflee/sandbox-test](https://github.com/micahflee/sandbox-test). If you inspect the\n\n[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:\n\nThe GitHub user interface doesn't make it easy to see, but if you run `git log`\n\n, you can see that that commit also is authored by \"Micah Lee (agent)\" instead of just \"Micah Lee\":\n\n```\ncommit 19e02adbc0ef631d401258f4a6bf924843c9e8ab (origin/readme-from-inside-the-box, readme-from-inside-the-box)\nAuthor: Micah Lee (agent) <micah@micahflee.com>\nDate:   Thu Aug 27 12:31:35 2026 -0700\n\n    Add a note from inside the sandbox to the README\n    \n    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>\n```\n\n## Bringing it all together\n\nI know this was a lot of setup, but you only have to do it once for each project. So in short, when you're setting up a new project:\n\n- Create a GitHub PAT with limited permissions\n- Create and configure your\n`sbx`\n\nsandboxes, making sure to set the PAT secret\n\nWhen you're getting ready to actually work on a project:\n\n- Run\n`source ~/code/start-isolated-ssh.sh`\n\nand unlock your agent signing key - Open your sandbox, and start prompting\n\nAnother cool trick: If you can, do this all on a server instead of your laptop (SSHed in and in a tmux session). If you're doing anything sensitive, do it on a home server instead of a cloud server.\n\nThis way, you can put a bunch of agents to work and then close your laptop lid. Awhile later, you'll have some PRs ready for review.\n\nNow, I'm gonna go take a nap.", "url": "https://wpnews.pro/news/sandboxing-coding-agents", "canonical_source": "https://micahflee.com/sandboxing-coding-agents/", "published_at": "2026-08-28 00:52:03+00:00", "updated_at": "2026-08-28 01:18:08.832007+00:00", "lang": "en", "topics": ["ai-tools", "ai-safety", "developer-tools"], "entities": ["Micah Lee", "GitHub", "Docker Sandboxes", "HalCTF", "AI Village"], "alternates": {"html": "https://wpnews.pro/news/sandboxing-coding-agents", "markdown": "https://wpnews.pro/news/sandboxing-coding-agents.md", "text": "https://wpnews.pro/news/sandboxing-coding-agents.txt", "jsonld": "https://wpnews.pro/news/sandboxing-coding-agents.jsonld"}}