{"repo":"NousResearch/hermes-agent","public_id":"15981","display_label":"Restrict writes to sensitive env credential files","summary":"These items concern identifying and enforcing write-deny protections on sensitive environment files to prevent accidental or unauthorized credential exposure.","subsystem":null,"counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":29687,"merged_at":"2026-05-21T06:37:37+00:00"},"resolution":{"state":"resolved","by":"pr-29687"},"freshness":{"state":"verified","last_verified_at":"2026-05-21T06:37:37+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":15981,"pr_number":29687,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-21T06:37:37+00:00"}],"members":[{"kind":"issue","number":15981,"title":"write_file tool bypasses credential protection for global ~/.hermes/.env","url":null,"state":"closed"},{"kind":"pr","number":17044,"title":"security(file-safety): also write-deny <root>/.env when running under a profile (#15981)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#29687","number":29687}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":17078,"title":"fix(security): deny writes to global ~/.hermes/.env","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#17044","number":17044},{"kind":"pr","value":"#15981","number":15981}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":29687,"title":"security(file-safety): also write-deny <root>/.env when running under a profile (#15981)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/file_safety.py","tests/tools/test_write_deny.py"],"fix_hunks":[{"file":"agent/file_safety.py","hunks":["@@ -16,9 +16,19 @@ def _hermes_home_path() -> Path:\n         return Path(os.path.expanduser(\"~/.hermes\"))\n \n \n+def _hermes_root_path() -> Path:\n+    \"\"\"Resolve the Hermes root dir (always the parent of any profile, never per-profile).\"\"\"\n+    try:\n+        from hermes_constants import get_default_hermes_root  # local import to avoid cycles\n+        return get_default_hermes_root()\n+    except Exception:\n+        return Path(os.path.expanduser(\"~/.hermes\"))\n+\n+\n def build_write_denied_paths(home: str) -> set[str]:\n     \"\"\"Return exact sensitive paths that must never be written.\"\"\"\n     hermes_home = _hermes_home_path()\n+    hermes_root = _hermes_root_path()\n     return {\n         os.path.realpath(p)\n         for p in [","@@ -26,7 +36,11 @@ def build_write_denied_paths(home: str) -> set[str]:\n             os.path.join(home, \".ssh\", \"id_rsa\"),\n             os.path.join(home, \".ssh\", \"id_ed25519\"),\n             os.path.join(home, \".ssh\", \"config\"),\n+            # Active profile .env (or top-level .env when not in profile mode).\n             str(hermes_home / \".env\"),\n+            # Top-level .env, even when running under a profile — overwriting it\n+            # leaks credentials across every profile that inherits from root (#15981).\n+            str(hermes_root / \".env\"),\n             os.path.join(home, \".bashrc\"),\n             os.path.join(home, \".zshrc\"),\n             os.path.join(home, \".profile\"),"]},{"file":"tests/tools/test_write_deny.py","hunks":["@@ -41,6 +41,31 @@ def test_hermes_env(self):\n         path = str(get_hermes_home() / \".env\")\n         assert _is_write_denied(path) is True\n \n+    def test_hermes_root_env_when_running_under_profile(self, tmp_path, monkeypatch):\n+        \"\"\"Top-level ``<root>/.env`` stays write-denied even when running under\n+        a profile (#15981).\n+\n+        Before the fix, ``build_write_denied_paths`` only added\n+        ``<active_profile>/.env`` to the deny list, so the global\n+        ``~/.hermes/.env`` (whose credentials are inherited by every profile)\n+        could be silently overwritten by ``write_file`` while a profile was\n+        active.\n+        \"\"\"\n+        root = tmp_path / \"hermes_root\"\n+        profile_home = root / \"profiles\" / \"coder\"\n+        profile_home.mkdir(parents=True)\n+        global_env = root / \".env\"\n+        global_env.write_text(\"OPENAI_API_KEY=[redacted:secret-assignment]\")\n+\n+        monkeypatch.setenv(\"HERMES_HOME\", str(profile_home))\n+\n+        # Sanity check: HERMES_HOME does point to the profile dir, not the root.\n+        from hermes_constants import get_hermes_home, get_default_hermes_root\n+        assert get_hermes_home() == profile_home\n+        assert get_default_hermes_root() == root\n+\n+        assert _is_write_denied(str(global_env)) is True\n+\n     def test_shell_profiles(self):\n         home = str(Path.home())\n         for name in [\".bashrc\", \".zshrc\", \".profile\", \".bash_profile\", \".zprofile\"]:\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}