Run Claude Code on a server, VPS, or homelab — then connect via SSH, web browser, Mosh, or Tailscale VPN from wherever you are. Your files, credentials, MCP servers, dotfiles, and Docker images all persist across restarts.
Think of it as your personal cloud dev machine with Claude Code built in.
A mobile-friendly web interface for Claude Code — start sessions, chat with Claude, browse files, and run shell commands, all from any browser.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Highlights:
- Session history — all past Claude sessions are listed in the sidebar, grouped by date, searchable by prompt. Pick up any conversation where you left off.
- Rich message rendering — responses use full Markdown with syntax-highlighted code blocks, bold/italic, numbered lists, and inline code.
- Tool call cards — every
Bash,Read,Edit,Write, and other tool call is shown as a collapsible card with the command and output. Groups of consecutive calls from the same tool are collapsed into a single summary. - Syntax-highlighted diffs —
WriteandEdittool results render as side-by-side file diffs with language-aware highlighting. - Subagent cards — when Claude spawns a subagent, the nested session appears inline as a collapsible card.
- Live thinking indicator — animated spinner shows extended thinking in progress; reasoning text streams in real time.
- Embedded terminal — full xterm.js terminal in the browser, connected to a real PTY inside the container via WebSocket. No SSH client needed.
- Slash commands — type
/in the composer for autocomplete of common Claude Code slash commands. - Tool approval UI — when Claude requests a permission (e.g. running a command), an inline approval card appears with Accept/Reject buttons. No need to watch the terminal.
- Git status bar — the bottom of each chat shows the current branch and number of changed files.
- Model & effort picker — switch between Haiku, Sonnet, and Opus per-session, or set a default in Settings. Effort level (Low → Max) is configurable; Max is Opus-only.
- Dark/light theme — toggle in Settings; preference is persisted locally.
- Mobile-friendly — responsive layout with a bottom nav bar and swipe gestures on small screens.
- Interactive API docs —
/api/docsserves a Scalar UI with the full OpenAPI 3.1 spec. Try endpoints directly in the browser.
Unlike ephemeral sandboxes (like Docker Sandboxes) that spin up for a single task and disappear, Hatchpod is a long-lived workstation.
| Ephemeral Sandboxes | Hatchpod | |
|---|---|---|
| Lifecycle | Task-scoped, disposable | Persistent — pick up where you left off |
| Access | Local only | SSH, Mosh, Web UI, Tailscale VPN |
| Customization | Pre-set image | Full Linux env with sudo, dotfiles, any tooling |
| Docker-in-Docker | Limited or none | Full DinD via Sysbox |
| Requires | Docker Desktop | Any Linux host with Docker Engine |
# 1. Clone and configure
git clone https://github.com/andresmorales07/hatchpod.git
cd hatchpod
cp .env.example .env # edit .env to set your passwords
# 2. Start (pulls the prebuilt image — no build step needed)
docker compose up -d
# 3. Connect
ssh -p 2222 hatchpod@localhost # password is SSH_PASSWORD from .env
# 4. Authenticate Claude Code (first time only)
claude # follow the login link that appearsNo Sysbox? The default
docker-compose.ymlsetsruntime: sysbox-runcfor Docker-in-Docker. If you don't have Sysbox installed, create a one-line override:echo 'services: { hatchpod: { runtime: runc } }' > docker-compose.override.yml docker compose up -dEverything except
dockercommands inside the container will work without Sysbox.
| Category | Software | Purpose |
| 🤖 AI | Claude Code | Anthropic's CLI agent |
| Web UI + API | Claude Code web interface (Tailwind CSS v4 + shadcn/ui) and REST/WebSocket API (port 8080) | |
| ⚡ Runtimes | Node.js 20 LTS | MCP servers (npx) |
| Python 3 + venv | MCP servers (uvx) | |
| 📦 Package Mgrs | npm | Node packages (global prefix persisted) |
| uv / uvx | Python packages and tool runner | |
| 🐳 Containers | Docker Engine + Compose | Docker-in-Docker (requires Sysbox on host) |
| 🌐 Access | OpenSSH server | Remote access (port 2222) |
| mosh | Resilient mobile shell (UDP 60000-60003) | |
| Tailscale | VPN access (opt-in, set TS_AUTHKEY) | |
| 🔧 Dev Tools | git | Version control |
| GitHub CLI (gh) | GitHub operations | |
| curl, jq | HTTP requests and JSON processing | |
| make, g++ | C++ build tools for native Node.js addons (node-pty) | |
| 🖥️ System | s6-overlay v3 | Process supervision |
| sudo (passwordless) | Root access for hatchpod user |
Connect from any machine — all access methods work both locally and remotely.
ssh -p 2222 hatchpod@localhostUse your SSH_PASSWORD to authenticate, or add your public key:
ssh-copy-id -p 2222 hatchpod@localhostMobile-friendly web interface for Claude Code. Works on phones, tablets, and desktops.
# Web UI
open http://localhost:8080
# API docs (interactive, no auth required)
open http://localhost:8080/api/docs
# REST API
curl -H "Authorization: Bearer $API_PASSWORD" http://localhost:8080/api/sessions
# Create a session
curl -X POST -H "Authorization: Bearer $API_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"prompt":"What files are in the workspace?"}' \
http://localhost:8080/api/sessions
# OpenAPI spec
curl http://localhost:8080/api/openapi.jsonAPI endpoints: GET /healthz, GET /api/openapi.json, GET /api/docs, POST /api/sessions, GET /api/sessions, GET /api/sessions/:id, DELETE /api/sessions/:id, GET /api/sessions/:id/history, GET /api/sessions/:id/messages, GET /api/browse, GET /api/config, GET /api/providers, GET /api/git/status, GET /api/settings, PATCH /api/settings. Chat streaming at WS /api/sessions/:id/stream; embedded terminal at WS /api/terminal/stream.
Resilient connection that survives WiFi switches, VPN reconnects, and laptop sleep/wake:
mosh --ssh='ssh -p 2222' hatchpod@localhostConnect from anywhere without exposing ports publicly. Set TS_AUTHKEY in your .env:
- Generate an auth key at Tailscale Admin → Settings → Keys
- Add to
.env:TS_AUTHKEY=tskey-auth-xxxxx - Restart:
make down && make up - Connect via your Tailscale IP:
ssh -p 2222 hatchpod@<tailscale-ip>
Networking mode: The container auto-detects TUN device availability at startup:
- Kernel TUN mode (default with
docker-compose.yml): Transparent routing — all apps can reach Tailscale peers without any proxy configuration. Requirescap_add: NET_ADMINand/dev/net/tundevice (both provided indocker-compose.yml). - Userspace fallback (no TUN device): Apps must use the SOCKS5 proxy at
localhost:1055explicitly. ATAILSCALE_PROXYvariable is written to/etc/profile.d/tailscale-proxy.shfor convenience, but is not exported to avoid breaking general internet connectivity.
| Variable | Description | Default |
|---|---|---|
SSH_PASSWORD |
SSH password for hatchpod user |
changeme |
API_PASSWORD |
API server + Web UI password | changeme |
TS_AUTHKEY |
Tailscale auth key (enables VPN) | (disabled) |
TS_HOSTNAME |
Tailscale node name | hatchpod |
DOTFILES_REPO |
Git URL for dotfiles repo | (disabled) |
DOTFILES_BRANCH |
Branch to checkout | (default) |
Hatchpod uses the interactive login flow. Run claude inside the container and follow the login link. Credentials are stored in ~/.claude/ which is backed by the home Docker volume, so they persist across restarts.
MCP servers configured inside the container persist across restarts:
ssh -p 2222 hatchpod@localhost
claude mcp add my-server -- npx some-mcp-serverSet DOTFILES_REPO in your .env to automatically clone and install dotfiles on first boot:
DOTFILES_REPO=https://github.com/youruser/dotfiles.git
On first boot, the repo is cloned to ~/dotfiles. If an install script (install.sh, setup.sh, or bootstrap.sh) is found, it runs automatically. Otherwise, if a Makefile is present, make is run.
| Volume | Container Path | Purpose |
|---|---|---|
home |
/home/hatchpod |
Claude config, workspace, dotfiles, npm globals |
docker-data |
/var/lib/docker |
Docker images, containers, layers |
Hatchpod includes Docker Engine inside the container. With Sysbox installed on the host, agents can build and run Docker containers securely without --privileged.
# Verify DinD works
make docker-test
# Use Docker inside the container
make shell
docker run --rm alpine echo "Hello from nested container"
docker build -t myapp .The docker-data volume persists pulled images and build cache across container restarts.
┌──────────────────────────────────────────────────────────┐
│ hatchpod container (sysbox-runc) │
│ │
│ ┌────────┐ ┌─────────┐ ┌──────┐ ┌──────────┐ │
│ │ api │ │ sshd │ │dockerd│ │tailscaled│ │
│ │ :8080 │ │ :2222 │ │ DinD │ │(opt-in) │ │
│ └────┬───┘ └────┬────┘ └───┬──┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌────┴───────────┴───────────┴───────────┘ │
│ │ Provider Abstraction Layer (NormalizedMessage) │
│ │ └─ ClaudeAdapter → Claude Code CLI │
│ └───────────────────────────────────────────────────────┘│
│ Node.js 20 · Python 3 · uv/uvx (MCP) │
│ │
│ Volumes: │
│ /home/hatchpod → home vol │
│ /var/lib/docker → docker-data vol │
└──────────────────────────────────────────────────────────┘
Process supervision by s6-overlay.
| Target | Description |
|---|---|
make build |
Build the Docker image |
make up |
Start the container |
make down |
Stop the container |
make logs |
Follow container logs |
make shell |
Open a shell in the container |
make ssh |
SSH into the container |
make mosh |
Connect via mosh |
make clean |
Stop container, remove volumes and image |
make docker-test |
Run hello-world inside the container (DinD smoke test) |
If you're upgrading from an earlier version named "claude-box", there are three breaking changes:
1. Volume name changed (claude-home → home). Migrate your data before starting:
# Stop the old container
docker compose down
# Create the new volume and copy data
docker volume create hatchpod_home
docker run --rm \
-v claude-box_claude-home:/from \
-v hatchpod_home:/to \
alpine sh -c "cp -a /from/. /to/"2. Linux user changed (claude → hatchpod). The migration above copies the files, but internal paths shift from /home/claude/ to /home/hatchpod/. The container's init script automatically fixes ownership on boot.
3. Env var renamed (CLAUDE_USER_PASSWORD → SSH_PASSWORD). Update your .env file. The old name still works temporarily but prints a deprecation warning.
- Change all default passwords in
.envbefore exposing to a network - The
.envfile is excluded from git via.gitignore - SSH root login is disabled
- For remote access, use SSH tunneling or put behind a reverse proxy with TLS
- The
hatchpoduser has passwordless sudo inside the container
# Backup
docker run --rm -v hatchpod_home:/data -v $(pwd):/backup alpine \
tar czf /backup/home-backup.tar.gz -C /data .
# Restore
docker run --rm -v hatchpod_home:/data -v $(pwd):/backup alpine \
tar xzf /backup/home-backup.tar.gz -C /dataBuilt with s6-overlay · Sysbox





