Everything above, plus a persistent Ubuntu container where the agent can apt/pip/npm install
info What's different from the standard install
The curl | bash installer manages Python, Node, and dependencies itself. The Nix flake replaces all of that — every Python dependency is a Nix derivation built by uv2nix, and runtime tools (Node.js, git, ripgrep, ffmpeg) are wrapped into the binary's PATH. There is no runtime pip, no venv activation, no npm install.
For non-NixOS users, this only changes the install step. Everything after (hermes setup, hermes gateway install, config editing) works identically to the standard install.
For NixOS module users, the entire lifecycle is different: configuration lives in configuration.nix, secrets go through sops-nix/agenix, the service is a systemd unit, and CLI config commands are blocked. You manage hermes the same way you manage any other NixOS service.
Prerequisites
Nix with flakes enabled — Determinate Nix recommended (enables flakes by default)
API keys for the services you want to use (at minimum: an OpenRouter or Anthropic key)
Quick Start (Any Nix User)
No clone needed. Nix fetches, builds, and runs everything:
# Run directly (builds on first use, cached after)
nixrungithub:NousResearch/hermes-agent--setup
nixrungithub:NousResearch/hermes-agent--chat
# Or install persistently
nixprofileinstallgithub:NousResearch/hermes-agent
hermessetup
hermeschat
After nix profile install, hermes, hermes-agent, and hermes-acp are on your PATH. From here, the workflow is identical to the standard installation — hermes setup walks you through provider selection, hermes gateway install sets up a launchd (macOS) or systemd user service, and config lives in ~/.hermes/.
The flake exports nixosModules.default — a full NixOS service module that declaratively manages user creation, directories, config generation, secrets, documents, and service lifecycle.
This module requires NixOS. For non-NixOS systems (macOS, other Linux distros), use `nix profile install` and the standard CLI workflow above.
Add the Flake Input
# /etc/nixos/flake.nix (or your system flake){inputs={
nixpkgs.url="github:NixOS/nixpkgs/nixos-unstable";
hermes-agent.url="github:NousResearch/hermes-agent";};outputs={ nixpkgs, hermes-agent,...}:{
nixosConfigurations.your-host= nixpkgs.lib.nixosSystem {system="x86_64-linux";modules=[
hermes-agent.nixosModules.default
./configuration.nix];};};}
That's it. nixos-rebuild switch creates the hermes user, generates config.yaml, wires up secrets, and starts the gateway — a long-running service that connects the agent to messaging platforms (Telegram, Discord, etc.) and listens for incoming messages.
warning Secrets are required
The environmentFiles line above assumes you have sops-nix or agenix configured. The file should contain at least one LLM provider key (e.g., OPENROUTER_API_KEY=sk-or-...). See Secrets Management for full setup. If you don't have a secrets manager yet, you can use a plain file as a starting point — just ensure it's not world-readable:
tip addToSystemPackages
Setting addToSystemPackages = true does two things: puts the hermes CLI on your system PATH and sets HERMES_HOME system-wide so the interactive CLI shares state (sessions, skills, cron) with the gateway service. Without it, running hermes in your shell creates a separate ~/.hermes/ directory.
Container-aware CLI
When `container.enable = true` and `addToSystemPackages = true`, **every** `hermes` command on the host automatically routes into the managed container. This means your interactive CLI session runs inside the same environment as the gateway service — with access to all container-installed packages and tools.
- The routing is transparent: `hermes chat`, `hermes sessions list`, `hermes version`, etc. all exec into the container under the hood
- All CLI flags are forwarded as-is
- If the container isn't running, the CLI retries briefly (5s with a spinner for interactive use, 10s silently for scripts) then fails with a clear error — no silent fallback
- For developers working on the hermes codebase, set `HERMES_DEV=1` to bypass container routing and run the local checkout directly
Set `container.hostUsers` to create a `~/.hermes` symlink to the service state directory, so the host CLI and the container share sessions, config, and memories:
Users listed in `hostUsers` are automatically added to the `hermes` group for file permission access.
**Podman users:** The NixOS service runs the container as root. Docker users get access via the `docker` group socket, but Podman's rootful containers require sudo. Grant passwordless sudo for your container runtime:
The CLI auto-detects when sudo is needed and uses it transparently. Without this, you'll need to run `sudo hermes chat` manually.
Verify It Works
After nixos-rebuild switch, check that the service is running:
# Check service status
systemctlstatushermes-agent
# Watch logs (Ctrl+C to stop)
journalctl-uhermes-agent-f
# If addToSystemPackages is true, test the CLI
hermesversion
hermesconfig# shows the generated config
Choosing a Deployment Mode
The module supports two modes, controlled by container.enable:
Native (default)
Container
How it runs
Hardened systemd service on the host
Persistent Ubuntu container with /nix/store bind-mounted
Security
NoNewPrivileges, ProtectSystem=strict, PrivateTmp
Container isolation, runs as unprivileged user inside
Agent can self-install packages
No — only tools on the Nix-provided PATH
Yes — apt, pip, npm installs persist across restarts
Config surface
Same
Same
When to choose
Standard deployments, maximum security, reproducibility
{
services.hermes-agent={enable=true;
container.enable=true;# ... rest of config is identical};}
Container mode auto-enables `virtualisation.docker.enable` via `mkDefault`. If you use Podman instead, set `container.backend = "podman"` and `virtualisation.docker.enable = false`.
Configuration
Declarative Settings
The settings option accepts an arbitrary attrset that is rendered as config.yaml. It supports deep merging across multiple module definitions (via lib.recursiveUpdate), so you can split config across files:
Both are deep-merged at evaluation time. Nix-declared keys always win over keys in an existing config.yaml on disk, but user-added keys that Nix doesn't touch are preserved. This means if the agent or a manual edit adds keys like skills.disabled or streaming.enabled, they survive nixos-rebuild switch.
note Model naming
settings.model.default uses the model identifier your provider expects. With OpenRouter (the default), these look like "anthropic/claude-sonnet-4" or "google/gemini-3-flash". If you're using a provider directly (Anthropic, OpenAI), set settings.model.base_url to point at their API and use their native model IDs (e.g., "claude-sonnet-4-20250514"). When no base_url is set, Hermes defaults to OpenRouter.
tip Discovering available config keys
Run nix build .#configKeys && cat result to see every leaf config key extracted from Python's DEFAULT_CONFIG. You can paste your existing config.yaml into the settings attrset — the structure maps 1:1.
danger Never put API keys in settings or environment
Values in Nix expressions end up in /nix/store, which is world-readable. Always use environmentFiles with a secrets manager.
Both environment (non-secret vars) and environmentFiles (secret files) are merged into $HERMES_HOME/.env at activation time (nixos-rebuild switch). Hermes reads this file on every startup, so changes take effect with a systemctl restart hermes-agent — no container recreation needed.
For platforms requiring OAuth (e.g., Discord), use authFile to seed credentials on first deploy:
{
services.hermes-agent={authFile= config.sops.secrets."hermes/auth.json".path;# authFileForceOverwrite = true; # overwrite on every activation};}
The file is only copied if auth.json doesn't already exist (unless authFileForceOverwrite = true). Runtime OAuth token refreshes are written to the state directory and preserved across rebuilds.
Documents
The documents option installs files into the agent's working directory (the workingDirectory, which the agent reads as its workspace). Hermes looks for specific filenames by convention:
USER.md — context about the user the agent is interacting with.
Any other files you place here are visible to the agent as workspace files.
The agent identity file is separate: Hermes loads its primary SOUL.md from $HERMES_HOME/SOUL.md, which in the NixOS module is ${services.hermes-agent.stateDir}/.hermes/SOUL.md. Putting SOUL.md in documents only creates a workspace file and will not replace the main persona file.
{
services.hermes-agent.documents={"USER.md"=./documents/USER.md;# path reference, copied from Nix store};}
Values can be inline strings or path references. Files are installed on every nixos-rebuild switch.
MCP Servers
The mcpServers option declaratively configures MCP (Model Context Protocol) servers. Each server uses either stdio (local command) or HTTP (remote URL) transport.
Stdio Transport (Local Servers)
{
services.hermes-agent.mcpServers={filesystem={command="npx";args=["-y""@modelcontextprotocol/server-filesystem""/data/workspace"];};github={command="npx";args=["-y""@modelcontextprotocol/server-github"];
env.GITHUB_PERSONAL_ACCESS_TOKEN="\${GITHUB_TOKEN}";# resolved from .env};};}
Environment variables in `env` values are resolved from `$HERMES_HOME/.env` at runtime. Use `environmentFiles` to inject secrets — never put tokens directly in Nix config.
Set auth = "oauth" for servers using OAuth 2.1. Hermes implements the full PKCE flow — metadata discovery, dynamic client registration, token exchange, and automatic refresh.
Tokens are stored in $HERMES_HOME/mcp-tokens/<server-name>.json and persist across restarts and rebuilds.
Initial OAuth authorization on headless servers
The first OAuth authorization requires a browser-based consent flow. In a headless deployment, Hermes prints the authorization URL to stdout/logs instead of opening a browser.
**Option A: Interactive bootstrap** — run the flow once via `docker exec` (container) or `sudo -u hermes` (native):
The container uses `--network=host`, so the OAuth callback listener on `127.0.0.1` is reachable from the host browser.
**Option B: Pre-seed tokens** — complete the flow on a workstation, then copy tokens:
When hermes runs via the NixOS module, the following CLI commands are blocked with a descriptive error pointing you to configuration.nix:
Blocked command
Why
hermes setup
Config is declarative — edit settings in your Nix config
hermes config edit
Config is generated from settings
hermes config set <key> <value>
Config is generated from settings
hermes gateway install
The systemd service is managed by NixOS
hermes gateway uninstall
The systemd service is managed by NixOS
This prevents drift between what Nix declares and what's on disk. Detection uses two signals:
HERMES_MANAGED=true environment variable — set by the systemd service, visible to the gateway process
.managed marker file in HERMES_HOME — set by the activation script, visible to interactive shells (e.g., docker exec -it hermes-agent hermes config set ... is also blocked)
To change configuration, edit your Nix config and run sudo nixos-rebuild switch.
Container Architecture
This section is only relevant if you're using `container.enable = true`. Skip it for native mode deployments.
When container mode is enabled, hermes runs inside a persistent Ubuntu container with the Nix-built binary bind-mounted read-only from the host:
The Nix-built binary works inside the Ubuntu container because /nix/store is bind-mounted — it brings its own interpreter and all dependencies, so there's no reliance on the container's system libraries. The container entrypoint resolves through a current-package symlink: /data/current-package/bin/hermes gateway run --replace. On nixos-rebuild switch, only the symlink is updated — the container keeps running.
What Persists Across What
Event
Container recreated?
/data (state)
/home/hermes
Writable layer (apt/pip/npm)
systemctl restart hermes-agent
No
Persists
Persists
Persists
nixos-rebuild switch (code change)
No (symlink updated)
Persists
Persists
Persists
Host reboot
No
Persists
Persists
Persists
nix-collect-garbage
No (GC root)
Persists
Persists
Persists
Image change (container.image)
Yes
Persists
Persists
Lost
Volume/options change
Yes
Persists
Persists
Lost
environment/environmentFiles change
No
Persists
Persists
Persists
The container is only recreated when its identity hash changes. The hash covers: schema version, image, extraVolumes, extraOptions, and the entrypoint script. Changes to environment variables, settings, documents, or the hermes package itself do not trigger recreation.
warning Writable layer loss
When the identity hash changes (image upgrade, new volumes, new container options), the container is destroyed and recreated from a fresh pull of container.image. Any apt install, pip install, or npm install packages in the writable layer are lost. State in /data and /home/hermes is preserved (these are bind mounts).
If the agent relies on specific packages, consider baking them into a custom image (container.image = "my-registry/hermes-base:latest") or scripting their installation in the agent's SOUL.md.
GC Root Protection
The preStart script creates a GC root at ${stateDir}/.gc-root pointing to the current hermes package. This prevents nix-collect-garbage from removing the running binary. If the GC root somehow breaks, restarting the service recreates it.
Plugins
The NixOS module supports declarative plugin installation — no imperative hermes plugins install needed.
Directory Plugins (extraPlugins)
For plugins that are just a source tree with plugin.yaml + __init__.py (e.g., hermes-lcm):
Plugins are symlinked into $HERMES_HOME/plugins/ at activation time. Hermes discovers them via its normal directory scan. Removing a plugin from the list and running nixos-rebuild switch removes the symlink.
Entry-Point Plugins (extraPythonPackages)
For pip-packaged plugins that register via [project.entry-points."hermes_agent.plugins"] (e.g., rtk-hermes):
A build-time collision check prevents plugin packages from shadowing core hermes dependencies. If a plugin provides a package already in the sealed venv, `nixos-rebuild` fails with a clear error.
Development
Dev Shell
The flake provides a development shell with Python 3.12, uv, Node.js, and all runtime tools:
cdhermes-agent
nixdevelop
# Shell provides:# - Python 3.12 + uv (deps installed into .venv on first entry)# - Node.js 22, ripgrep, git, openssh, ffmpeg on PATH# - Stamp-file optimization: re-entry is near-instant if deps haven't changed
hermessetup
hermeschat
direnv (Recommended)
The included .envrc activates the dev shell automatically:
cdhermes-agent
direnvallow# one-time# Subsequent entries are near-instant (stamp file skips dep install)
Flake Checks
The flake includes build-time verification that runs in CI and locally:
# Run all checks
nixflakecheck
# Individual checks
nixbuild.#checks.x86_64-linux.package-contents# binaries exist + version
nixbuild.#checks.x86_64-linux.entry-points-sync# pyproject.toml ↔ Nix package sync
nixbuild.#checks.x86_64-linux.cli-commands# gateway/config subcommands
nixbuild.#checks.x86_64-linux.managed-guard# HERMES_MANAGED blocks mutation
nixbuild.#checks.x86_64-linux.bundled-skills# skills present in package
nixbuild.#checks.x86_64-linux.config-roundtrip# merge script preserves user keys
What each check verifies
| Check | What it tests |
|---|---|
| `package-contents` | `hermes` and `hermes-agent` binaries exist and `hermes version` runs |
| `entry-points-sync` | Every `[project.scripts]` entry in `pyproject.toml` has a wrapped binary in the Nix package |
| `cli-commands` | `hermes --help` exposes `gateway` and `config` subcommands |
| `managed-guard` | `HERMES_MANAGED=true hermes config set ...` prints the NixOS error |
| `bundled-skills` | Skills directory exists, contains SKILL.md files, `HERMES_BUNDLED_SKILLS` is set in wrapper |
| `config-roundtrip` | 7 merge scenarios: fresh install, Nix override, user key preservation, mixed merge, MCP additive merge, nested deep merge, idempotency |
Options Reference
Core
Option
Type
Default
Description
enable
bool
false
Enable the hermes-agent service
package
package
hermes-agent
The hermes-agent package to use
user
str
"hermes"
System user
group
str
"hermes"
System group
createUser
bool
true
Auto-create user/group
stateDir
str
"/var/lib/hermes"
State directory (HERMES_HOME parent)
workingDirectory
str
"${stateDir}/workspace"
Agent working directory (MESSAGING_CWD)
addToSystemPackages
bool
false
Add hermes CLI to system PATH and set HERMES_HOME system-wide
Configuration
Option
Type
Default
Description
settings
attrs (deep-merged)
{}
Declarative config rendered as config.yaml. Supports arbitrary nesting; multiple definitions are merged via lib.recursiveUpdate
configFile
null or path
null
Path to an existing config.yaml. Overrides settings entirely if set
Secrets & Environment
Option
Type
Default
Description
environmentFiles
listOf str
[]
Paths to env files with secrets. Merged into $HERMES_HOME/.env at activation time
environment
attrsOf str
{}
Non-secret env vars. Visible in Nix store — do not put secrets here
authFile
null or path
null
OAuth credentials seed. Only copied on first deploy
authFileForceOverwrite
bool
false
Always overwrite auth.json from authFile on activation
Documents
Option
Type
Default
Description
documents
attrsOf (either str path)
{}
Workspace files. Keys are filenames, values are inline strings or paths. Installed into workingDirectory on activation
MCP Servers
Option
Type
Default
Description
mcpServers
attrsOf submodule
{}
MCP server definitions, merged into settings.mcp_servers
mcpServers.<name>.command
null or str
null
Server command (stdio transport)
mcpServers.<name>.args
listOf str
[]
Command arguments
mcpServers.<name>.env
attrsOf str
{}
Environment variables for the server process
mcpServers.<name>.url
null or str
null
Server endpoint URL (HTTP/StreamableHTTP transport)
Extra packages available to the agent. Added to the hermes user's per-user profile so terminal commands, skills, and cron jobs all see them
extraPlugins
listOf package
[]
Directory plugin packages to symlink into $HERMES_HOME/plugins/. Each must contain plugin.yaml
extraPythonPackages
listOf package
[]
Python packages added to PYTHONPATH for entry-point plugin discovery. Build with python312Packages
restart
str
"always"
systemd Restart= policy
restartSec
int
5
systemd RestartSec= value
Container
Option
Type
Default
Description
container.enable
bool
false
Enable OCI container mode
container.backend
enum ["docker" "podman"]
"docker"
Container runtime
container.image
str
"ubuntu:24.04"
Base image (pulled at runtime)
container.extraVolumes
listOf str
[]
Extra volume mounts (host:container:mode)
container.extraOptions
listOf str
[]
Extra args passed to docker create
container.hostUsers
listOf str
[]
Interactive users who get a ~/.hermes symlink to the service stateDir and are auto-added to the hermes group
Directory Layout
Native Mode
/var/lib/hermes/# stateDir (owned by hermes:hermes, 0750)├──.hermes/# HERMES_HOME│├──config.yaml# Nix-generated (deep-merged each rebuild)│├──.managed# Marker: CLI config mutation blocked│├──.env# Merged from environment + environmentFiles│├──auth.json# OAuth credentials (seeded, then self-managed)│├──gateway.pid│├──state.db│├──mcp-tokens/# OAuth tokens for MCP servers│├──sessions/│├──memories/│├──skills/│├──cron/│└──logs/├──home/# Agent HOME└──workspace/# MESSAGING_CWD├──SOUL.md# From documents option└──(agent-createdfiles)
Container Mode
Same layout, mounted into the container:
Container path
Host path
Mode
Notes
/nix/store
/nix/store
ro
Hermes binary + all Nix deps
/data
/var/lib/hermes
rw
All state, config, workspace
/home/hermes
${stateDir}/home
rw
Persistent agent home — pip install --user, tool caches
/usr, /usr/local, /tmp
(writable layer)
rw
apt/pip/npm installs — persists across restarts, lost on recreation
Updating
# Update the flake input (run from the directory containing flake.nix)cd/etc/nixos&&nixflakeupdatehermes-agent
# Rebuild
sudonixos-rebuildswitch
In container mode, the current-package symlink is updated and the agent picks up the new binary on restart. No container recreation, no loss of installed packages.
Troubleshooting
tip Podman users
All docker commands below work the same with podman. Substitute accordingly if you set container.backend = "podman".
Service Logs
# Both modes use the same systemd unit
journalctl-uhermes-agent-f
# Container mode: also available directly
dockerlogs-fhermes-agent