Gabe Firestone

programming, gardening, electronics, whatever I want

Containing Claude try1

Link to the files: Claude-Isolator

The Idea

I’m just trying to wet my beak a little on the AI-assisted coding stuff, since it seems important to keep a little up to date on that sort of thing. My goal is to set up a secure area where I can play with AI assist (integrated in IDE and ultimately progressing to autonomous agents) that doesnt have access to the ‘personal’ parts of my computer, but does have access to a shared git repo.

My thinking is to treat the AI assisted work as a separate user in a shared repo. they get their own permissions, tasks, commits, and open PRs. Though of course at first, its just me pretending to be a bot, and it becomes less pretend over time. I’m more familiar and comfortable with git/jira as the multi-user organizing mechanism for projects, so I’m sticking to what I know, till I know better.

step 1: Get a new user setup on codeberg (github) that will be my first collaborator. I creatively called my first bot ‘swicano-bot1’ and created an organization ‘swicano-swarm’ that has a project that both swicano (me) and swicano-bot1 (also me, for now) are members with different access levels. swicano is an admin swicano-bot1 is only allowed to push commits, open PRs, and read anything else.

Now how to get different repos to use different keys?

I started by generating a new set of keys, named id_ed25519_bot1, to sit alongside my keys, then I followed this guide to have multiple accounts via aliases

key being that I modified my ~/.ssh/config to have these two entries :

Host codeberg.org-swicano
  HostName codeberg.org
  User git
  IdentityFile ~/.ssh/id_ed25519

Host codeberg.org-bot1
  HostName codeberg.org
  User git
  IdentityFile ~/.ssh/id_ed25519_bot1

and then when you clone a repo you just have to pick which ‘host’ to use, instead of git clone ssh://git@codeberg.org/swicano-bot1/test1.git

you either git clone codeberg.org-swicano:swicano-bot1/test1.git OR git clone codeberg.org-bot1:swicano-bot1/test1.git

and that determines which key gets associated with the cloning and which key gets used to push.

That means that a single copy of the repo on disk defaults to a single key, with the slight downside if I want to make changes to the same repo as myself and as the bot, I need to have a second copy of the repo locally using each key. I usually didn’t bother, and just made changes under the name bot. I guess I could have changed the git config to sign commits with my name and email if I cared.

Lets see how to get this into a docker container tho

How do I get the right key into a docker container tho, is it the same thing? Do I need to clone the repo in a separate place for each entity that is planning on modifying it? thats probably true, but feels heavy. maybe thats the price I pay for separating AI at the remote-repo level.

Thinking for a second, it might be reasonable to have the dockerfile clone the repo during container creation, and use that to set the right ssh keys? But that raises a question:

Q: how does that actually pass the ssh key into the docker container? A: it seems by default VSCode uses SSH Agent Forwarding, and which lets the remote container (docker with claude) use the local (personal computer’s) ssh-agent as if it were local, so that seems not ideal since it could figure out that the other are useful for other stuff. How can we stop that? this stackoverflow user has a script that removes the ‘helpful’ vscode forwarded keys, and then I can manually pass in just the one that I want. It seems aggressive but it accomplishes my end goal, so we go with it for now.

Here is the docker file and .devcontainer.json that I ended up with.

Dockerfile:


FROM ubuntu:24.04
ARG TZ
ENV TZ="$TZ"
ARG CLAUDE_CODE_VERSION=latest

ENV DEBIAN_FRONTEND=noninteractive
# Install basic development tools and iptables/ipset
RUN apt-get update && apt-get install -y \
  --no-install-recommends less procps curl git sudo \
  bash fzf man-db unzip gh iptables ipset iproute2 \
  dnsutils nano openssh-client ca-certificates \
  && apt-get clean && rm -rf /var/lib/apt/lists/*

# Install tea (Forgejo/Gitea CLI) — pinned version for reproducibility
RUN curl -fsSL https://dl.gitea.com/tea/0.9.2/tea-0.9.2-linux-amd64 \
      -o /usr/local/bin/tea \
    && chmod +x /usr/local/bin/tea

# Set `DEVCONTAINER` environment variable to help with orientation
ENV DEVCONTAINER=true

# Create bot1 user with a home directory
ARG HOST_UID=1000
RUN userdel -r ubuntu 2>/dev/null || true \
    && useradd -m -s /bin/bash -u 1000 bot1 \
    && echo "bot1 ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# Create the workspace directory (parent of .devcontainer)
# and give bot1 full ownership
RUN mkdir -p /workspace \
    && chown -R bot1:bot1 /workspace

# setup .ssh folder and give it right permissions
RUN mkdir -p /home/bot1/.ssh \
    && mkdir -p /home/bot1/tmp \
    && touch /home/bot1/.ssh/known_hosts \
    && chown -R bot1:bot1 /home/bot1/.ssh \
    && chmod 700 /home/bot1/.ssh \
    && chmod 777 /home/bot1/tmp \
    && chmod 600 /home/bot1/.ssh/known_hosts

WORKDIR /workspace

# Set up non-root user
USER bot1

I removed some lines from this, and could remove more, but the gist is that it sets up a user bot1 and sets up some folders with the right permissions to receive the ssh fuckery that goes on in the .devcontainer folder

the devcontainer.json is this

{
  "name": "Claude Code Sandbox",
  "build": {
    "dockerfile": "Dockerfile",
    "args": {
      "TZ": "${localEnv:TZ:America/New_York}",
      "CLAUDE_CODE_VERSION": "latest",
      "HOST_UID": "1000" 
    }
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "anthropic.claude-code"
      ]
    }
  },
  "remoteUser": "bot1",  
  "mounts": [
    "source=/mnt/c/Users/fires/.ssh/id_ed25519_bot1,target=/home/bot1/tmp/id_ed25519_bot1,type=bind,readonly",
    "source=/mnt/c/Users/fires/.ssh/id_ed25519_bot1.pub,target=/home/bot1/tmp/id_ed25519_bot1.pub,type=bind,readonly",
    "source=/mnt/c/Users/fires/.ssh/codeberg_bot1,target=/home/bot1/tmp/codeberg_bot1,type=bind,readonly"
  ],
  "workspaceFolder": "/workspace",
  "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached",
  "postCreateCommand": "bash .devcontainer/setup.sh",
  "postStartCommand": "find /tmp -maxdepth 1 -name 'vscode-ssh-auth-*.sock' -delete 2>/dev/null || true",
  "containerEnv": {"SSH_AUTH_SOCK": ""   },
  "remoteEnv": { "SSH_AUTH_SOCK": "" }
}

where the important bits are the post-create command which runs setup.sh, the postStartCommand that deletes VSCodes SSH forwarding, and the mounts which copy ONLY the ssh keys that bot1 gets to use over into the container.

the setup.sh copies those files from tmp to .ssh and most importantly, sets the git config to use a bot username and email address, and sets up tea, which allows the bot to open PRs (and more if I set permissions) on Codeberg.

#!/bin/bash
# .devcontainer/setup.sh
cp /home/bot1/tmp/id_ed25519_bot1 /home/bot1/.ssh/id_ed25519_bot1
chmod 600 /home/bot1/.ssh/id_ed25519_bot1

cp /home/bot1/tmp/id_ed25519_bot1.pub /home/bot1/.ssh/id_ed25519_bot1.pub
chmod 600 /home/bot1/.ssh/id_ed25519_bot1.pub

echo "Host codeberg.org
    IdentityFile /home/bot1/.ssh/id_ed25519_bot1
    User git" > /home/bot1/.ssh/config
chmod 600 /home/bot1/.ssh/config

cp -r /workspace/.claude ~/

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519_bot1
ssh-keyscan codeberg.org >> ~/.ssh/known_hosts
tea login add --name codeberg --url https://codeberg.org --token "$(cat /home/bot1/tmp/codeberg_bot1)"

With these files, you should have a minimal set up that lets you run claude (you will still have to sign in and such) inside the container. Here’s how you would use it, and where the whole thing sort of falls apart:

Example project layout

heres what a folder layout looks like for me

workspace/
├── .claude/
│   └── etc whatever you normally do
├── .devcontainer/
│   ├── devcontainer.json
│   ├── dockerfile
│   ├── dockerfile.claudeonly  <-- backups
│   ├── devcontainer.json.claudeonly
│   └── setup.sh
├── .vscode/
│   └── whatever normal shit
└── PROJECT_REPO/
    ├── .git/
    ├── dockerfiles/
    │   ├── dockerfile <-- project only
    │   ├── docker-compose.yml <-- project only 
    │   └── source of the issues
    └── the rest of the repo

I keep devcontainer outside the project repo because I want to keep the various claude containment steps external to what im trying to accomplish in a single repo, but that can run into issues with a project repo that has its own dockerfiles for dev.

The (bad) way that I currently deal with that is to manually keep three separate dockerfiles, one for the project, the claude-only dockerfile above, and a ‘combined’ dockerfile that includes both sets of dependencies and setup. This is certainly not a good way to do it. If I was a longshoreman, I would know how to use one image as a base for the other or some shit like that, and I would know how to make the devcontainer.json and docker-compose play nice together. but i dont. so I have more dockerfiles than i ideally want, one for the project (that gets used for automated tests, which helps keep it up to date) and one for claude + the project, which i do active dev in. One benefit i’ve found is that it forces me to rebuild the container often, which helps keep me honest in terms of whether the dockerfiles are sufficient for dev work, and im not sneakily hiding manual configuration steps.

If you have any ideas how to do this better, please help! I’ll put this up as a project for comments: Claude-Isolator

edits based on feedback:

if you want to copy your claude convos OUT of the container so that they reappear when you rebuild the container, use this line

rm -rf /workspace/.claude/ && cp -r ~/.claude /workspace

~to overwrite the local folder’s .claude with the one being used by claude at /.claude. Or, idk, change the config so it uses this one, usually im not too sad about losing a few chats if i forget this. if its not in version control, its not real anyway, right?

The dockerfile was updated to set the CLAUDE_CONFIG_DIR to be in the workspace, which is a mounted volume already, so you get some cross-rebuild continuity. theres a chance on first setup it fails. i didnt try with a clean repo.