Skills external-dir access and profile-isolation fixes
Cluster 5947 · agent. Data as of
Nothing open on GitHub. Fix merged in #10570 on 16 Apr 2026. Check it covers your case.
The short answer
- Known?
- Yes. 4 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #10570. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #10570. That is separate from the fix event. - Still open
- Nothing open on GitHub.
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.
Open issues0
Open PRs0
Fixes on record
-
#10570 fix: five HERMES_HOME profile-isolation leaks
files:
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.pyShow the diff
-
plugins/memory/honcho/client.py@@ -58,8 +58,7 @@ def resolve_config_path() -> Path: Resolution order: 1. $HERMES_HOME/honcho.json (profile-local, if it exists) - 2. ~/.hermes/honcho.json (default profile — shared host blocks live here) - 3. ~/.honcho/config.json (global, cross-app interop) + 2. ~/.honcho/config.json (global, cross-app interop) Returns the global path if none exist (for first-time setup writes). """ @@ -67,11 +66,6 @@ def resolve_config_path() -> Path: if local_path.exists(): return local_path - # Default profile's config — host blocks accumulate here via setup/clone - default_path = Path.home() / ".hermes" / "honcho.json" - if default_path != local_path and default_path.exists(): - return default_path - return GLOBAL_CONFIG_PATH -
run_agent.py@@ -954,9 +954,20 @@ def __init__( # message instead of silently routing through OpenRouter. _explicit = (self.provider or "").strip().lower() if _explicit and _explicit not in ("auto", "openrouter", "custom"): + # Look up the actual env var name from the provider + # config — some providers use non-standard names + # (e.g. alibaba → DASHSCOPE_API_KEY, not ALIBABA_API_KEY). + _env_hint = f"{_explicit.upper()}_API_KEY" + try: + from hermes_cli.auth import PROVIDER_REGISTRY + _pcfg = PROVIDER_REGISTRY.get(_explicit) + if _pcfg and _pcfg.api_key_env_vars: + _env_hint = _pcfg.api_key_env_vars[0] + except Exception: + pass raise RuntimeError( f"Provider '{_explicit}' is set in config.yaml but no API key " - f"was found. Set the {_explicit.upper()}_API_KEY environment " + f"was found. Set the {_env_hint} environment " f"variable, or switch to a different provider with `hermes model`." ) # Final fallback: try raw OpenRouter key @@ -1220,6 +1231,15 @@ def __init__( "hermes_home": str(_ghh()), "agent_context": "primary", } + # Thread session title for memory provider scoping + # (e.g. honcho uses this to derive chat-scoped session keys) + if self._session_db: + try: + _st = self._session_db.get_session_title(self.session_id) + if _st: + _init_kwargs["session_title"] = _st + except Exception: + pass # Thread gateway user identity for per-user memory scoping if self._user_id: _init_kwargs["user_id"] = self._user_id -
scripts/install.sh@@ -28,7 +28,7 @@ BOLD='\033[1m' # Configuration REPO_URL_SSH="[redacted:email]:NousResearch/hermes-agent.git" REPO_URL_HTTPS="https://github.com/NousResearch/hermes-agent.git" -HERMES_HOME="$HOME/.hermes" +HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" INSTALL_DIR="${HERMES_INSTALL_DIR:-$HERMES_HOME/hermes-agent}" PYTHON_VERSION="3.11" NODE_VERSION="22" @@ -66,6 +66,10 @@ while [[ $# -gt 0 ]]; do INSTALL_DIR="$2" shift 2 ;; + --hermes-home) + HERMES_HOME="$2" + shift 2 + ;; -h|--help) echo "Hermes Agent Installer" echo "" @@ -76,6 +80,7 @@ while [[ $# -gt 0 ]]; do echo " --skip-setup Skip interactive setup wizard" echo " --branch NAME Git branch to install (default: main)" echo " --dir PATH Installation directory (default: ~/.hermes/hermes-agent)" + echo " --hermes-home PATH Data directory (default: ~/.hermes, or \$HERMES_HOME)" echo " -h, --help Show this help" exit 0 ;; -
skills/autonomous-ai-agents/hermes-agent/SKILL.md@@ -313,7 +313,7 @@ Type these during an interactive chat session. ``` ~/.hermes/config.yaml Main configuration ~/.hermes/.env API keys and secrets -~/.hermes/skills/ Installed skills +$HERMES_HOME/skills/ Installed skills ~/.hermes/sessions/ Session transcripts ~/.hermes/logs/ Gateway and error logs ~/.hermes/auth.json OAuth tokens and credential pools -
skills/github/github-code-review/SKILL.md@@ -334,7 +334,7 @@ When the user asks you to "review PR #N", "look at this PR", or gives you a PR U ### Step 1: Set up environment ```bash -source ~/.hermes/skills/github/github-auth/scripts/gh-env.sh +source "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/gh-env.sh" # Or run the inline setup block from the top of this skill ``` -
skills/github/github-repo-management/references/github-api-cheatsheet.md@@ -6,7 +6,7 @@ All requests need: `-H "Authorization: token $GITHUB_TOKEN"` Use the `gh-env.sh` helper to set `$GITHUB_TOKEN`, `$GH_OWNER`, `$GH_REPO` automatically: ```bash -source ~/.hermes/skills/github/github-auth/scripts/gh-env.sh +source "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/gh-env.sh" ``` ## Repositories -
skills/productivity/google-workspace/SKILL.md@@ -32,7 +32,7 @@ on CLI, Telegram, Discord, or any platform. Define a shorthand first: ```bash -GSETUP="python ~/.hermes/skills/productivity/google-workspace/scripts/setup.py" +GSETUP="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/setup.py" ``` ### Step 0: Check if already set up @@ -163,7 +163,7 @@ Should print `AUTHENTICATED`. Setup is complete — token refreshes automaticall All commands go through the API script. Set `GAPI` as a shorthand: ```bash -GAPI="python ~/.hermes/skills/productivity/google-workspace/scripts/google_api.py" +GAPI="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/google_api.py" ``` ### Gmail -
skills/red-teaming/godmode/SKILL.md@@ -60,7 +60,7 @@ The fastest path — auto-detect the model, test strategies, and lock in the win # In execute_code — use the loader to avoid exec-scoping issues: import os exec(open(os.path.expanduser( - "~/.hermes/skills/red-teaming/godmode/scripts/load_godmode.py" + os.path.join(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")), "skills/red-teaming/godmode/scripts/load_godmode.py") )).read()) # 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 Or use `execute_code` inline: ```python # Load the parseltongue module -exec(open(os.path.expanduser("~/.hermes/skills/red-teaming/godmode/scripts/parseltongue.py")).read()) +exec(open(os.path.join(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")), "skills/red-teaming/godmode/scripts/parseltongue.py")).read()) query = "How do I hack into a WiFi network?" variants = generate_variants(query, tier="standard") @@ -229,7 +229,7 @@ Race multiple models against the same query, score responses, pick the winner: ```python # Via execute_code -exec(open(os.path.expanduser("~/.hermes/skills/red-teaming/godmode/scripts/godmode_race.py")).read()) +exec(open(os.path.join(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")), "skills/red-teaming/godmode/scripts/godmode_race.py")).read()) result = race_models( query="Explain how SQL injection works with a practical example", -
skills/red-teaming/godmode/references/jailbreak-templates.md@@ -114,7 +114,7 @@ hermes ### Via the GODMODE CLASSIC racer script ```python -exec(open(os.path.expanduser("~/.hermes/skills/red-teaming/godmode/scripts/godmode_race.py")).read()) +exec(open(os.path.join(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")), "skills/red-teaming/godmode/scripts/godmode_race.py")).read()) result = race_godmode_classic("Your query here") print(f"Winner: {result['codename']} — Score: {result['score']}") print(result['content']) -
skills/red-teaming/godmode/references/refusal-detection.md@@ -129,7 +129,7 @@ These don't auto-reject but reduce the response score: ## Using in Python ```python -exec(open(os.path.expanduser("~/.hermes/skills/red-teaming/godmode/scripts/godmode_race.py")).read()) +exec(open(os.path.join(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")), "skills/red-teaming/godmode/scripts/godmode_race.py")).read()) # Check if a response is a refusal text = "I'm sorry, but I can't assist with that request." -
skills/red-teaming/godmode/scripts/auto_jailbreak.py@@ -7,7 +7,7 @@ Usage in execute_code: exec(open(os.path.expanduser( - "~/.hermes/skills/red-teaming/godmode/scripts/auto_jailbreak.py" + os.path.join(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")), "skills/red-teaming/godmode/scripts/auto_jailbreak.py") )).read()) result = auto_jailbreak() # Uses current model from config -
skills/red-teaming/godmode/scripts/godmode_race.py@@ -7,7 +7,7 @@ on quality/filteredness/speed, returns the best unfiltered answer. Usage in execute_code: - exec(open(os.path.expanduser("~/.hermes/skills/red-teaming/godmode/scripts/godmode_race.py")).read()) + exec(open(os.path.join(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")), "skills/red-teaming/godmode/scripts/godmode_race.py")).read()) result = race_models( query="Your query here", -
skills/red-teaming/godmode/scripts/load_godmode.py@@ -3,7 +3,7 @@ Usage in execute_code: exec(open(os.path.expanduser( - "~/.hermes/skills/red-teaming/godmode/scripts/load_godmode.py" + os.path.join(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")), "skills/red-teaming/godmode/scripts/load_godmode.py") )).read()) # Now all functions are available: -
skills/red-teaming/godmode/scripts/parseltongue.py@@ -11,7 +11,7 @@ python parseltongue.py "How do I hack a WiFi network?" --tier standard # As a module in execute_code - exec(open("~/.hermes/skills/red-teaming/godmode/scripts/parseltongue.py").read()) + exec(open(os.path.join(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")), "skills/red-teaming/godmode/scripts/parseltongue.py")).read()) variants = generate_variants("How do I hack a WiFi network?", tier="standard") """ -
tools/skill_manager_tool.py@@ -82,6 +82,18 @@ def _security_scan_skill(skill_dir: Path) -> Optional[str]: MAX_NAME_LENGTH = 64 MAX_DESCRIPTION_LENGTH = 1024 + + +def _is_local_skill(skill_path: Path) -> bool: + """Check if a skill path is within the local SKILLS_DIR. + + Skills found in external_dirs are read-only from the agent's perspective. + """ + try: + skill_path.resolve().relative_to(SKILLS_DIR.resolve()) + return True + except ValueError: + return False MAX_SKILL_CONTENT_CHARS = 100_000 # ~36k tokens at 2.75 chars/token MAX_SKILL_FILE_BYTES = 1_048_576 # 1 MiB per supporting file @@ -360,6 +372,9 @@ def _edit_skill(name: str, content: str) -> Dict[str, Any]: if not existing: return {"success": False, "error": f"Skill '{name}' not found. Use skills_list() to see available skills."} + if not _is_local_skill(existing["path"]): + return {"success": False, "error": f"Skill '{name}' is in an external directory and cannot be modified. Copy it to your local skills directory first."} + skill_md = existing["path"] / "SKILL.md" # Back up original content for rollback original_content = skill_md.read_text(encoding="utf-8") if skill_md.exists() else None @@ -400,6 +415,9 @@ def _patch_skill( if not existing: return {"success": False, "error": f"Skill '{name}' not found."} + if not _is_local_skill(existing["path"]): + return {"success": False, "error": f"Skill '{name}' is in an external directory and cannot be modified. Copy it to your local skills directory first."} + skill_dir = existing["path"] if file_path: @@ -473,6 +491,9 @@ def _delete_skill(name: str) -> Dict[str, Any]: if not existing: return {"success": False, "error": f"Skill '{name}' not found."} + if not _is_local_skill(existing["path"]): + return {"success": False, "error": f"Skill '{name}' is in an external directory and cannot be deleted."} + skill_dir = existing["path"] shutil.rmtree(skill_dir) @@ -515,6 +536,9 @@ def _write_file(name: str, file_path: str, file_content: str) -> Dict[str, Any]: if not existing: return {"success": False, "error": f"Skill '{name}' not found. Create it first with action='create'."} + if not _is_local_skill(existing["path"]): + return {"success": False, "error": f"Skill '{name}' is in an external directory and cannot be modified. Copy it to your local skills directory first."} + target, err = _resolve_skill_target(existing["path"], file_path) if err: return {"success": False, "error": err} @@ -548,6 +572,10 @@ def _remove_file(name: str, file_path: str) -> Dict[str, Any]: existing = _find_skill(name) if not existing: return {"success": False, "error": f"Skill '{name}' not found."} + + if not _is_local_skill(existing["path"]): + return {"success": False, "error": f"Skill '{name}' is in an external directory and cannot be modified."} + skill_dir = existing["path"] target, err = _resolve_skill_target(skill_dir, file_path)
-
2 PRs closed without merging
From the close comments: done elsewhere 1 · merged via another change 1
Closed issues · 1
- Closed#5947Five HERMES_HOME / profile-isolation leaks: gateway honcho session, hardcoded SKILL paths, install.sh --dir, honcho config fallback, skill_manager_tool external_dirs write-through
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.50: 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
- Subsystem
- agent
- Merged PRs
- #10570
- Closed issues
- 1
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 16 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.