commit 1a69f28eb883d61838f374e985c03514841cc98b Author: Nick Yeoman Date: Wed Jul 8 18:23:00 2026 -0700 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fac5ce7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.git +*.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..411bf48 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Per-user container name/settings — run `./claudaris config` to generate. +/.env + +# Claude Code's auto-generated per-user permission cache. +/.claude/settings.local.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c865e1f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,101 @@ +# 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. + `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. + +## 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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a377624 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# Environment notes + +- Docker is not installed on this host and will not be installed. Do not attempt to run `docker` commands (e.g. to test the Dockerfile) — verify Dockerfile changes by inspection instead. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..87a8088 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,68 @@ +FROM archlinux:latest + +RUN pacman -Sy --noconfirm archlinux-keyring && \ + pacman -Su --noconfirm && \ + pacman -S --noconfirm --needed \ + bash \ + ca-certificates \ + curl \ + fastfetch \ + fzf \ + git \ + lazygit \ + less \ + mariadb-clients \ + nodejs \ + npm \ + openssh \ + php \ + pwgen \ + python \ + sqlite \ + tmux \ + tree \ + unzip \ + uv \ + vim \ + zoxide && \ + pacman -Scc --noconfirm + +RUN printf '%s\n' \ + 'export PATH="/root/.local/bin:$PATH"' \ + 'eval "$(zoxide init bash)"' \ + > /etc/profile.d/nix_path.sh && \ + chmod 755 /etc/profile.d/nix_path.sh && \ + echo '[ -f /etc/profile.d/nix_path.sh ] && . /etc/profile.d/nix_path.sh' >> /etc/bash.bashrc + +RUN git config --system user.name "code" && \ + git config --system user.email "code@4lt.ca" && \ + git config --system --add safe.directory '*' + +# Dotfiles +COPY files/bash_aliases /opt/dotfiles/bash_aliases +COPY files/bashrc /root/.bashrc + +# Install Claude Code straight into /root, so a rebuild always picks up the +# latest release. Only the two files needed to stay logged in (not the CLI +# itself) are persisted across rebuilds, via individual bind mounts set up +# by `claudaris start` — see claudaris and AGENTS.md. +WORKDIR /tmp + +RUN curl -fsSL https://claude.ai/install.sh | bash + +# Claude Code usage statusline plugin +RUN PATH="/root/.local/bin:$PATH" bash -c \ + 'curl -fsSL https://raw.githubusercontent.com/leeguooooo/claude-code-usage-bar/main/web-install.sh | bash' + +# tmux wrapper +COPY files/tmux /usr/local/bin/tmux +COPY files/tmux.conf /etc/tmux.conf +RUN chmod 755 /usr/local/bin/tmux + +# Container entrypoint +COPY files/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod 755 /usr/local/bin/entrypoint.sh + +WORKDIR /projects + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d95210c --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# claudaris + +Claudaris (klaw-DAR-iss): A moveable docker container for Claude code. using Arch btw. + +## Configure + +Run the interactive wizard and answer its prompts (press enter to keep a +default): +``` +./claudaris config +``` +It writes `NAME`, `DATA_DIR`, and `WORKSPACE_DIR` to `.env` (gitignored — +every user keeps their own), which every other command sources. `NAME` +should be something unique to you (e.g. `chris-claude`) if more than one +person is running a container from this same repo checkout. `DATA_DIR` +defaults to `/data/$NAME`; `WORKSPACE_DIR` (the host dir mounted as +`/projects` in the container) defaults to `/home/$USER/projects`. +`config`/`configure` can be re-run any time to update `.env`. + +## Build container + +As root run build (always `--no-cache --pull`, so it picks up the latest Claude Code release): +``` +sudo ./claudaris build +``` +## Start the container + +Make sure the volumes are as you like and start the container. +``` +sudo ./claudaris start +``` + +## Connect to the container + +``` +sudo ./claudaris connect +``` + +## Stop and remove the container + +``` +sudo ./claudaris remove +``` +(aliases: `stop`, `rm`) + +## Optional: SSH access to other nodes + +To let the container SSH out to other machines, create `$DATA_DIR/ssh` +on the host and populate it with keys/config *before* running `./claudaris start`. If +present, it's bind-mounted read-only to `/root/.ssh`. It's off by default. + +## Notes + +Claude Code is installed at build time under `/root`, so a rebuild always +picks up the latest release. Two files are bind-mounted individually to +survive that: `~/.claude/.credentials.json` (the OAuth token) and +`~/.claude.json` (account/onboarding state — Claude Code checks this too, +so persisting the token alone isn't enough to avoid a re-login prompt after +a rebuild). `./claudaris start` seeds both from empty on the host the first +time it runs. Logging in once then survives rebuilds without carrying the +CLI binary or the rest of `~/.claude` (settings, history, projects, caches) +along with it. + +The container also stops itself once its tmux session ends (any shell +exit — Ctrl+D, `exit`, crash) instead of running forever, and there's no +Docker restart policy — `./claudaris connect` and `./claudaris start` both +bring it back up automatically if you find it stopped. + +`.bashrc` and `bash_aliases` are both bind-mounted as single files so they can +be tweaked per-host (e.g. a coworker importing their own aliases) without a +rebuild. `./claudaris start` seeds them into `$DATA_DIR/home/.bashrc` and +`$DATA_DIR/home/bash_aliases` from `files/bashrc` and `files/bash_aliases` the +first time it runs, without overwriting later edits. Anything that needs to +survive regardless (PATH, zoxide, git identity) lives in `/etc/...` inside +the image instead of under `/root`. + +The container is started with `--hostname "$NAME"`, so the prompt reads +`root@claudaris` (or whatever you set `NAME` to via `./claudaris config`) +instead of a random container ID. \ No newline at end of file diff --git a/claudaris b/claudaris new file mode 100755 index 0000000..7d97100 --- /dev/null +++ b/claudaris @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_DIR="$(pwd)" + +# Run `./claudaris config` to set your own image/container name (so multiple +# people can each run their own container from this repo) and other options. +# It writes them to .env, which is gitignored and sourced here if present. +NAME=claudaris +[ -f .env ] && set -a && . .env && set +a + +usage() { + cat < + +Commands: + config, configure Interactive wizard to write .env with your settings + build Build the container image (always --no-cache --pull) + start Start (or create) the container + connect Start the container if needed, then attach a tmux session + remove, stop, rm Stop and remove the container + help Show this message +EOF +} + +cmd_config() { + echo "claudaris config — press enter to keep the current/default value." + echo + + read -r -p "NAME [$NAME]: " input + NAME="${input:-$NAME}" + + local data_dir_default="${DATA_DIR:-/data/$NAME}" + read -r -p "DATA_DIR [$data_dir_default]: " input + DATA_DIR="${input:-$data_dir_default}" + + local workspace_dir_default="${WORKSPACE_DIR:-/home/${USER:-$(id -un)}/projects}" + read -r -p "WORKSPACE_DIR [$workspace_dir_default]: " input + WORKSPACE_DIR="${input:-$workspace_dir_default}" + + cat > .env </dev/null + docker exec -it "$NAME" tmux +} + +cmd_remove() { + docker stop "$NAME" >/dev/null 2>&1 || true + docker rm "$NAME" +} + +case "${1:-help}" in + config|configure) cmd_config ;; + build) cmd_build ;; + start) cmd_start ;; + connect) cmd_connect ;; + remove|stop|rm) cmd_remove ;; + help|-h|--help) usage ;; + *) + echo "Unknown command: $1" >&2 + usage >&2 + exit 1 + ;; +esac diff --git a/files/bash_aliases b/files/bash_aliases new file mode 100644 index 0000000..5ab690f --- /dev/null +++ b/files/bash_aliases @@ -0,0 +1,26 @@ +#!/bin/bash +# Bash aliases for the claudaris container + +# ls long hidden and human readable +alias ll='ls -lah' +alias l1='ls -1' +alias ls='ls --color=auto' + +# up levels (up #_of_dir_up) +function cd_up() { + cd $(printf "%0.0s../" $(seq 1 $1)); +} +alias up='cd_up' + +# or just ctrl+d +alias quit='exit' +alias e='exit' + +# tmux +alias tl='tmux list-sessions' + +# git +alias gs='git status' + +alias btw='fastfetch' +alias cd='z' \ No newline at end of file diff --git a/files/bashrc b/files/bashrc new file mode 100644 index 0000000..2650374 --- /dev/null +++ b/files/bashrc @@ -0,0 +1,51 @@ +# ~/.bashrc: executed by bash(1) for non-login shells. + +# If not running interactively, don't do anything +case $- in + *i*) ;; + *) return;; +esac + +# don't put duplicate lines or lines starting with space in the history. +HISTCONTROL=ignoreboth +HISTSIZE=10000 +HISTFILESIZE=20000 +HISTTIMEFORMAT="%F %T " + +# append to the history file, don't overwrite it +shopt -s histappend + +# check the window size after each command and, if necessary, +# update the values of LINES and COLUMNS. +shopt -s checkwinsize + +# Prints " (branch)" when inside a git repo, nothing otherwise. +__git_branch() { + local branch + branch=$(git symbolic-ref --short HEAD 2>/dev/null) || branch=$(git rev-parse --short HEAD 2>/dev/null) || return + printf ' (%s)' "$branch" +} + +# Tokyo Night palette, matching files/tmux.conf: purple user@host, blue cwd, +# green git branch, gray prompt symbol. +PS1='\[\033[38;2;187;154;247;1m\]\u@\h\[\033[0m\] \[\033[38;2;122;162;247m\]\w\[\033[0m\]\[\033[38;2;158;206;106m\]$(__git_branch)\[\033[0m\]\n\[\033[38;2;86;95;137m\]\$\[\033[0m\] ' + +if [ -x /usr/bin/dircolors ]; then + eval "$(dircolors -b)" + alias ls='ls --color=auto' +fi + +if [ -f /opt/dotfiles/bash_aliases ]; then + . /opt/dotfiles/bash_aliases +fi + +if ! shopt -oq posix; then + if [ -f /usr/share/bash-completion/bash_completion ]; then + . /usr/share/bash-completion/bash_completion + elif [ -f /etc/bash_completion ]; then + . /etc/bash_completion + fi +fi + +# Claude Code CLI installer env +[ -f /root/.local/bin/env ] && . /root/.local/bin/env diff --git a/files/entrypoint.sh b/files/entrypoint.sh new file mode 100644 index 0000000..9141e16 --- /dev/null +++ b/files/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +# Staying logged in across rebuilds is handled entirely by `claudaris start`, +# which bind-mounts ~/.claude/.credentials.json and ~/.claude.json individually +# from the host (see claudaris) — nothing to seed or symlink here. + +# Create the main tmux session if it does not exist. +/usr/bin/tmux new-session -d -s main 2>/dev/null || true + +# tmux's defaults apply here (remain-on-exit off, exit-empty on): a window +# closes when its shell exits, the session closes when its last window does, +# and the server exits when its last session does. Poll for that and exit +# the container once it happens, instead of running forever. +while /usr/bin/tmux has-session -t main 2>/dev/null; do + sleep 2 +done +exit 0 \ No newline at end of file diff --git a/files/tmux b/files/tmux new file mode 100644 index 0000000..473f540 --- /dev/null +++ b/files/tmux @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ "$#" -eq 0 ]; then + exec /usr/bin/tmux new-session -A -s main +else + exec /usr/bin/tmux "$@" +fi \ No newline at end of file diff --git a/files/tmux.conf b/files/tmux.conf new file mode 100644 index 0000000..a945077 --- /dev/null +++ b/files/tmux.conf @@ -0,0 +1,73 @@ +##### Terminal ##### + +set -g default-terminal "tmux-256color" +set -ga terminal-overrides ",*:Tc" + +##### General ##### + +set -g mouse on +set -g history-limit 100000 +set -g escape-time 0 +set -g focus-events on + +# Always use bash interactively +set-option -g default-shell /bin/bash +set-option -g default-command "bash -i" + +##### Status Bar ##### + +set -g status-position bottom +set -g status-interval 5 +set -g status-justify left + +# Deep indigo background +set -g status-style "bg=#24283b,fg=#c0caf5" + +# Remove default spacing +set -g status-left-length 40 +set -g status-right-length 100 + +# Left +set -g status-left "#[fg=#bb9af7,bold] #S #[fg=#565f89]|" + +# Right +set -g status-right "#[fg=#7aa2f7]%H:%M #[fg=#565f89]| #[fg=#9ece6a]%Y-%m-%d " + +##### Windows ##### + +set -g window-status-format " #I:#W " +set -g window-status-style "fg=#565f89,bg=#24283b" + +set -g window-status-current-format " #I:#W " +set -g window-status-current-style "fg=#1a1b26,bg=#bb9af7,bold" + +set -g window-status-activity-style "fg=#e0af68,bold" + +##### Pane Borders ##### + +set -g pane-border-style "fg=#414868" +set -g pane-active-border-style "fg=#bb9af7" + +##### Messages ##### + +set -g message-style "fg=#1a1b26,bg=#bb9af7" +set -g message-command-style "fg=#1a1b26,bg=#7aa2f7" + +##### Copy Mode ##### + +set -g mode-style "fg=#1a1b26,bg=#7dcfff" + +##### Clock ##### + +set -g clock-mode-colour "#bb9af7" + +##### Pane Numbers ##### + +set -g display-panes-active-colour "#bb9af7" +set -g display-panes-colour "#565f89" + +##### Bell ##### + +set -g visual-bell off +set -g bell-action none + diff --git a/index.html b/index.html new file mode 100644 index 0000000..118b331 --- /dev/null +++ b/index.html @@ -0,0 +1,544 @@ + + + + + +claudaris + + + +
+
+ +
+

CLAUDARIS

+
+ /klaw-DAR-iss/ — a moveable docker container for + Claude Code. using Arch btw. +
+
+ ARCHLINUX + TMUX + SELF-HOSTED + NO-RESTART-POLICY +
+
+ + + +
+

Configure

+

+ Run the interactive wizard — press enter at any prompt to keep the + current/default value: +

+
./claudaris config
+

+ It writes your answers to .env (gitignored — every user + keeps their own), which every other command sources. Set + NAME to something unique to you (e.g. chris-claude) + if more than one person is running a container from this same repo + checkout. Re-run config / configure any time + to update it. +

+ + + + + + + + + + + + + + + + + +
variablepurposedefault
NAMEImage name, container name, and hostname inside the container (shows up as root@$NAME in the prompt).claudaris
DATA_DIRHost directory for volume mount data — bashrc, aliases, Claude auth, ssh keys./data/$NAME
WORKSPACE_DIRHost directory mounted as /projects inside the container./home/$USER/projects
+
+ +
+

Commands

+

Everything runs through the single claudaris entry point.

+ +
+
+
configconfigure
+
Interactive wizard — writes NAME, DATA_DIR, WORKSPACE_DIR to .env.
+
./claudaris config
+
+
+
build
+
Builds the image (always --no-cache --pull, so it picks up the latest Claude Code release).
+
sudo ./claudaris build
+
+
+
start
+
Creates or restarts the container with all volumes wired up.
+
sudo ./claudaris start
+
+
+
connect
+
Starts the container if needed, then attaches your tmux session.
+
sudo ./claudaris connect
+
+
+
removestop · rm
+
Stops and removes the container so the next start recreates it fresh.
+
sudo ./claudaris remove
+
+
+
+ +
+

Optional: SSH access to other nodes

+

+ To let the container SSH out to other machines, create + $DATA_DIR/ssh on the host and populate it with keys/config + before running ./claudaris start. If present, it's + bind-mounted read-only to /root/.ssh. +

+
off by default — nothing is mounted unless the directory exists.
+
+ +
+

Internals

+ +

Persistent login. + Claude Code is installed at build time under /root, so a + rebuild always picks up the latest release. Two files are bind-mounted + individually to survive that: ~/.claude/.credentials.json + (the OAuth token) and ~/.claude.json (account/onboarding + state — Claude Code checks this too, so persisting the token alone + isn't enough to avoid a re-login prompt after a rebuild). + ./claudaris start seeds both from empty on the host the + first time it runs. +

+ +

Self-stopping. + The container stops itself once its tmux session ends (any shell exit + — Ctrl+D, exit, crash) instead of running forever, and + there's no Docker restart policy — connect and + start both bring it back up automatically if you find it + stopped. +

+ +

Dotfiles. + .bashrc and bash_aliases are both bind-mounted + as single files so they can be tweaked per-host (e.g. a coworker + importing their own aliases) without a rebuild. + ./claudaris start seeds them from files/bashrc + and files/bash_aliases the first time it runs, without + overwriting later edits. Anything that needs to survive regardless + (PATH, zoxide, git identity) lives in /etc/... inside the + image instead of under /root. +

+ +

Hostname. + The container is started with --hostname "$NAME", so the + prompt reads root@claudaris (or whatever you set + NAME to) instead of a random container ID. +

+
+ +
+

Repo layout

+
    +
  • claudaris — the entry point: build / start / connect / remove / help.
  • +
  • Dockerfile — Arch base image, dotfiles, Claude Code install, entrypoint.
  • +
  • .env — written by claudaris config, per-user, gitignored.
  • +
  • files/bashrc, bash_aliases, entrypoint.sh, tmux wrapper, tmux.conf.
  • +
  • AGENTS.md — deep-dive notes for anyone (human or agent) hacking on this repo.
  • +
+
+ + + +
+ +