Skip to content

dentarg/ai

Repository files navigation

ai image

A containerized sandbox for running coding agents (Claude Code, Gemini CLI, OpenAI Codex, GitHub Copilot) with --dangerously-skip-permissions / --dangerously-bypass-approvals-and-sandbox enabled by default.

Why

Agents work best when they can freely run shell commands, edit files, install packages, and poke at databases ��� but you don't want them doing that against your host. This image gives each session its own throwaway Linux environment with:

  • The project you're working on mounted at /app.
  • Language runtimes, databases (PostgreSQL, LavinMQ, Redis), and common tools preinstalled, so agents don't spend turns bootstrapping.
  • OAuth credentials and API keys mounted from ~/ai/settings, with multi-profile support and automatic token refresh.
  • Shell history, agent session history, cloned repos, and installed gems persisted on the host across container restarts.
  • A shared directory mounted at /share (from ~/ai/share) for passing files between the host and containers.
  • mitmproxy available for inspecting what the agent actually sends over the wire.

Setup

Drop per-profile OAuth credentials in $HOME/ai/settings as .credentials.<profile>.json. They get mounted into the container and copied into ~/.claude/ when you launch claude with a matching profile. See the OAuth Login section for how to generate these.

Optionally, add AGENTS.md to $HOME/ai/settings — it becomes CLAUDE.md for Claude Code, GEMINI.md for Gemini CLI, and is copied into Codex's session config.

Optionally, add sentry.token to $HOME/ai/settings to enable the Sentry MCP server in Claude Code. See MCP Servers.

# start podman and share the current working directory
bin/ai

# start podman and auto-launch "c <profile>" once the container is up.
# before launching, the shared "~/ai/settings" token for <profile> is
# refreshed on the host (silent refresh, falling back to interactive login
# only if it has fully expired) so every session reuses a valid token.
bin/ai <profile>

# start podman and auto-launch "cx" once the container is up.
bin/ai cx

# resume a prior Claude session: passed through to "c --resume <id>" on launch.
# works with or without a profile (c auto-detects it from the session).
bin/ai --resume <session-id>
bin/ai <profile> --resume <session-id>

# resume a prior Codex session: passed through to "cx --resume <id>" on launch.
bin/ai cx --resume <session-id>

# publish extra ports from the container to the host. each entry is either
# "PORT" (host==container) or "SRC:DST" (host:container); comma-separate many.
bin/ai --ports 9999             # host 9999 -> container 9999
bin/ai --ports 8888:7777        # host 8888 -> container 7777
bin/ai <profile> --ports 9999,8888:7777

# enable Claude Code remote control for the session (off by default).
# equivalently set AI_REMOTE=1 in your shell. see "Remote control" below.
bin/ai <profile> --remote
AI_REMOTE=1 bin/ai <profile>

# enable fast mode for the session (off by default).
# equivalently set AI_FAST=1 in your shell. see "Fast mode" below.
bin/ai <profile> --fast
AI_FAST=1 bin/ai <profile>

# start services (and run "bundle install" if Gemfile exists).
# also runs automatically as part of "c" below.
s

# launch claude with a specific oauth profile (runs "s" first)
c <profile>

# or launch claude with an Anthropic API key
c --apikey sk-ant-...

# resume a prior Claude session (searches /history for the session id; a prefix is enough).
# profile is auto-detected from the session's saved .profile file.
c --resume <session-id>

# launch gemini
g

# launch openai codex
cx

# resume a prior Codex session (searches /history for the session id; a prefix is enough).
cx --resume <session-id>

# exit the container
x

cx launches Codex through a temporary symlink named after the host directory captured by bin/ai, so Codex's terminal title and project/status fields show that host directory name instead of /app. Its generated statusline shows the project, git branch, model/reasoning, context used, and thread id. TUI notifications are disabled for quieter terminal sessions. cx --resume <id> searches archived Codex rollouts under /history and reuses the original Codex home before launching codex resume <id>.

Prerequisites

Your Anthropic API key in 1Password.

brew install podman

# init the VM, enable zram swap, set kernel.keys quotas
bin/setup-vm

# normal builds use the pinned agent versions in "versions/" and do not
# check upstream "latest" endpoints.
./build_image

# update pinned Claude Code and Codex version files, then rebuild
./build_image --update-agents

# update only one pinned agent version file
./build_image --update-claude
./build_image --update-codex

# rebuild all layers and pull latest base image
./build_image --force

OAuth Login

First-time setup to get OAuth credentials.

Claude Code:

# inside the container, or via podman run
refresh-tokens --login
refresh-tokens --login <profile>

This generates an OAuth authorization URL. Open it in your browser, sign in, and paste the code back into the terminal. Credentials are saved to ~/.claude/.credentials.json (or .credentials.<profile>.json).

OpenAI Codex:

# inside the container
codex-login

# or from the host
bin/codex-login

This standalone helper starts Codex's "Sign in with Device Code" flow without running the Codex CLI. Open the displayed URL, enter the one-time code, and finish sign-in in your browser. Credentials are saved to /settings/codex/auth.json in the container, or $HOME/ai/settings/codex/auth.json from the host.

Token Refresh Service

OAuth tokens expire periodically. A systemd service (refresh-tokens.service) runs in every container, keeping ~/.claude/.credentials*.json files fresh automatically.

# check service status
systemctl status refresh-tokens

# view logs
journalctl -u refresh-tokens

# follow logs
journalctl -u refresh-tokens -f

# one-shot refresh (e.g. before launching a session)
refresh-tokens --once

The service can also run as a standalone container to refresh /settings credentials:

podman run -d --name token-refresh \
  --env CREDENTIALS_DIR=/settings \
  --volume ${HOME}/ai/settings:/settings \
  ai:latest /usr/local/bin/refresh-tokens --daemon

Environment variables:

Variable Default Description
CREDENTIALS_DIR ~/.claude Directory containing .credentials*.json files
CHECK_INTERVAL 300 Seconds between checks
REFRESH_BEFORE 3600 Seconds before expiry to trigger refresh

MCP Servers

Sentry

To enable Sentry's MCP server so the agent can fetch issues, events, and stack traces directly, drop your Sentry user auth token in $HOME/ai/settings/sentry.token (just the token, no quotes or whitespace).

When you launch c <profile>, an mcpServers.sentry entry is injected into the session's ~/.claude.json that runs @sentry/mcp-server locally over stdio with SENTRY_ACCESS_TOKEN set. We don't use the hosted mcp.sentry.dev because its OAuth flow expects a callback on localhost:62880 inside the browser host — unreachable from this container.

For self-hosted Sentry, additionally drop the hostname in $HOME/ai/settings/sentry.host (e.g. sentry.example.com) — it's passed through as --host=....

No sentry.token → no MCP server registered.

Remote control

Remote control lets you monitor and steer a running Claude Code session from claude.ai or the Claude mobile app. The session keeps running in the container — only its I/O is mirrored, over the same outbound HTTPS the agent already uses.

It is off by default and opt-in per session, because it exposes the session (filesystem, MCP servers, every tool call) to anyone with your claude.ai login. Turn it on with either:

bin/ai <profile> --remote      # one-off flag
AI_REMOTE=1 bin/ai <profile>   # or set the env in your shell

bin/ai forwards this into the container as AI_REMOTE=1; c then sets remoteControlAtStartup in the session's ~/.claude/settings.json — the same key the /config "Enable Remote Control for all sessions" toggle writes, so the bridge starts automatically. Running c directly honours the same --remote flag and AI_REMOTE env. To make it the default for every session, export AI_REMOTE=1 in your host shell profile.

Remote sessions are labelled by the host directory in the claude.ai/mobile list — c sets CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX from HOST_DIR, so they show as <dir>-<random-words> instead of the container hostname.

To enable it on an already-running session, run /remote-control (or /rc) in the TUI; a /rc active link then appears in the footer.

Fast mode

Fast mode runs Opus with higher-speed output. It is off by default and opt-in per session, because it draws from usage credits at a higher rate and has separate rate limits. Turn it on with either:

bin/ai <profile> --fast      # one-off flag
AI_FAST=1 bin/ai <profile>   # or set the env in your shell

bin/ai forwards this into the container as AI_FAST=1; c then sets fastMode in the session's ~/.claude/settings.json — the same key the /fast toggle writes. Running c directly honours the same --fast flag and AI_FAST env. To make it the default for every session, export AI_FAST=1 in your host shell profile. Toggle it within a session with /fast [on|off].

c also exports CLAUDE_CODE_SKIP_FAST_MODE_ORG_CHECK=1 in the fast path. Without it the persisted fastMode is evaluated once at startup, while the async fast-mode availability check is still pending, so in a fresh container it resolves to off and never re-applies; skipping that check lets the setting engage immediately.

Tricks

zsh things:

# pod       # list all running containers
# pod <id>  # launch bash shell in selected container
# pod last  # launch bash shell in the youngest container
function pod() {
    [ $# -lt 1 ] && podman ps && return 0

    [ "$1" = "last" ] && podman exec -it $(podman ps | tail -1 | cut -d ' ' -f 1) ${2:-bash} && return

    local container
    container=$1
    podman exec -it $container ${2:-bash}
}

mitmproxy

Start it capturing everything:

./mitmdump --mode regular --listen-port 8080 --ssl-insecure --set flow_detail=3 -w claude.flow

mitmproxy generates its CA at ~/.mitmproxy/mitmproxy-ca-cert.pem on first run.

export NODE_EXTRA_CA_CERTS=~/.mitmproxy/mitmproxy-ca-cert.pem
export HTTPS_PROXY=http://127.0.0.1:8080

Start the agent

claude

Output the (partially) binary dump as text (--mode picking another port is important if proxy already running)

./mitmdump --mode regular@8082 --set flow_detail=3 -r claude.flow --set export_format=curl

Commands

tcpdump

podman run --rm -it --cap-add=NET_RAW --cap-add=NET_ADMIN --net=container:<container> nicolaka/netshoot tcpdump -i eth0

podman

# to see current settings
podman machine inspect

# when we can't build because we're out of space
podman system prune--all

# initial setup, or after `podman machine reset`:
# inits the VM (300 GB disk, 8 GB RAM), enables zram swap, and persists
# kernel.keys.maxkeys / maxbytes so we don't hit the keyring quota
# ("crun: join keyctl ... Disk quota exceeded")
bin/setup-vm

Stuff

  • Claude Code
  • GitHub Copilot
  • Google Gemini
  • Node.js
  • Bun TypeScript
  • Ruby
  • Crystal
  • Python
  • Rust
  • Go
  • ast-grep
  • tmux
  • SSH (ssh-keygen, ...)
  • PostgreSQL
  • LavinMQ
  • Redis
  • amqpcat
  • rusage — better time(1), prints full getrusage(2) stats
  • logcli — Grafana Loki CLI

About

Easily start the agent in a container

Resources

Stars

Watchers

Forks

Contributors