# NousResearch/hermes-agent · Nix/NixOS matrix integration and improvements

## Open now

- Nothing open on GitHub.

These items address Matrix support on NixOS, including bug fixes for platform gating, overlay and package overridability, declarative plugin installation via NixOS module, and a migration from matrix-nio to mautrix-python.

## TL;DR

- 6 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 2 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #7461. Verify it addresses your case.
- **Recommended:** read PR #7461 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-7461,pr-15953`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.29`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #4594: \[Setup\]: Matrix does not work on NixOS setup
- #14453: feat(nix): declarative plugin installation for NixOS module

## Prior & competing PRs

- **#7461: fix(nix): gate matrix extra to Linux in \[all\] profile**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `pyproject.toml`, `tests/test_project_metadata.py`
  - fix hunks:
  - `pyproject.toml`

    ```diff
    @@ -88,10 +88,10 @@ all = [
       "hermes-agent[modal]",
       "hermes-agent[daytona]",
       "hermes-agent[messaging]",
    -  # matrix excluded: python-olm (required by matrix-nio[e2e]) is upstream-broken
    -  # on modern macOS (archived libolm, C++ errors with Clang 21+). Including it
    -  # here causes the entire [all] install to fail, dropping all other extras.
    -  # Users who need Matrix can install manually: pip install 'hermes-agent[matrix]'
    +  # matrix: python-olm (required by matrix-nio[e2e]) is upstream-broken on
    +  # modern macOS (archived libolm, C++ errors with Clang 21+).  On Linux the
    +  # [matrix] extra's own marker pulls in the [e2e] variant automatically.
    +  "hermes-agent[matrix]; sys_platform == 'linux'",
       "hermes-agent[cron]",
       "hermes-agent[cli]",
       "hermes-agent[dev]",
    ```
  - `tests/test_project_metadata.py`

    ```diff
    @@ -11,12 +11,19 @@ def _load_optional_dependencies():
         return project["optional-dependencies"]
     
     
    -def test_matrix_extra_exists_but_excluded_from_all():
    +def test_matrix_extra_linux_only_in_all():
         """matrix-nio[e2e] depends on python-olm which is upstream-broken on modern
         macOS (archived libolm, C++ errors with Clang 21+).  The [matrix] extra is
    -    kept for opt-in install but deliberately excluded from [all] so one broken
    -    upstream dep doesn't nuke every other extra during ``hermes update``."""
    +    included in [all] but gated to Linux via a platform marker so that
    +    ``hermes update`` doesn't fail on macOS."""
         optional_dependencies = _load_optional_dependencies()
     
         assert "matrix" in optional_dependencies
    +    # Must NOT be unconditional — python-olm has no macOS wheels.
         assert "hermes-agent[matrix]" not in optional_dependencies["all"]
    +    # Must be present with a Linux platform marker.
    +    linux_gated = [
    +        dep for dep in optional_dependencies["all"]
    +        if "matrix" in dep and "linux" in dep
    +    ]
    +    assert linux_gated, "expected hermes-agent[matrix] with sys_platform=='linux' marker in [all]"
    ```
  - `uv.lock`

    ```diff
    @@ -1661,7 +1661,7 @@ dependencies = [
         { name = "fal-client" },
         { name = "fire" },
         { name = "firecrawl-py" },
    -    { name = "httpx" },
    +    { name = "httpx", extra = ["socks"] },
         { name = "jinja2" },
         { name = "openai" },
         { name = "parallel-web" },
    ```

    ```diff
    @@ -1691,6 +1691,8 @@ all = [
         { name = "faster-whisper" },
         { name = "honcho-ai" },
         { name = "lark-oapi" },
    +    { name = "markdown", marker = "sys_platform == 'linux'" },
    +    { name = "matrix-nio", extra = ["e2e"], marker = "sys_platform == 'linux'" },
         { name = "mcp" },
         { name = "mistralai" },
         { name = "modal" },
    ```

    ```diff
    @@ -1827,6 +1829,7 @@ requires-dist = [
         { name = "hermes-agent", extras = ["homeassistant"], marker = "extra == 'all'" },
         { name = "hermes-agent", extras = ["honcho"], marker = "extra == 'all'" },
         { name = "hermes-agent", extras = ["honcho"], marker = "extra == 'termux'" },
    +    { name = "hermes-agent", extras = ["matrix"], marker = "sys_platform == 'linux' and extra == 'all'" },
         { name = "hermes-agent", extras = ["mcp"], marker = "extra == 'all'" },
         { name = "hermes-agent", extras = ["mcp"], marker = "extra == 'termux'" },
         { name = "hermes-agent", extras = ["messaging"], marker = "extra == 'all'" },
    ```

    ```diff
    @@ -1839,7 +1842,7 @@ requires-dist = [
         { name = "hermes-agent", extras = ["tts-premium"], marker = "extra == 'all'" },
         { name = "hermes-agent", extras = ["voice"], marker = "extra == 'all'" },
         { name = "honcho-ai", marker = "extra == 'honcho'", specifier = ">=2.0.1,<3" },
    -    { name = "httpx", specifier = ">=0.28.1,<1" },
    +    { name = "httpx", extras = ["socks"], specifier = ">=0.28.1,<1" },
         { name = "jinja2", specifier = ">=3.1.5,<4" },
         { name = "lark-oapi", marker = "extra == 'feishu'", specifier = ">=1.5.3,<2" },
         { name = "markdown", marker = "extra == 'matrix'", specifier = ">=3.6,<4" },
    ```

    ```diff
    @@ -2033,6 +2036,9 @@ wheels = [
     http2 = [
         { name = "h2" },
     ]
    +socks = [
    +    { name = "socksio" },
    +]
     
     [[package]]
     name = "httpx-sse"
    ```

    ```diff
    @@ -4500,6 +4506,15 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
     ]
     
    +[[package]]
    +name = "socksio"
    +version = "1.0.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" },
    +]
    +
     [[package]]
     name = "sounddevice"
     version = "0.5.5"
    ```
- **#15953: feat(nix): declarative plugin installation for NixOS module**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `flake.nix`, `nix/checks.nix`, `nix/hermes-agent.nix`, `nix/nixosModules.nix`, `nix/overlays.nix`, `nix/packages.nix`, `nix/python.nix`, `website/docs/getting-started/nix-setup.md`, `website/docs/guides/build-a-hermes-plugin.md`, `website/docs/user-guide/features/plugins.md`
  - fix hunks:
  - `flake.nix`

    ```diff
    @@ -36,6 +36,7 @@
     
           imports = [
             ./nix/packages.nix
    +        ./nix/overlays.nix
             ./nix/nixosModules.nix
             ./nix/checks.nix
             ./nix/devShell.nix
    ```
  - `nix/checks.nix`

    ```diff
    @@ -7,9 +7,7 @@
       perSystem = { pkgs, system, lib, ... }:
         let
           hermes-agent = inputs.self.packages.${system}.default;
    -      hermesVenv = pkgs.callPackage ./python.nix {
    -        inherit (inputs) uv2nix pyproject-nix pyproject-build-systems;
    -      };
    +      hermesVenv = hermes-agent.hermesVenv;
     
           configMergeScript = pkgs.callPackage ./configMergeScript.nix { };
     
    ```

    ```diff
    @@ -193,6 +191,35 @@ json.dump(sorted(leaf_paths(DEFAULT_CONFIG)), sys.stdout, indent=2)
               echo "ok" > $out/result
             '';
     
    +        # Verify extraPythonPackages PYTHONPATH injection
    +        extra-python-packages = let
    +          testPkg = pkgs.python312Packages.pyfiglet;
    +          hermesWithExtra = hermes-agent.override {
    +            extraPythonPackages = [ testPkg ];
    +          };
    +        in pkgs.runCommand "hermes-extra-python-packages" { } ''
    +          set -e
    +          echo "=== Checking extraPythonPackages PYTHONPATH injection ==="
    +
    +          grep -q "PYTHONPATH" ${hermesWithExtra}/bin/hermes || \
    +            (echo "FAIL: PYTHONPATH not in wrapper"; exit 1)
    +          echo "PASS: PYTHONPATH present in wrapper"
    +
    +          grep -q "${testPkg}" ${hermesWithExtra}/bin/hermes || \
    +            (echo "FAIL: test package path not in PYTHONPATH"; exit 1)
    +          echo "PASS: test package path found in wrapper"
    +
    +          echo "=== Checking base package has no PYTHONPATH ==="
    +          if grep -q "PYTHONPATH" ${hermes-agent}/bin/hermes; then
    +            echo "FAIL: base package should not have PYTHONPATH"; exit 1
    +          fi
    +          echo "PASS: base package clean"
    +
    +          echo "=== All extraPythonPackages checks passed ==="
    +          mkdir -p $out
    +          echo "ok" > $out/result
    +        '';
    +
             # ── Config merge + round-trip test ────────────────────────────────
             # Tests the merge script (Nix activation behavior) across 7
             # scenarios, then verifies Python's load_config() reads correctly.
    ```
  - `nix/hermes-agent.nix`

    ```diff
    @@ -0,0 +1,186 @@
    +# nix/hermes-agent.nix — Overridable Hermes Agent package
    +#
    +# callPackage auto-wires nixpkgs args; flake inputs are passed explicitly.
    +# Users override via: pkgs.hermes-agent.override { extraPythonPackages = [...]; }
    +{
    +  lib,
    +  stdenv,
    +  makeWrapper,
    +  callPackage,
    +  python312,
    +  nodejs_22,
    +  ripgrep,
    +  git,
    +  openssh,
    +  ffmpeg,
    +  tirith,
    +  # Flake inputs — passed explicitly by packages.nix and overlays.nix
    +  uv2nix,
    +  pyproject-nix,
    +  pyproject-build-systems,
    +  npm-lockfile-fix,
    +  # Overridable parameters
    +  extraPythonPackages ? [ ],
    +}:
    +let
    +  hermesVenv = callPackage ./python.nix {
    +    inherit uv2nix pyproject-nix pyproject-build-systems;
    +  };
    +
    +  hermesNpmLib = callPackage ./lib.nix {
    +    inherit npm-lockfile-fix;
    +  };
    +
    +  hermesTui = callPackage ./tui.nix {
    +    inherit hermesNpmLib;
    +  };
    +
    +  hermesWeb = callPackage ./web.nix {
    +    inherit hermesNpmLib;
    +  };
    +
    +  bundledSkills = lib.cleanSourceWith {
    +    src = ../skills;
    +    filter = path: _type: !(lib.hasInfix "/index-cache/" path);
    +  };
    +
    +  runtimeDeps = [
    +    nodejs_22
    +    ripgrep
    +    git
    +    openssh
    +    ffmpeg
    +    tirith
    +  ];
    +
    +  runtimePath = lib.makeBinPath runtimeDeps;
    +
    +  sitePackagesPath = python312.sitePackages;
    +
    +  # Walk propagatedBuildInputs to include transitive Python deps in PYTHONPATH.
    +  # Without this, a plugin listing e.g. requests as a dep would fail at runtime
    +  # if requests isn't already in the sealed uv2nix venv.
    +  allExtraPythonPackages = python312.pkgs.requiredPythonModules extraPythonPackages;
    +
    +  pythonPath = lib.makeSearchPath sitePackagesPath allExtraPythonPackages;
    +
    +  pyprojectHash = builtins.hashString "sha256" (builtins.readFile ../pyproject.toml);
    +  uvLockHash =
    +    if builtins.pathExists ../uv.lock then
    +      builtins.hashString "sha256" (builtins.readFile ../uv.lock)
    +    else
    +      "none";
    +in
    +stdenv.mkDerivation {
    +  pname = "hermes-agent";
    +  version = (builtins.fromTOML (builtins.readFile ../pyproject.toml)).project.version;
    +
    +  dontUnpack = true;
    +  dontBuild = true;
    +  nativeBuildInputs = [ makeWrapper ];
    +
    +  installPhase = ''
    +    runHook preInstall
    +
    +    mkdir -p $out/share/hermes-agent $out/bin
    +    cp -r ${bundledSkills} $out/share/hermes-agent/skills
    +    cp -r ${hermesWeb} $out/share/hermes-agent/web_dist
    +
    +    mkdir -p $out/ui-tui
    +    cp -r ${hermesTui}/lib/hermes-tui/* $out/ui-tui/
    +
    +    ${lib.concatMapStringsSep "\n"
    +      (name: ''
    +        makeWrapper ${hermesVenv}/bin/${name} $out/bin/${name} \
    +          --suffix PATH : "${runtimePath}" \
    +          --set HERMES_BUNDLED_SKILLS $out/share/hermes-agent/skills \
    +          --set HERMES_WEB_DIST $out/share/hermes-agent/web_dist \
    +          --set HERMES_TUI_DIR $out/ui-tui \
    +          --set HERMES_PYTHON ${hermesVenv}/bin/python3 \
    +          --set HERMES_NODE ${nodejs_22}/bin/node \
    +          ${lib.optionalString (extraPythonPackages != [ ]) ''--suffix PYTHONPATH : "${pythonPath}"''}
    +      '')
    +      [
    +        "hermes"
    +        "hermes-agent"
    +        "hermes-acp"
    +      ]
    +    }
    +
    +    ${lib.optionalString (extraPythonPackages != [ ]) ''
    +      echo "=== Checking for plugin/core package collisions ==="
    +      ${hermesVenv}/bin/python3 -c "
    +import pathlib, sys, re
    +
    +def canonical(name):
    +    return re.sub(r'[-_.]+', '-', name).lower()
    +
    +# Collect core venv package names
    +core = set()
    +venv_sp = pathlib.Path('${hermesVenv}/${sitePackagesPath}')
    +for di in venv_sp.glob('*.dist-info'):
    +    meta = di / 'METADATA'
    +    if meta.exists():
    +        for line in meta.read_text().splitlines():
    +            if line.startswith('Name:'):
    +                core.add(canonical(line.split(':', 1)[1].strip()))
    +                break
    +
    +# Check each extra package for collisions
    +extras_dirs = [${lib.concatMapStringsSep ", " (p: "'${toString p}'") allExtraPythonPackages}]
    +for edir in extras_dirs:
    +    sp = pathlib.Path(edir) / '${sitePackagesPath}'
    +    if not sp.exists():
    +        continue
    +    for di in sp.glob('*.dist-info'):
    +        meta = di / 'METADATA'
    +        if not meta.exists():
    +            continue
    +        for line in meta.read_text().splitlines():
    +            if line.startswith('Name:'):
    +                pkg = canonical(line.split(':', 1)[1].strip())
    +                if pkg in core:
    +                    print(f'ERROR: plugin package \"{pkg}\" collides with a package in hermes sealed venv', file=sys.stderr)
    +                    print(f'  from: {di}', file=sys.stderr)
    +                    print(f'  Remove this dependency from extraPythonPackages.', file=sys.stderr)
    +                    sys.exit(1)
    +                break
    +
    +print('No collisions found.')
    +      "
    +      echo "=== No collisions ==="
    +    ''}
    +
    +    runHook postInstall
    +  '';
    +
    +  passthru = {
    +    inherit hermesTui hermesWeb hermesNpmLib hermesVenv;
    +
    +    devShellHook = ''
    +      STAMP=".nix-stamps/hermes-agent"
    +      STAMP_VALUE="${pyprojectHash}:${uvLockHash}"
    +      if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP")" != "$STAMP_VALUE" ]; then
    +        echo "hermes-agent: installing Python dependencies..."
    +        uv venv .venv --python ${python312}/bin/python3 2>/dev/null || true
    +        source .venv/bin/activate
    +        uv pip install -e ".[all]"
    +        [ -d mini-swe-agent ] && uv pip install -e ./mini-swe-agent 2>/dev/null || true
    +        [ -d tinker-atropos ] && uv pip install -e ./tinker-atropos 2>/dev/null || true
    +        mkdir -p .nix-stamps
    +        echo "$STAMP_VALUE" > "$STAMP"
    +      else
    +        source .venv/bin/activate
    +        export HERMES_PYTHON=${hermesVenv}/bin/python3
    +      fi
    +    '';
    +  };
    +
    +  meta = with lib; {
    +    description = "AI agent with advanced tool-calling capabilities";
    +    homepage = "https://github.com/NousResearch/hermes-agent";
    +    mainProgram = "hermes";
    +    license = licenses.mit;
    +    platforms = platforms.unix;
    +  };
    +}
    ```
  - `nix/nixosModules.nix`

    ```diff
    @@ -28,6 +28,8 @@
     
       let
         cfg = config.services.hermes-agent;
    +    effectivePackage = if cfg.extraPythonPackages == [ ] then cfg.package
    +      else cfg.package.override { inherit (cfg) extraPythonPackages; };
         hermes-agent = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.default;
     
         # Deep-merge config type (from 0xrsydn/nix-hermes-agent)
    ```

    ```diff
    @@ -456,6 +458,52 @@
             description = "Extra packages available on PATH.";
           };
     
    +      extraPlugins = mkOption {
    +        type = types.listOf types.package;
    +        default = [ ];
    +        description = ''
    +          Directory-based plugin packages to symlink into the hermes plugins
    +          directory. Each package should contain a plugin.yaml and __init__.py
    +          at its root. Hermes discovers these automatically on startup.
    +        '';
    +        example = literalExpression ''
    +          [
    +            (pkgs.fetchFromGitHub {
    +              owner = "stephenschoettler";
    +              repo = "hermes-lcm";
    +              name = "hermes-lcm";
    +              rev = "v0.7.0";
    +              hash = "sha256-...";
    +            })
    +          ]
    +        '';
    +      };
    +
    +      extraPythonPackages = mkOption {
    +        type = types.listOf types.package;
    +        default = [ ];
    +        description = ''
    +          Python packages to add to PYTHONPATH for entry-point plugin discovery.
    +          These are pip-packaged plugins that register via the
    +          hermes_agent.plugins entry-point group. Each package must be built
    +          with the same Python interpreter as hermes (python312).
    +        '';
    +        example = literalExpression ''
    +          [
    +            (pkgs.python312Packages.buildPythonPackage {
    +              pname = "rtk-hermes";
    +              version = "1.0.0";
    +              src = pkgs.fetchFromGitHub {
    +                owner = "ogallotti";
    +                repo = "rtk-hermes";
    +                rev = "main";
    +                hash = "sha256-...";
    +              };
    +            })
    +          ]
    +        '';
    +      };
    +
           restart = mkOption {
             type = types.str;
             default = "always";
    ```

    ```diff
    @@ -570,7 +618,7 @@
           # so interactive shells share state (sessions, skills, cron) with the
           # gateway service instead of creating a separate ~/.hermes/.
           (lib.mkIf cfg.addToSystemPackages {
    -        environment.systemPackages = [ cfg.package ];
    +        environment.systemPackages = [ effectivePackage ];
             environment.variables.HERMES_HOME = "${cfg.stateDir}/.hermes";
           })
     
    ```

    ```diff
    @@ -581,6 +629,16 @@
             });
           })
     
    +      # ── Assertions ─────────────────────────────────────────────────────
    +      {
    +        assertions = let
    +          names = map lib.getName cfg.extraPlugins;
    +        in [{
    +          assertion = (lib.length names) == (lib.length (lib.unique names));
    +          message = "services.hermes-agent.extraPlugins: duplicate plugin names detected: ${toString names}. If using fetchFromGitHub, set name = \"plugin-name\" to disambiguate.";
    +        }];
    +      }
    +
           # ── Warnings ──────────────────────────────────────────────────────
           (lib.mkIf (cfg.container.enable && !cfg.addToSystemPackages && cfg.container.hostUsers != []) {
             warnings = [
    ```

    ```diff
    @@ -602,6 +660,7 @@
               "d ${cfg.stateDir}/.hermes/sessions 2770 ${cfg.user} ${cfg.group} - -"
               "d ${cfg.stateDir}/.hermes/logs   2770 ${cfg.user} ${cfg.group} - -"
               "d ${cfg.stateDir}/.hermes/memories 2770 ${cfg.user} ${cfg.group} - -"
    +          "d ${cfg.stateDir}/.hermes/plugins 2770 ${cfg.user} ${cfg.group} - -"
               "d ${cfg.stateDir}/home           0750 ${cfg.user} ${cfg.group} - -"
               "d ${cfg.workingDirectory}         2770 ${cfg.user} ${cfg.group} - -"
             ];
    ```

    ```diff
    @@ -623,7 +682,7 @@
               find ${cfg.stateDir}/.hermes -maxdepth 1 \
                 \( -name "*.db" -o -name "*.db-wal" -o -name "*.db-shm" -o -name "SOUL.md" \) \
                 -exec chmod g+rw {} + 2>/dev/null || true
    -          for _subdir in cron sessions logs memories; do
    +          for _subdir in cron sessions logs memories plugins; do
                 mkdir -p "${cfg.stateDir}/.hermes/$_subdir"
                 chown ${cfg.user}:${cfg.group} "${cfg.stateDir}/.hermes/$_subdir"
                 chmod 2770 "${cfg.stateDir}/.hermes/$_subdir"
    ```

    ```diff
    @@ -732,6 +791,22 @@ HERMES_NIX_ENV_EOF
               ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: _value: ''
                 install -o ${cfg.user} -g ${cfg.group} -m 0640 ${documentDerivation}/${name} ${cfg.workingDirectory}/${name}
               '') cfg.documents)}
    +
    +        # ── Declarative plugins ─────────────────────────────────────────
    +        # Remove stale managed symlinks (plugins removed from config)
    +        find ${cfg.stateDir}/.hermes/plugins -maxdepth 1 -type l -name 'nix-managed-*' -delete 2>/dev/null || true
    +
    +        ${lib.concatStringsSep "\n" (map (plugin:
    +          let
    +            name = lib.getName plugin;
    +          in ''
    +            if [ ! -f "${plugin}/plugin.yaml" ]; then
    +              echo "ERROR: extraPlugins entry '${plugin}' has no plugin.yaml" >&2
    +              exit 1
    +            fi
    +            ln -sfn ${plugin} ${cfg.stateDir}/.hermes/plugins/nix-managed-${name}
    +            chown -h ${cfg.user}:${cfg.group} ${cfg.stateDir}/.hermes/plugins/nix-managed-${name}
    +          '') cfg.extraPlugins)}
             '';
           }
     
    ```

    ```diff
    @@ -762,7 +837,7 @@ HERMES_NIX_ENV_EOF
                 # reads them at Python startup — no systemd EnvironmentFile needed.
     
                 ExecStart = lib.concatStringsSep " " ([
    -              "${cfg.package}/bin/hermes"
    +              "${effectivePackage}/bin/hermes"
                   "gateway"
                 ] ++ cfg.extraArgs);
     
    ```

    ```diff
    @@ -785,7 +860,7 @@ HERMES_NIX_ENV_EOF
               };
     
               path = [
    -            cfg.package
    +            effectivePackage
                 pkgs.bash
                 pkgs.coreutils
                 pkgs.git
    ```

    ```diff
    @@ -810,11 +885,11 @@ HERMES_NIX_ENV_EOF
     
               preStart = ''
                 # Stable symlinks — container references these, not store paths directly
    -            ln -sfn ${cfg.package} ${cfg.stateDir}/current-package
    +            ln -sfn ${effectivePackage} ${cfg.stateDir}/current-package
                 ln -sfn ${containerEntrypoint} ${cfg.stateDir}/current-entrypoint
     
                 # GC roots so nix-collect-garbage doesn't remove store paths in use
    -            ${pkgs.nix}/bin/nix-store --add-root ${cfg.stateDir}/.gc-root --indirect -r ${cfg.package} 2>/dev/null || true
    +            ${pkgs.nix}/bin/nix-store --add-root ${cfg.stateDir}/.gc-root --indirect -r ${effectivePackage} 2>/dev/null || true
                 ${pkgs.nix}/bin/nix-store --add-root ${cfg.stateDir}/.gc-root-entrypoint --indirect -r ${containerEntrypoint} 2>/dev/null || true
     
                 # Check if container needs (re)creation
    ```
  - `nix/overlays.nix`

    ```diff
    @@ -0,0 +1,10 @@
    +# nix/overlays.nix — Expose pkgs.hermes-agent for external NixOS configs
    +{ inputs, ... }:
    +{
    +  flake.overlays.default = final: _: {
    +    hermes-agent = final.callPackage ./hermes-agent.nix {
    +      inherit (inputs) uv2nix pyproject-nix pyproject-build-systems;
    +      npm-lockfile-fix = inputs.npm-lockfile-fix.packages.${final.stdenv.hostPlatform.system}.default;
    +    };
    +  };
    +}
    ```
  - `nix/packages.nix`

    ```diff
    @@ -4,120 +4,19 @@
       perSystem =
         { pkgs, inputs', ... }:
         let
    -      hermesVenv = pkgs.callPackage ./python.nix {
    +      hermesAgent = pkgs.callPackage ./hermes-agent.nix {
             inherit (inputs) uv2nix pyproject-nix pyproject-build-systems;
    -      };
    -
    -      hermesNpmLib = pkgs.callPackage ./lib.nix {
             npm-lockfile-fix = inputs'.npm-lockfile-fix.packages.default;
           };
    -
    -      hermesTui = pkgs.callPackage ./tui.nix {
    -        inherit hermesNpmLib;
    -      };
    -
    -      # Import bundled skills, excluding runtime caches
    -      bundledSkills = pkgs.lib.cleanSourceWith {
    -        src = ../skills;
    -        filter = path: _type: !(pkgs.lib.hasInfix "/index-cache/" path);
    -      };
    -
    -      hermesWeb = pkgs.callPackage ./web.nix {
    -        inherit hermesNpmLib;
    -      };
    -
    -      runtimeDeps = with pkgs; [
    -        nodejs_22
    -        ripgrep
    -        git
    -        openssh
    -        ffmpeg
    -        tirith
    -      ];
    -
    -      runtimePath = pkgs.lib.makeBinPath runtimeDeps;
    -
    -      # Lockfile hashes for dev shell stamps
    -      pyprojectHash = builtins.hashString "sha256" (builtins.readFile ../pyproject.toml);
    -      uvLockHash =
    -        if builtins.pathExists ../uv.lock then
    -          builtins.hashString "sha256" (builtins.readFile ../uv.lock)
    -        else
    -          "none";
         in
         {
           packages = {
    -        default = pkgs.stdenv.mkDerivation {
    -          pname = "hermes-agent";
    -          version = (fromTOML (builtins.readFile ../pyproject.toml)).project.version;
    -
    -          dontUnpack = true;
    -          dontBuild = true;
    -          nativeBuildInputs = [ pkgs.makeWrapper ];
    -
    -          installPhase = ''
    -            runHook preInstall
    -
    -            mkdir -p $out/share/hermes-agent $out/bin
    -            cp -r ${bundledSkills} $out/share/hermes-agent/skills
    -            cp -r ${hermesWeb} $out/share/hermes-agent/web_dist
    -
    -            # copy pre-built TUI (same layout as dev: ui-tui/dist/ + node_modules/)
    -            mkdir -p $out/ui-tui
    -            cp -r ${hermesTui}/lib/hermes-tui/* $out/ui-tui/
    -
    -            ${pkgs.lib.concatMapStringsSep "\n"
    -              (name: ''
    -                makeWrapper ${hermesVenv}/bin/${name} $out/bin/${name} \
    -                  --suffix PATH : "${runtimePath}" \
    -                  --set HERMES_BUNDLED_SKILLS $out/share/hermes-agent/skills \
    -                  --set HERMES_WEB_DIST $out/share/hermes-agent/web_dist \
    -                  --set HERMES_TUI_DIR $out/ui-tui \
    -                  --set HERMES_PYTHON ${hermesVenv}/bin/python3 \
    -                  --set HERMES_NODE ${pkgs.nodejs_22}/bin/node
    -              '')
    -              [
    -                "hermes"
    -                "hermes-agent"
    -                "hermes-acp"
    -              ]
    -            }
    -
    -            runHook postInstall
    -          '';
    -
    -          passthru.devShellHook = ''
    -            STAMP=".nix-stamps/hermes-agent"
    -            STAMP_VALUE="${pyprojectHash}:${uvLockHash}"
    -            if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP")" != "$STAMP_VALUE" ]; then
    -              echo "hermes-agent: installing Python dependencies..."
    -              uv venv .venv --python ${pkgs.python312}/bin/python3 2>/dev/null || true
    -              source .venv/bin/activate
    -              uv pip install -e ".[all]"
    -              [ -d mini-swe-agent ] && uv pip install -e ./mini-swe-agent 2>/dev/null || true
    -              [ -d tinker-atropos ] && uv pip install -e ./tinker-atropos 2>/dev/null || true
    -              mkdir -p .nix-stamps
    -              echo "$STAMP_VALUE" > "$STAMP"
    -            else
    -              source .venv/bin/activate
    -              export HERMES_PYTHON=${hermesVenv}/bin/python3
    -            fi
    -          '';
    -
    -          meta = with pkgs.lib; {
    -            description = "AI agent with advanced tool-calling capabilities";
    -            homepage = "https://github.com/NousResearch/hermes-agent";
    -            mainProgram = "hermes";
    -            license = licenses.mit;
    -            platforms = platforms.unix;
    -          };
    -        };
    -
    -        tui = hermesTui;
    -        web = hermesWeb;
    +        default = hermesAgent;
    +        tui = hermesAgent.hermesTui;
    +        web = hermesAgent.hermesWeb;
     
    -        fix-lockfiles = hermesNpmLib.mkFixLockfiles {
    -          packages = [ hermesTui hermesWeb ];
    +        fix-lockfiles = hermesAgent.hermesNpmLib.mkFixLockfiles {
    +          packages = [ hermesAgent.hermesTui hermesAgent.hermesWeb ];
             };
           };
         };
    ```
  - `nix/python.nix`

    ```diff
    @@ -7,6 +7,7 @@
       pyproject-nix,
       pyproject-build-systems,
       stdenv,
    +  dependency-groups ? [ "all" ],
     }:
     let
       workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./..; };
    ```

    ```diff
    @@ -96,5 +97,5 @@ let
           ]);
     in
     pythonSet.mkVirtualEnv "hermes-agent-env" {
    -  hermes-agent = [ "all" ];
    +  hermes-agent = dependency-groups;
     }
    ```
  - `website/docs/getting-started/nix-setup.md`

    ```diff
    @@ -599,6 +599,93 @@ The `preStart` script creates a GC root at `${stateDir}/.gc-root` pointing to th
     
     ---
     
    +## 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](https://github.com/stephenschoettler/hermes-lcm)):
    +
    +```nix
    +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.
    +
    +### Entry-Point Plugins (`extraPythonPackages`)
    +
    +For pip-packaged plugins that register via `[project.entry-points."hermes_agent.plugins"]` (e.g., [rtk-hermes](https://github.com/ogallotti/rtk-hermes)):
    +
    +```nix
    +services.hermes-agent.extraPythonPackages = [
    +  (pkgs.python312Packages.buildPythonPackage {
    +    pname = "rtk-hermes";
    +    version = "1.0.0";
    +    src = pkgs.fetchFromGitHub {
    +      owner = "ogallotti";
    +      repo = "rtk-hermes";
    +      rev = "v1.0.0";
    +      hash = "sha256-...";
    +    };
    +    format = "pyproject";
    +    build-system = [ pkgs.python312Packages.setuptools ];
    +  })
    +];
    +```
    +
    +The package's `site-packages` is added to PYTHONPATH in the hermes wrapper. `importlib.metadata` discovers the entry point at session start.
    +
    +### Combining Both
    +
    +A directory plugin with third-party Python dependencies needs both options:
    +
    +```nix
    +services.hermes-agent = {
    +  extraPlugins = [ my-plugin-src ];          # plugin source
    +  extraPythonPackages = [ pkgs.python312Packages.redis ];  # its Python dep
    +  extraPackages = [ pkgs.redis ];            # system binary it needs
    +};
    +```
    +
    +### Using the Overlay
    +
    +External flakes can override the package directly:
    +
    +```nix
    +{
    +  inputs.hermes-agent.url = "github:NousResearch/hermes-agent";
    +  outputs = { hermes-agent, nixpkgs, ... }: {
    +    nixpkgs.overlays = [ hermes-agent.overlays.default ];
    +    # Then: pkgs.hermes-agent.override { extraPythonPackages = [...]; }
    +  };
    +}
    +```
    +
    +### Plugin Configuration
    +
    +Plugins still need to be enabled in `config.yaml`. Add them via the declarative settings:
    +
    +```nix
    +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.
    +:::
    +
    +---
    +
     ## Development
     
     ### Dev Shell
    ```

    ```diff
    @@ -721,6 +808,8 @@ nix build .#checks.x86_64-linux.config-roundtrip    # merge script preserves use
     |---|---|---|---|
     | `extraArgs` | `listOf str` | `[]` | Extra args for `hermes gateway` |
     | `extraPackages` | `listOf package` | `[]` | Extra packages on service PATH (native mode only) |
    +| `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 |
     
    ```
  - `website/docs/guides/build-a-hermes-plugin.md`

    ```diff
    @@ -633,6 +633,43 @@ pip install hermes-plugin-calculator
     # Plugin auto-discovered on next hermes startup
     ```
     
    +### Distribute for NixOS
    +
    +NixOS users can install your plugin declaratively if you provide a `pyproject.toml` with entry points:
    +
    +**Entry-point plugins** (recommended for distribution):
    +```nix
    +# User's configuration.nix
    +services.hermes-agent.extraPythonPackages = [
    +  (pkgs.python312Packages.buildPythonPackage {
    +    pname = "my-plugin";
    +    version = "1.0.0";
    +    src = pkgs.fetchFromGitHub {
    +      owner = "you";
    +      repo = "hermes-my-plugin";
    +      rev = "v1.0.0";
    +      hash = "sha256-...";  # nix-prefetch-url --unpack
    +    };
    +    format = "pyproject";
    +    build-system = [ pkgs.python312Packages.setuptools ];
    +  })
    +];
    +```
    +
    +**Directory plugins** (no `pyproject.toml` needed):
    +```nix
    +services.hermes-agent.extraPlugins = [
    +  (pkgs.fetchFromGitHub {
    +    owner = "you";
    +    repo = "hermes-my-plugin";
    +    rev = "v1.0.0";
    +    hash = "sha256-...";
    +  })
    +];
    +```
    +
    +See the [Nix Setup guide](/docs/getting-started/nix-setup#plugins) for complete documentation including overlay usage and collision checking.
    +
     ## Common mistakes
     
     **Handler doesn't return JSON string:**
    ```
  - `website/docs/user-guide/features/plugins.md`

    ```diff
    @@ -99,6 +99,7 @@ Project-local plugins under `./.hermes/plugins/` are disabled by default. Enable
     | User | `~/.hermes/plugins/` | Personal plugins |
     | Project | `.hermes/plugins/` | Project-specific plugins (requires `HERMES_ENABLE_PROJECT_PLUGINS=true`) |
     | pip | `hermes_agent.plugins` entry_points | Distributed packages |
    +| Nix | `services.hermes-agent.extraPlugins` / `extraPythonPackages` | NixOS declarative installs — see [Nix Setup](/docs/getting-started/nix-setup#plugins) |
     
     Later sources override earlier ones on name collision, so a user plugin with the same name as a bundled plugin replaces it.
     
    ```

    ```diff
    @@ -155,6 +156,23 @@ Hermes has three kinds of plugins:
     
     Memory providers and context engines are **provider plugins** — only one of each type can be active at a time. General plugins can be enabled in any combination.
     
    +## NixOS declarative plugins
    +
    +On NixOS, plugins can be installed declaratively via the module options — no `hermes plugins install` needed. See the **[Nix Setup guide](/docs/getting-started/nix-setup#plugins)** for full details.
    +
    +```nix
    +services.hermes-agent = {
    +  # Directory plugin (source tree with plugin.yaml)
    +  extraPlugins = [ (pkgs.fetchFromGitHub { ... }) ];
    +  # Entry-point plugin (pip package)
    +  extraPythonPackages = [ (pkgs.python312Packages.buildPythonPackage { ... }) ];
    +  # Enable in config
    +  settings.plugins.enabled = [ "my-plugin" ];
    +};
    +```
    +
    +Declarative plugins are symlinked with a `nix-managed-` prefix — they coexist with manually installed plugins and are cleaned up automatically when removed from the Nix config.
    +
     ## Managing plugins
     
     ```bash
    ```
- **#7229: feat(nix): add overlay and make package overridable**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
- **#7488: refactor(matrix): migrate from matrix-nio to mautrix-python**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#4594`

## Fix status

- Merged to main in PR #7461 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-27`.
- Subsystem: `environments`
