{"repo":"NousResearch/hermes-agent","public_id":"29885","display_label":"Honcho self-hosted setup JWT authentication support","summary":"These items concern adding and hardening JWT secret configuration for self-hosted (local) Honcho instances, including prompting users for credentials during setup. The work spans feature requests, implementation, and path-hardening fixes.","subsystem":"hermes_cli","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":35170},"resolution":{"state":"partially_resolved","by":"pr-35170"},"freshness":{"state":"verified","last_verified_at":"2026-05-30T05:29:49+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.33599999999999997},"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":29885,"title":"[Feature]: Honcho local host support - JWT support","url":null,"state":"closed"},{"kind":"issue","number":29886,"title":"Support AUTH_JWT_SECRET for local (self-hosted) Honcho instances","url":null,"state":"closed"},{"kind":"pr","number":30180,"title":"feat(honcho): prompt for local JWT during self-hosted setup","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"827ce602dbed199f665f3975b61303aace2963ea","number":0},{"kind":"pr","value":"#29885","number":29885}],"category":"implemented_elsewhere","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}}