| --- |
| sidebar_position: 3 |
| title: "Nix & NixOS Setup" |
| description: "Install and deploy Hermes Agent with Nix β from quick `nix run` to fully declarative NixOS module with container mode" |
| --- |
| |
| # Nix & NixOS Setup |
|
|
| Hermes Agent ships a Nix flake with three levels of integration: |
|
|
| | Level | Who it's for | What you get | |
| |-------|-------------|--------------| |
| | **`nix run` / `nix profile install`** | Any Nix user (macOS, Linux) | Pre-built binary with all deps β then use the standard CLI workflow | |
| | **NixOS module (native)** | NixOS server deployments | Declarative config, hardened systemd service, managed secrets | |
| | **NixOS module (container)** | Agents that need self-modification | 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](https://github.com/pyproject-nix/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](https://install.determinate.systems) 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: |
|
|
| ```bash |
| # Run directly (builds on first use, cached after) |
| nix run github:NousResearch/hermes-agent -- setup |
| nix run github:NousResearch/hermes-agent -- chat |
| |
| # Or install persistently |
| nix profile install github:NousResearch/hermes-agent |
| hermes setup |
| hermes chat |
| ``` |
|
|
| After `nix profile install`, `hermes`, `hermes-agent`, and `hermes-acp` are on your PATH. From here, the workflow is identical to the [standard installation](./installation.md) β `hermes setup` walks you through provider selection, `hermes gateway install` sets up a launchd (macOS) or systemd user service, and config lives in `~/.hermes/`. |
|
|
| <details> |
| <summary><strong>Building from a local clone</strong></summary> |
|
|
| ```bash |
| git clone https://github.com/NousResearch/hermes-agent.git |
| cd hermes-agent |
| nix build |
| ./result/bin/hermes setup |
| ``` |
|
|
| </details> |
|
|
| --- |
|
|
| ## NixOS Module |
|
|
| 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. |
| ::: |
|
|
| ### Add the Flake Input |
|
|
| ```nix |
| # /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 |
| ]; |
| }; |
| }; |
| } |
| ``` |
|
|
| ### Minimal Configuration |
|
|
| ```nix |
| # configuration.nix |
| { config, ... }: { |
| services.hermes-agent = { |
| enable = true; |
| settings.model.default = "anthropic/claude-sonnet-4"; |
| environmentFiles = [ config.sops.secrets."hermes-env".path ]; |
| addToSystemPackages = true; |
| }; |
| } |
| ``` |
|
|
| 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](https://github.com/Mic92/sops-nix) or [agenix](https://github.com/ryantm/agenix) configured. The file should contain at least one LLM provider key (e.g., `OPENROUTER_API_KEY=sk-or-...`). See [Secrets Management](#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: |
|
|
| ```bash |
| echo "OPENROUTER_API_KEY=sk-or-your-key" | sudo install -m 0600 -o hermes /dev/stdin /var/lib/hermes/env |
| ``` |
|
|
| ```nix |
| services.hermes-agent.environmentFiles = [ "/var/lib/hermes/env" ]; |
| ``` |
| ::: |
|
|
| :::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`, **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: |
|
|
| ```nix |
| 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: |
|
|
| ```nix |
| 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. |
| ::: |
|
|
| ### Verify It Works |
|
|
| After `nixos-rebuild switch`, check that the service is running: |
|
|
| ```bash |
| # Check service status |
| systemctl status hermes-agent |
| |
| # Watch logs (Ctrl+C to stop) |
| journalctl -u hermes-agent -f |
| |
| # If addToSystemPackages is true, test the CLI |
| hermes version |
| hermes config # 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 | Agent needs runtime package installation, mutable environment, experimental tools | |
|
|
| To enable container mode, add one line: |
|
|
| ```nix |
| { |
| services.hermes-agent = { |
| enable = true; |
| container.enable = true; |
| # ... rest of config is identical |
| }; |
| } |
| ``` |
|
|
| :::info |
| 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: |
|
|
| ```nix |
| # base.nix |
| services.hermes-agent.settings = { |
| model.default = "anthropic/claude-sonnet-4"; |
| toolsets = [ "all" ]; |
| terminal = { backend = "local"; timeout = 180; }; |
| }; |
| |
| # personality.nix |
| services.hermes-agent.settings = { |
| display = { compact = false; personality = "kawaii"; }; |
| memory = { memory_enabled = true; user_profile_enabled = true; }; |
| }; |
| ``` |
|
|
| 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](https://openrouter.ai) (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. |
| ::: |
|
|
| <details> |
| <summary><strong>Full example: all commonly customized settings</strong></summary> |
|
|
| ```nix |
| { 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 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| environmentFiles = [ config.sops.secrets."hermes-env".path ]; |
| |
| # ββ Documents ββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| documents = { |
| "USER.md" = ./documents/USER.md; |
| }; |
| |
| # ββ MCP Servers ββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| mcpServers.filesystem = { |
| command = "npx"; |
| args = [ "-y" "@modelcontextprotocol/server-filesystem" "/data/workspace" ]; |
| }; |
| |
| # ββ Container options ββββββββββββββββββββββββββββββββββββββββββββββ |
| container = { |
| image = "ubuntu:24.04"; |
| backend = "docker"; |
| hostUsers = [ "your-username" ]; |
| extraVolumes = [ "/home/user/projects:/projects:rw" ]; |
| extraOptions = [ "--gpus" "all" ]; |
| }; |
| |
| # ββ Service tuning βββββββββββββββββββββββββββββββββββββββββββββββββ |
| addToSystemPackages = true; |
| extraArgs = [ "--verbose" ]; |
| restart = "always"; |
| restartSec = 5; |
| }; |
| } |
| ``` |
|
|
| </details> |
|
|
| ### Escape Hatch: Bring Your Own Config |
|
|
| If you'd rather manage `config.yaml` entirely outside Nix, use `configFile`: |
|
|
| ```nix |
| services.hermes-agent.configFile = /etc/hermes/config.yaml; |
| ``` |
|
|
| This bypasses `settings` entirely β no merge, no generation. The file is copied as-is to `$HERMES_HOME/config.yaml` on each activation. |
|
|
| ### Customization Cheatsheet |
|
|
| Quick reference for the most common things Nix users want to customize: |
|
|
| | I want to... | Option | Example | |
| |---|---|---| |
| | Change the LLM model | `settings.model.default` | `"anthropic/claude-sonnet-4"` | |
| | Use a different provider endpoint | `settings.model.base_url` | `"https://openrouter.ai/api/v1"` | |
| | Add API keys | `environmentFiles` | `[ config.sops.secrets."hermes-env".path ]` | |
| | Give the agent a personality | `${services.hermes-agent.stateDir}/.hermes/SOUL.md` | manage the file directly | |
| | Add MCP tool servers | `mcpServers.<name>` | See [MCP Servers](#mcp-servers) | |
| | Mount host directories into container | `container.extraVolumes` | `[ "/data:/data:rw" ]` | |
| | Pass GPU access to container | `container.extraOptions` | `[ "--gpus" "all" ]` | |
| | Use Podman instead of Docker | `container.backend` | `"podman"` | |
| | Share state between host CLI and container | `container.hostUsers` | `[ "sidbin" ]` | |
| | Add tools to the service PATH (native only) | `extraPackages` | `[ pkgs.pandoc pkgs.imagemagick ]` | |
| | Use a custom base image | `container.image` | `"ubuntu:24.04"` | |
| | Override the hermes package | `package` | `inputs.hermes-agent.packages.${system}.default.override { ... }` | |
| | Change state directory | `stateDir` | `"/opt/hermes"` | |
| | Set the agent's working directory | `workingDirectory` | `"/home/user/projects"` | |
|
|
| --- |
|
|
| ## Secrets Management |
|
|
| :::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. |
|
|
| ### sops-nix |
|
|
| ```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 contains key-value pairs: |
|
|
| ```yaml |
| # secrets/hermes.yaml (encrypted with sops) |
| hermes-env: | |
| OPENROUTER_API_KEY=sk-or-... |
| TELEGRAM_BOT_TOKEN=123456:ABC... |
| ANTHROPIC_API_KEY=sk-ant-... |
| ``` |
|
|
| ### agenix |
|
|
| ```nix |
| { |
| 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: |
|
|
| ```nix |
| { |
| 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. |
|
|
| ```nix |
| { |
| 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)](https://modelcontextprotocol.io) servers. Each server uses either **stdio** (local command) or **HTTP** (remote URL) transport. |
|
|
| ### Stdio Transport (Local Servers) |
|
|
| ```nix |
| { |
| 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 |
| }; |
| }; |
| } |
| ``` |
|
|
| :::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. |
| ::: |
|
|
| ### HTTP Transport (Remote Servers) |
|
|
| ```nix |
| { |
| 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 |
|
|
| 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. |
|
|
| ```nix |
| { |
| 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. |
|
|
| <details> |
| <summary><strong>Initial OAuth authorization on headless servers</strong></summary> |
|
|
| 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): |
|
|
| ```bash |
| # 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 |
| ``` |
|
|
| 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: |
|
|
| ```bash |
| hermes mcp add my-oauth-server --url https://mcp.example.com/mcp --auth oauth |
| scp ~/.hermes/mcp-tokens/my-oauth-server{,.client}.json \ |
| server:/var/lib/hermes/.hermes/mcp-tokens/ |
| # Ensure: chown hermes:hermes, chmod 0600 |
| ``` |
|
|
| </details> |
|
|
| ### Sampling (Server-Initiated LLM Requests) |
|
|
| Some MCP servers can request LLM completions from the agent: |
|
|
| ```nix |
| { |
| 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; |
| }; |
| }; |
| } |
| ``` |
|
|
| --- |
|
|
| ## Managed Mode |
|
|
| 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: |
|
|
| 1. **`HERMES_MANAGED=true`** environment variable β set by the systemd service, visible to the gateway process |
| 2. **`.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 |
| |
| :::info |
| 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: |
| |
| ``` |
| Host Container |
| ββββ βββββββββ |
| /nix/store/...-hermes-agent-0.1.0 βββΊ /nix/store/... (ro) |
| ~/.hermes -> /var/lib/hermes/.hermes (symlink bridge, per hostUsers) |
| /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, deep-merged by activation) |
| β βββ .managed (marker file) |
| β βββ .container-mode (routing metadata: backend, exec_user, etc.) |
| β βββ state.db, sessions/, memories/ (runtime state) |
| β βββ mcp-tokens/ (OAuth tokens for MCP servers) |
| βββ home/ βββΊ /home/hermes (rw) |
| βββ workspace/ (MESSAGING_CWD) |
| βββ SOUL.md (from documents option) |
| βββ (agent-created files) |
| |
| Container writable layer (apt/pip/npm): /usr, /usr/local, /tmp |
| ``` |
| |
| 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. |
| |
| --- |
| |
| ## Development |
| |
| ### Dev Shell |
| |
| The flake provides a development shell with Python 3.11, uv, Node.js, and all runtime tools: |
| |
| ```bash |
| cd hermes-agent |
| nix develop |
| |
| # Shell provides: |
| # - Python 3.11 + uv (deps installed into .venv on first entry) |
| # - Node.js 20, ripgrep, git, openssh, ffmpeg on PATH |
| # - Stamp-file optimization: re-entry is near-instant if deps haven't changed |
| |
| hermes setup |
| hermes chat |
| ``` |
| |
| ### direnv (Recommended) |
| |
| The included `.envrc` activates the dev shell automatically: |
| |
| ```bash |
| cd hermes-agent |
| direnv allow # 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: |
| |
| ```bash |
| # 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.entry-points-sync # pyproject.toml β Nix package sync |
| nix build .#checks.x86_64-linux.cli-commands # gateway/config subcommands |
| nix build .#checks.x86_64-linux.managed-guard # HERMES_MANAGED blocks mutation |
| nix build .#checks.x86_64-linux.bundled-skills # skills present in package |
| nix build .#checks.x86_64-linux.config-roundtrip # merge script preserves user keys |
| ``` |
| |
| <details> |
| <summary><strong>What each check verifies</strong></summary> |
| |
| | 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 | |
| |
| </details> |
| |
| --- |
| |
| ## 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) | |
| | `mcpServers.<name>.headers` | `attrsOf str` | `{}` | HTTP headers, e.g. `Authorization` | |
| | `mcpServers.<name>.auth` | `null` or `"oauth"` | `null` | Authentication method. `"oauth"` enables OAuth 2.1 PKCE | |
| | `mcpServers.<name>.enabled` | `bool` | `true` | Enable or disable this server | |
| | `mcpServers.<name>.timeout` | `null` or `int` | `null` | Tool call timeout in seconds (default: 120) | |
| | `mcpServers.<name>.connect_timeout` | `null` or `int` | `null` | Connection timeout in seconds (default: 60) | |
| | `mcpServers.<name>.tools` | `null` or `submodule` | `null` | Tool filtering (`include`/`exclude` lists) | |
| | `mcpServers.<name>.sampling` | `null` or `submodule` | `null` | Sampling config 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=` 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-created files) |
| ``` |
| |
| ### 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 |
| |
| ```bash |
| # Update the flake input |
| nix flake update hermes-agent --flake /etc/nixos |
| |
| # Rebuild |
| sudo nixos-rebuild switch |
| ``` |
| |
| 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 |
| |
| ```bash |
| # Both modes use the same systemd unit |
| journalctl -u hermes-agent -f |
| |
| # Container mode: also available directly |
| docker logs -f hermes-agent |
| ``` |
| |
| ### Container Inspection |
| |
| ```bash |
| systemctl status hermes-agent |
| docker ps -a --filter name=hermes-agent |
| docker inspect hermes-agent --format='{{.State.Status}}' |
| docker exec -it hermes-agent bash |
| docker exec hermes-agent readlink /data/current-package |
| docker exec hermes-agent cat /data/.container-identity |
| ``` |
| |
| ### Force Container Recreation |
| |
| If you need to reset the writable layer (fresh Ubuntu): |
| |
| ```bash |
| sudo systemctl stop hermes-agent |
| docker rm -f hermes-agent |
| sudo rm /var/lib/hermes/.container-identity |
| sudo systemctl start hermes-agent |
| ``` |
| |
| ### Verify Secrets Are Loaded |
| |
| If the agent starts but can't authenticate with the LLM provider, check that the `.env` file was merged correctly: |
| |
| ```bash |
| # Native mode |
| sudo -u hermes cat /var/lib/hermes/.hermes/.env |
| |
| # Container mode |
| docker exec hermes-agent cat /data/.hermes/.env |
| ``` |
| |
| ### GC Root Verification |
| |
| ```bash |
| 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 β writable layer resets. Reinstall packages or use a custom image | |
| | `hermes version` shows old version | 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 | Restart the service (preStart recreates the GC root) | |
| | `no container with name or ID "hermes-agent"` (Podman) | Podman rootful container not visible to regular user | Add passwordless sudo for podman (see [Container-aware CLI](#container-aware-cli) section) | |
| | `unable to find user hermes` | Container still starting (entrypoint hasn't created user yet) | Wait a few seconds and retry β the CLI retries automatically | |
| |