hermes-stack runs Hermes as the only model-calling runtime, Open Design as its
native design workspace, and an optional tailnet-only HTTPS gateway. Open
Design launches the existing Hermes CLI through ACP; it does not use Vela or a
separate model credential.
Hermes and Open Design run as supervised processes in one application container. This gives OD direct access to Hermes' authenticated CLI and the same project filesystem while keeping their web interfaces on separate ports.
The host needs Docker with the Compose plugin and Python 3.9 or newer.
The container runtime must also have enough capacity for the limits in
compose.yaml. Allocate at least 4 CPUs, 10 GiB of memory, and 60 GiB of disk
to the Docker VM. For concurrent Hermes profiles, Kanban workers, browser
automation, and persistent project dependencies, 6 CPUs, 12 GiB of memory, and
100 GiB of disk are recommended. These are VM-wide resources; the current
Hermes service may use up to 4 CPUs and 8 GiB, while Docker and the sidecars
need the remaining headroom.
For an existing Colima VM, stop it and restart it with the larger allocation:
colima stop
colima start --cpus 6 --memory 12 --disk 100Increasing an existing Colima disk preserves its Docker data, but the disk cannot later be shrunk. Configure equivalent VM resources in Docker Desktop or another container runtime.
After confirming the runtime capacity and cloning the repository, make the launcher executable and link it into a user-owned binary directory:
chmod +x hermes-stack
mkdir -p ~/.local/bin
ln -sfn "$PWD/hermes-stack" ~/.local/bin/hermes-stackAdd that directory to the host shell's persistent PATH configuration:
# Fish: run once; fish_add_path persists the universal variable.
fish_add_path "$HOME/.local/bin"# Bash: add this line to ~/.bashrc, or ~/.bash_profile for a login shell.
export PATH="$HOME/.local/bin:$PATH"# Zsh: add this line to ~/.zshrc.
export PATH="$HOME/.local/bin:$PATH"Open a new shell (or source the Bash/Zsh startup file) before invoking
hermes-stack. The symlink continues to work if the repository contents are
updated in place. Recreate it after moving or renaming the repository directory.
The launcher generates dependency-free completions for Fish, Bash, and Zsh. Saved location names are read live from the registry, so adding or renaming a location does not require regenerating the completion file.
For Fish:
mkdir -p ~/.config/fish/completions
hermes-stack completions fish > ~/.config/fish/completions/hermes-stack.fishFor Bash, add this line to ~/.bashrc:
eval "$(hermes-stack completions bash)"For Zsh, generate an autoloadable completion and add its directory to fpath
before calling compinit:
mkdir -p ~/.zfunc
hermes-stack completions zsh > ~/.zfunc/_hermes-stack# ~/.zshrc
fpath=(~/.zfunc $fpath)
autoload -Uz compinit
compinitExisting installations using the old state directory should stop the running stack and move it before issuing any other command with this version:
hermes-stack stop
mv ~/.config/hermes-docker ~/.config/hermes-stackThe stop command recognizes the legacy directory specifically to make this
migration safe. No state is copied or moved automatically.
Register one or more named host projects while starting the stack:
hermes-stack start app=/absolute/path/to/app docs=/absolute/path/to/docsThe CLI stores these locations under ~/.config/hermes-stack and generates a
Compose override there. Later starts can use the saved names without repeating
host paths:
hermes-stack start app docsAn explicit name=/path adds or updates a registration. If that host path was
previously registered under a different name, the new name replaces the old
one. With no project arguments, start, restart, and update reuse the last
selection.
Inspect or edit the registry with:
hermes-stack locations
hermes-stack locations add app /absolute/path/to/app
hermes-stack locations set app --node auto
hermes-stack locations reset-node-modules app
hermes-stack locations remove app
hermes-stack projectslocations shows every registration and marks the active selection;
projects shows only the selection used by the next start or restart. Registry,
selection, and runtime workspace-manifest files are human-readable JSON written
atomically. Existing current-projects state from the Fish wrapper is imported
automatically. The CLI uses only the Python 3.9+ standard library, so it does
not require a package manager or a separate virtual environment on the host.
Locations use node: auto by default. A root package.json enables a
persistent Linux-only node_modules volume, mounted over node_modules at
both container path representations. The host's macOS dependencies remain
untouched and hidden inside the container; install once inside Hermes and
reuse that Linux dependency tree across restarts and image rebuilds. Override
detection with --node on or --node off. Resetting dependencies removes only
that location's Linux volume and requires the stack not to be using it.
Only node_modules is isolated. Framework caches and outputs such as .next,
.nuxt, .vite, and dist remain in the shared project, and simultaneous
host/container servers must still use different ports.
Each selected directory is mounted read-write at two synchronized paths:
/workspace/<name>is the operational path used by the dashboard and normal interactive file work.- Its original absolute host path is also present inside the container. Use that portable identity when generated code or automation must record an absolute path that works both inside and outside the container.
Both paths are bind mounts of the same directory, not copies; an edit or upload
through either path is immediately visible through the other. Prefer relative
paths inside a project. Hermes receives an always-on workspace contract and a
workspace-paths skill that select the portable path when an absolute reference
must persist. The selected mappings are available inside the container at
/run/hermes-stack/workspaces.json and on the host at
~/.config/hermes-stack/workspaces.json.
Only names in the current selection are mounted or included in that runtime
manifest. Other saved locations remain unavailable to the container. Open
Design's own state remains writable at
~/.config/hermes-stack/open-design, so its web workspace, previews, and
settings continue to work. The wrapper marks OD onboarding complete and selects
Hermes as its agent after startup.
The launcher refuses workspace paths that would defeat or destabilize this
boundary: the filesystem root, the home directory or its parents, known
credential and configuration trees (including .ssh, .config, .docker,
and .hermes), Docker runtime directories, system pseudo-filesystems, and
paths overlapping reserved container locations. It also rejects path-list
separator and control characters before constructing the write-safe-root
environment. Ordinary project directories and separately located output
directories such as Obsidian vaults remain valid.
Avoid running a Discord-driven Hermes edit and an OD-driven Hermes run against the same project at the same time. They are independent sessions with shared filesystem access and no cross-session write lease.
Open an interactive shell as the unprivileged hermes user with:
hermes-stack shellFish is that user's login shell. Supervised services and automated tool calls continue to use their explicitly configured interpreters.
Obsidian Headless can act as a third Sync client alongside independent Windows and Mac desktop vaults. Create a dedicated, initially empty host directory for the headless mirror; do not reuse either desktop client's local vault folder. Register and configure it without placing account credentials in this repository or an environment file:
mkdir -p /absolute/path/to/dev-vault-headless
hermes-stack locations add dev-vault /absolute/path/to/dev-vault-headless
hermes-stack obsidian configure dev-vault
hermes-stack obsidian login
hermes-stack obsidian setup --vault "Dev-Vault"login prompts interactively for the Obsidian account, password, and MFA.
setup accepts a remote vault name or ID and prompts for an end-to-end
encryption password when required. The authentication token and headless sync
database persist beneath ~/.config/hermes-stack/obsidian-headless, which is
mounted only into the sidecar.
After the next user-initiated stack restart, the sidecar runs bidirectional
continuous sync with all attachment and Obsidian configuration categories,
including community plugins and plugin data. It keeps syncing its mirror even
when the saved location is not selected for Hermes. Select dev-vault in the
normal start command when Hermes should access its notes.
When selected, the mirror uses the same dual workspace mounts as other
locations. Nested read-only mounts protect .obsidian at both Hermes paths,
while the sidecar retains read-write access. Hermes can edit notes and
attachments but cannot distribute changed plugin JavaScript or configuration
to desktop clients. Use hermes-stack obsidian status, logs obsidian, or
obsidian disable to inspect or disable the integration. Disabling does not
delete credentials, unlink the remote vault, or change the running stack.
Obsidian Headless is still beta. Concurrent edits to the same note can conflict like edits from any independent Sync clients, so keep the dev vault backed up.
Open Design invokes the local agent using its supported Hermes adapter:
hermes acp --accept-hooks
OD owns skill, design-system, craft, plugin, prompt, preview, continuation, and
run lifecycle composition. Hermes owns model calls and tool execution using its
existing state under ~/.hermes. The old Open Design MCP entry is removed at
container startup so Hermes cannot recursively invoke an OD run that launches
another Hermes process.
The pinned Open Design v0.19.0 source receives one temporary upstream backport:
signed-out Open Design Cloud state redirects to onboarding only when the AMR
cloud agent is selected, so it does not block Hermes. Remove
patches/open-design-v0.19-local-agent-cloud-gate.patch and its Dockerfile
application after OPEN_DESIGN_REV advances to a release containing upstream
commit 85d2e4893c.
The stack starts locally without Tailscale authorization. Authorize its persistent container identity once, then display the tailnet URLs:
hermes-stack tailscale-login
hermes-stack tailscale-urlsTailscale Serve exposes Open Design on standard HTTPS port 443 and Hermes on
HTTPS port 9119, only to the tailnet; Funnel is disabled. Its node identity
is stored at
~/.config/hermes-stack/tailscale-state, outside Docker's managed volume
storage. Local access remains available at
http://127.0.0.1:9119 and http://127.0.0.1:7456.
The resulting remote URLs are:
https://<tailscale-fqdn>
https://<tailscale-fqdn>:9119
Open Design disables its API-token middleware because Tailscale and Caddy are the trusted authentication boundary for remote browser access. Its published host port remains loopback-only, and the exact remote origin is allow-listed after Tailscale login. This lets the remote UI save OD settings without exposing OD directly on the LAN.
Open Design does not install or use Vela. Its native agent inherits
HERMES_HOME=/opt/data and uses the same host-persisted Hermes authentication
as the Discord gateway. The former
~/.config/hermes-stack/open-design-amr directory is left untouched during
migration but is no longer mounted or read.
The Hermes dashboard OAuth callback configured in Hermes must exactly match:
https://<tailscale-fqdn>:9119/auth/callback
The wrapper does not inspect or change Hermes' OAuth or other dashboard settings. After login it recreates Open Design with its exact remote HTTPS origin allowed and refreshes the Tailscale proxy's application upstream.
The stack assumes a single-user host and a trusted tailnet:
- Hermes and Open Design publish host ports only on
127.0.0.1. They are not directly reachable from the LAN or public internet. - Tailscale Serve is the remote access boundary. Funnel is explicitly disabled; tailnet membership and Tailscale grants determine who can connect.
- Open Design's bearer-token middleware is deliberately disabled because its browser and API traffic passes through the trusted Tailscale/Caddy boundary. Any process running as the local user can still reach OD through its loopback port, so this is not isolation from other local processes or users.
- Hermes authentication under
~/.hermes, OD state, Tailscale node state, and Obsidian Headless credentials, and the project-location registry are sensitive runtime data stored outside this repository. Obsidian credentials are available only to its sidecar. Do not copy this state into the repository or publish a snapshot made from a running container withdocker commit. - Selected host projects are mounted read-write at both their
/workspacealias and original absolute path. These are two names for the same files and do not expose either path's parent or sibling directories. Hermes sessions started from Discord and OD are independent writers, so do not run both against the same project concurrently.
Use narrow Tailscale grants for ports 443 and 9119, periodically review
tailnet membership, and treat anyone with access to either UI as able to invoke
the authenticated Hermes runtime.
External container and build images use readable release tags plus immutable
multi-platform SHA-256 digests. The Open Design source revision and CLI tool
versions are pinned separately in compose.yaml and the Dockerfiles. The
locally built Obsidian sidecar pins the official obsidian-headless npm package
and is not published as a redistributed image.
hermes-stack update rebuilds and restarts the reviewed versions in this
repository; it does not silently advance pinned dependencies. On GitHub,
Dependabot checks Dockerfiles, Compose, and workflow actions weekly and proposes
updates as pull requests. Review and merge an update, then run
hermes-stack update locally.
CI runs the Python CLI tests, syntax and configuration checks, and a full Git history scan for committed secrets. Workflow actions are themselves pinned to immutable commits.
Custom domains are not enabled by this stack. A future migration can use two
hostnames, such as design.example.com and hermes.example.com, without
making either service public:
- Keep the domain's authoritative DNS at the existing provider. Public
Arecords can point both names at this node's persisted Tailscale100.xaddress. Publishing that private address does not provide a public route; clients still need tailnet access and permission under Tailscale grants. - Replace Tailscale Serve's HTTPS termination with tailnet-only raw TCP
443forwarding to Caddy. Let Caddy route by hostname to OD on7456and Hermes on9119. - Obtain certificates through ACME DNS-01. Use a Caddy build containing the DNS module for the authoritative provider, or use Certbot/acme.sh with that provider's DNS API. HTTP-01 cannot validate a tailnet-only origin.
- Persist Caddy's
/dataoutside Docker-managed volumes (for example,~/.config/hermes-stack/caddy-data) and provide narrowly scoped DNS API credentials without committing them to this repository. - Allow
https://design.example.cominOD_ALLOWED_ORIGINS, and change the Hermes dashboard OAuth callback tohttps://hermes.example.com/auth/callback. - Verify both names from an authorized tailnet device, confirm Funnel remains
disabled, then remove the
*.ts.netServe routes.
DNS availability does not depend on the laptop being online because the DNS provider remains authoritative. The laptop must come online often enough for the persisted ACME client to renew its certificates before expiry.
Hermes Stack is licensed under the Apache License, Version 2.0. See LICENSE
for the terms and NOTICE for the Open Design attribution associated with the
temporary source backport.