AI Coding-Agent Dev Environment — Setup Runbook
Date: 2026-05-22 Machine: macOS 26.3.1 (build 25D2128), Apple Silicon (arm64) Decision: ADR-022 — AI coding-agent orchestration & terminal dev environment
Terminal-native stack for running Claude Code (planner/reviewer) + Codex (primary implementer) together: Ghostty → tmux → Agent of Empires (AoE) → lazygit + git-delta. This records what was actually installed and configured on the date above, with the real versions, so it can be rebuilt on a fresh machine.
Two install paths since ADR-023:
- Per-client (Mac) — sections 1–6 below + Redeploy a Mac. Ghostty, theming, the local terminal feel.
- Persistent host (homelab
agentsLXC) — section 7. The actual always-on runtime; makes the "from anywhere" access tiers work. This is the recommended target.
Installed versions (as built, 2026-05-22)
| Tool | Version | Source |
|---|---|---|
| Ghostty | 1.3.1 | brew install --cask ghostty |
| tmux | 3.6b | already present (skipped) |
Agent of Empires (aoe) |
1.8.0 | brew install aoe |
| lazygit | 0.61.1 | brew install lazygit |
git-delta (delta) |
0.19.2 | brew install git-delta |
difftastic (difft) |
0.69.0 | brew install difftastic |
| Docker Desktop | 4.74.0 | brew install --cask docker — pending (see note) |
Claude Code (claude) |
2.1.148 | npm @anthropic-ai/claude-code |
Codex (codex) |
0.133.0 | npm @openai/codex |
| git | 2.54.0 | — |
Docker note:
brew install --cask dockerneedssudo(to create/usr/local/cli-plugins) and cannot run unattended. Run it yourself in a real terminal; then launch Docker Desktop once before using sandboxed agents.
1. Install the stack (Homebrew)
# Terminal + review tools + orchestrator (tmux was already installed → skipped)
brew install lazygit git-delta difftastic aoe
brew install --cask ghostty
# Docker Desktop — run in a real terminal; it will prompt for your password
brew install --cask docker
2. Agent CLIs (already installed & logged in)
# Claude Code
npm install -g @anthropic-ai/claude-code
claude # then run /login inside the TUI
# Codex
npm install -g @openai/codex
codex login
Both were already present and authenticated on the build machine
(~/.claude.json has an oauthAccount; ~/.codex/auth.json exists).
3. Configure git-delta (pretty diffs everywhere)
The config lives version-controlled in this repo at
dev-env/delta.gitconfig; the live copy is included
from a stable path so it survives deleting ~/Downloads.
# from the homelab-docs repo root:
mkdir -p ~/.config/git
cp docs/workstation/dev-env/delta.gitconfig ~/.config/git/delta.gitconfig
printf '\n[include]\n\tpath = ~/.config/git/delta.gitconfig\n' >> ~/.gitconfig
# verify:
git config --get core.pager # -> delta
On-disk location: ~/.config/git/delta.gitconfig, referenced by an
[include] line in ~/.gitconfig.
4. Configure lazygit
Version-controlled at dev-env/lazygit-config.yml.
# from the homelab-docs repo root:
mkdir -p "$(lazygit --print-config-dir)"
cp docs/workstation/dev-env/lazygit-config.yml "$(lazygit --print-config-dir)/config.yml"
On-disk location (this machine): ~/Library/Application Support/lazygit/config.yml
(lazygit --print-config-dir is authoritative — on Linux it's ~/.config/lazygit).
5. First run with AoE (on the ready repo)
Prereqs: tmux installed (✓), and Docker Desktop running if you use
--sandbox.
cd /Users/gabriel/git/ready
aoe # launch the TUI
aoe add --cmd claude # Claude Code session (planner/reviewer)
aoe add --sandbox --cmd codex # Codex session, sandboxed in Docker (primary coder)
aoe serve # optional: web dashboard for phone access
TUI keys: n new session · Enter attach · t toggle terminal · D diff view ·
d delete · ? help. Ctrl+b d detaches an agent and returns to AoE. Per-repo
settings/hooks live in .aoe/config.toml.
6. Review loop
While agents work, use lazygit (run lazygit in the repo, or D in AoE) to read
each agent's diff with delta highlighting, then merge the worktree branch when
satisfied — the Claude-plans / Codex-codes / you-and-Claude-review loop, fully in
the terminal.
7. Homelab persistent install (ADR-023)
On 2026-05-28 the agent stack moved off the Mac into a dedicated Proxmox LXC
(agents, LXC 135) per ADR-023.
The Mac sections above remain for per-client polish (Ghostty, theming); the
homelab install is what makes the "from anywhere" access tiers actually work.
Source of truth: chizuru-v2/ansible/playbooks/agents.yml. The
post-install interactive steps live in that playbook's header comment so they
travel with the IaC.
Deploy / re-deploy
# From the chizuru-v2 repo on your workstation:
ansible-playbook -i ansible/inventory/hosts.yml ansible/playbooks/agents.yml
Idempotent — re-run any time. It (re-)installs nested Docker, Node 20, pinned
npm globals (@anthropic-ai/claude-code, @openai/codex), version-pinned
AoE / lazygit / difftastic release tarballs, the claude non-root service
user, a git credential helper (no Vault tokens embedded in repo URLs), repo
clones (holo/homelab-docs, senior-discount/ready), and the two systemd
units in services/agents/.
The claude-remote-control.service unit ships disabled — auth must be
seated interactively first; enabling pre-auth would crash-loop the daemon on
reboot.
Post-install (one-time, interactive)
These seven steps seat everything the systemd daemon needs before it can serve a usable Remote Control session. Each step's symptom-when-skipped is in lessons-learned 2026-06-16; the short version is all of them are required, and the daemon's failure mode for missing steps 4–6 is "browser session works but /mcp is empty" — i.e. silent.
# Reach the host. From outside the LAN, use the netbird hostname:
ssh chizuru # or: ssh [email protected] on LAN
su - claude
tmux new -s login # so we survive ssh hiccups
# ──────────────────────────────────────────────────────────────────────
# 1. Seat Claude CLI credentials (writes ~/.claude/.credentials.json).
# Use the auth subcommand, NOT the TUI's /login — the TUI's wizard
# triggers a workspace-trust + remote-control re-flow we'd rather
# handle declaratively below.
# ──────────────────────────────────────────────────────────────────────
claude auth login --claudeai
# follow the printed URL on your Mac, paste the returned code back
claude auth status # expect loggedIn: true
claude --print "ping" # expect "OK"-ish reply
# (do NOT pass --bare —
# --bare disables OAuth reads
# and produces a false negative)
# ──────────────────────────────────────────────────────────────────────
# 2. Seat Codex credentials (writes ~/.codex/auth.json).
# --device-auth is hidden from --help but is the headless path; codex
# will print the URL+code, accept it on your Mac.
# ──────────────────────────────────────────────────────────────────────
codex login --device-auth
# ──────────────────────────────────────────────────────────────────────
# 3. Confirm the ready repo is cloned at the path the daemon expects.
# (The playbook clones it for you; this is a sanity check.)
# ──────────────────────────────────────────────────────────────────────
test -d /home/claude/git/ready/.git && echo OK || echo MISSING
# ──────────────────────────────────────────────────────────────────────
# 4. Seat workspace trust for /home/claude/git/ready.
# Skips the "Do you trust this workspace?" prompt that would
# otherwise block the daemon on first start.
# ──────────────────────────────────────────────────────────────────────
jq '.projects["/home/claude/git/ready"].hasTrustDialogAccepted = true' \
~/.claude.json > ~/.claude.json.new && mv ~/.claude.json.new ~/.claude.json
# ──────────────────────────────────────────────────────────────────────
# 5. Allow-list the project's .mcp.json servers (this is required —
# .mcp.json alone does NOT auto-enable). Otherwise /mcp shows the
# codex tools as missing even though .mcp.json declares them.
# ──────────────────────────────────────────────────────────────────────
jq '.projects["/home/claude/git/ready"].enabledMcpjsonServers = ["codex"]' \
~/.claude.json > ~/.claude.json.new && mv ~/.claude.json.new ~/.claude.json
# ──────────────────────────────────────────────────────────────────────
# 6. Accept the per-machine "Enable Remote Control?" dialog once.
# Without this, the daemon exits with the misleading error
# "Remote Control is not yet enabled for your account".
# ──────────────────────────────────────────────────────────────────────
echo y | claude remote-control --name ready --spawn=worktree &
sleep 5
pkill -x claude # foreground only; daemon starts via systemd
# Confirm remoteDialogSeen now sticks:
jq -r '.remoteDialogSeen' ~/.claude.json # expect: true
# ──────────────────────────────────────────────────────────────────────
# 7. Bring the systemd unit up and verify.
# ──────────────────────────────────────────────────────────────────────
exit # back to root
systemctl enable --now claude-remote-control
systemctl status claude-remote-control # expect active (running)
journalctl -u claude-remote-control -n 30 --no-pager # expect "Remote Control session started"
After step 7, the ready environment will show up on claude.ai/code under its name (not as an env_… ID — the ID rotates each daemon restart, the name is the stable handle). Open it, ask Claude something, then run /mcp to confirm mcp__codex__codex and mcp__codex__codex-reply are listed — that's the canary for the ReadWritePaths fix from lessons-learned 2026-06-16.
Access tiers (per ADR-023)
| Device | Network | Access path | Personal creds on device? |
|---|---|---|---|
| Personal Mac at home | Home LAN or Netbird | SSH → tmux → AoE on agents |
yes |
| iPhone / iPad | Cellular or wifi | Blink Shell + Netbird + mosh → tmux → AoE; or claude.ai/code via Safari / Claude mobile app |
yes |
| Work-issued laptop | Corp VPN | Browser only at claude.ai/code. No Netbird, no SSH keys, no install. |
NO |
| Borrowed device | Any | Browser to claude.ai/code |
NO |
When to re-deploy
- Tool version bump — edit
claude_code_version/codex_version/aoe_version/lazygit_version/difftastic_versionvars inagents.yml, push, re-run. - Service hardening or environment change — edit
services/agents/claude-remote-control.service.j2, re-run. - New repo to clone — add to
forgejo_reposinagents.yml, re-run.
Operations
- Logs:
journalctl -u claude-remote-control -fon the LXC, or via Alloy → Loki → Grafana withalloy_job_name: agents. - Failure alerts:
OnFailure=ntfy-alert@%n.servicePOSTs tohttps://ntfy.eva-00.network/homelab-alertsafterStartLimitBurst=5failures withinStartLimitIntervalSec=15min. - Restart:
systemctl restart claude-remote-control— issues a freshenvironment=URL onclaude.ai/code; old in-flight browser sessions detach. The Glance link is intentionally the env-list page (https://claude.ai/code), not a per-session URL.
Caveats pinned by ADR-023
- Never set
ANTHROPIC_API_KEY,DISABLE_TELEMETRY, orCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICon the unit'sEnvironment=— any of them breaks Remote Control eligibility silently. - Do not copy
~/.claude.jsonfrom the Mac. Linux Claude Code stores credentials at~/.claude/.credentials.json; the macOS file is not the credential. - Treat
~/.codex/auth.jsonas a password-equivalent, never as routine IaC state. Rebuild path isagents.yml+ interactive device-code login, not credential restore.
Redeploy the Mac dev env on a fresh machine
# 0. Homebrew (if missing): https://brew.sh
# 1. Stack
brew install lazygit git-delta difftastic aoe
brew install --cask ghostty
brew install --cask docker # run in a real terminal (needs sudo); launch it once
# 2. Agent CLIs + login
npm install -g @anthropic-ai/claude-code @openai/codex
claude # /login
codex login
# 3. Configs from this repo (clone homelab-docs first, then from its root)
mkdir -p ~/.config/git
cp docs/workstation/dev-env/delta.gitconfig ~/.config/git/delta.gitconfig
printf '\n[include]\n\tpath = ~/.config/git/delta.gitconfig\n' >> ~/.gitconfig
git config --get core.pager # -> delta
mkdir -p "$(lazygit --print-config-dir)"
cp docs/workstation/dev-env/lazygit-config.yml "$(lazygit --print-config-dir)/config.yml"
# 4. Work
cd /path/to/ready
aoe
aoe add --cmd claude
aoe add --sandbox --cmd codex