{"repo":"NousResearch/hermes-agent","public_id":"20064","display_label":"Terminal safety filter false positives on quoted keywords","summary":"Multiple bug reports and fix attempts addressing false positives in the terminal safety filter when background-wrapper keywords like nohup, disown, and setsid appear inside quoted strings. The fixes focus on stripping or ignoring quoted content before applying the regex scan.","subsystem":"environments","counts":{"members":8,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":6},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":25784},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-14T15:02:01+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.25},"fix_edges":[{"issue_number":20064,"pr_number":25784,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":20064,"title":"bug(terminal): safety filter false-positives on background wrapper keywords inside quoted strings","url":null,"state":"closed"},{"kind":"pr","number":20066,"title":"fix(terminal): prevent safety filter false positives on keywords inside quoted strings","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#20064","number":20064}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":20079,"title":"fix(terminal): prevent safety filter false positives on keywords inside quoted strings","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#20066","number":20066},{"kind":"pr","value":"#20064","number":20064},{"kind":"pr","value":"#20066","number":20066}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":20099,"title":"fix: reduce false positives in terminal safety filter for nohup/disown/setsid","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"364ddd45e8dbfbcdf365794e7ca8e3a3e49de100","number":0},{"kind":"pr","value":"#20064","number":20064},{"kind":"pr","value":"#20066","number":20066}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":20101,"title":"fix(terminal): strip quoted strings before safety-filter regex scan (#20064)","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":20146,"title":"fix(terminal): stop false-positive on quoted nohup/disown/setsid (#20064)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"364ddd45e8dbfbcdf365794e7ca8e3a3e49de100","number":0},{"kind":"pr","value":"#20064","number":20064}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":25616,"title":"fix(terminal): prevent safety filter false positives on quoted background-wrapper keywords (salvage #20066)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#25784","number":25784}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":25784,"title":"fix(terminal): prevent safety filter false positives on quoted background-wrap (#25616)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tools/terminal_tool.py"],"fix_hunks":[{"file":"tools/terminal_tool.py","hunks":["@@ -1544,9 +1544,29 @@ def _command_requires_pipe_stdin(command: str) -> bool:\n     )\n \n \n-_SHELL_LEVEL_BACKGROUND_RE = re.compile(r\"\\b(?:nohup|disown|setsid)\\b\", re.IGNORECASE)\n+_SHELL_LEVEL_BACKGROUND_RE = re.compile(\n+    r\"(?:^|[;&|]\\s*|&&\\s*|\\|\\|\\s*|\\$\\(\\s*)(?:nohup|disown|setsid)\\b\", re.IGNORECASE | re.MULTILINE\n+)\n _INLINE_BACKGROUND_AMP_RE = re.compile(r\"\\s&\\s\")\n _TRAILING_BACKGROUND_AMP_RE = re.compile(r\"\\s&\\s*(?:#.*)?$\")\n+\n+\n+def _strip_quotes(command: str) -> str:\n+    \"\"\"Remove single- and double-quoted content so regex checks don't match inside strings.\n+\n+    This prevents false positives when keywords like 'nohup' or 'setsid' appear\n+    in commit messages, Python -c code, echo arguments, or PR body text.\n+    Also strips backtick-quoted content and heredoc-style inline text.\n+    \"\"\"\n+    # Remove single-quoted strings (no escaping inside single quotes in shell)\n+    result = re.sub(r\"'[^']*'\", \"''\", command)\n+    # Remove double-quoted strings (handle escaped quotes)\n+    result = re.sub(r'\"(?:[^\"\\\\]|\\\\.)*\"', '\"\"', result)\n+    # Remove backtick-quoted strings\n+    result = re.sub(r\"`[^`]*`\", \"``\", result)\n+    return result\n+\n+\n _LONG_LIVED_FOREGROUND_PATTERNS = (\n     re.compile(r\"\\b(?:npm|pnpm|yarn|bun)\\s+(?:run\\s+)?(?:dev|start|serve|watch)\\b\", re.IGNORECASE),\n     re.compile(r\"\\bdocker\\s+compose\\s+up\\b\", re.IGNORECASE),","@@ -1579,21 +1599,25 @@ def _foreground_background_guidance(command: str) -> str | None:\n     if _looks_like_help_or_version_command(command):\n         return None\n \n-    if _SHELL_LEVEL_BACKGROUND_RE.search(command):\n+    # Strip quoted content so keywords inside strings/arguments don't trigger\n+    # false positives (e.g., git commit -m \"... setsid ...\", python3 -c \"os.setsid\").\n+    unquoted = _strip_quotes(command)\n+\n+    if _SHELL_LEVEL_BACKGROUND_RE.search(unquoted):\n         return (\n             \"Foreground command uses shell-level background wrappers (nohup/disown/setsid). \"\n             \"Use terminal(background=true) so Hermes can track the process, then run \"\n             \"readiness checks and tests in separate commands.\"\n         )\n \n-    if _INLINE_BACKGROUND_AMP_RE.search(command) or _TRAILING_BACKGROUND_AMP_RE.search(command):\n+    if _INLINE_BACKGROUND_AMP_RE.search(unquoted) or _TRAILING_BACKGROUND_AMP_RE.search(unquoted):\n         return (\n             \"Foreground command uses '&' backgrounding. Use terminal(background=true) for long-lived \"\n             \"processes, then run health checks and tests in follow-up terminal calls.\"\n         )\n \n     for pattern in _LONG_LIVED_FOREGROUND_PATTERNS:\n-        if pattern.search(command):\n+        if pattern.search(unquoted):\n             return (\n                 \"This foreground command appears to start a long-lived server/watch process. \"\n                 \"Run it with background=true, verify readiness (health endpoint/log signal), \"\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}