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.
No clone needed. Nix fetches, builds, and runs everything:
Terminal window
# 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.
Note
This module requires NixOS. For non-NixOS systems (macOS, other Linux distros), use nix profile install and the standard CLI workflow above.
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.
Info: Container-aware CLI
When container.enable = true and addToSystemPackages = true, everyhermes 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:
services.hermes-agent= {
container.enable=true;
container.hostUsers=["your-username"];
addToSystemPackages=true;
};
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:
security.sudo.extraRules=[{
users=["your-username"];
commands=[{
command="/run/current-system/sw/bin/podman";
options=["NOPASSWD"];
}];
}];
The CLI auto-detects when sudo is needed and uses it transparently. Without this, you’ll need to run sudo hermes chat manually.
Info
Container mode auto-enables virtualisation.docker.enable via mkDefault. If you use Podman instead, set container.backend = "podman" and virtualisation.docker.enable = false.
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 namingsettings.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.
Full example: all commonly customized settings
{ config,... }: {
services.hermes-agent= {
enable=true;
container.enable=true;
# ── Model ──────────────────────────────────────────────────────────
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.
# 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.
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.
The mcpServers option declaratively configures MCP (Model Context Protocol) servers. Each server uses either stdio (local command) or HTTP (remote URL) transport.
env.GITHUB_PERSONAL_ACCESS_TOKEN="\${GITHUB_TOKEN}"; # resolved from .env
};
};
}
Tip
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):
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.
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.
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.
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.
For plugins that are just a source tree with plugin.yaml + __init__.py (e.g., hermes-lcm):
services.hermes-agent.extraPlugins=[
(pkgs.fetchFromGitHub {
owner="stephenschoettler";
repo="hermes-lcm";
rev="v0.7.0";
hash="sha256-...";
})
];
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.
Plugins still need to be enabled in config.yaml. Add them via the declarative settings:
services.hermes-agent.settings.plugins.enabled=[
"hermes-lcm"
"rtk-rewrite"
];
Note
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.
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.