# Secure Git Automation for Claude Code with Dev Containers and Fine-Grained PATs


## Overview

AI Agents like Claude Code require terminal access to run builds, tests, and git commands. Running them directly on your host machine opens you up to the possibility of an agent executing a rogue script, prompt injection, host system file deletion or running commands that could access secrets saved in `~/.aws` or `~/.ssh`. Allowing them access to your personal GitHub credentials means the agent can read/write to any of your GitHub repositories.

While AI Agents offer an incredible productivity boost, they are not without risk. To balance security against velocity, it helps to establish four key isolation boundaries:

*   **Host Isolation:** The agent operates inside a Docker container. The host file system, local SSH keys, and personal credentials should never be mounted or exposed.
    
*   **Identity Isolation**: The agent acts as a dedicated GitHub "machine user" and member to the Organization hosting the repo.
    
*   **Credential Isolation**: Git operations use an Organization-scoped Fine-Grained PAT restricted to a single repository, scoped to Contents and Pull Requests read/write access.
    
*   **Data Isolation**: Claude Code’s session data (`.claude/`) is isolated inside a named Docker volume and ignored by Git.
    

We can run AI Agents reliably and with some safety measures by implementing these isolation boundaries and pairing them with these tools:

*   **Dedicated GitHub Machine User** to separate agent identity from human accounts.
    
*   **Organization-Scoped Fine-Grained PAT** to restrict repository access to minimum required permissions.
    
*   **VS Code Dev Containers** to provide a consistent, sandbox environment that provides a layer of isolation to host resources.
    

## Pre-requisites

*   VS Code Dev Containers extension installed. Search for it on the marketplace.
    
*   [Docker Desktop](https://www.docker.com/products/docker-desktop/) or [Orbstack](https://orbstack.dev/) (MacOS) installed
    

## **Machine User & Fine-Grained Token Pattern**

### Organization & Machine User Setup

If you haven't already:

1.  [Create a free GitHub Organization](https://docs.github.com/en/organizations/collaborating-with-groups-in-organizations/creating-a-new-organization-from-scratch) and transfer your target repository into it.
    
2.  Ensure [Fine-Grained Personal Access Tokens are enabled](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization) under Organization Settings.
    
3.  [Invite your dedicated Machine User](https://docs.github.com/en/organizations/managing-membership-in-your-organization/inviting-users-to-join-your-organization) to the Organization and grant it **Write** / **Collaborator** access to the target repository. If you're a Gmail user you can setup your machine user with an alias using: [`your_gmail+machine_user_name@gmail.com`](mailto:your_gmail+machine_user_name@gmail.com)
    

### Issuing the Fine-Grained PAT

Log into GitHub as your **Machine User** and navigate to **Settings > Developer Settings > Personal Access Tokens > Fine-grained tokens**. Generate a token using these settings:

<table style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><td colspan="1" rowspan="1"><p><strong>Setting</strong></p></td><td colspan="1" rowspan="1"><p><strong>Value</strong></p></td><td colspan="1" rowspan="1"><p><strong>Why It Matters</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Resource Owner</strong></p></td><td colspan="1" rowspan="1"><p>Organization</p></td><td colspan="1" rowspan="1"><p>Ensures the token is governed by organizational policies.</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Repository Access</strong></p></td><td colspan="1" rowspan="1"><p>Selected repositories</p></td><td colspan="1" rowspan="1"><p>Prevents the token from accessing any other repos in the Org.</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Repository Permissions</strong></p></td><td colspan="1" rowspan="1"><p><strong>Contents:</strong> <code>Read and write</code><br><strong>Pull Requests:</strong> <code>Read and write</code></p></td><td colspan="1" rowspan="1"><p>Grants only what Claude Code needs to push code and open PRs. If you want Claude to create issues, enable those also.</p></td></tr></tbody></table>

## Machine User Token Helper Script

### **Store Tokens in a Secure Host Folder**

Keep all project tokens centrally on your host outside of any Git repository. Name the tokens after your repo-specific names:

```shell
~/.gh-tokens/
├── project-a.token
├── project-b.token
└── project-c.token
```

### **Create a Shell Alias on Your Host (~/.zshrc / ~/.bashrc)**

Add a simple [launcher helper function](https://gist.github.com/jksprattler/f54e48e096be5c87fcbc72b8c8949fce) to your host shell profile. Update the GH env vars to match the name of your machine user created in the previous step.

```shell
# Dev container token helper
dev-agent() {
  local project_name=$(basename "$PWD")
  local token_file="$HOME/.gh-tokens/${project_name}.token"

  if [ -f "$token_file" ]; then
    export GH_TOKEN=$(cat "$token_file")
    export GIT_AUTHOR_NAME="machine-username"
    export GIT_AUTHOR_EMAIL="machine-username@users.noreply.github.com"

    echo "Loaded project-scoped credentials for ${project_name}"
  else
    echo "Warning: No token file at ${token_file}. Launching with local host inherited credentials."
    unset GH_TOKEN; unset GIT_AUTHOR_NAME; unset GIT_AUTHOR_EMAIL
  fi

  code .
}
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You could also create a <code>dev-agent-cli</code> function by installing the dev containers cli with <code>npm install -g @devcontainers/cli</code> and replacing the <code>code</code> command with <code>devcontainer up --workspace-folder .</code></div>
</div>

## **Setting Up the Dev Container**

### Create `devcontainer.json`

Save the following [configuration](https://gist.github.com/jksprattler/65b596321d2d2ca50909e85b7ecbbb0c#file-devcontainer-json) as `.devcontainer/devcontainer.json` in your repository:

```json
{
  "name": "Claude Agent Workspace (Base Bookworm)",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {},
    "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
  },
  // Drop capabilities and block privilege escalation
  "runArgs": [
    "--cap-drop=ALL",
    "--security-opt=no-new-privileges"
  ],
  "mounts": [
    "source=claude-code-config-${devcontainerId},target=/home/vscode/.claude,type=volume"
  ],
  "containerEnv": {
    "CLAUDE_CONFIG_DIR": "/home/vscode/.claude"
  },
  "remoteEnv": {
    "GH_TOKEN": "${localEnv:GH_TOKEN}",
    "GIT_AUTHOR_NAME": "${localEnv:GIT_AUTHOR_NAME}",
    "GIT_AUTHOR_EMAIL": "${localEnv:GIT_AUTHOR_EMAIL}"
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "anthropic.claude-code",
        "shd101wyy.markdown-preview-enhanced"
      ]
    }
  },
  "postCreateCommand": "git config --global user.name \"$GIT_AUTHOR_NAME\" && git config --global user.email \"$GIT_AUTHOR_EMAIL\" && gh auth setup-git",
  "remoteUser": "vscode",
}
```

*   The `claude-code-config-${devcontainerId}` mount maps the container path to a persistent, named Docker volume.
    
*   The `${devcontainerId}` keeps credentials scoped to the Docker volume of the launched container. Moving or renaming the local project folder, will cause it to evaluate a new hash, creating a new named volume that forces re-authentication.
    
*   The `CLAUDE_CONFIG_DIR` env var stores credentials, history and config files instead of the default `~./claude` directory created by the root user.
    

### Create Dockerfile

Save the following simple [Dockerfile](https://gist.github.com/jksprattler/65b596321d2d2ca50909e85b7ecbbb0c#file-dockerfile) under `.devcontainer/` in your repository:

```dockerfile
FROM mcr.microsoft.com/devcontainers/base:bookworm

# Pre-create the directory under the non-root user so named volumes inherit ownership
USER vscode
RUN mkdir -p /home/vscode/.claude
```

*   You can use this as a base to iterate on depending on your workspace requirements.
    
*   Running as `vscode` (a non-root user) in the container allows you to run `claude --dangerously-skip-permissions` for fully autonomous workflows. However, as mentioned below, running AI Agents in Dev Containers still comes with risks.
    

### Launch the agent Dev Container

💡 For a working example of a Full-Stack Claude Agent Workspace, you can check out my Dev Container setup [here](https://github.com/jennasrunbooks/ai-dev-tools-zoomcamp-2026/blob/main/.devcontainer/devcontainer.json).

Whenever you start work on a project:

```shell
cd ~/code/project-a

dev-agent

# Working example:
❯ cd ai-dev-tools-zoomcamp-2026/
❯ dev-agent
Loaded project-scoped credentials for ai-dev-tools-zoomcamp-2026
```

*   **Zero sensitive files in the workspace:** There is no `.envrc`, `.env`, or configuration file inside the project for the agent to inspect.
    
*   **Automatic project matching:** dev-agent automatically picks up `~/.gh-tokens/ai-dev-tools-zoomcamp-2026.token` based on the folder name, sets `GH_TOKEN` in that terminal session only, and opens VS Code.
    
*   **Container Isolation:** `${localEnv:GH_TOKEN}` in `devcontainer.json` passes the token into the container environment seamlessly.
    

### **Verification Inside the Container**

Once VS Code builds and opens the Dev Container, launch an integrated terminal inside the container and test the security boundaries:

```shell
whoami && id
sudo whoami
# Expected Output: vscode for the user identity, followed by a permission error on sudo.

gh auth status
gh api user --jq '.login'
# Expected Output: gh auth status confirms token authentication, and the API query returns the GitHub machine username.

git fetch origin
git var GIT_AUTHOR_IDENT
# Expected Output: Success. git fetch completes without prompt, and Git outputs the machine user name and email.

git clone https://github.com/your-org/another-private-repo.git /tmp/test-repo
# Expected Output: Failure. Returns HTTP 403 (Forbidden) or HTTP 404 (Not Found), confirming that the token cannot reach or manipulate repositories outside its explicit scope.
```

## Conclusion

While Fine-Grained PATs prevent agents from deleting or reading your repos, and Dev Containers provide a helpful layer of workspace isolation, your host system can still be exposed to unpredictable code execution by AI Agents like Claude Code.

Dev Containers share the host kernel [by design](https://github.com/microsoft/vscode-remote-release/issues/6608#issuecomment-1112960548). If an AI agent encounters a malicious payload, prompt injection, or executes an unintended command, it is not prevented from:

*   Reading or modifying files mounted directly from your host filesystem.
    
*   Reaching out to arbitrary external IP addresses/URLs on the open internet.
    
*   Attempting container escapes via host [IPC bridges](https://github.com/microsoft/vscode-remote-release/issues/11446) or exposed Docker sockets.
    

While Dev Containers don't offer complete host isolation, they do provide a valuable layer of workspace isolation through reproducible, containerized environments. Combine them with a dedicated machine user and scoped, fine-grained PAT's and you get better visibility over your AI Agent's commits without compromising your developer experience.

## Additional Resources

*   [Claude Code Docs | Security](https://code.claude.com/docs/en/security)
    
*   [Claude Code Docs | Development Containers](https://code.claude.com/docs/en/devcontainer)
    
*   [VS Code Dev Containers Overview](https://code.visualstudio.com/docs/devcontainers/containers)
    
*   [Claude Code DevContainer](https://github.com/trailofbits/claude-code-devcontainer) by Trail of Bits
    

## Further Exploration

*   **Restricting Network Access via** `iptables` **/ Egress Filtering:** Applying custom `devcontainer` iptables rules or network proxies to restrict outbound URL access to whitelisted endpoints (e.g., only allowing traffic to Anthropic APIs and specific GitHub domains). Ref [Anthropic's iptables example](https://github.com/anthropics/claude-code/blob/main/.devcontainer/init-firewall.sh).
    
*   **Hardware-Level Isolation with Dedicated Virtual Machines:** Running Docker inside isolated local VMs (using tools like Lima on macOS, isolated WSL2 distros on Windows, or QEMU/KVM).
