Skip to content

dwhly/hermes-host-bootstrap

Repository files navigation

hermes-host-bootstrap

Turn a fresh Ubuntu / Debian VPS (or a new Mac) into a fully-loaded Hermes Agent host in one command.

curl -fsSL https://raw.githubusercontent.com/dwhly/hermes-host-bootstrap/main/bootstrap.sh \
  | bash -s -- --tier=recommended

Or clone first if you want to read the script before running it (recommended):

git clone https://github.com/dwhly/hermes-host-bootstrap.git
cd hermes-host-bootstrap
./bootstrap.sh --tier=recommended

What it does

A fresh DigitalOcean / Hostinger / Hetzner Linux box is missing a lot of the daily-driver tooling an AI agent (and the human running it) actually needs: a multiplexer, modern unix CLIs, a real Python/Node toolchain, container runtime, media tooling, security baseline, and Hermes itself.

bootstrap.sh is one idempotent script that installs all of that, in tiers, with sane defaults. Re-running it is safe — every step checks before it acts.

Roles

The script also asks what is this machine for? — because a VPS and your Mac want very different software.

Role Auto-detected when… Skips Adds
server Linux, no $DISPLAY Ghostty, MS Remote Desktop, mac apps xrdp/XFCE (with --tier=full)
client macOS xrdp, server-side daemons Ghostty, MS Remote Desktop, Tailscale GUI
both Linux with a desktop nothing both sides

Override with --role=server|client|both if auto-detect gets it wrong.

Examples:

# Fresh VPS — server bits only (auto-detected)
./bootstrap.sh --tier=recommended

# Fresh VPS but you want a remote desktop too
./bootstrap.sh --tier=full              # xrdp + XFCE included

# New Mac — client bits only (auto-detected)
./bootstrap.sh --tier=recommended

# Desktop Linux you use as your daily driver AND as a Hermes host
./bootstrap.sh --tier=full --role=both

Tiers

Tier What you get When to use
minimal 25 items — Hermes runs, ssh is locked down, tmux works small VPS, tight RAM, you'll add tooling on demand
recommended + zsh/omz, mosh, docker, tailscale, full media stack, Claude Code + Codex default — what most Hermes hosts should look like
full + small "nice-to-have" CLIs (tldr, tree, httpie, glances, etc.) when disk is cheap

Manifests: tiers/minimal.txt · tiers/recommended.txt · tiers/full.txt

Module layout

The work is split across prefixed modules in lib/. Numeric prefixes run in order (00 → 90), then letter-prefixed modules run last (A0, M5, …):

lib/
├── common.sh             shared helpers (logging, apt_install, ensure_line,
│                         tier_allows, role_includes, …)
├── 00-preflight.sh       hostname, tz, apt upgrade, swap, enable-linger
├── 05-ssh-access.sh      opt-in authorized_keys + macOS Remote Login helpers
├── 10-security.sh        openssh, ufw, fail2ban, unattended-upgrades, ssh hardening
├── 20-buildchain.sh      build-essential + libs (so pip/cargo wheels compile)
├── 30-shell.sh           tmux, zsh, oh-my-zsh, neovim, mosh, micro
├── 40-cli.sh             rg, fd, fzf, bat, jq, htop, btop, eza, zoxide, delta, …
├── 50-languages.sh       python + uv + pipx, fnm + Node LTS, Rust
├── 60-containers.sh      Docker + compose, user added to docker group
├── 70-network.sh         Tailscale, cloudflared
├── 80-media.sh           ffmpeg, imagemagick, poppler, tesseract, pandoc, espeak-ng
├── 90-agents.sh          hermes, gh, Claude Code CLI, Codex CLI, faster-whisper
├── 92-hermes-config.sh   personal ~/.hermes clone/pull, op inject, model config seed
├── 93-hermes-wiki.sh     local Hermes Automation Wiki clone/pull + HTML publish
├── 95-ghostty.sh         Ghostty terminal — client/both role only
├── A0-remote-desktop.sh  xrdp + XFCE (opt-in: --tier=full or --only=A0-remote-desktop)
└── M5-mac-client.sh      tmux + mosh + MS Remote Desktop + Tailscale GUI (macOS client only)

Each module can also be run on its own:

./bootstrap.sh --only=90-agents,60-containers

Flags

--tier=<minimal|recommended|full>   default: recommended
--skip=KEY1,KEY2,...                skip specific items (e.g. docker, zsh, ghostty)
--only=MOD1,MOD2,...                run only these modules
--dry-run                           print the plan, don't execute
--self-update                       git pull && re-exec

Useful skip keys

Key Skips
apt-upgrade the initial apt-get upgrade
swap swap file creation
linger loginctl enable-linger
authorized-keys skip ~/.ssh/authorized_keys management from HERMES_AUTHORIZED_KEYS(_FILE)
mac-remote-login macOS only: skip Remote Login enablement when HERMES_MAC_REMOTE_LOGIN=1
ssh-harden sshd_config edits (off by default — only fires if HERMES_SSH_HARDEN=1)
ufw ufw install + rules
fail2ban fail2ban
unattended unattended-upgrades
tmux / tmux-conf tmux package / .tmux.conf install
tmux-workspace-colors per-session statusbar coloring for hermes-workspace panes (ops/code/logs/scratch)
tmux-autoattach auto-attach to tmux session "main" on interactive SSH (snippet sourced from .bashrc/.zshrc)
hssh hssh [session] [host] shell function: ssh + attach/create named tmux session; defaults to session main and reads default_user: from ~/.hermes/hosts/<host>.yaml
hostname-rewrite rewrite stale long hostnames in user's HSSH_DEFAULT_HOST exports after fleet renames (table lives in lib/30-shell.sh)
aliases shell aliases (h, hm, hmreset, ...) — managed by the add-shell-alias skill
chsh-zsh make zsh the default login shell on Linux hosts (matches macOS default)
mac-hostname macOS only: rename HostName + LocalHostName to $HERMES_MAC_HOSTNAME (opt-in)
op install the 1Password CLI (for secrets resolution via .env.template)
op-resolve resolve ~/.hermes/.env.template~/.hermes/.env via op inject on each bootstrap run
hermes-wiki / hermes-wiki-pull / hermes-wiki-build local Hermes Automation Wiki clone/pull/build
ghostty-workspace macOS only: 2x2 Ghostty grid launcher (hermes-workspace) via AppleScript
mosh-firewall UFW rule for mosh UDP 60000-61000 (rule still added if ufw stays disabled)
zsh / oh-my-zsh zsh / OMZ
inputrc .inputrc install
mosh, neovim, micro per-tool
python, uv, node, rust, pnpm language runtimes
docker Docker engine
tailscale, cloudflared network tools
media ffmpeg + imagemagick + poppler + tesseract + pandoc
hermes, gh, claude-code, codex, faster-whisper, browser-deps agent layer
ghostty Ghostty terminal

Useful env vars

Var Effect
HERMES_HOSTNAME=mybox set the hostname during preflight
HERMES_HOST_DEFAULT_USER=root set default_user: in the host registry for hssh/user@host resolution
HERMES_TZ=America/Los_Angeles set timezone (default: UTC)
HERMES_AUTHORIZED_KEYS newline-separated public SSH keys to ensure in ~/.ssh/authorized_keys
HERMES_AUTHORIZED_KEYS_FILE file of public SSH keys to ensure in ~/.ssh/authorized_keys
HERMES_MAC_REMOTE_LOGIN=1 macOS only: enable Remote Login for SSH and allow the current user
HERMES_SSH_HARDEN=1 actually apply ssh hardening (off by default to avoid lockout)
HERMES_UFW_ENABLE=1 actually enable ufw (off by default to avoid lockout)
HERMES_CONFIG_REPO=git@github.com:USER/hermes-config.git clone/pull personal ~/.hermes config during bootstrap
HERMES_WIKI_REPO=git@github.com:USER/hermes-automation-wiki.git clone/pull local wiki during bootstrap
HERMES_WIKI_DIR=~/code/hermes-automation-wiki override local wiki checkout path
HERMES_MODEL_PROVIDER=openrouter non-interactively seed model.provider so hermes setup is unnecessary
HERMES_MODEL_DEFAULT=openai/gpt-5.5 non-interactively seed model.default
HERMES_GATEWAY_INSTALL=1 / HERMES_GATEWAY_START=1 install/start gateway after config + secrets are in place

A note on Ghostty

Ghostty is a GUI terminal emulator. It needs a display server, which a headless VPS does not have. The 95-ghostty.sh module detects this and skips with a friendly notice on headless boxes. It runs the real install on:

  • macOS (via brew install --cask ghostty) — for your client Mac
  • Desktop Linux (via snap or flatpak)
  • Fedora (via the pgdev/ghostty COPR)

So you can also point this script at a new Mac to set up your client machine. Pass --skip=docker,tailscale if you don't want a heavy client-side install.


Idempotency

Everything in lib/common.sh is designed so re-running the script is safe:

  • apt_install filters out already-installed packages
  • ensure_line only appends if the line isn't already in the file
  • backup_once only backs up if no .bak.<date> already exists
  • Each module checks have X / [[ -d X ]] before doing work
  • apt-get update runs at most once per bootstrap invocation

The full log is teed to ~/.hermes-host-bootstrap.log so you can audit what happened.


After install

The script prints a checklist at the end. The short version:

  1. Log out and back in — so PATH, the docker group, and linger actually take effect.
  2. If HERMES_CONFIG_REPO / HERMES_MODEL_* are set in ~/.hermes-bootstrap.conf, Hermes model/provider config is already seeded; no hermes setup provider picker is needed.
  3. hermes doctor — sanity-check the install.
  4. hermes gateway setup only if you did not preseed gateway/env config.
  5. sudo tailscale up — bring this node onto your tailnet.

On a new Mac, the first Homebrew install needs a real sudo prompt. If you are running over SSH and see Need sudo access on macOS, rerun from Terminal.app on the Mac, or use:

hermes-reload --interactive-sudo --prompt --role=both

The --interactive-sudo mode re-enters through ssh -tt localhost when possible so sudo/Homebrew can prompt instead of half-running later modules.


Tested on

  • Ubuntu 24.04 LTS (DigitalOcean, Hetzner)
  • Debian 12 (mostly — Ghostty falls back to flatpak)
  • macOS 14 (Sonoma) and 15 (Sequoia)

Probably works on Ubuntu 22.04 too. PRs welcome for Fedora / Arch / WSL.


Remote desktop on the VPS

A headless VPS doesn't have a graphical desktop by default. If you want to ssh in and be able to run GUI apps (browser dev tools, an IDE, whatever) on the box itself, opt into the A0-remote-desktop module:

# Either way enables it:
./bootstrap.sh --tier=full
./bootstrap.sh --only=A0-remote-desktop   # standalone
HERMES_RDP=1 ./bootstrap.sh                # any tier

What you get:

  • XFCE as the desktop (lightweight, ~150 MB)
  • xrdp as the protocol server, bound to 127.0.0.1

You connect from your Mac in one of two ways:

# Option A — ssh tunnel
ssh -L 3389:localhost:3389 you@vps
# then open Windows App / Microsoft Remote Desktop → connect to localhost:3389

# Option B — Tailscale
tailscale serve --tcp=3389 tcp://localhost:3389  # one-time
# then connect to your VPS's tailscale name on port 3389

Microsoft Remote Desktop (free, App Store; renamed "Windows App" in late 2024) is the recommended Mac client. The M5-mac-client module installs it automatically when --role=client or --role=both.

To expose xrdp publicly anyway (not recommended), set HERMES_RDP_PUBLIC=1. You'll want ufw + fail2ban configured first.


Personal config file (optional)

The repo itself is opinionated about what a Hermes host looks like (packages, hardening, verification) but neutral about who you are. If you want to set personal defaults — your preferred tier, your git identity for commits the script makes, your timezone, your lockout-risk overrides — drop them in ~/.hermes-bootstrap.conf and bootstrap.sh will source it on each run.

cp .hermes-bootstrap.conf.example ~/.hermes-bootstrap.conf
$EDITOR ~/.hermes-bootstrap.conf

See .hermes-bootstrap.conf.example for the full set of variables. This keeps the repo cleanly forkable — a new user clones, drops their own .hermes-bootstrap.conf, and never has to touch the repo itself.


Governance inheritance (how prime directives reach a new host)

A new fleet host does not just get packages — it inherits the fleet's governance wiring automatically, via the synced ~/.hermes config repo (92-hermes-config.sh clones/pulls HERMES_CONFIG_REPO). Two whitelisted paths in that repo carry it:

  • memories/MEMORY.md — injected into every agent turn. Holds the constitutional pointer: "Chief work is governed by playbooks + the wiki; load the chief-playbooks skill first; classify station-vs-direct and state it." This is the only always-on channel, so it holds pointers + a few reflexes, never process prose.
  • agent-authored skills/ (e.g. chief-playbooks, hermes-fleet-maintenance) — the loaders that read the live source-of-truth docs before acting.

So provisioning a host = it inherits the same prime directives the rest of the fleet runs under, no per-host setup. The authoritative model — source of truth (Chief docs/ playbooks + wiki) → loader (Hermes skills) → reflex (memory), plus the promotion path and the station-vs-direct directive — lives in the Chief umbrella at docs/playbooks/governing-knowledge-and-process.md (this README only points at it; one source of truth).

Known gap (memory is fleet-global, undifferentiated — by accident, not design). Because memories/ is whitelisted in the config repo's .gitignore, all memory propagates to every host on sync.sh pull. That is correct for universal facts (who the user is, the constitution) but wrong for host-specific ones ("this box is RAM-tight") which get broadcast to every machine as if universal. There is no universal-vs-host-specific split today; the fleet-global behaviour is an emergent property of one .gitignore line, not a declared policy. Tightening that whitelist would silently make memory host-local and nobody would notice until two boxes disagreed. Tracked as future work in governing-knowledge-and-process.md §8.


Shell sessions that survive Mac sleep (mosh + tmux)

The default ssh session from a Mac dies the moment the laptop sleeps — Wi-Fi drops, the TCP socket goes stale, and when you wake up you get No route to host / Broken pipe / client_loop: send disconnect. The fix is two tools, used together:

  • mosh replaces ssh for the transport. It uses UDP, so it survives sleep, Wi-Fi changes, and IP roaming. Auth still goes through ssh (mosh shells out to it under the hood), so your keys and ~/.ssh/config aliases still work.
  • tmux runs on the server and protects the running process state. Even if mosh itself dies, your shell + whatever it was running keeps going inside the tmux session; you reattach and you're back.

Both are installed by this bootstrap:

Side Where it's installed Notes
Linux server lib/30-shell.sh (tier R) tmux + ~/.tmux.conf, mosh package
macOS client lib/M5-mac-client.sh (role=client/both) tmux + mosh via Homebrew
Firewall lib/10-security.sh ufw allow 60000:61000/udp added even when ufw stays off

Daily usage from the Mac:

mosh you@vps -- tmux new -A -s main

or, via the bundled helper after the host registry has synced:

hssh h-do1        # session main on h-do1, using hosts/h-do1.yaml default_user
hssh code h-do1   # session code on h-do1

tmux new -A -s main creates a session named main if missing, or attaches to it if it exists. Detach with Ctrl-b d (or Ctrl-a d if you're using the bundled ~/.tmux.conf, which rebinds prefix to Ctrl-a). Re-run the same command tomorrow and you pick up exactly where you left off.

Cloud-provider firewalls (DigitalOcean, Hetzner, etc.) usually need their own rule — open UDP 60000-61000 in the provider's web console too, not just ufw.


Fleet registry & dashboard

Every bootstrap run drops a yaml snapshot of the host at ~/.hermes/hosts/<hostname>.yaml — OS, role, tier, IPs, tool versions, resource ceiling, install date, a default_user: for SSH helpers, and a free-form note: describing what the box is for. If ~/.hermes is git-tracked via the companion hermes-config-sync repo, those snapshots roll forward to every other machine, giving you a portable inventory.

Interactive host identity (first run)

When bootstrap.sh runs in a real terminal (stdin is a TTY) and HERMES_HOSTNAME isn't already set, it asks two short questions before installing anything:

Hostname for this machine in the Hermes fleet.
  current OS hostname: ubuntu-s-1vcpu-2gb-70gb-intel-sfo2
  pick a short, unique name (e.g. h-do1, h-mini, hetzner-builder)
  empty = keep 'ubuntu-s-...', no rename
> h-do1
  → will rename to: h-do1

One-line note — what is this machine for?
  e.g. 'main production VPS for hermes', 'macbook M2 daily driver', 'hetzner build farm'
  empty = skip (registry will have no note for this host)
> main Hermes host, SFO, gateway + agent

Existing fleet hostnames are listed first so you don't pick a collision. Both answers are optional — empty input keeps the current value. Skip the prompts entirely with HERMES_NONINTERACTIVE=1 or by pre-setting HERMES_HOSTNAME / HERMES_HOST_NOTE (in ~/.hermes-bootstrap.conf or the environment).

Tip: if you rename the box at the OS level, also rename it in your cloud provider's web console (DigitalOcean droplet name, Hetzner server name, etc.) so the dashboards and billing line up.

Dashboard

The bundled hermes-fleet script (installed to ~/.local/bin/) reads the registry and prints a one-screen dashboard:

$ hermes-fleet
  Hermes Fleet  (3 hosts in registry)

  HOST               TIER       ROLE     OS                    HERMES     DISK         NOTE
  hermes-do1         recommended server  Ubuntu 24.04.4 LTS    0.14.0     18%          main Hermes host, SFO, gateway + agent
  hetzner-builder    full       server   Debian 12             0.18.1     72%          background build farm + cache mirror
  mac-mini           recommended both    macOS 15.2            0.18.2     58%          M2 daily driver

  Detail for one host:  hermes-fleet <hostname>
  Live SSH probe:       hermes-fleet --live
  Refresh this box:     hermes-fleet --refresh

Modes:

  • hermes-fleet — static dashboard from snapshot files
  • hermes-fleet <hostname> — dump the full yaml for one host
  • hermes-fleet --live — also SSH into each reachable host for current uptime + live disk (uses BatchMode=yes, so it only works for hosts with passwordless key auth from this box, e.g. via Tailscale)
  • hermes-fleet --refresh — rewrite this host's entry (re-runs the 99-register-host module)
  • hermes-fleet --json — machine-readable output for scripting

To keep snapshots current between bootstrap runs (e.g. so disk and uptime are fresh on the dashboard), wire a cron job on each host:

# crontab -e
0 * * * * cd ~/path/to/hermes-host-bootstrap && ./bootstrap.sh --only=99-register-host >/dev/null 2>&1 && ~/.hermes/sync.sh save "hourly host snapshot" >/dev/null 2>&1

License

MIT. Use it however you like.

About

Idempotent bootstrap script: turn a fresh Ubuntu/Debian VPS (or new Mac) into a fully-loaded Hermes Agent host. Modular, tiered, safe to re-run.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors