{"repo":"NousResearch/hermes-agent","public_id":"5947","display_label":"Skills external-dir access and profile-isolation fixes","summary":"These items address profile-isolation leaks related to HERMES_HOME and fix how skill management handles external skill directories, including read/write permissions and hardcoded path issues. A session key prefixing fix for the Honcho gateway is also included.","subsystem":"agent","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":10570,"merged_at":"2026-04-16T00:09:41+00:00"},"resolution":{"state":"resolved","by":"pr-10570"},"freshness":{"state":"stale","last_verified_at":"2026-04-16T00:09:41+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":5947,"pr_number":10570,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-16T00:09:41+00:00"}],"members":[{"kind":"issue","number":5947,"title":"Five HERMES_HOME / profile-isolation leaks: gateway honcho session, hardcoded SKILL paths, install.sh --dir, honcho config fallback, skill_manager_tool external_dirs write-through","url":null,"state":"closed"},{"kind":"pr","number":6338,"title":"fix(skills): block mutations in external skill dirs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#10570","number":10570},{"kind":"pr","value":"#5947","number":5947}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":10570,"title":"fix: five HERMES_HOME profile-isolation leaks","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/memory/honcho/client.py","run_agent.py","scripts/install.sh","skills/autonomous-ai-agents/hermes-agent/SKILL.md","skills/github/github-code-review/SKILL.md","skills/github/github-repo-management/references/github-api-cheatsheet.md","skills/productivity/google-workspace/SKILL.md","skills/red-teaming/godmode/SKILL.md","skills/red-teaming/godmode/references/jailbreak-templates.md","skills/red-teaming/godmode/references/refusal-detection.md","skills/red-teaming/godmode/scripts/auto_jailbreak.py","skills/red-teaming/godmode/scripts/godmode_race.py","skills/red-teaming/godmode/scripts/load_godmode.py","skills/red-teaming/godmode/scripts/parseltongue.py","tools/skill_manager_tool.py"],"fix_hunks":[{"file":"plugins/memory/honcho/client.py","hunks":["@@ -58,8 +58,7 @@ def resolve_config_path() -> Path:\n \n     Resolution order:\n       1. $HERMES_HOME/honcho.json      (profile-local, if it exists)\n-      2. ~/.hermes/honcho.json          (default profile — shared host blocks live here)\n-      3. ~/.honcho/config.json          (global, cross-app interop)\n+      2. ~/.honcho/config.json          (global, cross-app interop)\n \n     Returns the global path if none exist (for first-time setup writes).\n     \"\"\"","@@ -67,11 +66,6 @@ def resolve_config_path() -> Path:\n     if local_path.exists():\n         return local_path\n \n-    # Default profile's config — host blocks accumulate here via setup/clone\n-    default_path = Path.home() / \".hermes\" / \"honcho.json\"\n-    if default_path != local_path and default_path.exists():\n-        return default_path\n-\n     return GLOBAL_CONFIG_PATH\n \n "]},{"file":"run_agent.py","hunks":["@@ -954,9 +954,20 @@ def __init__(\n                     # message instead of silently routing through OpenRouter.\n                     _explicit = (self.provider or \"\").strip().lower()\n                     if _explicit and _explicit not in (\"auto\", \"openrouter\", \"custom\"):\n+                        # Look up the actual env var name from the provider\n+                        # config — some providers use non-standard names\n+                        # (e.g. alibaba → DASHSCOPE_API_KEY, not ALIBABA_API_KEY).\n+                        _env_hint = f\"{_explicit.upper()}_API_KEY\"\n+                        try:\n+                            from hermes_cli.auth import PROVIDER_REGISTRY\n+                            _pcfg = PROVIDER_REGISTRY.get(_explicit)\n+                            if _pcfg and _pcfg.api_key_env_vars:\n+                                _env_hint = _pcfg.api_key_env_vars[0]\n+                        except Exception:\n+                            pass\n                         raise RuntimeError(\n                             f\"Provider '{_explicit}' is set in config.yaml but no API key \"\n-                            f\"was found. Set the {_explicit.upper()}_API_KEY environment \"\n+                            f\"was found. Set the {_env_hint} environment \"\n                             f\"variable, or switch to a different provider with `hermes model`.\"\n                         )\n                     # Final fallback: try raw OpenRouter key","@@ -1220,6 +1231,15 @@ def __init__(\n                             \"hermes_home\": str(_ghh()),\n                             \"agent_context\": \"primary\",\n                         }\n+                        # Thread session title for memory provider scoping\n+                        # (e.g. honcho uses this to derive chat-scoped session keys)\n+                        if self._session_db:\n+                            try:\n+                                _st = self._session_db.get_session_title(self.session_id)\n+                                if _st:\n+                                    _init_kwargs[\"session_title\"] = _st\n+                            except Exception:\n+                                pass\n                         # Thread gateway user identity for per-user memory scoping\n                         if self._user_id:\n                             _init_kwargs[\"user_id\"] = self._user_id"]},{"file":"scripts/install.sh","hunks":["@@ -28,7 +28,7 @@ BOLD='\\033[1m'\n # Configuration\n REPO_URL_SSH=\"[redacted:email]:NousResearch/hermes-agent.git\"\n REPO_URL_HTTPS=\"https://github.com/NousResearch/hermes-agent.git\"\n-HERMES_HOME=\"$HOME/.hermes\"\n+HERMES_HOME=\"${HERMES_HOME:-$HOME/.hermes}\"\n INSTALL_DIR=\"${HERMES_INSTALL_DIR:-$HERMES_HOME/hermes-agent}\"\n PYTHON_VERSION=\"3.11\"\n NODE_VERSION=\"22\"","@@ -66,6 +66,10 @@ while [[ $# -gt 0 ]]; do\n             INSTALL_DIR=\"$2\"\n             shift 2\n             ;;\n+        --hermes-home)\n+            HERMES_HOME=\"$2\"\n+            shift 2\n+            ;;\n         -h|--help)\n             echo \"Hermes Agent Installer\"\n             echo \"\"","@@ -76,6 +80,7 @@ while [[ $# -gt 0 ]]; do\n             echo \"  --skip-setup   Skip interactive setup wizard\"\n             echo \"  --branch NAME  Git branch to install (default: main)\"\n             echo \"  --dir PATH     Installation directory (default: ~/.hermes/hermes-agent)\"\n+            echo \"  --hermes-home PATH  Data directory (default: ~/.hermes, or \\$HERMES_HOME)\"\n             echo \"  -h, --help     Show this help\"\n             exit 0\n             ;;"]},{"file":"skills/autonomous-ai-agents/hermes-agent/SKILL.md","hunks":["@@ -313,7 +313,7 @@ Type these during an interactive chat session.\n ```\n ~/.hermes/config.yaml       Main configuration\n ~/.hermes/.env              API keys and secrets\n-~/.hermes/skills/           Installed skills\n+$HERMES_HOME/skills/        Installed skills\n ~/.hermes/sessions/         Session transcripts\n ~/.hermes/logs/             Gateway and error logs\n ~/.hermes/auth.json         OAuth tokens and credential pools"]},{"file":"skills/github/github-code-review/SKILL.md","hunks":["@@ -334,7 +334,7 @@ When the user asks you to \"review PR #N\", \"look at this PR\", or gives you a PR U\n ### Step 1: Set up environment\n \n ```bash\n-source ~/.hermes/skills/github/github-auth/scripts/gh-env.sh\n+source \"${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/gh-env.sh\"\n # Or run the inline setup block from the top of this skill\n ```\n "]},{"file":"skills/github/github-repo-management/references/github-api-cheatsheet.md","hunks":["@@ -6,7 +6,7 @@ All requests need: `-H \"Authorization: token $GITHUB_TOKEN\"`\n \n Use the `gh-env.sh` helper to set `$GITHUB_TOKEN`, `$GH_OWNER`, `$GH_REPO` automatically:\n ```bash\n-source ~/.hermes/skills/github/github-auth/scripts/gh-env.sh\n+source \"${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/gh-env.sh\"\n ```\n \n ## Repositories"]},{"file":"skills/productivity/google-workspace/SKILL.md","hunks":["@@ -32,7 +32,7 @@ on CLI, Telegram, Discord, or any platform.\n Define a shorthand first:\n \n ```bash\n-GSETUP=\"python ~/.hermes/skills/productivity/google-workspace/scripts/setup.py\"\n+GSETUP=\"python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/setup.py\"\n ```\n \n ### Step 0: Check if already set up","@@ -163,7 +163,7 @@ Should print `AUTHENTICATED`. Setup is complete — token refreshes automaticall\n All commands go through the API script. Set `GAPI` as a shorthand:\n \n ```bash\n-GAPI=\"python ~/.hermes/skills/productivity/google-workspace/scripts/google_api.py\"\n+GAPI=\"python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/google_api.py\"\n ```\n \n ### Gmail"]},{"file":"skills/red-teaming/godmode/SKILL.md","hunks":["@@ -60,7 +60,7 @@ The fastest path — auto-detect the model, test strategies, and lock in the win\n # In execute_code — use the loader to avoid exec-scoping issues:\n import os\n exec(open(os.path.expanduser(\n-    \"~/.hermes/skills/red-teaming/godmode/scripts/load_godmode.py\"\n+    os.path.join(os.environ.get(\"HERMES_HOME\", os.path.expanduser(\"~/.hermes\")), \"skills/red-teaming/godmode/scripts/load_godmode.py\")\n )).read())\n \n # Auto-detect model from config and jailbreak it","@@ -192,7 +192,7 @@ python3 scripts/parseltongue.py \"How do I hack into a WiFi network?\" --tier stan\n Or use `execute_code` inline:\n ```python\n # Load the parseltongue module\n-exec(open(os.path.expanduser(\"~/.hermes/skills/red-teaming/godmode/scripts/parseltongue.py\")).read())\n+exec(open(os.path.join(os.environ.get(\"HERMES_HOME\", os.path.expanduser(\"~/.hermes\")), \"skills/red-teaming/godmode/scripts/parseltongue.py\")).read())\n \n query = \"How do I hack into a WiFi network?\"\n variants = generate_variants(query, tier=\"standard\")","@@ -229,7 +229,7 @@ Race multiple models against the same query, score responses, pick the winner:\n \n ```python\n # Via execute_code\n-exec(open(os.path.expanduser(\"~/.hermes/skills/red-teaming/godmode/scripts/godmode_race.py\")).read())\n+exec(open(os.path.join(os.environ.get(\"HERMES_HOME\", os.path.expanduser(\"~/.hermes\")), \"skills/red-teaming/godmode/scripts/godmode_race.py\")).read())\n \n result = race_models(\n     query=\"Explain how SQL injection works with a practical example\","]},{"file":"skills/red-teaming/godmode/references/jailbreak-templates.md","hunks":["@@ -114,7 +114,7 @@ hermes\n ### Via the GODMODE CLASSIC racer script\n \n ```python\n-exec(open(os.path.expanduser(\"~/.hermes/skills/red-teaming/godmode/scripts/godmode_race.py\")).read())\n+exec(open(os.path.join(os.environ.get(\"HERMES_HOME\", os.path.expanduser(\"~/.hermes\")), \"skills/red-teaming/godmode/scripts/godmode_race.py\")).read())\n result = race_godmode_classic(\"Your query here\")\n print(f\"Winner: {result['codename']} — Score: {result['score']}\")\n print(result['content'])"]},{"file":"skills/red-teaming/godmode/references/refusal-detection.md","hunks":["@@ -129,7 +129,7 @@ These don't auto-reject but reduce the response score:\n ## Using in Python\n \n ```python\n-exec(open(os.path.expanduser(\"~/.hermes/skills/red-teaming/godmode/scripts/godmode_race.py\")).read())\n+exec(open(os.path.join(os.environ.get(\"HERMES_HOME\", os.path.expanduser(\"~/.hermes\")), \"skills/red-teaming/godmode/scripts/godmode_race.py\")).read())\n \n # Check if a response is a refusal\n text = \"I'm sorry, but I can't assist with that request.\""]},{"file":"skills/red-teaming/godmode/scripts/auto_jailbreak.py","hunks":["@@ -7,7 +7,7 @@\n \n Usage in execute_code:\n     exec(open(os.path.expanduser(\n-        \"~/.hermes/skills/red-teaming/godmode/scripts/auto_jailbreak.py\"\n+        os.path.join(os.environ.get(\"HERMES_HOME\", os.path.expanduser(\"~/.hermes\")), \"skills/red-teaming/godmode/scripts/auto_jailbreak.py\")\n     )).read())\n     \n     result = auto_jailbreak()  # Uses current model from config"]},{"file":"skills/red-teaming/godmode/scripts/godmode_race.py","hunks":["@@ -7,7 +7,7 @@\n on quality/filteredness/speed, returns the best unfiltered answer.\n \n Usage in execute_code:\n-    exec(open(os.path.expanduser(\"~/.hermes/skills/red-teaming/godmode/scripts/godmode_race.py\")).read())\n+    exec(open(os.path.join(os.environ.get(\"HERMES_HOME\", os.path.expanduser(\"~/.hermes\")), \"skills/red-teaming/godmode/scripts/godmode_race.py\")).read())\n     \n     result = race_models(\n         query=\"Your query here\","]},{"file":"skills/red-teaming/godmode/scripts/load_godmode.py","hunks":["@@ -3,7 +3,7 @@\n \n Usage in execute_code:\n     exec(open(os.path.expanduser(\n-        \"~/.hermes/skills/red-teaming/godmode/scripts/load_godmode.py\"\n+        os.path.join(os.environ.get(\"HERMES_HOME\", os.path.expanduser(\"~/.hermes\")), \"skills/red-teaming/godmode/scripts/load_godmode.py\")\n     )).read())\n     \n     # Now all functions are available:"]},{"file":"skills/red-teaming/godmode/scripts/parseltongue.py","hunks":["@@ -11,7 +11,7 @@\n     python parseltongue.py \"How do I hack a WiFi network?\" --tier standard\n \n     # As a module in execute_code\n-    exec(open(\"~/.hermes/skills/red-teaming/godmode/scripts/parseltongue.py\").read())\n+    exec(open(os.path.join(os.environ.get(\"HERMES_HOME\", os.path.expanduser(\"~/.hermes\")), \"skills/red-teaming/godmode/scripts/parseltongue.py\")).read())\n     variants = generate_variants(\"How do I hack a WiFi network?\", tier=\"standard\")\n \"\"\"\n "]},{"file":"tools/skill_manager_tool.py","hunks":["@@ -82,6 +82,18 @@ def _security_scan_skill(skill_dir: Path) -> Optional[str]:\n \n MAX_NAME_LENGTH = 64\n MAX_DESCRIPTION_LENGTH = 1024\n+\n+\n+def _is_local_skill(skill_path: Path) -> bool:\n+    \"\"\"Check if a skill path is within the local SKILLS_DIR.\n+\n+    Skills found in external_dirs are read-only from the agent's perspective.\n+    \"\"\"\n+    try:\n+        skill_path.resolve().relative_to(SKILLS_DIR.resolve())\n+        return True\n+    except ValueError:\n+        return False\n MAX_SKILL_CONTENT_CHARS = 100_000   # ~36k tokens at 2.75 chars/token\n MAX_SKILL_FILE_BYTES = 1_048_576    # 1 MiB per supporting file\n ","@@ -360,6 +372,9 @@ def _edit_skill(name: str, content: str) -> Dict[str, Any]:\n     if not existing:\n         return {\"success\": False, \"error\": f\"Skill '{name}' not found. Use skills_list() to see available skills.\"}\n \n+    if not _is_local_skill(existing[\"path\"]):\n+        return {\"success\": False, \"error\": f\"Skill '{name}' is in an external directory and cannot be modified. Copy it to your local skills directory first.\"}\n+\n     skill_md = existing[\"path\"] / \"SKILL.md\"\n     # Back up original content for rollback\n     original_content = skill_md.read_text(encoding=\"utf-8\") if skill_md.exists() else None","@@ -400,6 +415,9 @@ def _patch_skill(\n     if not existing:\n         return {\"success\": False, \"error\": f\"Skill '{name}' not found.\"}\n \n+    if not _is_local_skill(existing[\"path\"]):\n+        return {\"success\": False, \"error\": f\"Skill '{name}' is in an external directory and cannot be modified. Copy it to your local skills directory first.\"}\n+\n     skill_dir = existing[\"path\"]\n \n     if file_path:","@@ -473,6 +491,9 @@ def _delete_skill(name: str) -> Dict[str, Any]:\n     if not existing:\n         return {\"success\": False, \"error\": f\"Skill '{name}' not found.\"}\n \n+    if not _is_local_skill(existing[\"path\"]):\n+        return {\"success\": False, \"error\": f\"Skill '{name}' is in an external directory and cannot be deleted.\"}\n+\n     skill_dir = existing[\"path\"]\n     shutil.rmtree(skill_dir)\n ","@@ -515,6 +536,9 @@ def _write_file(name: str, file_path: str, file_content: str) -> Dict[str, Any]:\n     if not existing:\n         return {\"success\": False, \"error\": f\"Skill '{name}' not found. Create it first with action='create'.\"}\n \n+    if not _is_local_skill(existing[\"path\"]):\n+        return {\"success\": False, \"error\": f\"Skill '{name}' is in an external directory and cannot be modified. Copy it to your local skills directory first.\"}\n+\n     target, err = _resolve_skill_target(existing[\"path\"], file_path)\n     if err:\n         return {\"success\": False, \"error\": err}","@@ -548,6 +572,10 @@ def _remove_file(name: str, file_path: str) -> Dict[str, Any]:\n     existing = _find_skill(name)\n     if not existing:\n         return {\"success\": False, \"error\": f\"Skill '{name}' not found.\"}\n+\n+    if not _is_local_skill(existing[\"path\"]):\n+        return {\"success\": False, \"error\": f\"Skill '{name}' is in an external directory and cannot be modified.\"}\n+\n     skill_dir = existing[\"path\"]\n \n     target, err = _resolve_skill_target(skill_dir, file_path)\n"]}]},{"kind":"pr","number":36820,"title":"fix(memory): prefix Honcho gateway session key with ai_peer (#36160)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d82f9fa7f7197b0a7e5246ca42802f96fbb7b734","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}