Concepts
Three words carry the whole tool.
Account
One claude.ai login. At most one is the default; every other account gets its own config dir (~/.claude-<name>) and a root folder (~/work/<name>).
Environment
One project: a git repo under an account root, with its own Remote Control service, a block of 100 ports, and optional Docker Compose services.
Routing
direnv sets CLAUDE_CONFIG_DIR below each account root and git switches identity per root, so the right login and identity follow the folder.
Global flags & exit codes
These work on every command.
| Flag | Meaning |
|---|---|
| --json | Machine-readable output in the versioned contract envelope. What the desktop app uses. |
| --verbose | Show every command devhost runs. |
| -y, --yes | Never ask questions; take the defaults. |
| --config <path> | Use another config file (default ~/.config/devhost/config.yaml). |
| -V, --cli-version | Print the CLI version. |
| Exit code | Meaning |
|---|---|
| 0 | Success. |
| 1 | A check or step failed (for example doctor found a ❌). |
| 2 | Bad flags or arguments, or the config is missing or invalid. |
| 3 | A human has to act first; the output says exactly what (for example macOS blocking Remote Login). |
| 130 | Cancelled with Ctrl-C. devhost finishes the current step and leaves a consistent state. |
Quick start
On the Mac mini, from a clone of the repository, ./bootstrap.sh installs the tools, builds devhost, then runs init, apply --dry-run and apply. After that:
# your main login
$ devhost account add --name personal --default --git-email me@example.com
# a second login: prints a URL, open it anywhere, sign in, paste the code
$ devhost account add --name business --git-email me@work.example
# one project, with its own Remote Control service
$ devhost env create --account business --name acme-api --repo git@github.com:acme/api.git
$ devhost doctor # everything green?
$ devhost top # live view, works over SSH from a phone
The environment now shows up in the Claude app under the business account and comes back by itself after a reboot.
Set up the machine
init
A short wizard that writes ~/.config/devhost/config.yaml. It asks about three things (hostname, Docker runtime, Tailscale) and generates a random ntfy topic for notifications. Without a terminal it uses the flags and defaults.
| Flag | Meaning |
|---|---|
| --force | Overwrite an existing config; a timestamped backup is kept. |
| --hostname <name> | The host's name. |
| --docker <runtime> | colima, orbstack or none. |
| --no-tailscale | Do not set up Tailscale. |
apply
Makes this Mac match the config. It is idempotent: run it as often as you like, and only what differs is changed. Any existing file is backed up first, every sudo is announced with its reason, and it is safe to interrupt and re-run.
| Flag | Meaning |
|---|---|
| --dry-run | Print the plan, including every sudo command, and change nothing. Run this first. |
| --only <step…> | Run only these steps. A prefix selects a group: system = all system.*, storage = all storage.*, env.acme-api = one environment. |
The steps, in order:
| Step | Does |
|---|---|
| system.helper | With machine.rootHelper: true, installs a small root helper plus a sudoers rule so later root operations need no password (the desktop app and ssh then need nobody at the keyboard). |
| system.hostname / power / ssh | Hostname, pmset power settings (never sleep, restart after power loss, wake on network), Remote Login. |
| packages | Renders a Brewfile and installs what is missing (direnv, Colima/OrbStack + docker, tailscale, claude-code). Never upgrades; that is update. |
| runtimes | Installs mise and the language runtimes listed in the config. |
| shell | A managed block in ~/.zshrc: Homebrew, mise, the direnv hook, claude-<account> aliases. |
| docker.config / runtime | Compose plugin path; Colima as a boot-time service with the configured resources. |
| tailscale | Starts the system daemon. Logging in is never automated; you get the tailscale up instructions. |
| claude.binary | Records the absolute path of claude for the service definitions. |
| accounts.claude / routing / git | Per account: merged Claude settings (OTel and hooks), a managed CLAUDE.md block, the account root with its .envrc, git identity per root. |
| collector | The collector LaunchDaemon (com.devhost.collector). |
| agents | The subagent definitions chosen per account and environment. |
| storage.* | With a work volume: identity check, ownership, Spotlight off, automount, boot-time unlock. |
| services.orphans | Finds devhost services nothing owns any more and asks before removing them. |
| env.<name> | Per environment: repo, .envrc, compose, and the Remote Control LaunchDaemon (KeepAlive, waits for the network at boot). |
$ devhost apply --dry-run # read the plan
$ devhost apply # do it
$ devhost apply --only agents # after changing agent choices
$ devhost apply --only env.acme-api
config
Read and change the config file. Every write is validated against the schema first, comments are preserved, and a backup is kept. Changing the config does not apply it: run devhost apply afterwards.
| Subcommand | Does |
|---|---|
| get [pointer] | Print the effective config, or the value at a JSON pointer such as /machine/hostname. |
| set <pointer> <value> | Set a value (JSON/YAML syntax) and validate the result. Nothing is written if it is invalid. |
| validate [--stdin] | Validate the file, or a candidate document read from stdin. |
| edit | Open the file in $VISUAL/$EDITOR and validate afterwards. Needs a terminal. |
| raw | Print the file exactly as on disk, comments included (used by the app's editor). |
| put | Replace the file with the YAML on stdin, only if it validates. |
$ devhost config get /machine/hostname
$ devhost config set /machine/hostname mini
$ devhost config validate
update
Updates Claude Code and moves everything onto it. It upgrades the claude-code cask, restarts every Remote Control service (they run from a versioned path the upgrade deletes) and lists the sessions that were open; each continues as soon as you write in it on claude.ai. It waits while a session is in the middle of a turn, and refuses to run from inside a session of a service it would restart.
| Flag | Meaning |
|---|---|
| --check | Only report what is outdated. |
| --all | Also upgrade the other outdated Brewfile packages. |
| --force | Do not wait for sessions mid-turn; they are interrupted, then continue on claude.ai. |
| --dry-run | Only print the plan. |
The collector can do this by itself inside claude.updateWindow (default 02:00–06:00) when claude.update is auto.
uninstall
Removes what devhost installed: launchd jobs (including orphaned com.devhost.* ones), the managed shell and git blocks, and generated files. It never touches your projects, Claude config dirs or credentials.
| Flag | Meaning |
|---|---|
| --keep-packages | Leave the Homebrew packages installed. |
| --purge-data | Also delete the collector database. |
| --purge-storage | Also remove the work volume's unlock service and Keychain item (never the volume or its data). |
| --dry-run | Print what would be removed, including every sudo command. |
Accounts & projects
account
list shows each account with its login state and environment count.
add creates the account's config, settings, routing and git identity, then starts the login. A second account prints a URL: open it on any device, sign in, paste the code back.
| Flag (add) | Meaning |
|---|---|
| --name <name> | Required. Account name (a slug). |
| --git-email <email> | Required. Git email for repositories under the account root. |
| --git-name <name> | Git author name. |
| --default | Use Claude Code's default config dir. Only one account may. |
| --daily-cost-alert <usd> | Notify when the estimated cost today passes this. |
| --no-login | Skip the login; doctor will report it as not logged in. |
| --root <path> | Account root (default: on the work volume if configured, else ~/work/<name>). |
remove <name> is refused while the account has environments. It never deletes the root, the config dir or credentials; --logout also signs the account out.
login
Signs an account in (again), accepts workspace trust and the one-time Remote Control confirmation for each of its environments, and restarts their services. Use it when doctor reports an expired login.
The first sign-in records which claude.ai login the account belongs to. A sign-in as anyone else is signed out again and refused, because the environments would otherwise register under the wrong claude.ai account. --switch-login accepts a new login on purpose and makes the account's environments register afresh.
env
An environment is one project with its own Remote Control service.
| Subcommand | Does |
|---|---|
| list | Environments and their service state. |
| create | Clones the repo (or git init), allocates a port block, writes .envrc and compose, installs the service. Running it again continues a setup that failed part-way. |
| delete <name> | Stops and removes the environment. Files stay unless --delete-files (shows unpushed commits and asks you to type the name). |
| start / stop / restart <name> | Control the Remote Control service. |
| sleep <name> | Stop the compose services that are running now, until a session uses the environment. Only those come back on wake. |
| wake <name> | Start exactly the compose services the environment had running when it fell asleep. |
| logs <name> | The service log. -n, --lines (default 50), -f, --follow. |
| Flag (create) | Meaning |
|---|---|
| --account <account> | Required. Owning account. |
| --name <name> | Required. Environment name (a slug). |
| --repo <url> | Repository to clone. Omit for git init. |
| --no-rc | Do not install a Remote Control service. |
| --compose auto|off | Detect and manage Docker Compose services (default auto). |
| --spawn worktree|same-dir | Where Remote Control sessions run; passed to claude remote-control. |
| --permission-mode <mode> | Permission mode for its sessions; passed to claude remote-control. |
| --daily-cost-alert / --hourly-cost-alert <usd> | Cost alerts for this environment. The hourly one names the session that spent most, to catch a runaway. |
$ devhost env create --account business --name acme-api --repo git@github.com:acme/api.git --spawn worktree
$ devhost env logs acme-api -f
$ devhost env delete acme-api --delete-files
With docker.idleSleepMinutes (default 60) the collector puts an environment to sleep after an hour with no session and wakes it with the next one. A service you stopped by hand stays stopped.
agents
Shows the subagent catalog (built-in explorer, reviewer, test-runner, docs, plus your agents.custom) and which agents each account and environment has. To change them, set agents: [name, …] on an account or environment with config set, then run devhost apply --only agents. Only files devhost marked are ever changed.
session
reattach brings an ended Remote Control session back (for example after a reboot) by running it again in its worktree, once, as a one-shot service. It usually is not needed: writing in the session on claude.ai continues it under the environment's service. It only works for sessions a service started in its own worktree.
handoff prints a summary of a session so you can continue its work in a new one: the note written since the session's last activity, or, with --new, a fresh one. The note is written by a one-off fork of the session, which costs about one cache write of its context. The collector writes one on its own for a session that has been idle for 55 minutes, just before its prompt cache expires.
Watch it run
doctor
Read-only health checks, each with a one-line fix: ✅ fine, ⚠️ worth a look, ❌ broken. Exits 1 if any check fails. It reads the service logs too, so an expired login shows up as devhost login <account>. Plugins add their own checks as plugin.<name>.<check>.
status
A one-screen summary of the host, its accounts and environments.
dash & top
dash prints a snapshot of sessions, tokens, cost and CPU/RAM; with --json it is the data behind the desktop app's dashboard. --follow streams a new snapshot every sample interval (NDJSON with --json). The filters narrow it to one account, environment or model.
top is the same data as a refreshing terminal table. It works over SSH, so you can check the server from a phone. --once prints once and exits.
notify
Sends a test notification through your ntfy topic, so you know alerts reach your phone. Alerts cover memory, disk, cost limits, a restart loop and an expired login. The default relay is the public ntfy.sh; set notify.server to use your own.
collector
The collector is the one long-running process: it receives Claude Code telemetry and hook calls on 127.0.0.1:4318, samples CPU, RAM, Docker, disk and Tailscale, and stores everything in a local SQLite database. launchd runs it for you; run starts it in the foreground for debugging, and logs shows its log. It listens on localhost only and requires a bearer token.
More
storage
Keep project data on an external work volume instead of the internal disk.
| Subcommand | Does |
|---|---|
| list | External disks and their APFS volumes. |
| status | Whether the work volume is mounted, its identity, encryption, ownership, free space, Spotlight. |
| wait [--timeout s] | Exit 0 once the configured volume is mounted (default 300 s). Services start through this at boot. |
| adopt <volume> [--root p] | Make an existing APFS volume the work volume (config plus a marker file on it). |
| prepare <disk> --label n [--encrypted] --yes-erase | Erases the disk as APFS (encrypted with --encrypted, passphrase from stdin), then adopts it. CLI only, never from the app. |
| set-passphrase | Store or rotate the passphrase in the System keychain (from stdin). |
| migrate --account a [--cleanup] [--dry-run] | Move an account's root onto the volume. The old copy stays until a later --cleanup. |
--yes-erase.plugin
Plugins add actions, checks and dashboard cards. See the plugins page for what exists and how to build one.
| Subcommand | Does |
|---|---|
| list | Plugins with their actions and checks. |
| widgets [--param plugin.filter=value] | The dashboard controls and cards of enabled plugins (what the app shows). |
| run <plugin> <action> | Run an action. --env / --session pick the target, --dry-run shows what would change. Anything that removes or stops something asks first (--yes answers). |
| install <source> | From GitHub: URL or owner/repo, with #branch or #tag. Stays off until enabled. --name, --enable, --dry-run. |
| upgrade <name> | Fetch the latest commit; its configuration stays. |
| uninstall <name> | Remove an installed plugin and its config entry. For built-in ones, disable. |
| enable / disable <name> | Turn a plugin on or off. |
version
Prints the CLI version, the --json contract version, the commit it was built from, and the Node and platform it runs on.