Add comprehensive Nix documentation to the Docusaurus site at website/docs/getting-started/nix-setup.md, covering nix run/profile install, NixOS module (native + container modes), declarative settings, secrets management, MCP servers, managed mode, container architecture, dev shell, flake checks, and full options reference. - Register nix-setup in sidebar after installation page - Add Nix callout tip to installation.md linking to new guide - Add canonical version pointer in docs/nixos-setup.md
24 KiB
Nix Setup Guide for Hermes Agent
Canonical version: This document is maintained in the docs site at
website/docs/getting-started/nix-setup.md. The version below may be out of date.
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: nix run
nix run github:NousResearch/hermes-agent -- setup
nix run github:NousResearch/hermes-agent -- chat
No clone needed. Nix fetches and builds everything. All Python dependencies are Nix derivations via uv2nix — no runtime pip.
Install to Profile
# From remote
nix profile install github:NousResearch/hermes-agent
hermes setup
# From a clone
git clone https://github.com/NousResearch/hermes-agent.git
cd hermes-agent
nix build
./result/bin/hermes setup
Development Shell
cd hermes-agent
nix develop
# Shell provides:
# - Python 3.11 venv with all deps (via uv)
# - npm deps (agent-browser)
# - ripgrep, git, node on PATH
hermes setup
hermes chat
direnv (recommended)
The included .envrc activates the dev shell automatically:
cd hermes-agent
direnv allow # one-time
# Subsequent entries are near-instant (stamp file skips dep install)
NixOS Module
The flake exports nixosModules.default with two deployment modes:
| Mode | container.enable |
How it runs | Use case |
|---|---|---|---|
| Native (default) | false |
Hardened systemd service, runs directly on host | Standard deployment, maximum security |
| Container | true |
Persistent Ubuntu container, hermes binary bind-mounted from /nix/store |
Agent needs apt/pip/npm self-install capability |
Both modes share the same option surface. The module manages user creation, directory setup, config generation, secrets, documents, and service lifecycle.
Note: This module requires NixOS. For non-NixOS systems, use
nix profile install+ the CLI's built-inhermes gateway install.
Add the Flake Input
# /etc/nixos/flake.nix (or your system flake)
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
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
];
};
};
}
Minimal Configuration (Native Mode)
# configuration.nix
{
services.hermes-agent = {
enable = true;
settings.model.default = "anthropic/claude-sonnet-4";
environmentFiles = [ config.sops.secrets."hermes/env".path ];
addToSystemPackages = true; # puts `hermes` CLI on PATH
};
}
Important:
addToSystemPackages = truealso setsHERMES_HOMEsystem-wide so the interactive CLI shares state (sessions, skills, cron) with the gateway service. Without it, runninghermesin your shell creates a separate~/.hermesdirectory.
Minimal Configuration (Container Mode)
{
services.hermes-agent = {
enable = true;
container.enable = true;
settings.model.default = "anthropic/claude-sonnet-4";
environmentFiles = [ config.sops.secrets."hermes/env".path ];
addToSystemPackages = true;
};
}
Container mode auto-enables
virtualisation.docker.enableviamkDefault. Override withvirtualisation.docker.enable = false;if using podman (container.backend = "podman").
Full Example
{ config, ... }: {
services.hermes-agent = {
enable = true;
container.enable = true;
# ── Model ──────────────────────────────────────────────────────────
settings = {
model = {
base_url = "https://openrouter.ai/api/v1";
default = "anthropic/claude-opus-4.6";
};
toolsets = [ "all" ];
max_turns = 100;
terminal = { backend = "local"; cwd = "."; timeout = 180; };
compression = {
enabled = true;
threshold = 0.85;
summary_model = "google/gemini-3-flash-preview";
};
memory = { memory_enabled = true; user_profile_enabled = true; };
display = { compact = false; personality = "kawaii"; };
agent = { max_turns = 60; verbose = false; };
};
# ── Secrets ────────────────────────────────────────────────────────
# See "Secrets Management" section below
environmentFiles = [ config.sops.secrets."hermes/env".path ];
# ── Documents ──────────────────────────────────────────────────────
documents = {
"SOUL.md" = builtins.readFile /home/user/.hermes/SOUL.md;
"USER.md" = ./documents/USER.md; # path reference
};
# ── MCP Servers ────────────────────────────────────────────────────
mcpServers.filesystem = {
command = "npx";
args = [ "-y" "@modelcontextprotocol/server-filesystem" "/data/workspace" ];
};
mcpServers.remote-tools = {
url = "https://mcp.example.com/mcp";
auth = "oauth";
};
# ── Container options ──────────────────────────────────────────────
container = {
image = "ubuntu:24.04"; # default
backend = "docker"; # or "podman"
extraVolumes = [
"/home/user/projects:/projects:rw"
];
extraOptions = [
"--gpus" "all" # GPU passthrough
];
};
# ── Service tuning ─────────────────────────────────────────────────
addToSystemPackages = true;
extraArgs = [ "--verbose" ];
restart = "always";
restartSec = 5;
};
}
Secrets Management
Never put API keys in environment — values end up in the Nix store
(world-readable). Use environmentFiles with a secrets manager.
Both environment and environmentFiles are merged into $HERMES_HOME/.env
at activation time (nixos-rebuild switch). Hermes reads this file on every
startup via load_hermes_dotenv(), so changes take effect on
systemctl restart hermes-agent — no container recreation needed.
sops-nix
{
sops = {
defaultSopsFile = ./secrets/hermes.yaml;
age.keyFile = "/home/user/.config/sops/age/keys.txt";
secrets."hermes-env" = { format = "yaml"; };
};
services.hermes-agent.environmentFiles = [
config.sops.secrets."hermes-env".path
];
}
The secrets file should contain key-value pairs:
# secrets/hermes.yaml (encrypted with sops)
hermes-env: |
OPENROUTER_API_KEY=sk-or-...
TELEGRAM_BOT_TOKEN=123456:ABC...
ANTHROPIC_API_KEY=sk-ant-...
agenix
{
age.secrets.hermes-env.file = ./secrets/hermes-env.age;
services.hermes-agent.environmentFiles = [
config.age.secrets.hermes-env.path
];
}
OAuth / Auth Seeding
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 back to the state directory and preserved across rebuilds.
Container Mode: Architecture
When container.enable = true, hermes runs inside a persistent Ubuntu
container with the Nix-built binary bind-mounted from the host:
Host Container
──── ─────────
/nix/store/...-hermes-agent-0.1.0 ──► /nix/store/... (ro)
/var/lib/hermes/ ──► /data/ (rw)
├── current-package -> /nix/store/... (symlink, updated each rebuild)
├── .gc-root -> /nix/store/... (prevents nix-collect-garbage)
├── .container-identity (sha256 hash, triggers recreation)
├── .hermes/ (HERMES_HOME)
│ ├── .env (merged from environment + environmentFiles)
│ ├── config.yaml (Nix-generated, copied by activation)
│ ├── .managed (marker file)
│ ├── state.db
│ ├── mcp-tokens/ (OAuth tokens for MCP servers)
│ ├── sessions/
│ ├── memories/
│ └── ...
├── home/ ──► /home/hermes (rw)
└── workspace/ (MESSAGING_CWD)
├── SOUL.md (from documents option)
└── (agent-created files)
Container writable layer (apt/pip/npm): /usr, /tmp
The container entrypoint is /data/current-package/bin/hermes gateway run --replace,
which resolves through the symlink to the current Nix store path.
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. Changes to environment,
environmentFiles, settings, documents, or the hermes package itself do
not trigger recreation — environment variables are written to
$HERMES_HOME/.env by the activation script and read by hermes at startup.
A systemctl restart hermes-agent is sufficient for env changes.
When to Use Container Mode
Use container mode when:
- The agent needs to
apt install,pip install, ornpm installpackages at runtime - You want the agent to have a mutable Linux environment it can customize
- You're running untrusted or experimental tool configurations
Use native mode when:
- You want maximum security (systemd hardening:
NoNewPrivileges,ProtectSystem=strict) - The agent only needs tools already on the Nix-provided PATH
- You prefer a minimal, reproducible deployment
Managed Mode
When hermes runs via the NixOS module, the following CLI commands are blocked with a descriptive error:
| Blocked command | Reason |
|---|---|
hermes setup |
Config is declarative in configuration.nix |
hermes config edit |
Config is generated from settings |
hermes config set <key> <value> |
Config is generated from settings |
hermes gateway install |
Service is managed by NixOS |
hermes gateway uninstall |
Service is managed by NixOS |
Detection uses two signals:
HERMES_MANAGED=trueenvironment variable (set by the systemd service).managedmarker file inHERMES_HOME(set by the activation script, visible to interactive shells)
If you need to change configuration, edit your configuration.nix and run
sudo nixos-rebuild switch.
MCP Servers
The mcpServers option lets you declaratively configure
MCP (Model Context Protocol) servers.
Each server uses either stdio (local command) or HTTP (remote URL)
transport.
Stdio Transport (Local Servers)
For MCP servers that run as local subprocesses:
{
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 (with sops-nix or agenix) to inject
secrets — never put tokens directly in Nix config.
HTTP Transport (Remote Servers)
For remote MCP servers accessible via HTTP/StreamableHTTP:
{
services.hermes-agent.mcpServers = {
remote-api = {
url = "https://mcp.example.com/v1/mcp";
headers.Authorization = "Bearer \${MCP_REMOTE_API_KEY}";
timeout = 180;
};
};
}
HTTP Transport with OAuth
For remote MCP servers that use OAuth 2.1 for authentication, set
auth = "oauth". Hermes implements the full OAuth 2.1 PKCE flow via the
MCP SDK — including metadata discovery, dynamic client registration, token
exchange, and automatic refresh.
{
services.hermes-agent.mcpServers = {
my-oauth-server = {
url = "https://mcp.example.com/mcp";
auth = "oauth";
};
};
}
Tokens are stored in $HERMES_HOME/mcp-tokens/<server-name>.json and
persist across restarts and rebuilds. Token refresh is automatic.
Initial Authorization (Headless / Container)
The first OAuth authorization requires completing a browser-based consent flow. In a headless NixOS deployment (native or container), Hermes detects the absence of a display and prints the authorization URL to stdout/logs instead of opening a browser.
Option A: Interactive bootstrap — run the OAuth flow once via docker exec
(container mode) or sudo -u hermes (native mode):
# Container mode
docker exec -it hermes-agent \
hermes mcp add my-oauth-server --url https://mcp.example.com/mcp --auth oauth
# Native mode
sudo -u hermes HERMES_HOME=/var/lib/hermes/.hermes \
hermes mcp add my-oauth-server --url https://mcp.example.com/mcp --auth oauth
Since the container uses --network=host, the OAuth callback listener on
127.0.0.1 is reachable from the host. Open the printed URL in your
browser, complete consent, and the callback is received by Hermes inside
the container. Tokens are saved and reused automatically from then on.
Option B: Pre-seed tokens — complete the OAuth flow on a workstation first, then copy the token files to the server:
# On your workstation
hermes mcp add my-oauth-server --url https://mcp.example.com/mcp --auth oauth
# Copy tokens to the server
scp ~/.hermes/mcp-tokens/my-oauth-server.json \
server:/var/lib/hermes/.hermes/mcp-tokens/
scp ~/.hermes/mcp-tokens/my-oauth-server.client.json \
server:/var/lib/hermes/.hermes/mcp-tokens/
Ensure the files are owned by the hermes user (chown hermes:hermes)
and have mode 0600.
Sampling (Server-Initiated LLM Requests)
Some MCP servers can request LLM completions from the agent. Configure
this per-server with the sampling option:
{
services.hermes-agent.mcpServers.analysis = {
command = "npx";
args = [ "-y" "analysis-server" ];
sampling = {
enabled = true;
model = "google/gemini-3-flash";
max_tokens_cap = 4096;
timeout = 30;
max_rpm = 10;
};
};
}
Mixed Example
{
services.hermes-agent = {
environmentFiles = [ config.sops.secrets."hermes/env".path ];
mcpServers = {
# Local stdio server
filesystem = {
command = "npx";
args = [ "-y" "@modelcontextprotocol/server-filesystem" "/data/workspace" ];
};
# Remote server with API key auth
ink = {
url = "https://mcp.ml.ink/mcp";
headers.Authorization = "Bearer \${INK_API_KEY}";
};
# Remote server with OAuth
cloud-tools = {
url = "https://tools.example.com/mcp";
auth = "oauth";
timeout = 300;
connect_timeout = 30;
};
};
};
}
Options Reference
Core
| Option | Type | Default | Description |
|---|---|---|---|
enable |
bool |
false |
Enable the hermes-agent service |
package |
package |
hermes-agent |
The hermes-agent package |
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 so CLI and gateway share state |
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 (API keys). Passed as systemd EnvironmentFile= or docker --env-file |
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 |
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 (stdio transport) |
mcpServers.<name>.env |
attrsOf str |
{} |
Environment variables for the server process (stdio transport) |
mcpServers.<name>.url |
null or str |
null |
Server endpoint URL (HTTP/StreamableHTTP transport) |
mcpServers.<name>.headers |
attrsOf str |
{} |
HTTP headers, e.g. Authorization (HTTP transport) |
mcpServers.<name>.auth |
null or "oauth" |
null |
Authentication method. "oauth" enables OAuth 2.1 PKCE |
mcpServers.<name>.timeout |
null or int |
null |
Tool call timeout in seconds (default: 120) |
mcpServers.<name>.connect_timeout |
null or int |
null |
Initial connection timeout in seconds (default: 60) |
mcpServers.<name>.sampling |
null or submodule |
null |
Sampling configuration for server-initiated LLM requests |
Service Behavior
| Option | Type | Default | Description |
|---|---|---|---|
extraArgs |
listOf str |
[] |
Extra args for hermes gateway |
extraPackages |
listOf package |
[] |
Extra packages on service PATH (native mode only) |
restart |
str |
"always" |
systemd Restart= policy |
restartSec |
int |
5 |
systemd RestartSec= |
Container
| Option | Type | Default | Description |
|---|---|---|---|
container.enable |
bool |
false |
Enable OCI container mode |
container.backend |
enum ["docker" "podman"] |
"docker" |
Container runtime. Auto-enables virtualisation.docker.enable when "docker" |
container.image |
str |
"ubuntu:24.04" |
Base image. Pulled at runtime by Docker/Podman |
container.extraVolumes |
listOf str |
[] |
Extra volume mounts (host:container:mode) |
container.extraOptions |
listOf str |
[] |
Extra args passed to docker create |
Directory Layout
Native Mode
/var/lib/hermes/ # stateDir (owned by hermes:hermes, 0750)
├── .hermes/ # HERMES_HOME
│ ├── config.yaml # Nix-generated (overwritten each rebuild)
│ ├── .managed # Marker: CLI config mutation blocked
│ ├── .env # (not used — secrets via 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 (container mode: /home/hermes)
└── workspace/ # MESSAGING_CWD
├── SOUL.md # From documents option
└── (agent-created files)
Container Mode
Same layout, but mounted into the container as /data:
| 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 |
(container layer) | rw |
Persists — apt/pip/npm installs |
/tmp |
(container layer) | rw |
Persists across restarts (lost on recreation) |
Updating
# Update the flake input
nix flake update hermes-agent --flake /etc/nixos
# Rebuild — in container mode, the symlink updates without recreating the container
sudo nixos-rebuild switch
In container mode, the agent picks up the new binary immediately on restart.
No container recreation, no loss of apt/pip/npm installs.
Flake Checks
The flake includes build-time verification:
# Run all checks
nix flake check
# Individual checks
nix build .#checks.x86_64-linux.package-contents # binaries exist + version
nix build .#checks.x86_64-linux.cli-commands # gateway/config subcommands
nix build .#checks.x86_64-linux.managed-guard # HERMES_MANAGED blocks mutation
Troubleshooting
Service logs
# Native mode
journalctl -u hermes-agent -f
# Container mode — same unit name
journalctl -u hermes-agent -f
# Or directly from the container
docker logs -f hermes-agent
Container inspection
# Service status
systemctl status hermes-agent
# Container state
docker ps -a --filter name=hermes-agent
docker inspect hermes-agent --format='{{.State.Status}}'
# Shell into the container
docker exec -it hermes-agent bash
# Check symlink
docker exec hermes-agent readlink /data/current-package
# Check identity hash
docker exec hermes-agent cat /data/.container-identity
Force container recreation
If you need to reset the container writable layer (fresh Ubuntu):
sudo systemctl stop hermes-agent
docker rm -f hermes-agent
sudo rm /var/lib/hermes/.container-identity
sudo systemctl start hermes-agent
# Container will be recreated from scratch
GC root verification
# Ensure the running package is protected
nix-store --query --roots $(docker exec hermes-agent readlink /data/current-package)
Common issues
| Symptom | Cause | Fix |
|---|---|---|
Cannot save configuration: managed by NixOS |
CLI guards active | Edit configuration.nix and nixos-rebuild switch |
| Container recreated unexpectedly | extraVolumes, extraOptions, or image changed |
Expected behavior — writable layer is reset. Reinstall packages if needed |
hermes version shows old version after rebuild |
Container not restarted | systemctl restart hermes-agent |
Permission denied on /var/lib/hermes |
State dir is 0750 hermes:hermes |
Use docker exec or sudo -u hermes |
nix-collect-garbage removed hermes |
GC root missing or broken | Restart the service (preStart recreates the GC root) |