{"repo":"NousResearch/hermes-agent","public_id":"43719","display_label":"Restrict plugin access on public insecure dashboards","summary":"These items address security controls around dashboard plugins, focusing on suppressing or blocking plugin APIs, installs, and backend auto-imports in public or insecure dashboard contexts. A related report describes malicious third-party plugins targeting dashboards.","subsystem":"dashboard","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":50773,"merged_at":"2026-06-22T17:27:34+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-22T17:27:34+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":43719,"pr_number":50773,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-22T17:27:34+00:00"}],"members":[{"kind":"issue","number":43719,"title":"Security: Malicious third-party plugins targeting Hermes dashboards (jellyfinuser/gitea.com)","url":null,"state":"closed"},{"kind":"pr","number":43786,"title":"fix(security): suppress user dashboard plugin APIs on public insecure dashboards (#43719)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#44472","number":44472},{"kind":"pr","value":"#50773","number":50773}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":43794,"title":"fix(dashboard): block public insecure plugin installs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#43719","number":43719},{"kind":"pr","value":"#50773","number":50773}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":50773,"title":"fix(security): restrict dashboard plugin backend auto-import to bundled plugins — defense-in-depth (#43719)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/web_server.py","plugins/hermes-achievements/README.md","tests/hermes_cli/test_project_plugin_rce_bypass.py","tests/hermes_cli/test_web_server.py","website/docs/reference/environment-variables.md","website/docs/user-guide/features/extending-the-dashboard.md"],"fix_hunks":[{"file":"hermes_cli/web_server.py","hunks":["@@ -12178,12 +12178,20 @@ def _safe_plugin_api_relpath(api_field: Any, *, dashboard_dir: Path) -> Optional\n     return api_field\n \n \n+# Plugin sources whose Python backend (dashboard manifest `api` file) must NEVER\n+# be auto-imported by the dashboard web server — only bundled plugins may. Shared\n+# by the discovery-time scrub and the mount-time refuse guards so a typo in one\n+# site cannot silently disable a security gate (GHSA-5qr3-c538-wm9j / #43719).\n+_NON_BUNDLED_PLUGIN_SOURCES = frozenset({\"user\", \"project\"})\n+\n+\n def _discover_dashboard_plugins() -> list:\n     \"\"\"Scan plugins/*/dashboard/manifest.json for dashboard extensions.\n \n-    Checks three plugin sources (same as hermes_cli.plugins):\n-    1. User plugins:    ~/.hermes/plugins/<name>/dashboard/manifest.json\n-    2. Bundled plugins: <repo>/plugins/<name>/dashboard/manifest.json  (memory/, etc.)\n+    Checks three plugin sources. Bundled dashboard plugins win name conflicts\n+    so non-bundled plugins cannot shadow trusted backend-capable routes:\n+    1. Bundled plugins: <repo>/plugins/<name>/dashboard/manifest.json  (memory/, etc.)\n+    2. User plugins:    ~/.hermes/plugins/<name>/dashboard/manifest.json\n     3. Project plugins: ./.hermes/plugins/  (only if HERMES_ENABLE_PROJECT_PLUGINS)\n     \"\"\"\n     plugins = []","@@ -12192,9 +12200,9 @@ def _discover_dashboard_plugins() -> list:\n     from hermes_cli.plugins import get_bundled_plugins_dir\n     bundled_root = get_bundled_plugins_dir()\n     search_dirs = [\n-        (get_hermes_home() / \"plugins\", \"user\"),\n         (bundled_root / \"memory\", \"bundled\"),\n         (bundled_root, \"bundled\"),\n+        (get_hermes_home() / \"plugins\", \"user\"),\n     ]\n     # GHSA-5qr3-c538-wm9j (#29156): the previous ``os.environ.get(...)``\n     # check treated *any* non-empty string as truthy, so ``=0``, ``=false``,","@@ -12253,10 +12261,20 @@ def _discover_dashboard_plugins() -> list:\n                 raw_api = data.get(\"api\")\n                 dashboard_dir = child / \"dashboard\"\n                 safe_api = _safe_plugin_api_relpath(raw_api, dashboard_dir=dashboard_dir)\n+                if source in _NON_BUNDLED_PLUGIN_SOURCES and safe_api:\n+                    _log.warning(\n+                        \"Plugin %s: refusing dashboard backend api=%s \"\n+                        \"(only bundled plugins may auto-import Python \"\n+                        \"backend routes; non-bundled plugins may extend \"\n+                        \"the dashboard with static UI assets only)\",\n+                        name, safe_api,\n+                    )\n+                    safe_api = None\n+                    raw_api = None\n                 if raw_api and safe_api is None:\n                     _log.warning(\n                         \"Plugin %s: refusing unsafe api path %r (must be a \"\n-                        \"relative file inside the plugin's dashboard/ \"\n+                        \"relative file inside a bundled plugin's dashboard/ \"\n                         \"directory); backend routes from this plugin will \"\n                         \"not be mounted\",\n                         name, raw_api,","@@ -12663,23 +12681,36 @@ def _mount_plugin_api_routes():\n     a ``router`` (FastAPI APIRouter).  Routes are mounted under\n     ``/api/plugins/<name>/``.\n \n-    Backend import is restricted to ``bundled`` and ``user`` sources.\n-    Project plugins (``./.hermes/plugins/``) ship with the CWD and are\n-    therefore attacker-controlled in any threat model where the user\n-    opens a malicious repo; they can extend the dashboard UI via\n-    static JS/CSS but their Python ``api`` file is never auto-imported\n-    by the web server.  See GHSA-5qr3-c538-wm9j (#29156).\n+    Backend import is restricted to bundled plugins. User and project\n+    plugins can extend the dashboard UI via static JS/CSS, but their\n+    Python ``api`` files are never auto-imported by the web server.\n+    See GHSA-5qr3-c538-wm9j (#29156) and #43719.\n     \"\"\"\n     for plugin in _get_dashboard_plugins():\n         api_file_name = plugin.get(\"_api_file\")\n         if not api_file_name:\n             continue\n-        if plugin.get(\"source\") == \"project\":\n+        source = plugin.get(\"source\")\n+        if source in _NON_BUNDLED_PLUGIN_SOURCES:\n+            # Backend Python auto-import is reserved for bundled plugins; user\n+            # and project plugins extend the dashboard with static UI assets\n+            # only (GHSA-5qr3-c538-wm9j / #43719). Defence-in-depth: discovery\n+            # already nulls _api_file for these sources, but re-refusing here —\n+            # at the actual importlib call site — keeps the import primitive\n+            # contained even if a future caller or a tampered cache entry slips\n+            # a non-bundled plugin through with an _api_file set.\n+            _reason = {\n+                \"user\": (\n+                    \"user-installed plugins may not auto-import Python code\"\n+                ),\n+                \"project\": (\n+                    \"project plugins may not auto-import Python code; backend \"\n+                    \"auto-import is reserved for bundled plugins\"\n+                ),\n+            }.get(source, \"only bundled plugins may auto-import Python code\")\n             _log.warning(\n-                \"Plugin %s: ignoring backend api=%s (project plugins may \"\n-                \"not auto-import Python code; move the plugin to \"\n-                \"~/.hermes/plugins/ if you trust it)\",\n-                plugin[\"name\"], api_file_name,\n+                \"Plugin %s: ignoring backend api=%s (%s)\",\n+                plugin[\"name\"], api_file_name, _reason,\n             )\n             continue\n         dashboard_dir = Path(plugin[\"_dir\"])"]},{"file":"plugins/hermes-achievements/README.md","hunks":["@@ -77,7 +77,9 @@ Then rescan dashboard plugins:\n curl http://127.0.0.1:9119/api/dashboard/plugins/rescan\n ```\n \n-If backend API routes 404, restart `hermes dashboard`; plugin APIs are mounted at dashboard startup.\n+When installed as a user plugin, the dashboard UI loads but Python backend API\n+routes are not auto-imported. Backend routes are available when this plugin is\n+bundled with Hermes.\n \n ## Updating\n ","@@ -89,7 +91,11 @@ git pull --ff-only\n curl http://127.0.0.1:9119/api/dashboard/plugins/rescan\n ```\n \n-If the update changes backend routes or `plugin_api.py`, restart `hermes dashboard` after pulling.\n+For a user-installed plugin at `~/.hermes/plugins/hermes-achievements`, a plugin\n+rescan is enough because Python backend routes are not auto-imported. If you\n+update the bundled plugin by pulling changes in the hermes-agent repository, and\n+that bundled plugin update changes backend routes or `plugin_api.py`, restart\n+`hermes dashboard` after pulling.\n \n As of 2026-04-29, updating is strongly recommended because scan performance changed significantly:\n - removed duplicate `/overview` scan path","@@ -118,6 +124,9 @@ dashboard/\n \n ## API\n \n+These backend routes are mounted for the bundled plugin. User-installed copies\n+load their dashboard UI but do not auto-import Python backend routes.\n+\n Routes are mounted under:\n \n ```text"]},{"file":"tests/hermes_cli/test_project_plugin_rce_bypass.py","hunks":["@@ -24,7 +24,7 @@\n * ``_safe_plugin_api_relpath`` rejects absolute paths, ``..``\n   traversal, and non-string / empty values.\n * ``_mount_plugin_api_routes`` re-validates at import time and\n-  refuses project-source plugins outright.\n+  refuses user/project-source plugin backend code outright.\n * End-to-end the original PoC manifest no longer triggers\n   ``importlib`` for ``/tmp/payload.py``.\n \"\"\"","@@ -216,7 +216,7 @@ def test_traversal_api_path_in_manifest_is_scrubbed(self, user_plugin_factory):\n         assert entry[\"_api_file\"] is None\n         assert entry[\"has_api\"] is False\n \n-    def test_safe_api_path_survives(self, user_plugin_factory, tmp_path):\n+    def test_user_safe_api_path_is_scrubbed(self, user_plugin_factory, tmp_path):\n         user_plugin_factory(\"safe\", {\n             \"name\": \"safe\",\n             \"label\": \"Safe\",","@@ -230,6 +230,86 @@ def test_safe_api_path_survives(self, user_plugin_factory, tmp_path):\n         )\n         plugins = web_server._get_dashboard_plugins(force_rescan=True)\n         entry = next(p for p in plugins if p[\"name\"] == \"safe\")\n+        assert entry[\"_api_file\"] is None\n+        assert entry[\"has_api\"] is False\n+\n+    def test_project_safe_api_path_is_scrubbed(self, tmp_path, monkeypatch):\n+        monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path / \"home\"))\n+        (tmp_path / \"home\").mkdir()\n+        monkeypatch.setenv(\"HERMES_ENABLE_PROJECT_PLUGINS\", \"1\")\n+        cwd = tmp_path / \"project\"\n+        cwd.mkdir()\n+        monkeypatch.chdir(cwd)\n+        dashboard = _write_plugin_manifest(\n+            cwd / \".hermes\" / \"plugins\",\n+            \"safe-project\",\n+            {\n+                \"name\": \"safe-project\",\n+                \"label\": \"Safe Project\",\n+                \"api\": \"api.py\",\n+                \"entry\": \"dist/index.js\",\n+            },\n+        )\n+        (dashboard / \"api.py\").write_text(\"router = None\\n\")\n+\n+        plugins = web_server._get_dashboard_plugins(force_rescan=True)\n+        entry = next(p for p in plugins if p[\"name\"] == \"safe-project\")\n+        assert entry[\"_api_file\"] is None\n+        assert entry[\"has_api\"] is False\n+\n+    def test_bundled_safe_api_path_survives(self, tmp_path, monkeypatch):\n+        hermes_home = tmp_path / \"home\"\n+        monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+        hermes_home.mkdir()\n+        monkeypatch.setenv(\"HERMES_BUNDLED_PLUGINS\", str(tmp_path / \"bundled\"))\n+        dashboard = _write_plugin_manifest(\n+            tmp_path / \"bundled\",\n+            \"safe-bundled\",\n+            {\n+                \"name\": \"safe-bundled\",\n+                \"label\": \"Safe Bundled\",\n+                \"api\": \"api.py\",\n+                \"entry\": \"dist/index.js\",\n+            },\n+        )\n+        (dashboard / \"api.py\").write_text(\"router = None\\n\")\n+\n+        plugins = web_server._get_dashboard_plugins(force_rescan=True)\n+        entry = next(p for p in plugins if p[\"name\"] == \"safe-bundled\")\n+        assert entry[\"_api_file\"] == \"api.py\"\n+        assert entry[\"has_api\"] is True\n+\n+    def test_user_plugin_does_not_shadow_bundled_backend(self, tmp_path, monkeypatch):\n+        hermes_home = tmp_path / \"home\"\n+        monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+        hermes_home.mkdir()\n+        monkeypatch.setenv(\"HERMES_BUNDLED_PLUGINS\", str(tmp_path / \"bundled\"))\n+\n+        bundled_dashboard = _write_plugin_manifest(\n+            tmp_path / \"bundled\",\n+            \"shadowed\",\n+            {\n+                \"name\": \"shadowed\",\n+                \"label\": \"Bundled Shadowed\",\n+                \"api\": \"api.py\",\n+                \"entry\": \"dist/index.js\",\n+            },\n+        )\n+        (bundled_dashboard / \"api.py\").write_text(\"router = None\\n\")\n+        _write_plugin_manifest(\n+            hermes_home / \"plugins\",\n+            \"shadowed\",\n+            {\n+                \"name\": \"shadowed\",\n+                \"label\": \"User Shadowed\",\n+                \"api\": \"api.py\",\n+                \"entry\": \"dist/index.js\",\n+            },\n+        )\n+\n+        plugins = web_server._get_dashboard_plugins(force_rescan=True)\n+        entry = next(p for p in plugins if p[\"name\"] == \"shadowed\")\n+        assert entry[\"source\"] == \"bundled\"\n         assert entry[\"_api_file\"] == \"api.py\"\n         assert entry[\"has_api\"] is True\n ","@@ -276,6 +356,16 @@ def test_project_source_api_is_not_imported(self, tmp_path):\n             \"GHSA-5qr3-c538-wm9j defence-in-depth regression\"\n         )\n \n+    def test_user_source_api_is_not_imported(self, tmp_path):\n+        plugin = self._payload_plugin(tmp_path, source=\"user\")\n+        web_server._dashboard_plugins_cache = [plugin]\n+        with patch(\"importlib.util.spec_from_file_location\") as spec:\n+            web_server._mount_plugin_api_routes()\n+        assert spec.call_count == 0, (\n+            \"user-installed plugin api file was imported — \"\n+            \"third-party dashboard plugin backend code must stay inert\"\n+        )\n+\n     def test_bundled_source_api_imports_normally(self, tmp_path):\n         plugin = self._payload_plugin(tmp_path, source=\"bundled\")\n         web_server._dashboard_plugins_cache = [plugin]"]},{"file":"tests/hermes_cli/test_web_server.py","hunks":["@@ -5070,14 +5070,8 @@ class TestPluginAPIAuth:\n     \"\"\"Tests that plugin API routes require the session token (issue #19533).\"\"\"\n \n     @pytest.fixture(autouse=True)\n-    def _setup_test_client(self, monkeypatch, _isolate_hermes_home, _install_example_plugin):\n-        \"\"\"Create a TestClient without the session token header.\n-\n-        Pulls in ``_install_example_plugin`` so ``test_plugin_route_allows_auth``\n-        has the ``/api/plugins/example/hello`` endpoint available — the\n-        example plugin is no longer a bundled plugin, so the fixture\n-        installs it into the per-test ``HERMES_HOME``.\n-        \"\"\"\n+    def _setup_test_client(self, monkeypatch, _isolate_hermes_home):\n+        \"\"\"Create TestClients with and without the session token header.\"\"\"\n         try:\n             from starlette.testclient import TestClient\n         except ImportError:","@@ -5102,19 +5096,15 @@ def test_plugin_route_requires_auth(self):\n     def test_plugin_route_allows_auth(self):\n         \"\"\"Plugin API routes should work with a valid session token.\n \n-        Uses ``/api/plugins/example/hello`` from the example-dashboard\n-        test fixture (installed into HERMES_HOME by the class-level\n-        ``_install_example_plugin`` fixture) — a stable, side-effect-free\n-        GET that's only loaded for tests. With a valid token the handler\n-        should run (200); without one the middleware should 401 before\n-        the handler is reached.\n+        Uses a bundled plugin route so the test covers authenticated plugin\n+        API access without relying on user-installed plugin backend imports.\n         \"\"\"\n         # Without auth: middleware blocks before reaching the handler.\n-        resp = self.client.get(\"/api/plugins/example/hello\")\n+        resp = self.client.get(\"/api/plugins/kanban/board\")\n         assert resp.status_code == 401\n \n         # With auth: handler runs.\n-        resp = self.auth_client.get(\"/api/plugins/example/hello\")\n+        resp = self.auth_client.get(\"/api/plugins/kanban/board\")\n         assert resp.status_code == 200\n \n     def test_plugin_post_requires_auth(self):"]},{"file":"website/docs/reference/environment-variables.md","hunks":["@@ -625,7 +625,7 @@ Advanced per-platform knobs for throttling the outbound message batcher. Most us\n | `HERMES_AGENT_NOTIFY_INTERVAL` | Gateway: interval in seconds between progress notifications on long-running agent turns. |\n | `HERMES_CHECKPOINT_TIMEOUT` | Timeout for filesystem checkpoint creation in seconds (default: `30`). |\n | `HERMES_EXEC_ASK` | Enable execution approval prompts in gateway mode (`true`/`false`) |\n-| `HERMES_ENABLE_PROJECT_PLUGINS` | Enable auto-discovery of repo-local plugins from `./.hermes/plugins/` for both the agent loader and the dashboard web server. Accepts the standard truthy set: `1` / `true` / `yes` / `on` (case-insensitive). Everything else — including `0`, `false`, `no`, `off`, and the empty string — is treated as **disabled** (default). Note: as of GHSA-5qr3-c538-wm9j (#29156) the dashboard web server refuses to auto-import a project plugin's Python `api` file even when this var is enabled — project plugins may extend the UI via static JS/CSS but their backend routes are only loaded when moved under `~/.hermes/plugins/`. |\n+| `HERMES_ENABLE_PROJECT_PLUGINS` | Enable auto-discovery of repo-local plugins from `./.hermes/plugins/` for both the agent loader and the dashboard web server. Accepts the standard truthy set: `1` / `true` / `yes` / `on` (case-insensitive). Everything else — including `0`, `false`, `no`, `off`, and the empty string — is treated as **disabled** (default). Note: as of GHSA-5qr3-c538-wm9j (#29156) and #43719, the dashboard web server refuses to auto-import Python `api` files from project or user-installed plugins — they may extend the UI via static JS/CSS, while backend routes are reserved for bundled plugins. |\n | `HERMES_PLUGINS_DEBUG` | `1`/`true` to surface verbose plugin-discovery logs on stderr — directories scanned, manifests parsed, skip reasons, and full tracebacks on parse or `register()` failure. Aimed at plugin authors. |\n | `HERMES_BACKGROUND_NOTIFICATIONS` | Background process notification mode in gateway: `all` (default), `result`, `error`, `off` |\n | `HERMES_EPHEMERAL_SYSTEM_PROMPT` | Ephemeral system prompt injected at API-call time (never persisted to sessions) |"]},{"file":"website/docs/user-guide/features/extending-the-dashboard.md","hunks":["@@ -431,14 +431,14 @@ If you prefer JSX, use any bundler (esbuild, Vite, rollup) with React as an exte\n     ├── dist/\n     │   ├── index.js         # required — pre-built JS bundle (IIFE)\n     │   └── style.css        # optional — custom CSS\n-    └── plugin_api.py        # optional — backend API routes (FastAPI)\n+    └── plugin_api.py        # bundled plugins only — backend API routes (FastAPI)\n ```\n \n A single plugin directory can carry three orthogonal extensions:\n \n - `plugin.yaml` + `__init__.py` — CLI/gateway plugin ([see plugins page](./plugins)).\n - `dashboard/manifest.json` + `dashboard/dist/index.js` — dashboard UI plugin.\n-- `dashboard/plugin_api.py` — dashboard backend routes.\n+- `dashboard/plugin_api.py` — bundled plugins only; backend API routes.\n \n None of them are required; include only the layers you need.\n ","@@ -743,7 +743,10 @@ Routes are mounted under `/api/plugins/<name>/`, so the above becomes:\n - `GET  /api/plugins/my-plugin/data`\n - `POST /api/plugins/my-plugin/action`\n \n-Plugin API routes bypass session-token authentication since the dashboard server binds to localhost by default. **Don't expose the dashboard on a public interface with `--host 0.0.0.0` if you run untrusted plugins** — their routes become reachable too.\n+Security notes:\n+\n+- Bundled plugin API routes bypass session-token authentication. The dashboard server binds to localhost by default, which mitigates the risks of this bypass.\n+- User-installed and project dashboard plugins may still extend the UI with static JS/CSS, but their Python `api` files are not auto-imported by the dashboard server. Backend routes are reserved for bundled plugins.\n \n #### Accessing Hermes internals\n ","@@ -804,11 +807,14 @@ The dashboard scans three directories for `dashboard/manifest.json`:\n \n | Priority | Directory | Source label |\n |----------|-----------|--------------|\n-| 1 (wins on conflict) | `~/.hermes/plugins/<name>/dashboard/` | `user` |\n-| 2 | `<repo>/plugins/memory/<name>/dashboard/` | `bundled` |\n-| 2 | `<repo>/plugins/<name>/dashboard/` | `bundled` |\n+| 1 (wins on conflict) | `<repo>/plugins/memory/<name>/dashboard/` | `bundled` |\n+| 1 (wins on conflict) | `<repo>/plugins/<name>/dashboard/` | `bundled` |\n+| 2 | `~/.hermes/plugins/<name>/dashboard/` | `user` |\n | 3 | `./.hermes/plugins/<name>/dashboard/` | `project` — only when `HERMES_ENABLE_PROJECT_PLUGINS` is set |\n \n+Bundled dashboard plugins win name conflicts because only bundled plugins may\n+register backend routes. Give user and project dashboard plugins unique names.\n+\n Discovery results are cached per dashboard process. After adding a new plugin, either:\n \n ```bash","@@ -908,10 +914,11 @@ Check that the file is in `~/.hermes/dashboard-themes/` and ends in `.yaml` or `\n The `sidebar` slot only renders when the active theme has `layoutVariant: cockpit`. Other slots always render. If you're registering into a slot with no hits, add `console.log` inside `registerSlot` to confirm the plugin bundle ran at all.\n \n **Plugin backend routes return 404.**\n-1. Confirm the manifest has `\"api\": \"plugin_api.py\"` pointing to an existing file inside `dashboard/`.\n-2. Restart `hermes dashboard` — plugin API routes are mounted once at startup, **not** on rescan.\n-3. Check that `plugin_api.py` exports a module-level `router = APIRouter()`. Other export names are not picked up.\n-4. Tail `~/.hermes/logs/errors.log` for `Failed to load plugin <name> API routes` — import errors are logged there.\n+1. Confirm the plugin is bundled with Hermes. User-installed and project dashboard plugins can extend the UI, but their Python backend routes are not auto-imported.\n+2. Confirm the manifest has `\"api\": \"plugin_api.py\"` pointing to an existing file inside `dashboard/`.\n+3. Restart `hermes dashboard` — plugin API routes are mounted once at startup, **not** on rescan.\n+4. Check that `plugin_api.py` exports a module-level `router = APIRouter()`. Other export names are not picked up.\n+5. Tail `~/.hermes/logs/errors.log` for `Failed to load plugin <name> API routes` — import errors are logged there.\n \n **Theme change drops my color overrides.**\n `colorOverrides` are scoped to the active theme and cleared on theme switch — that's by design. If you want overrides that persist, put them in your theme's YAML, not in the live switcher.\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}