A native, multi-device client for Claude Code — talk to your agent from your phone, tablet, or laptop, over your own private network.
Quick start · Architecture · How it works · Repo layout · Docs
Anvil lets you run Claude Code on your dev machine and drive it from anywhere — your
phone on the couch, a tablet, or another laptop. A small daemon (anvild) supervises your
Claude Code sessions and streams them as structured data — markdown, tool calls, diffs,
git state — to thin native clients over Tailscale. Nothing is
public; your machine and your devices talk directly over your private tailnet.
It's the kind of thing you reach for when you want to kick off a task on your workstation, put your phone in your pocket, and get pulled back by a push notification when Claude finishes — or needs your permission for something risky.
Note
Anvil started life as a Zellij-in-a-WebView app and was rebuilt from the ground up.
The old approach scraped a terminal grid; the current one spawns the real
claude CLI per turn in
stream-json mode and forwards typed events.
See Why a rebuild? and docs/plans/anvil-native-architecture.md.
(This fork replaced the Agent SDK transport with the CLI itself — design:
docs/plans/2026-08-13-cc-cli-transport-design.md.)
- 💬 Conversation, not a terminal grid — reflowable markdown, syntax-highlighted code, tables, math, and mermaid diagrams, rendered once on the daemon and shown identically on every device. Proportional fonts, real text inputs (Shift+Enter is a newline).
- 📱 Multi-device, no shared viewport — pick up your phone mid-conversation and it reconciles to exactly where your laptop left off. No "disconnect the other client" dance.
- 🌳 Worktree-per-session — each task can spin up its own git worktree off a base branch. Branch, diffstat, and git lifecycle (commit / push / PR / merge) are first-class.
- 🛡️ Your Claude Code's permissions, on your phone — Anvil doesn't second-guess the
CLI's permission engine; it is the prompt surface for it. Your
~/.claudesettings decide what runs silently, and anything that would have prompted a terminal becomes a native dialog answerable from any device (first answer wins, and it waits indefinitely). Per-session permission mode is Claude Code's own —default·acceptEdits·plan·bypassPermissions— and you pick a model per session (opus·sonnet·haiku·fable). Claude's multiple-choice questions ride the same channel. - 🤖 Autopilot — connect a Todoist project and Anvil bundles your tasks into units of work, writes an implementation plan for each (optionally red-teamed by a panel of independent models), and can run overnight on a schedule — kicking off build sessions and filing a run report back to your journal (lapo / Logseq).
- ⚔️ Adversarial dev pipeline — an opt-in, fully unattended path where two decorrelated models (Claude for design/judgment, GLM for agentic work) take a task through a requirements → design → implement → verify → validate gauntlet and open a PR, with a Design History File as the PR body.
- 🔔 Push when it matters — a notification when a session needs a decision or a turn completes. Web Push today; FCM (Android) / APNs (Apple) for native shells.
- 🖥️ A real terminal when you need one — a persistent, server-side PTY per session, with durable scrollback across device switches.
- 📄 Live markdown reader — open a doc and chat about it side-by-side; it re-renders in place as Claude edits it, with select-to-cite back into the conversation.
- 📎 Attachments & deliverables — drop images, PDFs, and files into a turn; generated reports/archives/media come back as download cards (with best-effort Tailscale Taildrop).
- 🧩 Reusable prompts & skills — a device-synced prompt library in the composer, plus
/-autocomplete for your Claude Code user/project skills. - 🔗 Fleet-ready — one client can manage
anvildacross several machines on one Max plan, with a persistent concierge session that can see and spin up work across the fleet. Macs and headless Linux boxes can join: a machine with no login boots into a setup screen in its own web UI, shows a 6-digit code, and the hub pushes the fleet's credentials over the tailnet.
You need a machine with Bun ≥ 1.3.14, a Claude Max subscription, and Tailscale on every device you want to drive from.
# 1. Install dependencies
cd anvild
bun install
# 2. Authenticate with your Claude subscription (one-time).
# This uses the subscription pool — NOT a metered API key (see "Auth & billing" below).
# On first start this token is migrated into the account roster as "default"; from then on you
# manage logins in Settings → Models, and can add more than one (see "Multiple accounts").
export CLAUDE_CODE_OAUTH_TOKEN="$(claude setup-token)"
# You don't need to install Claude Code separately: if there's no `claude` on your PATH,
# the daemon downloads and smoke-tests one on the first turn and manages it from then on
# (Settings → your server → Claude Code). If you already have one, it uses that.
# 3. Run the daemon — serves the web client + WebSocket API on :7701
bun run start
# → open http://localhost:7701Then expose it to your other devices over Tailscale:
tailscale serve --bg --https=443 http://localhost:7701
# → open https://<your-magicdns-host>/ from your phone or tabletFor an always-on install (macOS LaunchAgent that restarts on crash and at login), and a non-technical, terminal-free setup, see Running it for real.
Important
Anvil bills exactly the way your terminal's claude bills. It spawns the CLI, so a turn
costs whatever that CLI is authenticated as — normally your subscription's OAuth token, drawn
from the subscription pool. Anvil never calls the metered Messages API on its own.
The flip side: ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN outrank the OAuth token, and a
stray one silently switches every turn to metered pay-per-token billing. The daemon no
longer refuses to start over them (this fork defers config authority to Claude Code), so
that check is yours to keep. What Anvil still does: spawn turns under an allow-list env that
never forwards a key it wasn't handed, and unset both variables in the launcher
service.sh writes. Full rationale:
anvil-native-architecture.md §3 and
2026-08-13-cc-cli-transport-design.md §4.3.
A single daemon on your dev box hosts Claude Code and forwards structured events; thin native shells render them. Everything between them rides your private tailnet.
flowchart TB
subgraph dev["🖥️ Your dev machine"]
direction TB
anvild["<b>anvild</b> — the daemon<br/>session supervisor · CC stream-json<br/>event log · git/worktree · render pipeline<br/>permissions · budget · push · CC installs"]
cc["claude CLI<br/>(one process per turn)"]
wt["git worktrees<br/>(one per session)"]
anvild <-->|"drives"| cc
anvild <-->|"owns"| wt
end
subgraph cloud["☁️ Push (optional)"]
push["FCM · APNs · Web Push"]
end
anvild -.->|"alerts"| push
subgraph clients["📱 Your devices"]
direction LR
web["Web client<br/>(browser)"]
android["Android app<br/>(WebView shell)"]
apple["Apple app<br/>(SwiftUI shell)"]
end
anvild <==>|"WebSocket + REST<br/><b>Tailscale</b> (MagicDNS · ACLs)"| clients
push -.->|"wake"| clients
classDef daemon fill:#D39450,stroke:#2F2739,color:#2F2739;
classDef box fill:#635F6A,stroke:#2F2739,color:#F0F6FC;
class anvild daemon;
class web,android,apple,cc,wt,push box;
anvild replaces both the old Python status server and Zellij — it owns the session
lifecycle end-to-end, so there are no sockets to negotiate or husk processes to reap.
You create a session by pointing it at an existing directory (a quick poke) or asking for a fresh git worktree off a base branch (an isolated task). The daemon spawns a supervised Claude Code process in its own process group, records the session, and persists an append-only event log that is the source of truth for replay.
stateDiagram-v2
[*] --> idle: session.create
idle --> thinking: prompt.send
thinking --> running_tool: tool_use
running_tool --> thinking: tool_result
thinking --> awaiting_permission: CC would prompt
running_tool --> awaiting_permission: CC would prompt
awaiting_permission --> running_tool: permission.respond (allow)
awaiting_permission --> thinking: permission.respond (deny)
thinking --> idle: result (turn complete)
running_tool --> error: crash
idle --> exited: session.kill
error --> exited: reap
exited --> [*]
Every server→client event carries a per-session monotonic seq, which is the backbone of
resume: a client persists the highest seq it has rendered and, on reconnect, asks the
daemon to replay everything newer (or sends a full snapshot if the client is too far behind).
sequenceDiagram
participant U as You (any device)
participant D as anvild
participant C as claude CLI (stream-json)
U->>D: prompt.send { text }
D->>C: drive turn
C-->>D: assistant deltas (streaming)
D-->>U: assistant.delta … (seq++)
C->>D: tool_use (e.g. Edit)
D->>D: autonomy policy + danger-list
alt risky op
D-->>U: permission.request + 📲 push
U->>D: permission.respond { allow }
end
D->>C: run tool
C-->>D: tool_result
D-->>U: tool.use / tool.result (seq++)
C->>D: turn complete
D-->>U: result + budget + 📲 push
Markdown (chat bubbles and the reader pane) is rendered once, in the daemon, and displayed in a scoped, read-only WebView in the native shells:
flowchart LR
md["Markdown"] --> mdit["markdown-it<br/>(+ data-line attrs)"]
mdit --> shiki["Shiki<br/>syntax highlight"]
shiki --> katex["KaTeX<br/>math → HTML"]
katex --> purify["DOMPurify<br/>sanitize"]
purify --> html["Safe HTML"]
html --> wv["WebView"]
mermaid["mermaid.js<br/>(strict CSP)"] --> wv
classDef step fill:#635F6A,stroke:#2F2739,color:#F0F6FC;
class mdit,shiki,katex,purify,mermaid step;
classDef out fill:#D39450,stroke:#2F2739,color:#2F2739;
class html,wv out;
This buys one rendering pipeline across web/Android/Apple (mermaid, math, and
select-to-cite all work) instead of maintaining divergent native renderers. The native
shells still own everything else: navigation, layout, lists, input, terminal, and file tree.
Full reasoning in anvil-native-architecture.md §8.3.
| Component | Path | Stack | What it is |
|---|---|---|---|
| Daemon | anvild/ |
TypeScript · Bun | Session supervisor, CC stream-json transport, managed CC installs, event log, git/worktree ops, permissions, budget, render pipeline, push, autopilot + adversarial pipeline, and the Todoist/lapo/OpenRouter integrations. The keystone. |
| Web client | anvild/web/ |
Vanilla TS | The daily-driver UI and the reusable render surface, served by the daemon at /. Also bundled into the native shells. |
| Android app | app/ |
Kotlin | A WebView shell hosting the web client over Tailscale + native FCM push, ADB-over-Tailscale, offline app-shell. com.gte619n.anvil. |
| Apple app | apple/ |
SwiftUI · WKWebView | macOS-first hybrid shell (same model as Android); iOS + APNs gated on an Apple Developer account. |
| Build & release scripts | scripts/ |
Bash · TS | CI release notes + Apple Developer ID signing. |
The daemon is built to run unattended on a macOS LaunchAgent.
cd anvild
./scripts/service.sh install # build web, install + load the LaunchAgent, wire tailscale serve
./scripts/service.sh status # service state + /api/health
./scripts/service.sh restart # kickstart past launchd backoff
./scripts/service.sh logs # tail the daemon logIt installs a launcher that sources ~/.config/anvil/env, strips any
ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN from the environment, runs at login, and
restarts on crash. No secrets live in the plist. See anvild/README.md.
service.sh install handles the one-time bootstrap on any host: it installs Bun if it's missing
(pinned), builds the web bundle, loads the LaunchAgent (macOS) or systemd unit (Linux), and wires
tailscale serve. Everything after that — the Claude login and joining a fleet — happens in the
browser (see below); there's no separate native setup app.
service.sh install no longer requires a Claude login up front. With no token the daemon starts
degraded: it serves its API and web UI and reports subscriptionAuthOk: false, but refuses agent
turns (terminal, files, and git keep working). Open that machine's own web UI over Tailscale —
https://<machine>.<tailnet>.ts.net:7701 — and it takes the screen over with a setup flow:
- Join a fleet → shows a 6-digit code. On an existing machine, go to Settings → Servers → Add a machine, pick this one (it's labelled needs setup), and enter the code. The hub pushes its Claude login — plus the Todoist/OpenRouter keys — over the tailnet.
- Enter a token directly → paste a
claude setup-tokenvalue for a standalone machine.
Nothing after the one-time install needs a terminal. The setup screen is browser-only in this
release — the Android/iOS/macOS apps bundle their own copy of the web UI, so they need an app update
before it appears there; point a browser at the machine instead. Details:
docs/plans/anvil-headless-join.md.
The original Anvil drove Claude Code inside a PTY, inside Zellij, surfaced through Zellij's browser web client, wrapped in a WebView. Nearly every frustration traced to one root cause: using a terminal multiplexer for something that isn't fundamentally terminal work.
| Pain point (Zellij era) | Root cause | Fixed by |
|---|---|---|
| Monospace prose hard to read | bytes-on-a-grid | structured markdown, proportional fonts |
| Shift+Enter ≠ newline | terminal key encoding | native text inputs |
| Viewport fights across devices / foldables | one shared character grid | reflowable structure, no shared viewport |
| Can't paste/drag images & files | a PTY only takes byte streams | first-class structured attachments |
| Sessions opaque, won't die | Zellij owned lifecycle via sockets/husks | daemon owns lifecycle + process-group kill |
| All tabs/titles look identical | no structured metadata | rich session list with previews + git state |
The reframe: Anvil is a Claude client that occasionally needs a terminal, not a
terminal that occasionally talks to Claude. The full design — including the load-bearing
auth/billing constraint and the protocol — lives in
docs/plans/anvil-native-architecture.md.
anvil/
├── anvild/ # 🔨 the daemon (TS/Bun) — the keystone
│ ├── src/ # server · session · agent · render · git · push · fleet …
│ │ # integrations (autopilot · Todoist · lapo · schedule) · pipeline · prompts
│ ├── web/ # the web client + render surface (served at /)
│ ├── protocol.ts # → symlink to docs/plans/anvil-protocol.ts
│ └── scripts/ # service.sh (LaunchAgent), merge-session.sh
├── app/ # 🤖 Android WebView shell (Kotlin) — com.gte619n.anvil
├── apple/ # 🍎 Apple SwiftUI WebView shell (macOS first)
├── docs/
│ ├── ARCHITECTURE.md # approachable overview with diagrams (start here)
│ ├── assets/ # brand assets (logo, banners)
│ └── plans/ # deep design + implementation specs
├── scripts/ # build/release utilities (CI release notes, Apple signing)
└── .github/workflows/ # CI gate + "full release" (Firebase, TestFlight, Sparkle) on merge to main
| Doc | What's in it |
|---|---|
docs/ARCHITECTURE.md |
Start here. Approachable architecture tour with diagrams. |
docs/plans/anvil-native-architecture.md |
The full design: auth/billing, sessions, protocol, render pipeline, decisions. |
docs/plans/anvil-protocol.ts |
The wire protocol — every envelope, event, and command (the source of truth). |
docs/plans/anvil-impl-INDEX.md |
Index of the per-component implementation plans. |
docs/plans/anvil-multi-server.md |
Multi-server fleet design (one client, many Macs, one Max plan). |
docs/plans/anvil-headless-join.md |
Tokenless boot + joining a fleet from a headless (non-Mac) machine. |
docs/plans/anvil-autopilot-ui.md · anvil-todoist-integration.md |
Todoist autopilot + the plan-review UI. |
docs/plans/anvil-adversarial-pipeline.md |
The OpenRouter/GLM adversarial planning panel + the unattended dev pipeline. |
docs/lapo-integration.md |
Posting autopilot run reports to a lapo/Logseq journal over OAuth2. |
docs/CI-CD.md |
The build & release pipeline — every target, what to push to ship it. |
anvild/README.md |
Running, building, and developing the daemon + web client. |
app/README.md · apple/README.md |
Android + Apple client build notes. |
MIT