# 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. - `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. - `build` — builds the image, tagged `$NAME` (see `.env` above). - `start` — runs the container as `$NAME` with `--hostname "$NAME"` (so the shell prompt reads `root@$NAME`, e.g. `root@claudaris`, instead of a random container ID). `/root/.bashrc`, `/opt/dotfiles/bash_aliases`, and the two auth files (`~/.claude/.credentials.json`, `~/.claude.json`) are each bind-mounted individually — seeded once (empty, for the auth files; from `files/bashrc`/`files/bash_aliases`, for the dotfiles) into `$DATA_DIR/home/{.bashrc,bash_aliases}` and `$DATA_DIR/claude/{credentials,claude}.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 likely saves 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. `DATA_DIR` defaults to `/data/$NAME`; `WORKSPACE_DIR` defaults to `/home/$USER/projects` (override via `claudaris config`). - `connect` — `docker start`s `$NAME` (a no-op if it's already running) then attaches to its tmux session, so it also works right after the container has auto-exited (see `Dockerfile`/entrypoint above). - `remove` (aliases `stop`, `rm`) — stops and removes the container so a subsequent `start` recreates it 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 ```bash # build the image docker build -t "$NAME" . # start the container docker run -d \ --name "$NAME" \ --hostname "$NAME" \ -v "$DATA_DIR/home/.bashrc:/root/.bashrc" \ -v "$DATA_DIR/home/bash_aliases:/opt/dotfiles/bash_aliases" \ -v "$DATA_DIR/claude/credentials.json:/root/.claude/.credentials.json" \ -v "$DATA_DIR/claude/claude.json:/root/.claude.json" \ -v "$WORKSPACE_DIR:/projects" \ "$NAME" # attach to the running container's tmux session docker exec -it "$NAME" tmux ``` ## 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.