{"repo":"NousResearch/hermes-agent","public_id":"26459","display_label":"Honcho workspace/peer ID sanitization for dots","summary":"Multiple issues and fixes addressing invalid workspace and peer IDs generated when profile or host names contain dots or special characters, which violate Honcho's ID format requirements. Several fix PRs apply sanitization at different points in the ID construction path.","subsystem":"agent","counts":{"members":7,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":35170},"resolution":{"state":"resolved","by":"pr-35170"},"freshness":{"state":"verified","last_verified_at":"2026-05-30T05:29:49+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.39809523809523806},"fix_edges":[{"issue_number":20688,"pr_number":35170,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":26459,"pr_number":35170,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":29885,"pr_number":35170,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":30246,"pr_number":35170,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":26459,"title":"_host_key() returns dot-separated form (\"hermes.fundraising\") that violates Honcho workspace ID regex","url":null,"state":"closed"},{"kind":"issue","number":30246,"title":"Honcho memory plugin generates invalid workspace/peer IDs for profile names containing dots","url":null,"state":"closed"},{"kind":"pr","number":26478,"title":"fix(honcho): use safe profile host keys","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"827ce602dbed199f665f3975b61303aace2963ea","number":0},{"kind":"pr","value":"#26459","number":26459}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":26750,"title":"fix(honcho): sanitize dotted host fallback in workspace_id (#26459)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#26478","number":26478},{"kind":"pr","value":"#26459","number":26459}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":30291,"title":"fix(honcho): sanitize workspace/peer IDs to strip dots and special characters (#30246)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#26478","number":26478}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":33954,"title":"fix(honcho): harden self-hosted setup paths","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"827ce602d","number":0},{"kind":"pr","value":"#35170","number":35170},{"kind":"pr","value":"#34851","number":34851}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":35170,"title":"fix(honcho): harden self-hosted setup paths","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/run.py","hermes_cli/main.py","hermes_cli/memory_setup.py","hermes_cli/profiles.py","optional-skills/autonomous-ai-agents/honcho/SKILL.md","plugins/memory/hindsight/__init__.py","plugins/memory/honcho/README.md","plugins/memory/honcho/__init__.py","plugins/memory/honcho/cli.py","plugins/memory/honcho/client.py","plugins/memory/mem0/__init__.py","plugins/memory/supermemory/__init__.py","tests/gateway/test_agent_cache.py","tests/hermes_cli/test_memory_setup_provider_arg.py","tests/hermes_cli/test_profiles.py","tests/honcho_plugin/test_cli.py","tests/honcho_plugin/test_client.py","tests/honcho_plugin/test_pin_peer_name.py","tests/plugins/memory/test_hindsight_provider.py","tests/plugins/memory/test_mem0_v2.py","tests/plugins/memory/test_supermemory_provider.py","tests/test_honcho_client_config.py","utils.py","website/docs/user-guide/features/honcho.md","website/docs/user-guide/features/memory-providers.md"],"fix_hunks":[{"file":"gateway/run.py","hunks":["@@ -15331,8 +15331,52 @@ async def _run_process_watcher(self, watcher: dict) -> None:\n         (\"compression\", \"target_ratio\"),\n         (\"compression\", \"protect_last_n\"),\n         (\"agent\", \"disabled_toolsets\"),\n+        (\"memory\", \"provider\"),\n     )\n \n+    _HONCHO_CACHE_BUSTING_KEYS = (\n+        \"honcho.peer_name\",\n+        \"honcho.ai_peer\",\n+        \"honcho.pin_peer_name\",\n+        \"honcho.runtime_peer_prefix\",\n+        \"honcho.user_peer_aliases\",\n+    )\n+    _HONCHO_CACHE_BUSTING_MEMO: dict[tuple[str, int | None], dict[str, Any]] = {}\n+\n+    @classmethod\n+    def _empty_honcho_cache_busting_config(cls) -> dict[str, Any]:\n+        return {key: None for key in cls._HONCHO_CACHE_BUSTING_KEYS}\n+\n+    @classmethod\n+    def _extract_honcho_cache_busting_config(cls) -> dict[str, Any]:\n+        \"\"\"Extract Honcho identity keys, memoized by honcho.json mtime.\"\"\"\n+        try:\n+            from plugins.memory.honcho.client import HonchoClientConfig, resolve_config_path\n+\n+            path = resolve_config_path()\n+            try:\n+                mtime_ns = path.stat().st_mtime_ns\n+            except OSError:\n+                mtime_ns = None\n+            memo_key = (str(path), mtime_ns)\n+            cached = cls._HONCHO_CACHE_BUSTING_MEMO.get(memo_key)\n+            if cached is not None:\n+                return dict(cached)\n+\n+            hcfg = HonchoClientConfig.from_global_config(config_path=path)\n+            aliases = hcfg.user_peer_aliases or {}\n+            values = {\n+                \"honcho.peer_name\": hcfg.peer_name,\n+                \"honcho.ai_peer\": hcfg.ai_peer,\n+                \"honcho.pin_peer_name\": bool(hcfg.pin_peer_name),\n+                \"honcho.runtime_peer_prefix\": hcfg.runtime_peer_prefix or \"\",\n+                \"honcho.user_peer_aliases\": sorted(aliases.items()) if isinstance(aliases, dict) else [],\n+            }\n+            cls._HONCHO_CACHE_BUSTING_MEMO = {memo_key: values}\n+            return dict(values)\n+        except Exception:\n+            return cls._empty_honcho_cache_busting_config()\n+\n     @classmethod\n     def _extract_cache_busting_config(cls, user_config: dict | None) -> dict:\n         \"\"\"Pull values that must bust the cached agent.","@@ -15363,26 +15407,12 @@ def _extract_cache_busting_config(cls, user_config: dict | None) -> dict:\n             out[\"tools.registry_generation\"] = None\n \n         # Honcho identity-mapping keys live in honcho.json, not user_config.\n-        # HonchoSessionManager freezes the resolved peer_name / ai_peer /\n-        # pin / aliases / prefix at construction; without busting here,\n-        # mid-flight honcho.json edits go unread until the next unrelated\n-        # cache eviction.\n-        try:\n-            from plugins.memory.honcho.client import HonchoClientConfig\n-\n-            hcfg = HonchoClientConfig.from_global_config()\n-            out[\"honcho.peer_name\"] = hcfg.peer_name\n-            out[\"honcho.ai_peer\"] = hcfg.ai_peer\n-            out[\"honcho.pin_peer_name\"] = bool(hcfg.pin_peer_name)\n-            out[\"honcho.runtime_peer_prefix\"] = hcfg.runtime_peer_prefix or \"\"\n-            aliases = hcfg.user_peer_aliases or {}\n-            out[\"honcho.user_peer_aliases\"] = sorted(aliases.items()) if isinstance(aliases, dict) else []\n-        except Exception:\n-            out[\"honcho.peer_name\"] = None\n-            out[\"honcho.ai_peer\"] = None\n-            out[\"honcho.pin_peer_name\"] = None\n-            out[\"honcho.runtime_peer_prefix\"] = None\n-            out[\"honcho.user_peer_aliases\"] = None\n+        # Only read that file when Honcho is the active memory provider.\n+        provider = cfg_get(cfg, \"memory\", \"provider\")\n+        if isinstance(provider, str) and provider.lower() == \"honcho\":\n+            out.update(cls._extract_honcho_cache_busting_config())\n+        else:\n+            out.update(cls._empty_honcho_cache_busting_config())\n \n         return out\n "]},{"file":"hermes_cli/main.py","hunks":["@@ -13029,9 +13029,15 @@ def cmd_plugins(args):\n         ),\n     )\n     memory_sub = memory_parser.add_subparsers(dest=\"memory_command\")\n-    memory_sub.add_parser(\n+    _setup_parser = memory_sub.add_parser(\n         \"setup\", help=\"Interactive provider selection and configuration\"\n     )\n+    _setup_parser.add_argument(\n+        \"provider\",\n+        nargs=\"?\",\n+        default=None,\n+        help=\"Provider to configure directly (e.g. honcho), skipping the picker\",\n+    )\n     memory_sub.add_parser(\"status\", help=\"Show current memory provider config\")\n     memory_sub.add_parser(\"off\", help=\"Disable external provider (built-in only)\")\n     _reset_parser = memory_sub.add_parser("]},{"file":"hermes_cli/memory_setup.py","hunks":["@@ -452,7 +452,11 @@ def memory_command(args) -> None:\n     \"\"\"Route memory subcommands.\"\"\"\n     sub = getattr(args, \"memory_command\", None)\n     if sub == \"setup\":\n-        cmd_setup(args)\n+        provider = getattr(args, \"provider\", None)\n+        if provider:\n+            cmd_setup_provider(provider)\n+        else:\n+            cmd_setup(args)\n     elif sub == \"status\":\n         cmd_status(args)\n     else:"]},{"file":"hermes_cli/profiles.py","hunks":["@@ -1471,8 +1471,9 @@ def import_profile(archive_path: str, name: Optional[str] = None) -> Path:\n \n def _migrate_honcho_profile_host(old_name: str, new_name: str, new_dir: Path) -> None:\n     \"\"\"Rename Honcho host blocks for a renamed profile without changing peers.\"\"\"\n-    old_host = f\"hermes.{old_name}\"\n-    new_host = f\"hermes.{new_name}\"\n+    old_host = f\"hermes_{old_name}\"\n+    legacy_old_host = f\"hermes.{old_name}\"\n+    new_host = f\"hermes_{new_name}\"\n \n     candidates = [\n         new_dir / \"honcho.json\",","@@ -1496,18 +1497,24 @@ def _migrate_honcho_profile_host(old_name: str, new_name: str, new_dir: Path) ->\n             continue\n \n         hosts = raw.get(\"hosts\")\n-        if not isinstance(hosts, dict) or old_host not in hosts:\n+        if not isinstance(hosts, dict):\n+            continue\n+        source_host = old_host if old_host in hosts else legacy_old_host\n+        if source_host not in hosts:\n             continue\n \n         if new_host in hosts:\n             print(f\"⚠ Honcho host block not migrated: {new_host} already exists in {path}\")\n             continue\n \n-        block = hosts[old_host]\n+        block = hosts[source_host]\n         if isinstance(block, dict) and \"aiPeer\" not in block:\n-            bare = old_host.split(\".\", 1)[1] if \".\" in old_host else old_host\n+            if source_host.startswith(\"hermes_\"):\n+                bare = source_host.split(\"_\", 1)[1]\n+            else:\n+                bare = source_host.split(\".\", 1)[1] if \".\" in source_host else source_host\n             block[\"aiPeer\"] = bare\n-        hosts[new_host] = hosts.pop(old_host)\n+        hosts[new_host] = hosts.pop(source_host)\n         tmp = path.with_suffix(path.suffix + \".tmp\")\n         try:\n             tmp.write_text(json.dumps(raw, indent=2, ensure_ascii=False) + \"\\n\", encoding=\"utf-8\")","@@ -1519,7 +1526,7 @@ def _migrate_honcho_profile_host(old_name: str, new_name: str, new_dir: Path) ->\n                 pass\n             continue\n \n-        print(f\"✓ Honcho host updated: {old_host} → {new_host}\")\n+        print(f\"✓ Honcho host updated: {source_host} → {new_host}\")\n \n \n def rename_profile(old_name: str, new_name: str) -> Path:"]},{"file":"optional-skills/autonomous-ai-agents/honcho/SKILL.md","hunks":["@@ -32,14 +32,14 @@ Honcho provides AI-native cross-session user modeling. It learns who the user is\n ### Cloud (app.honcho.dev)\n \n ```bash\n-hermes honcho setup\n+hermes memory setup honcho\n # select \"cloud\", paste API key from https://app.honcho.dev\n ```\n \n ### Self-hosted\n \n ```bash\n-hermes honcho setup\n+hermes memory setup honcho\n # select \"local\", enter base URL (e.g. http://localhost:8000)\n ```\n "]},{"file":"plugins/memory/hindsight/__init__.py","hunks":["@@ -633,7 +633,8 @@ def save_config(self, values, hermes_home):\n             except Exception:\n                 pass\n         existing.update(values)\n-        config_path.write_text(json.dumps(existing, indent=2))\n+        from utils import atomic_json_write\n+        atomic_json_write(config_path, existing, mode=0o600)\n \n     def post_setup(self, hermes_home: str, config: dict) -> None:\n         \"\"\"Custom setup wizard — installs only the deps needed for the selected mode.\"\"\""]},{"file":"plugins/memory/honcho/README.md","hunks":["@@ -12,8 +12,8 @@ AI-native cross-session user modeling with multi-pass dialectic reasoning, sessi\n ## Setup\n \n ```bash\n-hermes honcho setup    # full interactive wizard (cloud or local)\n-hermes memory setup    # generic picker, also works\n+hermes memory setup honcho   # configure Honcho directly (works on a fresh install)\n+hermes memory setup          # generic picker, choose Honcho from the list\n ```\n \n Or manually:","@@ -22,6 +22,10 @@ hermes config set memory.provider honcho\n echo \"HONCHO_API_KEY=***\" >> ~/.hermes/.env\n ```\n \n+> `hermes honcho setup` also works, but only **after** Honcho is the active\n+> memory provider — the `honcho` subcommand is registered for the active\n+> provider only. On a fresh install, use `hermes memory setup honcho`.\n+\n ## Architecture Overview\n \n ### Two-Layer Context Injection","@@ -109,7 +113,7 @@ Config is read from the first file that exists:\n | 2 | `~/.hermes/honcho.json` | Default profile (shared host blocks) |\n | 3 | `~/.honcho/config.json` | Global (cross-app interop) |\n \n-Host key is derived from the active Hermes profile: `hermes` (default) or `hermes.<profile>`.\n+Host key is derived from the active Hermes profile: `hermes` (default) or `hermes_<profile>`.\n \n For every key, resolution order is: **host block > root > env var > default**.\n ","@@ -154,7 +158,7 @@ In gateway deployments (Telegram, Discord, Slack, etc.) each user arrives with a\n \n **Host vs root semantics.** All three keys are accepted at both root and `hosts.<host>` levels. Host-level wins. For maps and prefixes, host-level *replaces* the root value as a whole (not merge), so a host can intentionally own its identity universe or wipe it with `userPeerAliases: {}` / `runtimePeerPrefix: \"\"`.\n \n-**Deployment shapes** (`hermes honcho setup` asks one prompt to set these):\n+**Deployment shapes** (`hermes memory setup honcho` asks one prompt to set these):\n \n - **Single-operator** — `pinUserPeer: true`. All gateway users → `peerName`. Recommended for personal use where you connect Hermes to your own Telegram/Discord/etc.\n - **Multi-user gateway** — `pinUserPeer: false`, optional `runtimePeerPrefix`. Each runtime user → own peer. Recommended for bots serving many humans.","@@ -225,7 +229,7 @@ Multiple Hermes profiles can share one workspace while maintaining separate AI i\n       \"recallMode\": \"hybrid\",\n       \"sessionStrategy\": \"per-directory\"\n     },\n-    \"hermes.coder\": {\n+    \"hermes_coder\": {\n       \"aiPeer\": \"coder\",\n       \"recallMode\": \"tools\",\n       \"sessionStrategy\": \"per-repo\"","@@ -236,7 +240,7 @@ Multiple Hermes profiles can share one workspace while maintaining separate AI i\n \n Both profiles see the same user (`yourname`) in the same shared environment (`hermes`), but each AI peer builds its own observations, conclusions, and behavior patterns. The coder's memory stays code-oriented; the main agent's stays broad.\n \n-Host key is derived from the active Hermes profile: `hermes` (default) or `hermes.<profile>` (e.g. `hermes -p coder` → host key `hermes.coder`).\n+Host key is derived from the active Hermes profile: `hermes` (default) or `hermes_<profile>` (e.g. `hermes -p coder` -> host key `hermes_coder`). Older `hermes.<profile>` host blocks are still read for compatibility and are migrated when the CLI writes profile-scoped Honcho config.\n \n ### Dialectic & Reasoning\n ","@@ -307,7 +311,8 @@ Presets:\n \n | Command | Description |\n |---------|-------------|\n-| `hermes honcho setup` | Full interactive setup wizard |\n+| `hermes memory setup honcho` | Configure Honcho directly — works on a fresh install |\n+| `hermes honcho setup` | Interactive setup wizard (only registered once Honcho is the active provider; redirects to `hermes memory setup`) |\n | `hermes honcho status` | Show resolved config for active profile |\n | `hermes honcho enable` / `disable` | Toggle Honcho for active profile |\n | `hermes honcho mode <mode>` | Change recall or observation mode |","@@ -344,7 +349,7 @@ Presets:\n       \"dialecticMaxChars\": 600,\n       \"saveMessages\": true\n     },\n-    \"hermes.coder\": {\n+    \"hermes_coder\": {\n       \"enabled\": true,\n       \"aiPeer\": \"coder\",\n       \"sessionStrategy\": \"per-repo\","]},{"file":"plugins/memory/honcho/__init__.py","hunks":["@@ -249,6 +249,7 @@ def is_available(self) -> bool:\n     def save_config(self, values, hermes_home):\n         \"\"\"Write config to $HERMES_HOME/honcho.json (Honcho SDK native format).\"\"\"\n         import json\n+        import os\n         from pathlib import Path\n         config_path = Path(hermes_home) / \"honcho.json\"\n         existing = {}","@@ -258,7 +259,8 @@ def save_config(self, values, hermes_home):\n             except Exception:\n                 pass\n         existing.update(values)\n-        config_path.write_text(json.dumps(existing, indent=2))\n+        from utils import atomic_json_write\n+        atomic_json_write(config_path, existing, mode=0o600)\n \n     def get_config_schema(self):\n         return ["]},{"file":"plugins/memory/honcho/cli.py","hunks":["@@ -11,7 +11,7 @@\n from pathlib import Path\n \n from hermes_constants import get_hermes_home\n-from plugins.memory.honcho.client import resolve_active_host, resolve_config_path, HOST\n+from plugins.memory.honcho.client import _host_block, profile_host_key, resolve_active_host, resolve_config_path, HOST\n from hermes_cli.config import cfg_get\n \n ","@@ -36,7 +36,7 @@ def clone_honcho_for_profile(profile_name: str) -> bool:\n     if not default_block and not has_key:\n         return False\n \n-    new_host = f\"{HOST}.{profile_name}\"\n+    new_host = profile_host_key(profile_name)\n     if new_host in hosts:\n         return False  # already exists\n ","@@ -192,7 +192,7 @@ def cmd_sync(args) -> None:\n         if p.name == \"default\":\n             continue\n         if clone_honcho_for_profile(p.name):\n-            print(f\"  + {p.name} -> hermes.{p.name}\")\n+            print(f\"  + {p.name} -> {profile_host_key(p.name)}\")\n             created += 1\n         else:\n             skipped += 1","@@ -243,7 +243,7 @@ def _host_key() -> str:\n     if _profile_override:\n         if _profile_override in {\"default\", \"custom\"}:\n             return HOST\n-        return f\"{HOST}.{_profile_override}\"\n+        return profile_host_key(_profile_override)\n     return resolve_active_host()\n \n ","@@ -275,10 +275,8 @@ def _read_config() -> dict:\n def _write_config(cfg: dict, path: Path | None = None) -> None:\n     path = path or _local_config_path()\n     path.parent.mkdir(parents=True, exist_ok=True)\n-    path.write_text(\n-        json.dumps(cfg, indent=2, ensure_ascii=False) + \"\\n\",\n-        encoding=\"utf-8\",\n-    )\n+    from utils import atomic_json_write\n+    atomic_json_write(path, cfg, mode=0o600)\n \n \n def _resolve_api_key(cfg: dict) -> str:","@@ -292,7 +290,7 @@ def _resolve_api_key(cfg: dict) -> str:\n     config shapes, e.g. ``localhost:8000``) still pass — the Honcho SDK\n     will reject them itself with a clearer error than ours.\n     \"\"\"\n-    host_key = ((cfg.get(\"hosts\") or {}).get(_host_key()) or {}).get(\"apiKey\")\n+    host_key = _host_block(cfg, _host_key()).get(\"apiKey\")\n     key = host_key or cfg.get(\"apiKey\", \"\") or os.environ.get(\"HONCHO_API_KEY\", \"\")\n     if not key:\n         base_url = cfg.get(\"baseUrl\") or cfg.get(\"base_url\") or os.environ.get(\"HONCHO_BASE_URL\", \"\")","@@ -462,21 +460,58 @@ def cmd_setup(args) -> None:\n     cfg.pop(\"base_url\", None)\n \n     if is_local:\n-        # --- Local: ask for base URL, skip or clear API key ---\n+        # --- Local: ask for base URL, optionally accept a JWT for auth ---\n         current_url = cfg.get(\"baseUrl\") or \"\"\n         new_url = _prompt(\"Base URL\", default=current_url or \"http://localhost:8000\")\n         if new_url:\n             cfg[\"baseUrl\"] = new_url\n \n-        # For local no-auth, the SDK must not send an API key.\n-        # We keep the key in config (for cloud switching later) but\n-        # the client should skip auth when baseUrl is local.\n-        current_key = cfg.get(\"apiKey\", \"\")\n-        if current_key:\n-            print(f\"\\n  API key present in config (kept for cloud/hybrid use).\")\n-            print(\"  Local connections will skip auth automatically.\")\n+        # Self-hosted Honcho can run with AUTH_USE_AUTH=true and an\n+        # AUTH_JWT_SECRET on the server side. In that case clients must\n+        # send a JWT signed with that secret as the bearer token (the\n+        # Honcho SDK takes it via ``api_key=``). Cloud users got prompted\n+        # for a key already; the local path historically skipped this and\n+        # forced users to disable auth on the server. Offer the prompt\n+        # here too. We store it under the host block (not the top-level\n+        # apiKey) so ``get_honcho_client`` recognises it as an explicit\n+        # local auth opt-in (see ``_host_has_key`` in client.py) and\n+        # cloud/hybrid switching is unaffected.\n+        current_host_key = hermes_host.get(\"apiKey\", \"\")\n+        masked = (\n+            f\"...{current_host_key[-8:]}\"\n+            if len(current_host_key) > 8\n+            else (\"set\" if current_host_key else \"not set\")\n+        )\n+        print(\n+            \"\\n  Local Honcho auth (JWT signed with the server's \"\n+            \"AUTH_JWT_SECRET).\"\n+        )\n+        print(\n+            \"  Leave blank if your server runs with AUTH_USE_AUTH=false. \"\n+            f\"Current: {masked}\"\n+        )\n+        new_local_key = _prompt(\n+            \"Local JWT / bearer token (blank to skip / keep current)\",\n+            secret=True,\n+        )\n+        if new_local_key:\n+            hermes_host[\"apiKey\"] = new_local_key\n+        elif current_host_key:\n+            print(\"  Keeping existing local JWT.\")\n         else:\n-            print(\"\\n  No API key set. Local no-auth ready.\")\n+            # Surface the top-level key situation for transparency.\n+            top_key = cfg.get(\"apiKey\", \"\")\n+            if top_key:\n+                print(\n+                    \"\\n  Top-level API key present in config (kept for \"\n+                    \"cloud/hybrid use).\"\n+                )\n+                print(\n+                    \"  Local connections will skip auth automatically \"\n+                    \"until a local JWT is set above.\"\n+                )\n+            else:\n+                print(\"\\n  No local JWT set. Local no-auth ready.\")\n     else:\n         # --- Cloud: set default base URL, require API key ---\n         cfg.pop(\"baseUrl\", None)  # cloud uses SDK default"]},{"file":"plugins/memory/honcho/client.py","hunks":["@@ -32,6 +32,24 @@\n HOST = \"hermes\"\n \n \n+def profile_host_key(profile: str | None) -> str:\n+    \"\"\"Return the safe Honcho host key for a Hermes profile.\"\"\"\n+    if not profile or profile in {\"default\", \"custom\"}:\n+        return HOST\n+    sanitized = \"\".join(c if c.isalnum() or c in \"_-\" else \"_\" for c in profile).strip(\"_\")\n+    return f\"{HOST}_{sanitized or 'profile'}\"\n+\n+\n+def _host_block(raw: dict, host: str) -> dict:\n+    \"\"\"Return host config, accepting legacy dot-form profile host keys.\"\"\"\n+    hosts = raw.get(\"hosts\") or {}\n+    block = hosts.get(host, {})\n+    if block or not host.startswith(f\"{HOST}_\"):\n+        return block\n+    legacy = f\"{HOST}.{host[len(HOST) + 1:]}\"\n+    return hosts.get(legacy, {})\n+\n+\n def resolve_active_host() -> str:\n     \"\"\"Derive the Honcho host key from the active Hermes profile.\n ","@@ -47,8 +65,7 @@ def resolve_active_host() -> str:\n     try:\n         from hermes_cli.profiles import get_active_profile_name\n         profile = get_active_profile_name()\n-        if profile and profile not in {\"default\", \"custom\"}:\n-            return f\"{HOST}.{profile}\"\n+        return profile_host_key(profile)\n     except Exception:\n         pass\n     return HOST","@@ -406,7 +423,7 @@ def from_global_config(\n             logger.warning(\"Failed to read %s: %s, falling back to env\", path, e)\n             return cls.from_env(host=resolved_host)\n \n-        host_block = (raw.get(\"hosts\") or {}).get(resolved_host, {})\n+        host_block = _host_block(raw, resolved_host)\n         # A hosts.hermes block or explicit enabled flag means the user\n         # intentionally configured Honcho for this host.\n         _explicitly_configured = bool(host_block) or raw.get(\"enabled\") is True","@@ -811,7 +828,10 @@ def get_honcho_client(config: HonchoClientConfig | None = None) -> Honcho:\n         or \"::1\" in resolved_base_url\n     )\n     if _is_local:\n-        # Check if the host block has its own apiKey (explicit local auth)\n+        # Check if the host block has its own apiKey (explicit local auth).\n+        # Auth-skipping is loopback-only: a stored key is likely a cloud key\n+        # that would break a no-auth local server, so we substitute the SDK's\n+        # required-non-empty placeholder unless the host block opts in.\n         _raw = config.raw or {}\n         _host_block = (_raw.get(\"hosts\") or {}).get(config.host, {})\n         _host_has_key = bool(_host_block.get(\"apiKey\"))","@@ -819,6 +839,18 @@ def get_honcho_client(config: HonchoClientConfig | None = None) -> Honcho:\n     else:\n         effective_api_key = [redacted:secret-assignment]\n \n+    # The Honcho SDK's route builders (e.g. routes.workspaces()) already\n+    # include the version prefix (e.g. \"/v3/workspaces\").  When a user-supplied\n+    # base_url already ends in a version segment (e.g.\n+    # \"http://localhost:38000/v3\", \"https://honcho.my.ts.net/v3\"), concatenating\n+    # the two produces \"/v3/v3/workspaces\" → 404 on every call.  This is a pure\n+    # routing concern independent of host, so strip a trailing version segment\n+    # from ANY base_url — loopback, LAN, custom domain, or cloud alike.  The\n+    # SDK then appends its own versioned paths correctly.\n+    if resolved_base_url:\n+        import re as _re\n+        resolved_base_url = _re.sub(r\"/v\\d+/*$\", \"\", resolved_base_url).rstrip(\"/\")\n+\n     kwargs: dict = {\n         \"workspace_id\": config.workspace_id,\n         \"api_key\": effective_api_key,"]},{"file":"plugins/memory/mem0/__init__.py","hunks":["@@ -155,7 +155,8 @@ def save_config(self, values, hermes_home):\n             except Exception:\n                 pass\n         existing.update(values)\n-        config_path.write_text(json.dumps(existing, indent=2))\n+        from utils import atomic_json_write\n+        atomic_json_write(config_path, existing, mode=0o600)\n \n     def get_config_schema(self):\n         return ["]},{"file":"plugins/memory/supermemory/__init__.py","hunks":["@@ -152,7 +152,8 @@ def _save_supermemory_config(values: dict, hermes_home: str) -> None:\n         except Exception:\n             existing = {}\n     existing.update(values)\n-    config_path.write_text(json.dumps(existing, indent=2, sort_keys=True) + \"\\n\", encoding=\"utf-8\")\n+    from utils import atomic_json_write\n+    atomic_json_write(config_path, existing, mode=0o600, sort_keys=True)\n \n \n def _detect_category(text: str) -> str:"]},{"file":"tests/gateway/test_agent_cache.py","hunks":["@@ -276,6 +276,111 @@ def test_extract_includes_live_tool_registry_generation(self, monkeypatch):\n \n         assert out[\"tools.registry_generation\"] == 12345\n \n+\n+    def test_skips_honcho_config_read_when_provider_is_not_honcho(self, monkeypatch):\n+        \"\"\"Non-Honcho gateways must not read/parse honcho.json on every message.\"\"\"\n+        from gateway.run import GatewayRunner\n+\n+        called = False\n+\n+        def _boom():\n+            nonlocal called\n+            called = True\n+            raise AssertionError(\"should not read Honcho config\")\n+\n+        monkeypatch.setattr(GatewayRunner, \"_extract_honcho_cache_busting_config\", _boom)\n+\n+        out = GatewayRunner._extract_cache_busting_config({\"memory\": {\"provider\": \"mem0\"}})\n+\n+        assert called is False\n+        assert out[\"honcho.peer_name\"] is None\n+        assert out[\"honcho.user_peer_aliases\"] is None\n+\n+    def test_reads_honcho_config_only_when_provider_is_honcho(self, monkeypatch):\n+        from gateway.run import GatewayRunner\n+\n+        calls = []\n+\n+        def _fake():\n+            calls.append(True)\n+            return {\n+                \"honcho.peer_name\": \"eri\",\n+                \"honcho.ai_peer\": \"hermes\",\n+                \"honcho.pin_peer_name\": True,\n+                \"honcho.runtime_peer_prefix\": \"tg_\",\n+                \"honcho.user_peer_aliases\": [(\"123\", \"eri\")],\n+            }\n+\n+        monkeypatch.setattr(GatewayRunner, \"_extract_honcho_cache_busting_config\", _fake)\n+\n+        out = GatewayRunner._extract_cache_busting_config({\"memory\": {\"provider\": \"honcho\"}})\n+\n+        assert calls == [True]\n+        assert out[\"honcho.peer_name\"] == \"eri\"\n+        assert out[\"honcho.user_peer_aliases\"] == [(\"123\", \"eri\")]\n+\n+    def test_memory_provider_change_busts_signature(self, monkeypatch):\n+        \"\"\"Switching memory.provider must itself change the cache-busting\n+        signature, so the agent is rebuilt when a user swaps providers\n+        mid-gateway (independent of the honcho.json identity keys).\"\"\"\n+        from gateway.run import GatewayRunner\n+\n+        # Neutralize honcho.json reads so the only varying input is the\n+        # provider value itself.\n+        monkeypatch.setattr(\n+            GatewayRunner,\n+            \"_extract_honcho_cache_busting_config\",\n+            classmethod(lambda cls: cls._empty_honcho_cache_busting_config()),\n+        )\n+\n+        sig_honcho = GatewayRunner._extract_cache_busting_config({\"memory\": {\"provider\": \"honcho\"}})\n+        sig_mem0 = GatewayRunner._extract_cache_busting_config({\"memory\": {\"provider\": \"mem0\"}})\n+\n+        assert sig_honcho[\"memory.provider\"] == \"honcho\"\n+        assert sig_mem0[\"memory.provider\"] == \"mem0\"\n+        assert sig_honcho != sig_mem0\n+\n+    def test_honcho_cache_busting_config_memoized_by_mtime(self, monkeypatch, tmp_path):\n+        \"\"\"Repeated Honcho extraction for unchanged honcho.json should reuse parse result.\"\"\"\n+        from types import SimpleNamespace\n+        from gateway.run import GatewayRunner\n+\n+        config_path = tmp_path / \"honcho.json\"\n+        config_path.write_text(\"{}\")\n+        parse_calls = []\n+\n+        class FakeConfig:\n+            peer_name = \"eri\"\n+            ai_peer = \"hermes\"\n+            pin_peer_name = False\n+            runtime_peer_prefix = \"tg_\"\n+            user_peer_aliases = {\"123\": \"eri\"}\n+\n+            @classmethod\n+            def from_global_config(cls, config_path=None):\n+                parse_calls.append(config_path)\n+                return cls()\n+\n+        fake_client = SimpleNamespace(\n+            HonchoClientConfig=FakeConfig,\n+            resolve_config_path=lambda: config_path,\n+        )\n+        monkeypatch.setitem(__import__(\"sys\").modules, \"plugins.memory.honcho.client\", fake_client)\n+        monkeypatch.setattr(GatewayRunner, \"_HONCHO_CACHE_BUSTING_MEMO\", {})\n+\n+        first = GatewayRunner._extract_honcho_cache_busting_config()\n+        second = GatewayRunner._extract_honcho_cache_busting_config()\n+\n+        assert first == second\n+        assert first[\"honcho.user_peer_aliases\"] == [(\"123\", \"eri\")]\n+        assert parse_calls == [config_path]\n+\n+        config_path.write_text(\"{\\n  \\\"changed\\\": true\\n}\")\n+        third = GatewayRunner._extract_honcho_cache_busting_config()\n+\n+        assert third == first\n+        assert parse_calls == [config_path, config_path]\n+\n     def test_full_round_trip_busts_cache_on_real_edit(self):\n         \"\"\"End-to-end: simulate a config edit on main and verify the\n         extracted cache_keys change produces a new signature.\"\"\""]},{"file":"tests/hermes_cli/test_memory_setup_provider_arg.py","hunks":["@@ -0,0 +1,50 @@\n+\"\"\"Tests for `hermes memory setup [provider]` routing.\n+\n+The `memory setup` subcommand accepts an optional positional ``provider`` so a\n+fresh install can configure a specific provider directly (e.g.\n+``hermes memory setup honcho``) without the interactive picker — which matters\n+because the per-provider ``hermes <provider>`` subcommand is only registered\n+once that provider is active.\n+\"\"\"\n+\n+from types import SimpleNamespace\n+from unittest.mock import patch\n+\n+from hermes_cli import memory_setup\n+\n+\n+class TestMemorySetupProviderRouting:\n+    def test_setup_with_provider_arg_skips_picker(self):\n+        \"\"\"`memory setup honcho` routes straight to cmd_setup_provider.\"\"\"\n+        args = SimpleNamespace(memory_command=\"setup\", provider=\"honcho\")\n+        with patch.object(memory_setup, \"cmd_setup_provider\") as direct, \\\n+             patch.object(memory_setup, \"cmd_setup\") as picker:\n+            memory_setup.memory_command(args)\n+        direct.assert_called_once_with(\"honcho\")\n+        picker.assert_not_called()\n+\n+    def test_setup_without_provider_runs_picker(self):\n+        \"\"\"`memory setup` (no provider) runs the interactive picker.\"\"\"\n+        args = SimpleNamespace(memory_command=\"setup\", provider=None)\n+        with patch.object(memory_setup, \"cmd_setup_provider\") as direct, \\\n+             patch.object(memory_setup, \"cmd_setup\") as picker:\n+            memory_setup.memory_command(args)\n+        picker.assert_called_once_with(args)\n+        direct.assert_not_called()\n+\n+    def test_setup_with_missing_provider_attr_runs_picker(self):\n+        \"\"\"A SimpleNamespace lacking `provider` must not crash — fall back to picker.\"\"\"\n+        args = SimpleNamespace(memory_command=\"setup\")\n+        with patch.object(memory_setup, \"cmd_setup_provider\") as direct, \\\n+             patch.object(memory_setup, \"cmd_setup\") as picker:\n+            memory_setup.memory_command(args)\n+        picker.assert_called_once_with(args)\n+        direct.assert_not_called()\n+\n+    def test_unknown_provider_reports_and_returns_early(self, capsys):\n+        \"\"\"An unknown provider name surfaces a helpful message and returns\n+        before any config load/save (the not-found guard precedes those imports).\"\"\"\n+        memory_setup.cmd_setup_provider(\"notaprovider\")\n+        out = capsys.readouterr().out\n+        assert \"not found\" in out\n+        assert \"hermes memory setup\" in out"]},{"file":"tests/hermes_cli/test_profiles.py","hunks":["@@ -754,8 +754,8 @@ def test_renames_root_honcho_host_without_changing_ai_peer(self, profile_env):\n \n         cfg = json.loads(honcho_path.read_text())\n         assert \"hermes.ssi_health\" not in cfg[\"hosts\"]\n-        assert cfg[\"hosts\"][\"hermes.heimdall\"][\"aiPeer\"] == \"ssi_health\"\n-        assert cfg[\"hosts\"][\"hermes.heimdall\"][\"peerName\"] == \"user-peer\"\n+        assert cfg[\"hosts\"][\"hermes_heimdall\"][\"aiPeer\"] == \"ssi_health\"\n+        assert cfg[\"hosts\"][\"hermes_heimdall\"][\"peerName\"] == \"user-peer\"\n \n     def test_pins_ai_peer_when_absent_on_honcho_host_rename(self, profile_env):\n         tmp_path = profile_env","@@ -772,8 +772,8 @@ def test_pins_ai_peer_when_absent_on_honcho_host_rename(self, profile_env):\n \n         cfg = json.loads(honcho_path.read_text())\n         assert \"hermes.ssi_health\" not in cfg[\"hosts\"]\n-        assert cfg[\"hosts\"][\"hermes.heimdall\"][\"aiPeer\"] == \"ssi_health\"\n-        assert cfg[\"hosts\"][\"hermes.heimdall\"][\"workspace\"] == \"hermes\"\n+        assert cfg[\"hosts\"][\"hermes_heimdall\"][\"aiPeer\"] == \"ssi_health\"\n+        assert cfg[\"hosts\"][\"hermes_heimdall\"][\"workspace\"] == \"hermes\"\n \n     def test_does_not_overwrite_existing_honcho_host_on_rename(self, profile_env):\n         tmp_path = profile_env","@@ -782,7 +782,7 @@ def test_does_not_overwrite_existing_honcho_host_on_rename(self, profile_env):\n         honcho_path.write_text(json.dumps({\n             \"hosts\": {\n                 \"hermes.ssi_health\": {\"aiPeer\": \"ssi_health\"},\n-                \"hermes.heimdall\": {\"aiPeer\": \"heimdall\"},\n+                \"hermes_heimdall\": {\"aiPeer\": \"heimdall\"},\n             }\n         }))\n ","@@ -791,7 +791,7 @@ def test_does_not_overwrite_existing_honcho_host_on_rename(self, profile_env):\n \n         cfg = json.loads(honcho_path.read_text())\n         assert cfg[\"hosts\"][\"hermes.ssi_health\"][\"aiPeer\"] == \"ssi_health\"\n-        assert cfg[\"hosts\"][\"hermes.heimdall\"][\"aiPeer\"] == \"heimdall\"\n+        assert cfg[\"hosts\"][\"hermes_heimdall\"][\"aiPeer\"] == \"heimdall\"\n \n     def test_default_raises_value_error(self, profile_env):\n         with pytest.raises(ValueError, match=\"default\"):"]},{"file":"tests/honcho_plugin/test_cli.py","hunks":["@@ -1,6 +1,7 @@\n \"\"\"Tests for plugins/memory/honcho/cli.py.\"\"\"\n \n from types import SimpleNamespace\n+import json\n \n \n class TestResolveApiKey:","@@ -100,6 +101,84 @@ def test_accepts_legacy_schemeless_host(self, monkeypatch):\n                 f\"expected local sentinel for legacy schemeless {legacy!r}\"\n \n \n+class TestCmdSetupLocalJwt:\n+    \"\"\"Local-deployment setup must allow configuring a JWT for AUTH_JWT_SECRET-backed Honcho servers.\"\"\"\n+\n+    def _run_setup(self, monkeypatch, tmp_path, initial_cfg, prompt_answers):\n+        import plugins.memory.honcho.cli as honcho_cli\n+\n+        # Avoid touching real config / SDK / filesystem.\n+        cfg_path = tmp_path / \"honcho.json\"\n+        monkeypatch.setattr(honcho_cli, \"_read_config\", lambda: dict(initial_cfg))\n+        monkeypatch.setattr(honcho_cli, \"_local_config_path\", lambda: cfg_path)\n+        monkeypatch.setattr(honcho_cli, \"_config_path\", lambda: cfg_path)\n+        monkeypatch.setattr(honcho_cli, \"_host_key\", lambda: \"hermes\")\n+        monkeypatch.setattr(honcho_cli, \"_ensure_sdk_installed\", lambda: True)\n+\n+        written = {}\n+\n+        def _capture_write(cfg, path=None):\n+            written[\"cfg\"] = cfg\n+            written[\"path\"] = path\n+\n+        monkeypatch.setattr(honcho_cli, \"_write_config\", _capture_write)\n+\n+        # Feed scripted prompt answers in order.\n+        answers = list(prompt_answers)\n+\n+        def _fake_prompt(label, default=None, secret=[redacted:secret-assignment]\n+            if not answers:\n+                # Default-through any remaining prompts to keep the wizard moving.\n+                return default or \"\"\n+            return answers.pop(0)\n+\n+        monkeypatch.setattr(honcho_cli, \"_prompt\", _fake_prompt)\n+\n+        honcho_cli.cmd_setup(SimpleNamespace())\n+        return written.get(\"cfg\")\n+\n+    def test_local_setup_stores_jwt_under_host_block(self, monkeypatch, tmp_path):\n+        \"\"\"Self-hosted users supplying a JWT must have it written under hosts.<host>.apiKey,\n+        not as the top-level cloud apiKey, so cloud/hybrid switching is preserved and\n+        get_honcho_client treats it as an explicit local auth opt-in.\"\"\"\n+        cfg = self._run_setup(\n+            monkeypatch,\n+            tmp_path,\n+            initial_cfg={},\n+            prompt_answers=[\n+                \"local\",                       # deployment\n+                \"http://localhost:8000\",       # base URL\n+                \"my-local-jwt-token\",          # local JWT\n+            ],\n+        )\n+        assert cfg is not None\n+        assert cfg.get(\"baseUrl\") == \"http://localhost:8000\"\n+        # Top-level apiKey must remain unset (cloud field).\n+        assert not cfg.get(\"apiKey\")\n+        # The new local JWT belongs under the host block.\n+        host_block = (cfg.get(\"hosts\") or {}).get(\"hermes\") or {}\n+        assert host_block.get(\"apiKey\") == \"my-local-jwt-token\"\n+\n+    def test_local_setup_blank_jwt_keeps_local_no_auth(self, monkeypatch, tmp_path):\n+        \"\"\"Blank JWT prompt response on a fresh local config must not introduce an apiKey\n+        anywhere (local no-auth Honcho deployments must still work out of the box).\"\"\"\n+        cfg = self._run_setup(\n+            monkeypatch,\n+            tmp_path,\n+            initial_cfg={},\n+            prompt_answers=[\n+                \"local\",\n+                \"http://localhost:8000\",\n+                \"\",  # blank JWT\n+            ],\n+        )\n+        assert cfg is not None\n+        assert cfg.get(\"baseUrl\") == \"http://localhost:8000\"\n+        assert not cfg.get(\"apiKey\")\n+        host_block = (cfg.get(\"hosts\") or {}).get(\"hermes\") or {}\n+        assert not host_block.get(\"apiKey\")\n+\n+\n class TestCmdStatus:\n     def test_reports_connection_failure_when_session_setup_fails(self, monkeypatch, capsys, tmp_path):\n         import plugins.memory.honcho.cli as honcho_cli","@@ -192,7 +271,7 @@ def test_user_peer_aliases_carry_into_cloned_profile(self, monkeypatch, tmp_path\n         honcho_cli, written = self._setup_clone_env(monkeypatch, tmp_path, cfg)\n         ok = honcho_cli.clone_honcho_for_profile(\"coder\")\n         assert ok is True\n-        new_block = written[\"cfg\"][\"hosts\"][\"hermes.coder\"]\n+        new_block = written[\"cfg\"][\"hosts\"][\"hermes_coder\"]\n         assert new_block[\"userPeerAliases\"] == {\"86701400\": \"eri\", \"discord-491827364\": \"eri\"}\n \n     def test_runtime_peer_prefix_carries_into_cloned_profile(self, monkeypatch, tmp_path):","@@ -208,7 +287,7 @@ def test_runtime_peer_prefix_carries_into_cloned_profile(self, monkeypatch, tmp_\n         honcho_cli, written = self._setup_clone_env(monkeypatch, tmp_path, cfg)\n         ok = honcho_cli.clone_honcho_for_profile(\"coder\")\n         assert ok is True\n-        new_block = written[\"cfg\"][\"hosts\"][\"hermes.coder\"]\n+        new_block = written[\"cfg\"][\"hosts\"][\"hermes_coder\"]\n         assert new_block[\"runtimePeerPrefix\"] == \"telegram_\"\n \n     def test_pin_peer_name_carries_into_cloned_profile(self, monkeypatch, tmp_path):","@@ -224,7 +303,7 @@ def test_pin_peer_name_carries_into_cloned_profile(self, monkeypatch, tmp_path):\n         honcho_cli, written = self._setup_clone_env(monkeypatch, tmp_path, cfg)\n         ok = honcho_cli.clone_honcho_for_profile(\"coder\")\n         assert ok is True\n-        new_block = written[\"cfg\"][\"hosts\"][\"hermes.coder\"]\n+        new_block = written[\"cfg\"][\"hosts\"][\"hermes_coder\"]\n         assert new_block[\"pinPeerName\"] is True\n \n     def test_unset_identity_keys_do_not_appear_in_cloned_profile(self, monkeypatch, tmp_path):","@@ -235,7 +314,7 @@ def test_unset_identity_keys_do_not_appear_in_cloned_profile(self, monkeypatch,\n         honcho_cli, written = self._setup_clone_env(monkeypatch, tmp_path, cfg)\n         ok = honcho_cli.clone_honcho_for_profile(\"coder\")\n         assert ok is True\n-        new_block = written[\"cfg\"][\"hosts\"][\"hermes.coder\"]\n+        new_block = written[\"cfg\"][\"hosts\"][\"hermes_coder\"]\n         assert \"userPeerAliases\" not in new_block\n         assert \"runtimePeerPrefix\" not in new_block\n         assert \"pinPeerName\" not in new_block","@@ -572,5 +651,5 @@ def test_clone_inherits_host_pin_user_peer(self, monkeypatch, tmp_path):\n \n         ok = honcho_cli.clone_honcho_for_profile(\"partner\")\n         assert ok is True\n-        new_block = written[\"cfg\"][\"hosts\"][\"hermes.partner\"]\n+        new_block = written[\"cfg\"][\"hosts\"][\"hermes_partner\"]\n         assert new_block[\"pinUserPeer\"] is True"]},{"file":"tests/honcho_plugin/test_client.py","hunks":["@@ -13,6 +13,7 @@\n from plugins.memory.honcho.client import (\n     HonchoClientConfig,\n     get_honcho_client,\n+    profile_host_key,\n     reset_honcho_client,\n     resolve_active_host,\n     resolve_config_path,","@@ -430,6 +431,10 @@ def test_from_global_config_uses_local_path(self, tmp_path):\n \n \n class TestResolveActiveHost:\n+    def test_profile_host_key_uses_honcho_safe_separator(self):\n+        assert profile_host_key(\"coder\") == \"hermes_coder\"\n+        assert profile_host_key(\"default\") == \"hermes\"\n+\n     def test_default_returns_hermes(self):\n         with patch.dict(os.environ, {}, clear=True):\n             os.environ.pop(\"HERMES_HONCHO_HOST\", None)","@@ -444,7 +449,7 @@ def test_profile_name_derives_host(self):\n         with patch.dict(os.environ, {}, clear=False):\n             os.environ.pop(\"HERMES_HONCHO_HOST\", None)\n             with patch(\"hermes_cli.profiles.get_active_profile_name\", return_value=\"coder\"):\n-                assert resolve_active_host() == \"hermes.coder\"\n+                assert resolve_active_host() == \"hermes_coder\"\n \n     def test_default_profile_returns_hermes(self):\n         with patch.dict(os.environ, {}, clear=False):","@@ -477,10 +482,10 @@ def test_profiles_import_failure_falls_back(self):\n class TestProfileScopedConfig:\n     def test_from_env_uses_profile_host(self):\n         with patch.dict(os.environ, {\"HONCHO_API_KEY\": \"key\"}):\n-            config = HonchoClientConfig.from_env(host=\"hermes.coder\")\n-        assert config.host == \"hermes.coder\"\n+            config = HonchoClientConfig.from_env(host=\"hermes_coder\")\n+        assert config.host == \"hermes_coder\"\n         assert config.workspace_id == \"hermes\"  # shared workspace\n-        assert config.ai_peer == \"hermes.coder\"\n+        assert config.ai_peer == \"hermes_coder\"\n \n     def test_from_env_default_workspace_preserved_for_default_host(self):\n         with patch.dict(os.environ, {\"HONCHO_API_KEY\": \"key\"}):","@@ -494,19 +499,19 @@ def test_from_global_config_reads_profile_host_block(self, tmp_path):\n             \"apiKey\": \"shared-key\",\n             \"hosts\": {\n                 \"hermes\": {\"aiPeer\": \"hermes\", \"peerName\": \"alice\"},\n-                \"hermes.coder\": {\n-                    \"aiPeer\": \"hermes.coder\",\n+                \"hermes_coder\": {\n+                    \"aiPeer\": \"hermes_coder\",\n                     \"peerName\": \"alice-coder\",\n                     \"workspace\": \"coder-ws\",\n                 },\n             },\n         }))\n         config = HonchoClientConfig.from_global_config(\n-            host=\"hermes.coder\", config_path=config_file,\n+            host=\"hermes_coder\", config_path=config_file,\n         )\n-        assert config.host == \"hermes.coder\"\n+        assert config.host == \"hermes_coder\"\n         assert config.workspace_id == \"coder-ws\"\n-        assert config.ai_peer == \"hermes.coder\"\n+        assert config.ai_peer == \"hermes_coder\"\n         assert config.peer_name == \"alice-coder\"\n \n     def test_from_global_config_auto_resolves_host(self, tmp_path):","@@ -514,14 +519,30 @@ def test_from_global_config_auto_resolves_host(self, tmp_path):\n         config_file.write_text(json.dumps({\n             \"apiKey\": \"key\",\n             \"hosts\": {\n-                \"hermes.dreamer\": {\"peerName\": \"dreamer-user\"},\n+                \"hermes_dreamer\": {\"peerName\": \"dreamer-user\"},\n             },\n         }))\n-        with patch(\"plugins.memory.honcho.client.resolve_active_host\", return_value=\"hermes.dreamer\"):\n+        with patch(\"plugins.memory.honcho.client.resolve_active_host\", return_value=\"hermes_dreamer\"):\n             config = HonchoClientConfig.from_global_config(config_path=config_file)\n-        assert config.host == \"hermes.dreamer\"\n+        assert config.host == \"hermes_dreamer\"\n         assert config.peer_name == \"dreamer-user\"\n \n+    def test_from_global_config_reads_legacy_dot_profile_host_block(self, tmp_path):\n+        config_file = tmp_path / \"config.json\"\n+        config_file.write_text(json.dumps({\n+            \"apiKey\": \"key\",\n+            \"hosts\": {\n+                \"hermes.dreamer\": {\"peerName\": \"dreamer-user\"},\n+            },\n+        }))\n+        config = HonchoClientConfig.from_global_config(\n+            host=\"hermes_dreamer\",\n+            config_path=config_file,\n+        )\n+        assert config.host == \"hermes_dreamer\"\n+        assert config.peer_name == \"dreamer-user\"\n+        assert config.workspace_id == \"hermes_dreamer\"\n+\n \n class TestObservationModeMigration:\n     \"\"\"Existing configs without explicit observationMode keep 'unified' default.\"\"\"","@@ -890,3 +911,176 @@ def test_depth_levels_invalid_values_default_to_low(self, tmp_path):\n         }))\n         config = HonchoClientConfig.from_global_config(config_path=config_file)\n         assert config.dialectic_depth_levels == [\"low\", \"high\"]\n+\n+\n+class TestGetHonchoClientBaseUrlDoublePrefixFix:\n+    \"\"\"Regression tests for #20688 — Honcho SDK double-prefixing of /v3 for\n+    self-hosted instances where base_url already contains a version path.\"\"\"\n+\n+    def teardown_method(self):\n+        reset_honcho_client()\n+\n+    @pytest.mark.skipif(\n+        not importlib.util.find_spec(\"honcho\"),\n+        reason=\"honcho SDK not installed\"\n+    )\n+    def test_local_base_url_with_v3_suffix_stripped(self):\n+        \"\"\"base_url 'http://localhost:38000/v3' must become 'http://localhost:38000'\n+        before passing to the Honcho SDK to avoid double '/v3/v3' prefixing.\"\"\"\n+        fake_honcho = MagicMock(name=\"Honcho\")\n+        cfg = HonchoClientConfig(\n+            api_key=None,\n+            base_url=\"http://localhost:38000/v3\",\n+            workspace_id=\"hermes\",\n+            environment=\"production\",\n+        )\n+\n+        with patch(\"honcho.Honcho\", return_value=fake_honcho) as mock_honcho, \\\n+             patch(\"hermes_cli.config.load_config\", return_value={}):\n+            get_honcho_client(cfg)\n+\n+        mock_honcho.assert_called_once()\n+        passed_base_url = mock_honcho.call_args.kwargs.get(\"base_url\")\n+        assert passed_base_url == \"http://localhost:38000\", (\n+            f\"Expected 'http://localhost:38000', got {passed_base_url!r}\"\n+        )\n+\n+    @pytest.mark.skipif(\n+        not importlib.util.find_spec(\"honcho\"),\n+        reason=\"honcho SDK not installed\"\n+    )\n+    def test_local_base_url_without_version_unchanged(self):\n+        \"\"\"base_url 'http://localhost:38000' (no version) must be passed unchanged.\"\"\"\n+        fake_honcho = MagicMock(name=\"Honcho\")\n+        cfg = HonchoClientConfig(\n+            api_key=None,\n+            base_url=\"http://localhost:38000\",\n+            workspace_id=\"hermes\",\n+            environment=\"production\",\n+        )\n+\n+        with patch(\"honcho.Honcho\", return_value=fake_honcho) as mock_honcho, \\\n+             patch(\"hermes_cli.config.load_config\", return_value={}):\n+            get_honcho_client(cfg)\n+\n+        mock_honcho.assert_called_once()\n+        passed_base_url = mock_honcho.call_args.kwargs.get(\"base_url\")\n+        assert passed_base_url == \"http://localhost:38000\", (\n+            f\"Expected 'http://localhost:38000', got {passed_base_url!r}\"\n+        )\n+\n+    @pytest.mark.skipif(\n+        not importlib.util.find_spec(\"honcho\"),\n+        reason=\"honcho SDK not installed\"\n+    )\n+    def test_cloud_base_url_without_version_unchanged(self):\n+        \"\"\"A cloud base_url with no version segment must pass through untouched.\"\"\"\n+        fake_honcho = MagicMock(name=\"Honcho\")\n+        cfg = HonchoClientConfig(\n+            api_key=\"[redacted:secret-assignment]\",\n+            base_url=\"https://api.honcho.dev\",\n+            workspace_id=\"hermes\",\n+            environment=\"production\",\n+        )\n+\n+        with patch(\"honcho.Honcho\", return_value=fake_honcho) as mock_honcho, \\\n+             patch(\"hermes_cli.config.load_config\", return_value={}):\n+            get_honcho_client(cfg)\n+\n+        mock_honcho.assert_called_once()\n+        passed_base_url = mock_honcho.call_args.kwargs.get(\"base_url\")\n+        assert passed_base_url == \"https://api.honcho.dev\", (\n+            f\"Expected 'https://api.honcho.dev', got {passed_base_url!r}\"\n+        )\n+\n+    @pytest.mark.skipif(\n+        not importlib.util.find_spec(\"honcho\"),\n+        reason=\"honcho SDK not installed\"\n+    )\n+    def test_cloud_base_url_with_version_stripped(self):\n+        \"\"\"A version segment double-prefixes regardless of host, so a cloud\n+        base_url that ends in '/v3' must also be stripped (the SDK re-adds it).\"\"\"\n+        fake_honcho = MagicMock(name=\"Honcho\")\n+        cfg = HonchoClientConfig(\n+            api_key=\"[redacted:secret-assignment]\",\n+            base_url=\"https://api.honcho.dev/v3\",\n+            workspace_id=\"hermes\",\n+            environment=\"production\",\n+        )\n+\n+        with patch(\"honcho.Honcho\", return_value=fake_honcho) as mock_honcho, \\\n+             patch(\"hermes_cli.config.load_config\", return_value={}):\n+            get_honcho_client(cfg)\n+\n+        mock_honcho.assert_called_once()\n+        passed_base_url = mock_honcho.call_args.kwargs.get(\"base_url\")\n+        assert passed_base_url == \"https://api.honcho.dev\", (\n+            f\"Expected 'https://api.honcho.dev', got {passed_base_url!r}\"\n+        )\n+\n+    @pytest.mark.skipif(\n+        not importlib.util.find_spec(\"honcho\"),\n+        reason=\"honcho SDK not installed\"\n+    )\n+    @pytest.mark.parametrize(\n+        \"raw_url, expected\",\n+        [\n+            # LAN IP self-host\n+            (\"http://10.0.0.5:8000/v3\", \"http://10.0.0.5:8000\"),\n+            (\"http://192.168.1.20:38000/v3/\", \"http://192.168.1.20:38000\"),\n+            # Tailscale / custom-domain self-host\n+            (\"https://honcho.my.ts.net/v3\", \"https://honcho.my.ts.net\"),\n+            (\"https://honcho.lab.internal/v3\", \"https://honcho.lab.internal\"),\n+            (\"https://honcho.fly.dev/v3\", \"https://honcho.fly.dev\"),\n+            # higher version segments are also stripped\n+            (\"https://honcho.lab.internal/v12\", \"https://honcho.lab.internal\"),\n+            # self-host without a version segment is left unchanged\n+            (\"https://honcho.my.ts.net\", \"https://honcho.my.ts.net\"),\n+            (\"http://10.0.0.5:8000\", \"http://10.0.0.5:8000\"),\n+        ],\n+    )\n+    def test_self_hosted_base_url_version_stripped(self, raw_url, expected):\n+        \"\"\"Non-loopback self-hosted instances (LAN IPs, Tailscale, custom\n+        domains) must get the same version-segment stripping as localhost.\n+        Regression for #20688 recurring on any non-loopback self-host.\"\"\"\n+        fake_honcho = MagicMock(name=\"Honcho\")\n+        cfg = HonchoClientConfig(\n+            api_key=\"[redacted:secret-assignment]\",\n+            base_url=raw_url,\n+            workspace_id=\"hermes\",\n+            environment=\"production\",\n+        )\n+\n+        with patch(\"honcho.Honcho\", return_value=fake_honcho) as mock_honcho, \\\n+             patch(\"hermes_cli.config.load_config\", return_value={}):\n+            get_honcho_client(cfg)\n+\n+        mock_honcho.assert_called_once()\n+        passed_base_url = mock_honcho.call_args.kwargs.get(\"base_url\")\n+        assert passed_base_url == expected, (\n+            f\"Expected {expected!r}, got {passed_base_url!r}\"\n+        )\n+\n+    @pytest.mark.skipif(\n+        not importlib.util.find_spec(\"honcho\"),\n+        reason=\"honcho SDK not installed\"\n+    )\n+    def test_local_base_url_with_trailing_slash_stripped(self):\n+        \"\"\"base_url 'http://127.0.0.1:38000/v3/' must also be cleaned up.\"\"\"\n+        fake_honcho = MagicMock(name=\"Honcho\")\n+        cfg = HonchoClientConfig(\n+            api_key=None,\n+            base_url=\"http://127.0.0.1:38000/v3/\",\n+            workspace_id=\"hermes\",\n+            environment=\"production\",\n+        )\n+\n+        with patch(\"honcho.Honcho\", return_value=fake_honcho) as mock_honcho, \\\n+             patch(\"hermes_cli.config.load_config\", return_value={}):\n+            get_honcho_client(cfg)\n+\n+        mock_honcho.assert_called_once()\n+        passed_base_url = mock_honcho.call_args.kwargs.get(\"base_url\")\n+        assert passed_base_url == \"http://127.0.0.1:38000\", (\n+            f\"Expected 'http://127.0.0.1:38000', got {passed_base_url!r}\"\n+        )"]},{"file":"tests/honcho_plugin/test_pin_peer_name.py","hunks":["@@ -745,10 +745,10 @@ def test_cache_busting_signature_reflects_pin_peer_name(self, tmp_path, monkeypa\n         monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path))\n \n         cfg_path.write_text(json.dumps({\"apiKey\": \"k\", \"peerName\": \"Igor\", \"pinPeerName\": True}))\n-        sig_pinned = GatewayRunner._extract_cache_busting_config({})\n+        sig_pinned = GatewayRunner._extract_cache_busting_config({\"memory\": {\"provider\": \"honcho\"}})\n \n         cfg_path.write_text(json.dumps({\"apiKey\": \"k\", \"peerName\": \"Igor\", \"pinPeerName\": False}))\n-        sig_unpinned = GatewayRunner._extract_cache_busting_config({})\n+        sig_unpinned = GatewayRunner._extract_cache_busting_config({\"memory\": {\"provider\": \"honcho\"}})\n \n         assert sig_pinned[\"honcho.pin_peer_name\"] != sig_unpinned[\"honcho.pin_peer_name\"]\n ","@@ -759,14 +759,14 @@ def test_cache_busting_signature_reflects_user_peer_aliases(self, tmp_path, monk\n         monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path))\n \n         cfg_path.write_text(json.dumps({\"apiKey\": \"k\", \"peerName\": \"Igor\"}))\n-        sig_no_aliases = GatewayRunner._extract_cache_busting_config({})\n+        sig_no_aliases = GatewayRunner._extract_cache_busting_config({\"memory\": {\"provider\": \"honcho\"}})\n \n         cfg_path.write_text(json.dumps({\n             \"apiKey\": \"k\",\n             \"peerName\": \"Igor\",\n             \"userPeerAliases\": {\"86701400\": \"Igor\"},\n         }))\n-        sig_with_aliases = GatewayRunner._extract_cache_busting_config({})\n+        sig_with_aliases = GatewayRunner._extract_cache_busting_config({\"memory\": {\"provider\": \"honcho\"}})\n \n         assert sig_no_aliases[\"honcho.user_peer_aliases\"] != sig_with_aliases[\"honcho.user_peer_aliases\"]\n ","@@ -777,14 +777,14 @@ def test_cache_busting_signature_reflects_runtime_peer_prefix(self, tmp_path, mo\n         monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path))\n \n         cfg_path.write_text(json.dumps({\"apiKey\": \"k\", \"peerName\": \"Igor\"}))\n-        sig_no_prefix = GatewayRunner._extract_cache_busting_config({})\n+        sig_no_prefix = GatewayRunner._extract_cache_busting_config({\"memory\": {\"provider\": \"honcho\"}})\n \n         cfg_path.write_text(json.dumps({\n             \"apiKey\": \"k\",\n             \"peerName\": \"Igor\",\n             \"runtimePeerPrefix\": \"telegram_\",\n         }))\n-        sig_with_prefix = GatewayRunner._extract_cache_busting_config({})\n+        sig_with_prefix = GatewayRunner._extract_cache_busting_config({\"memory\": {\"provider\": \"honcho\"}})\n \n         assert sig_no_prefix[\"honcho.runtime_peer_prefix\"] != sig_with_prefix[\"honcho.runtime_peer_prefix\"]\n ","@@ -805,14 +805,14 @@ def test_cache_busting_signature_reflects_ai_peer(self, tmp_path, monkeypatch):\n             \"peerName\": \"Igor\",\n             \"aiPeer\": \"hermes\",\n         }))\n-        sig_before = GatewayRunner._extract_cache_busting_config({})\n+        sig_before = GatewayRunner._extract_cache_busting_config({\"memory\": {\"provider\": \"honcho\"}})\n \n         cfg_path.write_text(json.dumps({\n             \"apiKey\": \"k\",\n             \"peerName\": \"Igor\",\n             \"aiPeer\": \"hermetika\",\n         }))\n-        sig_after = GatewayRunner._extract_cache_busting_config({})\n+        sig_after = GatewayRunner._extract_cache_busting_config({\"memory\": {\"provider\": \"honcho\"}})\n \n         assert sig_before[\"honcho.ai_peer\"] != sig_after[\"honcho.ai_peer\"]\n "]},{"file":"tests/plugins/memory/test_hindsight_provider.py","hunks":["@@ -6,7 +6,9 @@\n \"\"\"\n \n import json\n+import os\n import re\n+import stat\n import sys\n from types import SimpleNamespace\n from unittest.mock import AsyncMock, MagicMock","@@ -1570,3 +1572,13 @@ def test_local_embedded_shutdown_closes_inner_async_client_on_shared_loop(self,\n         assert embedded._client is None\n         assert provider._client is None\n \n+\n[redacted:email](os.name == \"nt\", reason=\"POSIX mode bits not enforced on Windows\")\n+def test_save_config_sets_owner_only_permissions(tmp_path):\n+    \"\"\"hindsight/config.json must be written with 0o600 so API key is not world-readable.\"\"\"\n+    provider = HindsightMemoryProvider()\n+    provider.save_config({\"api_key\": \"hd-test-key\"}, str(tmp_path))\n+    config_file = tmp_path / \"hindsight\" / \"config.json\"\n+    assert config_file.exists()\n+    mode = stat.S_IMODE(config_file.stat().st_mode)\n+    assert mode == 0o600, f\"Expected 0o600 (owner-only), got {oct(mode)}\""]},{"file":"tests/plugins/memory/test_mem0_v2.py","hunks":["@@ -4,6 +4,10 @@\n \"\"\"\n \n import json\n+import os\n+import stat\n+\n+import pytest\n \n from plugins.memory.mem0 import Mem0MemoryProvider\n ","@@ -202,6 +206,17 @@ def test_prefetch_dict_response(self, monkeypatch):\n # ---------------------------------------------------------------------------\n \n \n[redacted:email](os.name == \"nt\", reason=\"POSIX mode bits not enforced on Windows\")\n+def test_save_config_sets_owner_only_permissions(tmp_path):\n+    \"\"\"mem0.json must be written with 0o600 so API key is not world-readable.\"\"\"\n+    provider = Mem0MemoryProvider()\n+    provider.save_config({\"api_key\": \"m0-test-key\"}, str(tmp_path))\n+    config_file = tmp_path / \"mem0.json\"\n+    assert config_file.exists()\n+    mode = stat.S_IMODE(config_file.stat().st_mode)\n+    assert mode == 0o600, f\"Expected 0o600 (owner-only), got {oct(mode)}\"\n+\n+\n class TestMem0Defaults:\n     \"\"\"Ensure we don't break existing users' defaults.\"\"\"\n "]},{"file":"tests/plugins/memory/test_supermemory_provider.py","hunks":["@@ -1,4 +1,6 @@\n import json\n+import os\n+import stat\n import threading\n \n import pytest","@@ -409,3 +411,13 @@ def test_get_config_schema_minimal():\n     assert len(schema) == 1\n     assert schema[0][\"key\"] == \"api_key\"\n     assert schema[0][\"secret\"] is True\n+\n+\n[redacted:email](os.name == \"nt\", reason=\"POSIX mode bits not enforced on Windows\")\n+def test_save_config_sets_owner_only_permissions(tmp_path):\n+    \"\"\"supermemory.json must be written with 0o600 so API key is not world-readable.\"\"\"\n+    _save_supermemory_config({\"api_key\": \"sm-test-key\"}, str(tmp_path))\n+    config_file = tmp_path / \"supermemory.json\"\n+    assert config_file.exists()\n+    mode = stat.S_IMODE(config_file.stat().st_mode)\n+    assert mode == 0o600, f\"Expected 0o600 (owner-only), got {oct(mode)}\""]},{"file":"tests/test_honcho_client_config.py","hunks":["@@ -2,9 +2,13 @@\n \n import json\n import os\n+import stat\n+from pathlib import Path\n \n+import pytest\n \n from plugins.memory.honcho.client import HonchoClientConfig\n+from plugins.memory.honcho import HonchoMemoryProvider\n \n \n class TestHonchoClientConfigAutoEnable:","@@ -100,3 +104,24 @@ def test_falls_back_to_env_when_no_config_file(self, tmp_path, monkeypatch):\n \n         assert cfg.api_key == \"fallback-key\"\n         assert cfg.enabled is True  # from_env() sets enabled=True\n+\n+\n[redacted:email](os.name == \"nt\", reason=\"POSIX mode bits not enforced on Windows\")\n+def test_save_config_sets_owner_only_permissions(tmp_path, monkeypatch):\n+    \"\"\"honcho.json is created atomically with 0o600, not chmod-after-write.\"\"\"\n+    import utils\n+    calls = []\n+    real_atomic = utils.atomic_json_write\n+\n+    def spy(path, data, **kwargs):\n+        calls.append(kwargs.get(\"mode\"))\n+        return real_atomic(path, data, **kwargs)\n+\n+    monkeypatch.setattr(utils, \"atomic_json_write\", spy)\n+    provider = HonchoMemoryProvider()\n+    provider.save_config({\"api_key\": \"hc-test-key\"}, str(tmp_path))\n+    assert calls == [0o600]\n+    config_file = tmp_path / \"honcho.json\"\n+    assert config_file.exists()\n+    mode = stat.S_IMODE(config_file.stat().st_mode)\n+    assert mode == 0o600, f\"Expected 0o600 (owner-only), got {oct(mode)}\""]},{"file":"utils.py","hunks":["@@ -87,6 +87,7 @@ def atomic_json_write(\n     data: Any,\n     *,\n     indent: int = 2,\n+    mode: int | None = None,\n     **dump_kwargs: Any,\n ) -> None:\n     \"\"\"Write JSON data to a file atomically.","@@ -99,13 +100,16 @@ def atomic_json_write(\n         path: Target file path (will be created or overwritten).\n         data: JSON-serializable data to write.\n         indent: JSON indentation (default 2).\n+        mode: Optional final permission mode. When set, the temp file is\n+            created and replaced with this mode, avoiding chmod-after-write\n+            TOCTOU exposure for secret-bearing files.\n         **dump_kwargs: Additional keyword args forwarded to json.dump(), such\n             as default=str for non-native types.\n     \"\"\"\n     path = Path(path)\n     path.parent.mkdir(parents=True, exist_ok=True)\n \n-    original_mode = _preserve_file_mode(path)\n+    original_mode = None if mode is not None else _preserve_file_mode(path)\n \n     fd, tmp_path = tempfile.mkstemp(\n         dir=str(path.parent),","@@ -113,6 +117,8 @@ def atomic_json_write(\n         suffix=\".tmp\",\n     )\n     try:\n+        if mode is not None:\n+            os.fchmod(fd, mode)\n         with os.fdopen(fd, \"w\", encoding=\"utf-8\") as f:\n             json.dump(\n                 data,","@@ -125,7 +131,13 @@ def atomic_json_write(\n             os.fsync(f.fileno())\n         # Preserve symlinks — swap in-place on the real file (GitHub #16743).\n         real_path = atomic_replace(tmp_path, path)\n-        _restore_file_mode(real_path, original_mode)\n+        if mode is not None:\n+            try:\n+                os.chmod(real_path, mode)\n+            except OSError:\n+                pass\n+        else:\n+            _restore_file_mode(Path(real_path), original_mode)\n     except BaseException:\n         # Intentionally catch BaseException so temp-file cleanup still runs for\n         # KeyboardInterrupt/SystemExit before re-raising the original signal."]},{"file":"website/docs/user-guide/features/honcho.md","hunks":["@@ -106,6 +106,10 @@ The auto-injected dialectic scales `dialecticReasoningLevel` by query length: +1\n \n Honcho is configured in `~/.honcho/config.json` (global) or `$HERMES_HOME/honcho.json` (profile-local). The setup wizard handles this for you.\n \n+### Self-Hosted Honcho with Authentication\n+\n+When pointing Hermes at a self-hosted Honcho server, `hermes honcho setup` (and `hermes memory setup`) ask for a **local JWT / bearer token** after the base URL. Paste a JWT signed with the server's `AUTH_JWT_SECRET` (the Honcho compose env var) to enable authenticated access; leave it blank for servers running with `AUTH_USE_AUTH=false`. The local token is stored under the host block (`hosts.<host>.apiKey` in `honcho.json`), separate from any cloud `apiKey`, so you can flip the `Cloud or local?` prompt back to `cloud` later without losing either credential.\n+\n ### Full Config Reference\n \n | Key | Default | Description |","@@ -199,11 +203,12 @@ When Honcho is active as the memory provider, five tools become available:\n \n ## CLI Commands\n \n-The `hermes honcho` subcommand is **only registered when Honcho is the active memory provider** (`memory.provider: honcho` in `config.yaml`). Run `hermes memory setup` and pick Honcho first; the subcommand appears on the next invocation.\n+The `hermes honcho` subcommand is **only registered when Honcho is the active memory provider** (`memory.provider: honcho` in `config.yaml`). On a fresh install, configure Honcho directly with `hermes memory setup honcho` (or run `hermes memory setup` and pick it from the list); the `hermes honcho` subcommand then appears on the next invocation.\n \n ```bash\n+hermes memory setup honcho    # Configure Honcho directly (works before activation)\n hermes honcho status          # Connection status, config, and key settings\n-hermes honcho setup           # Redirects to `hermes memory setup`\n+hermes honcho setup           # Redirects to `hermes memory setup` (post-activation alias)\n hermes honcho strategy        # Show or set session strategy (per-session/per-directory/per-repo/global)\n hermes honcho peer            # Show or update peer names + dialectic reasoning level\n hermes honcho mode            # Show or set recall mode (hybrid/context/tools)"]},{"file":"website/docs/user-guide/features/memory-providers.md","hunks":["@@ -66,7 +66,7 @@ AI-native cross-session user modeling with dialectic reasoning, session-scoped c\n hermes memory setup        # select \"honcho\" — runs the Honcho-specific post-setup\n ```\n \n-The legacy `hermes honcho setup` command still works (it now redirects to `hermes memory setup`), but is only registered after Honcho is selected as the active memory provider.\n+On a fresh install, configure Honcho directly with `hermes memory setup honcho`. The legacy `hermes honcho setup` command still works (it now redirects to `hermes memory setup`), but is only registered after Honcho is selected as the active memory provider.\n \n **Config:** `$HERMES_HOME/honcho.json` (profile-local) or `~/.honcho/config.json` (global). Resolution order: `$HERMES_HOME/honcho.json` > `~/.hermes/honcho.json` > `~/.honcho/config.json`. See the [config reference](https://github.com/NousResearch/hermes-agent/blob/main/plugins/memory/honcho/README.md) and the [Honcho integration guide](https://docs.honcho.dev/v3/guides/integrations/hermes).\n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}