{"repo":"NousResearch/hermes-agent","public_id":"35580","display_label":"Docker env forwarding empty-secret fallback fixes","summary":"These items address a bug where empty-string forwarded secrets bypass the ~/.hermes/.env disk fallback in Docker, along with related hardening and an attribution mapping. Most members share the empty-env-forwarding theme.","subsystem":null,"counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":2,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":35583},"resolution":{"state":"resolved","by":"pr-35583"},"freshness":{"state":"verified","last_verified_at":"2026-06-01T02:20:01+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.4},"fix_edges":[{"issue_number":35580,"pr_number":35583,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":35580,"title":"[Bug]: docker_forward_env forwards empty-string secrets into sandbox, bypassing the ~/.hermes/.env disk fallback","url":null,"state":"closed"},{"kind":"pr","number":35583,"title":"fix(tools): fall back to .hermes/.env when forwarded secret is empty","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_docker_environment.py","tools/environments/docker.py"],"fix_hunks":[{"file":"tests/tools/test_docker_environment.py","hunks":["@@ -302,6 +302,40 @@ def test_init_env_args_prefers_shell_env_over_hermes_dotenv(monkeypatch):\n     assert \"value_from_dotenv\" not in args_str\n \n \n+def test_init_env_args_uses_hermes_dotenv_for_empty_shell_env(monkeypatch):\n+    \"\"\"A transient empty-string in the live env must fall back to .env, not win.\n+\n+    Regression: the disk fallback used to fire only on `value is None`, so a\n+    present-but-empty `MY_SECRET=\"\"` skipped it and was forwarded as `-e\n+    MY_SECRET=`, clobbering the correct value sitting in ~/.hermes/.env.\n+    \"\"\"\n+    env = _make_execute_only_env([\"MY_SECRET\"])\n+\n+    monkeypatch.setenv(\"MY_SECRET\", \"\")\n+    monkeypatch.setattr(docker_env, \"_load_hermes_env_vars\", lambda: {\"MY_SECRET\": \"value_from_dotenv\"})\n+\n+    args = env._build_init_env_args()\n+\n+    # Assert on the resolved value, not the printed -e flag: the disk value\n+    # must win and a blank \"MY_SECRET=\" flag must never be emitted.\n+    assert \"MY_SECRET=[redacted:secret-assignment]\" in args\n+    assert \"MY_SECRET=\" not in args\n+\n+\n+def test_init_env_args_never_forwards_blank_secret(monkeypatch):\n+    \"\"\"A legitimately-empty key with no disk value is not forwarded as -e KEY=.\"\"\"\n+    env = _make_execute_only_env([\"MY_SECRET\"])\n+\n+    monkeypatch.setenv(\"MY_SECRET\", \"\")\n+    monkeypatch.setattr(docker_env, \"_load_hermes_env_vars\", lambda: {})\n+\n+    args = env._build_init_env_args()\n+\n+    # The key must not appear at all — not even as an empty -e MY_SECRET= flag.\n+    assert not any(a.startswith(\"MY_SECRET=\") for a in args)\n+    assert \"MY_SECRET\" not in \" \".join(args)\n+\n+\n # ── docker_env tests ──────────────────────────────────────────────\n \n "]},{"file":"tools/environments/docker.py","hunks":["@@ -814,9 +814,9 @@ def _build_init_env_args(self) -> list[str]:\n         hermes_env = _load_hermes_env_vars() if forward_keys else {}\n         for key in sorted(forward_keys):\n             value = os.getenv(key)\n-            if value is None:\n+            if not value:\n                 value = hermes_env.get(key)\n-            if value is not None:\n+            if value:\n                 exec_env[key] = value\n \n         args = []\n"]}]},{"kind":"pr","number":35951,"title":"security: harden env and cron safeguards","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":36008,"title":"fix(docker): ignore empty forwarded env values","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#35583","number":35583},{"kind":"pr","value":"#35580","number":35580},{"kind":"pr","value":"#35583","number":35583},{"kind":"pr","value":"#35580","number":35580}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":36189,"title":"chore(attribution): map SiTaggart for PR #35583","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true}],"trust":{"default_tier":"verified","includes_suggested":false}}