Files
claudaris/AGENTS.md
T
2026-07-21 02:17:06 -07:00

8.3 KiB

AGENTS.md

Purpose

Docker scaffold for running Claude Code in an isolated container (see README.md).

Structure

  • .env — gitignored, per-user, written by claudaris config. Holds NAME (image/container name — the default claudaris is meant to be overridden per person, e.g. chris-claude, so multiple users can each run their own container off this same repo), DATA_DIR, and WORKSPACE_DIR. claudaris sources it if present; docker-compose.yml reads the same vars, and docker compose picks up .env on its own since it lives in the project directory compose runs from — so claudaris and plain docker compose commands agree without any extra wiring.
  • docker-compose.yml — the actual container definition (build context, image/container/hostname name, volumes); claudaris shells out to docker compose rather than raw docker build/run/exec. The one mount it can't express is the opt-in SSH mount (see claudaris start below), which needs a host path to exist first — that's added via a gitignored docker-compose.override.yml that claudaris start writes or removes as needed; compose merges it automatically when present.
  • Dockerfile — archlinux base image with bash, git, nodejs/npm, tmux, vim, fastfetch, etc. files/bash_aliases is baked in at /opt/dotfiles/bash_aliases and files/bashrc becomes the image's default /root/.bashrc — both are rebuild-time defaults, but the host shadows both with its own single-file bind mounts at runtime (see claudaris start below). Claude Code is installed at build time straight into /root ($HOME), so a rebuild always picks up the latest release. Staying logged in across rebuilds is handled entirely by claudaris start, which bind-mounts two individual host files onto ~/.claude/.credentials.json (the OAuth token) and ~/.claude.json (account/onboarding state) — see claudaris start below for why these are per-file mounts rather than a directory volume. Three MCP servers for Claude Code are also installed at build time: n8n-mcp (npm, /usr/local/bin/n8n-mcp), the official gitea-mcp (latest release binary, /usr/local/bin/gitea-mcp), and invokeai-mcp-server (PyPI via uv tool install; upstream hardcodes the InvokeAI URL, so the Dockerfile sed-patches it to honor $INVOKEAI_BASE_URL, with a grep that fails the build if upstream changes and the patch stops landing). They're only installed by the Dockerfile — registration happens in files/entrypoint.sh on every container start (claude mcp add --scope user, skipped per-server if already present), because user-scope MCP config lives in ~/.claude.json, which is bind-mounted from the host and would shadow anything registered at build time. Consequence of the "add if missing" guard: hand edits to a server's config survive restarts, but a server removed with claude mcp remove is re-added on the next container start. The servers read their instance URLs/tokens (GITEA_HOST, GITEA_ACCESS_TOKEN, N8N_API_URL, N8N_API_KEY, INVOKEAI_BASE_URL) from the environment Claude Code runs in — commented export templates live in files/bash_aliases, which is host-persisted, so tokens stay out of the image and the repo. git config --system --add safe.directory '*' is set so git works on /projects repos bind-mounted from the host (which the container, always running as root, would otherwise treat as untrusted). files/entrypoint.sh starts the tmux server and creates a main session, then polls and exits once the session ends (tmux's defaults apply: remain-on-exit off, exit-empty on, so a shell exiting — Ctrl+D, exit, crash — tears the session down) rather than running forever; there's no --restart policy, so a stopped container needs claudaris connect or claudaris start to bring it back up (both do this automatically). /etc/profile.d/nix_path.sh points PATH at /root/.local/bin. files/tmux is copied in as a /usr/local/bin/tmux wrapper that shadows the real tmux binary so that plain docker exec -it "$NAME" tmux runs tmux new-session -A -s main (attaches to main if it exists, creates it otherwise) instead of always starting a new session.
  • claudaris — single entry point with subcommands:
    • config (alias configure) — interactive wizard prompting for NAME, DATA_DIR, WORKSPACE_DIR (showing current/default values, enter to keep) and writing them to .env. Re-run any time to update it.
    • builddocker compose build --no-cache --pull (see .env above for $NAME).
    • startdocker compose up -d, so the shell prompt reads root@$NAME, e.g. root@claudaris, instead of a random container ID (hostname: in docker-compose.yml). /root/.bashrc, /opt/dotfiles/bash_aliases, and the auth files (~/.claude/.credentials.json, ~/.claude.json, ~/.local/share/opencode/auth.json) are each bind-mounted individually in docker-compose.yml — seeded once by start (empty, for the auth files; from files/bashrc/files/bash_aliases, for the dotfiles) into $DATA_DIR/home/{.bashrc,bash_aliases}, $DATA_DIR/claude/{credentials,claude}.json, and $DATA_DIR/opencode/auth.json on the host, so they can be edited/persist without a rebuild — including a coworker dropping in their own aliases. Individual-file mounts matter for the auth files specifically: Claude Code and OpenCode likely save them atomically (write a temp file, then rename() over the target), and rename() onto a symlink replaces the symlink instead of writing through it — silently breaking persistence after the first write. A bind mount doesn't have that failure mode, which is why these aren't just symlinked from a directory volume the way an earlier version of this setup did it. $WORKSPACE_DIR is mounted at /projects. Also offers an opt-in /root/.ssh mount (read-only) for reaching other nodes: create $DATA_DIR/ssh on the host and populate it before starting the container to enable it — start writes or removes a gitignored docker-compose.override.yml for this mount, since compose can't make a volume conditional on a host path existing the way the script itself can. DATA_DIR defaults to /data/$NAME; WORKSPACE_DIR defaults to /home/$USER/projects (override via claudaris config).
    • connectdocker compose up -d (a no-op if already running) then docker compose exec claudaris bash, so it also works right after the container has auto-exited (see Dockerfile/entrypoint above).
    • remove (aliases stop, rm) — docker compose down, so a subsequent start recreates the container fresh.
    • help — usage.
  • files/ — plain (non-dot) source files COPY'd into the image at build time: bashrc/bash_aliases (dotfiles — see Dockerfile above), entrypoint.sh, and the tmux wrapper. Kept dotless so they're easy to see/edit directly in a normal directory listing; the Dockerfile adds the leading dot back on for bashrc's destination. Both bashrc and bash_aliases are also seeded onto the host by claudaris start so their bind-mounted copies can diverge without a rebuild.
  • README.md — usage instructions.
  • docs/ — user documentation in markdown: README.md (index), getting-started.md (setup and daily use), claudaris.md (the executable, subcommand by subcommand, including the mount table), mcp.md (the baked-in MCP servers and their env vars). Keep these in sync when changing the script, Dockerfile, or entrypoint.

Common commands

# build the image
docker compose build --no-cache --pull

# start the container
docker compose up -d

# attach to the running container
docker compose exec claudaris bash

docker compose reads NAME/DATA_DIR/WORKSPACE_DIR from .env automatically (no claudaris involvement needed), but the host-side seeding claudaris start does (dotfiles, auth files, the SSH override) isn't replicated by compose itself — run claudaris start at least once per host before using raw compose commands.

Notes

No test suite or CI — this is infra/config, not application code. Verify changes by rebuilding the image (./claudaris build) and exec'ing in (./claudaris connect) to confirm the container behaves as expected.