You Become Responsible, Forever, for What You Have Launched

· 13 min read
Contents

Coding agents are great. They write code, run tests, open pull requests and answer review comments. But almost everyone runs them as themselves: with their own SSH key, their own gh/glab token, their own git author. GitHub and GitLab don’t see an agent. They see you.

The usual guardrail is CLAUDE.md/AGENTS.md: “don’t push to main”, “don’t merge your own PRs”, “never use --force”. That’s an instruction, not a control. The model follows it almost every time. Almost.

Once my agent decided not to wait for my approval and merged its branch into master “to see how it behaves in production”. Nothing technically stopped it: it had my permissions.

An agent is a different identity

The first instinct is to cut its permissions: a fine-grained token, one repository, read-only, no admin. In practice that gets in the way. The agent still needs to push branches, open PRs and reply in threads. More importantly, it’s still you.

That breaks every “four eyes” mechanism GitHub and GitLab give you:

The idea: run the agent as someone else

Give the agent its own account. It authors commits, pushes, opens PRs and answers comments. You’re the co-author of its commits (Co-authored-by) and the one who approves and merges. The usual rules work again: required approvals, code owners, protected branches. And the bot physically can’t merge its own PR.

GitHub

Option How Caveats
Machine user A second regular account (mine is markeli-agent), invited as a collaborator The ToS allows one free personal account plus a machine account. 2FA is required
GitHub App An app installed on the repositories [bot] badge, tokens live for an hour, but you have to mint them from a private key via a JWT, so you need a wrapper

A machine user is the simpler start. What I learned along the way:

GitLab.com

Self-hosted GitLab

On self-managed GitLab, project and group access tokens are available on every tier, so you get a bot user without a second account. That’s a conversation with your instance admins and your company policy, though.

Pros and cons

Pros: the usual review and branch protection rules work; history shows what the agent did; you can revoke the bot’s access without touching your own; the agent gets exactly the repositories it needs.

Cons: a second account with its own 2FA; a classic PAT on GitHub; your own PRs need a bypass; your GitLab contribution calendar thins out.

Running it locally

It looks like a small job: swap the agent’s git author and tokens. I went all the way down that road, and here’s what I found.

All of it is solvable: GIT_CONFIG_GLOBAL with includeIf hasconfig:remote.*.url, wrappers around gh/glab, a separate ssh config. But it gets complicated and hacky.

And above all, it isn’t safe. The agent runs as your OS user: it can reach your ssh-agent, ~/.config/glab-cli and the Keychain. gh stores its token through /usr/bin/security, which can read it back without a prompt. An agent that hits a 403 as the bot may well “help itself” with your credentials. The separation becomes attribution, not a boundary.

The fix is to move the agent into a sandbox where your credentials simply don’t exist.

Docker Sandboxes in a nutshell

Docker Sandboxes (the sbx CLI) runs the agent in a microVM. The old docker sandbox command is already gone in Docker 29.8; it’s a separate sbx now.

My setup

Everything lives in Markeli/ai-agents-tooling:

sandbox/
├── image/                  ghcr.io/markeli/claude-sandbox, built by CI
│   ├── Dockerfile          .NET, glab, plugins, bot git identity
│   ├── managed-settings.json
│   └── CLAUDE.md
├── kits/markeli-claude/    kit: built-in claude + network + GitLab credential
└── bin/agent-sandbox       create a sandbox for a repository and attach

The image builds on docker/sandbox-templates:claude-code-docker. The bot identity is pinned with environment variables; plugins and instructions go into Claude Code’s managed config:

# sbx copies the host's git identity into the sandbox ~/.gitconfig; env vars override any config file
ENV GIT_AUTHOR_NAME=markeli-agent \
	GIT_AUTHOR_EMAIL=markelow.dev+agent@gmail.com \
	GIT_COMMITTER_NAME=markeli-agent \
	GIT_COMMITTER_EMAIL=markelow.dev+agent@gmail.com

# Clone anonymously over HTTPS, push over SSH with the bot's key
RUN git config --system url."git@github.com:".pushInsteadOf "https://github.com/"

# sbx regenerates ~/.claude/settings.json, but /etc/claude-code survives
COPY managed-settings.json /etc/claude-code/managed-settings.json
COPY CLAUDE.md /etc/claude-code/CLAUDE.md
{
	"attribution": {
		"commit": "Co-authored-by: Maxim Markelow <markelow.dev@gmail.com>",
		"pr": ""
	},
	"enabledPlugins": {
		"superpowers@claude-plugins-official": true,
		"csharp-lsp@claude-plugins-official": true
	},
	"env": { "ENABLE_CLAUDEAI_MCP_SERVERS": "false" }
}

The kit extends the built-in claude agent, keeping its login, MCP gateway and token injection, and adds only what I need:

schemaVersion: "2"
kind: sandbox
name: markeli-claude
extends: claude
sandbox:
  image: ghcr.io/markeli/claude-sandbox:latest
permissions:
  network:
    allow: [github.com:22, gitlab.com:22, api.nuget.org]
credentials:
  - service: gitlab
    apiKey:
      name: GITLAB_TOKEN
      proxyManaged: true
      inject:
        - domain: gitlab.com
          header: PRIVATE-TOKEN
          format: "%s"

Quick setup

  1. Bot accounts on GitHub and GitLab, invited to the repositories. Create a separate browser profile for the bot accounts: you sign in as the bot there and stay signed in to your own accounts in your main profile.

  2. A bot SSH key with a passphrase and a dedicated agent for it:

    ssh-agent -a ~/.ssh/markeli-agent.sock
    SSH_AUTH_SOCK=~/.ssh/markeli-agent.sock ssh-add --apple-use-keychain ~/.ssh/markeli_agent
  3. The bot’s tokens in the Keychain:

    security add-generic-password -U -s agent/personal-github -a markeli-agent -w
    security add-generic-password -U -s agent/personal-gitlab -a markeli-agent -w
  4. sbx:

    brew trust docker/tap && brew install docker/tap/sbx
    sbx login
    sbx policy init balanced
    sbx settings set ssh.agentSocketPath ~/.ssh/markeli-agent.sock && sbx daemon restart
  5. Run it in any repository:

    agent-sandbox ~/Development/Personal/<repo>

The script starts the bot’s ssh-agent, gives the sandbox its secrets from the Keychain (sbx secret set … --command 'security …'), creates a clone-mode sandbox and attaches. On the first run sbx asks you to approve the kit’s credentials.

I tested it on a real repository: the bot in the sandbox created a branch, committed, pushed and opened a draft PR. I left a general comment and an inline comment; the bot read both and replied in their threads as markeli-agent. GitHub linked the co-author to my account.

Pitfalls I hit

  1. sbx copies your git identity into the sandbox. In ~/.gitconfig it overrides /etc/gitconfig. There’s no setting to turn it off; only environment variables in the image help.
  2. insteadOf instead of pushInsteadOf rewrites anonymous clones to SSH too. It broke the plugin marketplace install during the image build.
  3. The balanced policy allows nuget.org but not api.nuget.org. dotnet restore gets a 403.
  4. claude.ai connectors arrive with a subscription login, all of them at once: Gmail, Drive, company MCP servers. The network policy doesn’t stop them because they go through mcp-proxy.anthropic.com. Turn them off with ENABLE_CLAUDEAI_MCP_SERVERS=false.
  5. Without ssh.agentSocketPath sbx forwards the ssh-agent of whatever terminal you attached from. Attach from a regular one and your key is inside.
  6. Custom secrets are only substituted in headers. Git over HTTPS sends the token in Basic auth, base64-encoded, so the proxy can’t find the placeholder. SSH only.
  7. A “third-party” kit, including your own kit extending a built-in agent, only gets credentials after explicit approval. Without it sbx silently starts without the token.
  8. A ruleset requiring CodeQL blocks merges forever in a new repository: default setup can’t be enabled until main has a supported language. An explicit CodeQL workflow fixed it.

Extra benefits

Drawbacks

Limitations

What’s next

Agents are becoming team members, not IDE plugins. A team member needs their own account, their own permissions and the same rules as people: reviews, protected branches, audit.

What I’d really like is sub-identities in GitHub, GitLab and every other service: child identities that belong to you, whose permissions you control explicitly and which can never have more than you do. An agent would act as “you, through this agent”, with its own audit trail, its own revocation and a scope you narrowed down yourself.

That would change the whole model of authentication and authorization the industry has been building for decades, where an account is a person and a token is that person. Until it happens, a separate account per agent is the practical way to get most of it today.