{"repo":"NousResearch/hermes-agent","public_id":"18083","display_label":"Approval regex over-flags absolute paths as dangerous deletes","summary":"A DANGEROUS_PATTERNS regex incorrectly matches ordinary absolute paths as root-delete operations, prompting several fixes to tighten the pattern to genuine system-root targets and carve out exceptions for verifier and Hermes-owned temp file cleanup.","subsystem":"agent","counts":{"members":9,"open_issues":1,"open_issues_distinct":1,"open_prs":1,"closed_issues":1,"merged_prs":1,"attempt_prs":6},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":63120,"merged_at":"2026-07-12T11:32:53+00:00"},"resolution":{"state":"partially_resolved","by":"pr-63120"},"freshness":{"state":"verified","last_verified_at":"2026-07-12T11:32:53+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.20444444444444443},"fix_edges":[{"issue_number":62377,"pr_number":63120,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-12T11:32:53+00:00"}],"members":[{"kind":"issue","number":18083,"title":"Over-broad DANGEROUS_PATTERNS regex flags every absolute path as 'delete in root path'","url":null,"state":"open"},{"kind":"issue","number":62377,"title":"Verify-on-stop mandates temp cleanup that dangerous-command approval blocks","url":null,"state":"closed"},{"kind":"pr","number":18089,"title":"fix(approval): tighten 'delete in root path' regex to system-root targets only","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#26829","number":26829},{"kind":"pr","value":"#18083","number":18083}],"category":"volume_quota","implication":"active"},"merged":false},{"kind":"pr","number":18533,"title":"fix(approval): tighten 'delete in root path' regex to system roots only","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#18089","number":18089},{"kind":"pr","value":"#12584","number":12584},{"kind":"pr","value":"#12592","number":12592}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":39629,"title":"fix(approval): avoid flagging ordinary absolute paths as root deletes (#18083)","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":62379,"title":"fix(tools): allow cleanup of Hermes-owned temp files without approval","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#63120","number":63120},{"kind":"pr","value":"#62388","number":62388}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":62388,"title":"fix(approval): allow verifier temp cleanup","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#63120","number":63120}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":62400,"title":"fix(agent): instruct background review fork to call skill_view before patching","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#60331","number":60331},{"kind":"pr","value":"#62400","number":62400},{"kind":"pr","value":"#62377","number":62377}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":63120,"title":"fix(approval): allow verifier temp cleanup","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/verification_stop.py","scripts/release.py","tests/agent/test_verification_stop.py","tests/tools/test_approval.py","tools/approval.py"],"fix_hunks":[{"file":"agent/verification_stop.py","hunks":["@@ -289,7 +289,7 @@ def build_verify_on_stop_nudge(\n             + \"), read any failure, repair the code, and summarize what passed.\"\n         )\n     else:\n-        temp_dir = tempfile.gettempdir()\n+        temp_dir = os.path.realpath(tempfile.gettempdir())\n         command_instruction = (\n             \"No canonical test/lint/build command was detected. Create a focused \"\n             f\"temporary verification script under `{temp_dir}` using an OS-safe \""]},{"file":"scripts/release.py","hunks":["@@ -45,6 +45,7 @@\n \n # Auto-extracted from noreply emails + manual overrides\n AUTHOR_MAP = {\n+    \"135129512+ansel-f@users.noreply.github.com\": \"ansel-f\",  # PR #62388 salvage (approval: allow exact verifier temp cleanup without broadening rm safety boundary)\n     \"[redacted:email]\": \"professorpalmer\",  # PR #48591 salvage (sessions: CLI workspace filter + restore-cwd-on-resume)\n     \"[redacted:email]\": \"Supersynergy\",  # PR #59241 salvage (desktop: workspace path status-bar action)\n     \"[redacted:email]\": \"esthonjr\",  # PR #61950 salvage (desktop: legacy non-git workspace grouping + Windows path identity)"]},{"file":"tests/agent/test_verification_stop.py","hunks":["@@ -260,6 +260,27 @@ def test_no_suite_nudge_requests_temp_script(tmp_path, monkeypatch):\n     assert \"creative UI/visual work\" in nudge\n \n \n+def test_no_suite_nudge_uses_canonical_temp_dir(tmp_path, monkeypatch):\n+    monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path / \".hermes\"))\n+    project = tmp_path / \"project\"\n+    project.mkdir()\n+    (project / \"package.json\").write_text(\"{}\", encoding=\"utf-8\")\n+    real_temp = tmp_path / \"real-temp\"\n+    real_temp.mkdir()\n+    linked_temp = tmp_path / \"linked-temp\"\n+    linked_temp.symlink_to(real_temp, target_is_directory=True)\n+    monkeypatch.setattr(tempfile, \"gettempdir\", lambda: str(linked_temp))\n+\n+    nudge = build_verify_on_stop_nudge(\n+        session_id=\"s1\",\n+        changed_paths=[str(project / \"src\" / \"app.ts\")],\n+    )\n+\n+    assert nudge is not None\n+    assert str(real_temp) in nudge\n+    assert str(linked_temp) not in nudge\n+\n+\n def test_verify_guidance_can_be_disabled(tmp_path, monkeypatch):\n     monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path / \".hermes\"))\n     _node_project(tmp_path)"]},{"file":"tests/tools/test_approval.py","hunks":["@@ -2,6 +2,7 @@\n \n import ast\n import os\n+import tempfile\n import threading\n import time\n from pathlib import Path","@@ -116,6 +117,53 @@ def test_rm_recursive_long_flag(self):\n         assert key is not None\n         assert \"delete\" in desc.lower()\n \n+    def test_nonrecursive_verification_artifact_cleanup_is_not_dangerous(self):\n+        with mock_patch(\"tempfile.gettempdir\", return_value=\"/tmp\"):\n+            for prefix in (\"hermes-verify-\", \"hermes-ad-hoc-\"):\n+                assert detect_dangerous_command(f\"rm -f /tmp/{prefix}example.py\") == (\n+                    False,\n+                    None,\n+                    None,\n+                )\n+\n+    def test_symlinked_temp_dir_only_exempts_canonical_target(self, tmp_path):\n+        real_temp = tmp_path / \"real-temp\"\n+        real_temp.mkdir()\n+        linked_temp = tmp_path / \"linked-temp\"\n+        linked_temp.symlink_to(real_temp, target_is_directory=True)\n+        basename = \"hermes-verify-example.py\"\n+\n+        with mock_patch(\"tempfile.gettempdir\", return_value=str(linked_temp)):\n+            assert detect_dangerous_command(f\"rm -f {linked_temp / basename}\")[0] is True\n+            assert detect_dangerous_command(f\"rm -f {real_temp / basename}\") == (\n+                False,\n+                None,\n+                None,\n+            )\n+\n+    def test_verification_cleanup_exemption_rejects_broader_deletions(self):\n+        commands = (\n+            \"rm -rf /tmp/hermes-verify-example.py\",\n+            \"rm -f /tmp/hermes-verify-example.py /tmp/other.py\",\n+            \"rm -f /tmp/nested/hermes-verify-example.py\",\n+            \"rm -f /tmp/nested/../hermes-verify-example.py\",\n+            \"rm -f /tmp/./hermes-verify-example.py\",\n+            \"rm -f /tmp//hermes-verify-example.py\",\n+            \"rm -f /tmp/a/../../tmp/hermes-verify-example.py\",\n+            \"rm -f /var/tmp/hermes-verify-example.py\",\n+            \"rm -f /tmp/unrelated.py\",\n+            \"rm -f /tmp/hermes-verify-*\",\n+            \"rm -f /tmp/hermes-verify-$(touch>/tmp/pwned).py\",\n+            \"rm -f /tmp/hermes-ad-hoc-`touch>/tmp/pwned`.py\",\n+            \"rm -f /tmp/hermes-verify-example.py; touch /tmp/pwned\",\n+        )\n+        with mock_patch(\"tempfile.gettempdir\", return_value=\"/tmp\"):\n+            for command in commands:\n+                is_dangerous, key, desc = detect_dangerous_command(command)\n+                assert is_dangerous is True, command\n+                assert key is not None, command\n+                assert \"delete\" in desc.lower(), command\n+\n \n class TestWindowsShellDestructiveCommands:\n     def test_cmd_del_requires_approval(self):"]},{"file":"tools/approval.py","hunks":["@@ -17,6 +17,7 @@\n import re\n import shlex\n import sys\n+import tempfile\n import threading\n import time\n import unicodedata","@@ -1383,12 +1384,36 @@ def _command_detection_variants(command: str):\n         yield variant\n \n \n+def _is_verification_artifact_cleanup(command: str) -> bool:\n+    \"\"\"Return whether *command* only removes one Hermes ad-hoc temp script.\"\"\"\n+    try:\n+        argv = shlex.split(command, posix=True)\n+    except ValueError:\n+        return False\n+    if len(argv) != 3 or argv[0] != \"rm\" or argv[1] != \"-f\":\n+        return False\n+\n+    operand = argv[2]\n+    temp_dir = os.path.realpath(tempfile.gettempdir())\n+    basename = os.path.basename(operand)\n+    if operand != os.path.join(temp_dir, basename):\n+        return False\n+\n+    target = os.path.realpath(operand)\n+    if os.path.dirname(target) != temp_dir:\n+        return False\n+    return re.fullmatch(r\"hermes-(?:verify|ad-hoc)-[A-Za-z0-9_.-]+\", basename) is not None\n+\n+\n def detect_dangerous_command(command: str) -> tuple:\n     \"\"\"Check if a command matches any dangerous patterns.\n \n     Returns:\n         (is_dangerous, pattern_key, description) or (False, None, None)\n     \"\"\"\n+    if _is_verification_artifact_cleanup(command):\n+        return (False, None, None)\n+\n     for command_variant in _command_detection_variants(command):\n         command_lower = command_variant.lower()\n         for pattern_re, description in DANGEROUS_PATTERNS_COMPILED:\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}