Protecting Hermes control-plane files from write_file/patch modification
Cluster 14072. Data as of
Nothing open on GitHub. Fix merged in #30397 on 22 May 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #30397. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #30397. That is separate from the fix event. - Still open
- Nothing open on GitHub.
One issue reports that write_file/patch can modify ~/.hermes control-plane files such as auth.json, config.yaml, and webhook subscriptions. Two pull requests with near-identical titles propose protecting those files from prompt injection; the titles alone do not show their status or implementation details.
Open issues0
Open PRs0
Fixes on record
-
#30397 fix(security): protect Hermes control-plane files from prompt injection (#14157)
files:
agent/file_safety.py,infographic/pr-14157-control-plane-write-deny/infographic.png,tests/tools/test_file_operations.pyShow the diff
-
agent/file_safety.py@@ -97,6 +97,37 @@ def is_write_denied(path: str) -> bool: if resolved.startswith(prefix): return True + # Hermes control-plane files: block both the ACTIVE profile's view + # (hermes_home) AND the global root view. Without the root pass, a + # profile-mode session leaves <root>/auth.json + <root>/config.yaml + # writable — letting a prompt-injected write_file overwrite the global + # files that every profile inherits from (same shape as #15981). + control_file_names = ("auth.json", "config.yaml", "webhook_subscriptions.json") + mcp_tokens_dir_name = "[redacted:secret-assignment]" + + hermes_dirs = [] + for base in (_hermes_home_path(), _hermes_root_path()): + try: + real = os.path.realpath(base) + if real not in hermes_dirs: + hermes_dirs.append(real) + except Exception: + continue + + for base_real in hermes_dirs: + for name in control_file_names: + try: + if resolved == os.path.realpath(os.path.join(base_real, name)): + return True + except Exception: + continue + try: + mcp_real = os.path.realpath(os.path.join(base_real, mcp_tokens_dir_name)) + if resolved == mcp_real or resolved.startswith(mcp_real + os.sep): + return True + except Exception: + pass + safe_root = get_safe_write_root() if safe_root and not (resolved == safe_root or resolved.startswith(safe_root + os.sep)): return True -
tests/tools/test_file_operations.py@@ -60,6 +60,86 @@ def test_project_file_allowed(self): def test_tilde_expansion(self): assert _is_write_denied("~/.ssh/authorized_keys") is True + @pytest.mark.parametrize( + "path", + [ + "auth.json", + "config.yaml", + "webhook_subscriptions.json", + "mcp-tokens/token1.json", + "mcp-tokens/subdir/token2.json", + ], + ) + def test_hermes_control_files_and_mcp_tokens_denied(self, path): + """Hermes control files and mcp-tokens entries must be write-denied.""" + from hermes_constants import get_hermes_home + hermes_home = get_hermes_home() + full_path = str(hermes_home / path) + assert _is_write_denied(full_path) is True + + @pytest.mark.parametrize( + "path", + [ + "dummy/../config.yaml", + "./auth.json", + "mcp-tokens/../config.yaml", + ], + ) + def test_hermes_control_files_traversal_denied(self, path): + """Path traversal attempts to control files must be blocked by realpath.""" + from hermes_constants import get_hermes_home + hermes_home = get_hermes_home() + full_path = str(hermes_home / path) + assert _is_write_denied(full_path) is True + + @pytest.mark.parametrize( + "path", + [ + "/tmp/standard_file.txt", + "~/projects/myapp/main.py", + "/var/log/app.log", + ], + ) + def test_standard_paths_allowed(self, path): + """Unrelated paths must still be allowed.""" + assert _is_write_denied(path) is False + + @pytest.mark.parametrize( + "name", + ["auth.json", "config.yaml", "webhook_subscriptions.json"], + ) + def test_control_files_protected_in_profile_mode(self, tmp_path, monkeypatch, name): + """Under a profile, BOTH <profile>/X and <root>/X must be denied (#15981 shape). + + Without the root-level pass, a profile-mode session leaves the + global ~/.hermes/{auth.json,config.yaml,webhook_subscriptions.json} + writable — the same gap PR #15981 fixed for .env. + """ + # Simulate a profile-mode HERMES_HOME layout: + # <root>/profiles/coder/{auth.json,config.yaml,...} + # <root>/{auth.json,config.yaml,...} ← must also be denied + root = tmp_path / "hermes" + profile = root / "profiles" / "coder" + profile.mkdir(parents=True) + monkeypatch.setenv("HERMES_HOME", str(profile)) + + # Profile copy + assert _is_write_denied(str(profile / name)) is True + # Root copy — the gap this widening closes + assert _is_write_denied(str(root / name)) is True + + def test_mcp_tokens_dir_protected_in_profile_mode(self, tmp_path, monkeypatch): + """mcp-tokens/ under profile AND under root must both be denied.""" + root = tmp_path / "hermes" + profile = root / "profiles" / "coder" + profile.mkdir(parents=True) + monkeypatch.setenv("HERMES_HOME", str(profile)) + + assert _is_write_denied(str(profile / "mcp-tokens" / "tok.json")) is True + assert _is_write_denied(str(root / "mcp-tokens" / "tok.json")) is True + # The directory itself must also be denied (not just files inside) + assert _is_write_denied(str(root / "mcp-tokens")) is True + # =========================================================================
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#14157 fix(security): protect Hermes control-plane files from prompt injection #14072 · merged via another change →
1f5219f,#30397,#15981,#14072
Closed issues · 1
- Closed#14072Security: write_file/patch can modify ~/.hermes control-plane files (auth.json, config.yaml, webhook subscriptions)
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #30397
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 22 May 2026