feat(config): add env.d/ drop-in directory for additional env files - #10139
feat(config): add env.d/ drop-in directory for additional env files#10139yzx9 wants to merge 6 commits into
Conversation
|
hi @yzx9 will triage and get this into upstream soon 🫡 |
|
@yzx9 pushed 4 commits on top of your branch — container mode was broken and needed fixes. The problemSymlinks to What changedThe key commit is
Also added Container mode securityThe copy means secrets hit persistent disk in container mode, which is the same as the old Ideally we wouldn't need to diverge the native/container paths in the NixOS module. One approach would be a tmpfs-backed staging dir ( Tested on NixOS unstable, container mode, sops-nix, Docker — service runs, all env vars load inside the container. |
|
@alt-glitch How about mounting the secrets directly into the container? This way, we can avoid copying them and instead let the secret manager handle the entire lifecycle. ${lib.concatStringsSep " " (lib.imap0 (i: f:
"--volume ${f}:${containerDataDir}/.hermes/env.d/nix-${toString i}.env:ro"
) cfg.environmentFiles)}and skip the secret symlink in the activation script of container mode: ${lib.concatStringsSep "\n" (lib.imap0 (i: f: ''
${lib.optionalString (!cfg.container.enable) ''
ln -sfn "${f}" "${cfg.stateDir}/.hermes/env.d/nix-${toString i}.env"
''}
'') cfg.environmentFiles)} |
|
@yzx9 Docker bakes --volume args at create time, not start time. Since environmentFiles would now affect the docker create command, it'd need to be part of containerIdentity ( But adding it to the identity hash means every change to environmentFiles (rotation, new key, path change) triggers docker rm -f + recreate. That wipes the writable layer: apt packages, pip/uv installs, ~/.venv, npm globals — all gone, and first-boot provisioning re-runs. See the recreation table in |
|
@alt-glitch Ah yeah, I completely forgot about this constraint on the Docker side.
I’m good with the current approach. I will add a comment explaining the divergence in container mode and its potential security implications. |
|
Would you like to take a look at the docker fixes in another PR? Would prefer standardisation between nix and nix container mode! |
|
@alt-glitch sure, happy to take a look |
|
@yzx9 if you can add a PR / update this PR for the docker updates as well, would love to get both of them merged together preferably. |
|
Hi @alt-glitch, As discussed above, we currently have two possible ideas:
|
|
@alt-glitch Hi, any updates? I think copying the secret with a note is enough for now. In practice, agenix also provides this workaround via |
Load $HERMES_HOME/env.d/*.env files (sorted alphabetically, override=True) after the main .env. This allows external secret managers (sops-nix, agenix) to provide env files via symlinks without copying secrets into .env.
environmentFiles are now symlinked into $HERMES_HOME/env.d/ (no copying), and environment vars are written to env.d/nix-environment.env. This allows sops-nix/agenix managed secrets to stay as symlinks to /nix/store.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- env-d-activation: runCommand check with 4 scenarios (env-only, symlinks, atomic replacement, stale symlink detection) - env-d-vm: nixosTest VM boot verifying permissions, symlinks, ownership, and env.d glob discoverability - Scenario D documents known stale symlink bug (non-fatal warning) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Container mode: symlink targets (e.g. /run/secrets/) are not mounted inside the container, so env.d/nix-N.env symlinks were broken. Now copies content via install(1) in container mode, preserving symlinks for native mode. Stale cleanup: find -delete removes old nix-*.env entries before creating new ones, so reducing environmentFiles count no longer leaves orphans. Co-Authored-By: Zexin Yuan <git@yzx9.xyz> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Scenario D: now tests the find-delete cleanup instead of warning about a bug that was already fixed - Scenario A: fix grep guard that silently swallowed failures - Remove redundant docstrings from symlink test functions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused Nix secret-lifecycle work. Current origin/main still concatenates environmentFiles into .env (nix/nixosModules.nix:832-846) and has no env.d loader (hermes_cli/env_loader.py:245-265), so the premise remains valid.
Problems
nix/nixosModules.nix:847wraps the cleanup incfg.environment != {} || cfg.environmentFiles != []. If both values are removed after a prior deployment, thefind ... -deleteblock is omitted and old Nix-managed env files remain loaded. The added test covers three files becoming one, not becoming zero (nix/checks.nix:647-664).website/docs/getting-started/nix-setup.md:360and :861 say secret files are symlinked with no copying, but container mode usesinstallto copy them (nix/nixosModules.nix:864-869). This should retain the container security caveat from the PR discussion.
Suggested changes
- Make targeted cleanup unconditional and add a transition-to-empty test.
- Document native symlinks versus container copies in both affected docs locations.
Automated hermes-sweeper review.
| # symlinked (native mode) or copied (container mode) into env.d/. | ||
| # Container mode copies because symlink targets (e.g. /run/secrets/) | ||
| # are not accessible inside the container. | ||
| ${lib.optionalString (cfg.environment != {} || cfg.environmentFiles != []) '' |
There was a problem hiding this comment.
This guard also suppresses find ... -delete when a user removes the last environmentFiles entry and clears environment. Existing nix-*.env / nix-environment.env files then survive and continue to load. Please run the targeted cleanup outside this conditional and add a transition-to-empty test.
| ::: | ||
|
|
||
| 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. | ||
| Both `environment` (non-secret vars) and `environmentFiles` (secret files) are placed in `$HERMES_HOME/env.d/` at activation time (`nixos-rebuild switch`). Non-secret vars are written to `env.d/nix-environment.env`; secret files are symlinked (no copying). Hermes reads all `*.env` files from this directory on every startup, so changes take effect with a `systemctl restart hermes-agent` — no container recreation needed. |
There was a problem hiding this comment.
Container mode does copy each secret via install in nix/nixosModules.nix:864-869; this statement's universal “symlinked (no copying)” claim is inaccurate. Please distinguish native symlinks from container copies and retain the persistent-disk security caveat here and in the option table.
What does this PR do?
Add
$HERMES_HOME/env.d/drop-in directory. Any*.envfiles placed there are loaded alphabetically after the main.envwithoverride=True. Useful for separating secrets from config, providing managed defaults that users shouldn't edit, or integrating with secret managers (sops-nix, agenix) via symlinks.$HERMES_HOME/env.d/drop-in directory for loading additional.envfilesenvironmentFilesintoenv.d/instead of copying secrets into.env. Non-secretenvironmentvars are written toenv.d/nix-environment.env.cc @alt-glitch as you create the NixOS modules
Related Issue
Previously, the NixOS activation script concatenated all
environmentFiles(e.g., sops-nix/agenix secrets) into$HERMES_HOME/.envviacat >>. This defeated the purpose of secret managers — secrets were copied into persistent state instead of staying as symlinks to secrets.Fixes #
Type of Change
Changes Made
hermes_cli/env_loader.py: After loading the main.env,load_hermes_dotenv()now scans$HERMES_HOME/env.d/*.env(sorted alphabetically) and loads each file withoverride=True. This works for all users, not just NixOS — drop any.envfile into~/.hermes/env.d/and it gets picked up.nix/nixosModules.nix: The activation script now:cfg.environmenttoenv.d/nix-environment.env(instead of.env)cfg.environmentFilesentry toenv.d/nix-<i>.env(instead ofcatmerging)env.d/to tmpfiles rulesowner/groupset to the hermes userDocs: Updated
environment-variables.md(env loading order) andnix-setup.md(secrets management, directory layout, options reference, troubleshooting).How to Test
pytest tests/hermes_cli/test_env_loader.py -v~/.hermes/env.d/test.envwithTEST_VAR=from-env-d, runhermes configto verify it's loadednix build .#nixosConfigurations.<host>and verifyenv.d/is created with correct symlinksChecklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AFor New Skills
hermes --toolsets skills -q "Use the X skill to do Y"Screenshots / Logs