Skip to content

Instantly share code, notes, and snippets.

@arcanemachine
Created June 15, 2026 20:06
Show Gist options
  • Select an option

  • Save arcanemachine/c9ebdc0255941229a19f50db69a6e5a8 to your computer and use it in GitHub Desktop.

Select an option

Save arcanemachine/c9ebdc0255941229a19f50db69a6e5a8 to your computer and use it in GitHub Desktop.
A simple, portable script to run multiple coding harnesses in a container (Claude Code, Pi, OpenCode)
#!/usr/bin/env bash
#
# agcon — run a coding agent (or a shell) inside a minimal, disposable Docker
# container with the current directory and the agent's config bind-mounted in.
#
# The invoked name selects the agent. Symlink this script to get per-agent
# commands; the base name `agcon` (or any unknown name) uses DEFAULT_AGENT:
#
# ln -s agcon clcon # clcon -> claude
# ln -s agcon picon # picon -> pi
# ln -s agcon occon # occon -> opencode
#
# Agents (image and config are per-agent; host config is mounted in):
# claude @anthropic-ai/claude-code ~/.claude + ~/.claude.json
# pi @earendil-works/pi-coding-agent ~/.pi
# opencode opencode-ai ~/.config/opencode + ~/.local/share/opencode
# (env-key providers: pass keys via -e, e.g.
# occon -e OPENROUTER_API_KEY="$OPENROUTER_API_KEY")
#
# Usage:
# agcon [shell] [docker-run-args...] [-- command-args...]
#
# agcon # run DEFAULT_AGENT in ./
# picon # run pi (via symlink)
# agcon shell # start a bash shell instead
# agcon --network=host # forward flags straight to `docker run`
# agcon -- --resume # pass args to the inner command
# agcon shell -e FOO=bar # combine: shell + forwarded docker flags
#
# Mounts:
# $PWD -> /workspace (your project, read-write)
# <agent config> -> <container home> (per-agent; see profiles below)
# agcon-history -> /commandhistory (named volume; bash history)
#
# Everything between the leading options and `--` is passed verbatim to
# `docker run`; everything after `--` is passed to the in-container command.
# The container is removed on exit (--rm); only the mounts persist.
#
# The image is rebuilt every run from the Dockerfile heredoc below, but
# Docker's layer cache makes that near-instant unless something changed.
# To extend it, add apt packages to the apt-get line; per-agent npm packages
# come from the profiles below.
set -euo pipefail
# --- agent selection --------------------------------------------------------
DEFAULT_AGENT="claude"
case "$(basename "$0")" in
clcon) AGENT="claude" ;;
picon) AGENT="pi" ;;
occon) AGENT="opencode" ;;
*) AGENT="$DEFAULT_AGENT" ;;
esac
# --- agent profiles ---------------------------------------------------------
# Each profile sets the npm package to install, the default command, and the
# host->container config mount(s). Add an agent by adding a case arm here (and
# a name above if you want a dedicated symlinked command).
AGENT_MOUNTS=()
case "$AGENT" in
claude)
AGENT_PKG="@anthropic-ai/claude-code"
AGENT_CMD="claude --dangerously-skip-permissions"
AGENT_MOUNTS+=(-v "$HOME/.claude:/home/node/.claude")
# Claude also keeps a stray dotfile beside the dir; mount it if present.
[ -f "$HOME/.claude.json" ] && AGENT_MOUNTS+=(-v "$HOME/.claude.json:/home/node/.claude.json")
;;
pi)
AGENT_PKG="@earendil-works/pi-coding-agent"
AGENT_CMD="pi"
# pi keeps everything in a single ~/.pi dir.
[ -d "$HOME/.pi" ] && AGENT_MOUNTS+=(-v "$HOME/.pi:/home/node/.pi")
;;
opencode)
AGENT_PKG="opencode-ai"
AGENT_CMD="opencode"
# opencode uses XDG paths: config in ~/.config/opencode, auth/state in
# ~/.local/share/opencode. (For env-key auth, pass -e VARS via docker args.)
[ -d "$HOME/.config/opencode" ] && AGENT_MOUNTS+=(-v "$HOME/.config/opencode:/home/node/.config/opencode")
[ -d "$HOME/.local/share/opencode" ] && AGENT_MOUNTS+=(-v "$HOME/.local/share/opencode:/home/node/.local/share/opencode")
;;
*)
echo "agcon: unknown agent '$AGENT'" >&2; exit 1 ;;
esac
IMAGE="agcon-$AGENT:latest"
HISTORY_VOLUME="agcon-history"
# --- detect host timezone (best-effort) for the image build -----------------
HOST_TZ="${TZ:-}"
if [ -z "$HOST_TZ" ]; then
if [ -f /etc/timezone ]; then
HOST_TZ="$(cat /etc/timezone)"
elif [ -L /etc/localtime ]; then
HOST_TZ="$(readlink /etc/localtime | sed 's#.*/zoneinfo/##')"
fi
fi
HOST_TZ="${HOST_TZ:-UTC}"
# --- parse leading script options (order-independent) -----------------------
CMD="$AGENT_CMD"
while [ $# -gt 0 ]; do
case "$1" in
shell) CMD="bash"; shift ;;
*) break ;;
esac
done
# --- docker args (everything up to `--`) ------------------------------------
DOCKER_ARGS=()
while [ $# -gt 0 ] && [ "$1" != "--" ]; do
DOCKER_ARGS+=("$1"); shift
done
# --- inner command args (everything after `--`) -----------------------------
CMD_ARGS=()
if [ "${1:-}" = "--" ]; then
shift
CMD_ARGS=("$@")
fi
# --- build the image (cached; quietly rebuilds only changed layers) ---------
echo "(Re)building the container if necessary. This may take a moment..." >&2
docker build -q \
--build-arg "TZ=$HOST_TZ" \
--build-arg "AGENT_PKG=$AGENT_PKG" \
-t "$IMAGE" - >/dev/null <<'DOCKERFILE'
FROM node:lts-slim
ARG TZ
ENV TZ="$TZ"
# Minimal toolset. Add packages here as needed.
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
jq \
less \
locales \
nano \
ripgrep \
tzdata \
&& rm -rf /var/lib/apt/lists/*
# Generate a UTF-8 locale.
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen en_US.UTF-8
# Coding agent (selected per-agent by the calling script).
ARG AGENT_PKG
RUN npm install -g "$AGENT_PKG"
# Bash history persists via a named volume mounted at /commandhistory.
RUN mkdir /commandhistory && touch /commandhistory/.bash_history \
&& chown -R node:node /commandhistory
ENV SHELL=/bin/bash \
EDITOR=nano \
VISUAL=nano \
LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8 \
NODE_OPTIONS=--max-old-space-size=4096 \
HISTFILE=/commandhistory/.bash_history \
PROMPT_COMMAND="history -a"
# node:slim ships a non-root `node` user (uid 1000).
USER node
WORKDIR /workspace
DOCKERFILE
# --- run --------------------------------------------------------------------
exec docker run --rm -it \
--name "agcon-$AGENT" \
-v "$PWD:/workspace" \
-v "$HISTORY_VOLUME:/commandhistory" \
${AGENT_MOUNTS[@]+"${AGENT_MOUNTS[@]}"} \
-w /workspace \
${DOCKER_ARGS[@]+"${DOCKER_ARGS[@]}"} \
"$IMAGE" \
$CMD ${CMD_ARGS[@]+"${CMD_ARGS[@]}"}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment