{"repo":"NousResearch/hermes-agent","public_id":"9635","display_label":"Root-level prefill_messages_file config handling in CLI","summary":"Member titles all concern the CLI and config handling of a root-level `prefill_messages_file` key in config.yaml, including legacy support and key alignment. One title reads as an issue report that the CLI ignores this key; the others read as fixes. The titles alone do not show whether any change was merged or resolves the problem.","subsystem":"hermes_cli","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":38760,"merged_at":"2026-06-04T06:52:48+00:00"},"resolution":{"state":"resolved","by":"pr-38760"},"freshness":{"state":"verified","last_verified_at":"2026-06-04T06:52:48+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":9635,"pr_number":38760,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-04T06:52:48+00:00"}],"members":[{"kind":"issue","number":9635,"title":"CLI ignores root-level prefill_messages_file from config.yaml","url":null,"state":"closed"},{"kind":"pr","number":9655,"title":"fix(cli): honor root-level prefill_messages_file from config.yaml","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#38760","number":38760},{"kind":"pr","value":"#9635","number":9635}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":9872,"title":"fix(cli): preserve legacy root prefill_messages_file support","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#38760","number":38760},{"kind":"pr","value":"#9635","number":9635}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":38760,"title":"fix(config): align prefill messages key handling","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","cron/scheduler.py","gateway/run.py","skills/red-teaming/godmode/SKILL.md","skills/red-teaming/godmode/scripts/auto_jailbreak.py","tests/cli/test_prefill_config.py","tests/cron/test_scheduler.py","tests/gateway/test_runtime_config_env_expansion.py","website/docs/user-guide/skills/bundled/red-teaming/red-teaming-godmode.md","website/docs/user-guide/skills/godmode.md","website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/red-teaming/red-teaming-godmode.md","website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/godmode.md"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -313,6 +313,25 @@ def _load_prefill_messages(file_path: str) -> List[Dict[str, Any]]:\n         return []\n \n \n+def _resolve_prefill_messages_file(config: Dict[str, Any]) -> str:\n+    \"\"\"Resolve the prefill file path from env/config.\n+\n+    ``prefill_messages_file`` at the top level is the canonical config key.\n+    ``agent.prefill_messages_file`` remains a legacy fallback for older CLI and\n+    godmode-generated configs.\n+    \"\"\"\n+    env_path = os.getenv(\"HERMES_PREFILL_MESSAGES_FILE\", \"\").strip()\n+    if env_path:\n+        return env_path\n+    top_level = str(config.get(\"prefill_messages_file\", \"\") or \"\").strip()\n+    if top_level:\n+        return top_level\n+    agent_cfg = config.get(\"agent\", {})\n+    if isinstance(agent_cfg, dict):\n+        return str(agent_cfg.get(\"prefill_messages_file\", \"\") or \"\").strip()\n+    return \"\"\n+\n+\n def _parse_reasoning_config(effort: str) -> dict | None:\n     \"\"\"Parse a reasoning effort level into an OpenRouter reasoning config dict.\"\"\"\n     from hermes_constants import parse_reasoning_effort","@@ -3272,7 +3291,7 @@ def __init__(\n         \n         # Ephemeral prefill messages (few-shot priming, never persisted)\n         self.prefill_messages = _load_prefill_messages(\n-            CLI_CONFIG[\"agent\"].get(\"prefill_messages_file\", \"\")\n+            _resolve_prefill_messages_file(CLI_CONFIG)\n         )\n         \n         # Reasoning config (OpenRouter reasoning effort level)"]},{"file":"cron/scheduler.py","hunks":["@@ -1551,9 +1551,16 @@ def _run_job_impl(job: dict) -> tuple[bool, str, str, Optional[str]]:\n         effort = str(_cfg.get(\"agent\", {}).get(\"reasoning_effort\", \"\")).strip()\n         reasoning_config = parse_reasoning_effort(effort)\n \n-        # Prefill messages from env or config.yaml\n+        # Prefill messages from env or config.yaml. The top-level\n+        # prefill_messages_file key is canonical; agent.prefill_messages_file is\n+        # retained as a legacy fallback for older CLI/godmode configs.\n         prefill_messages = None\n-        prefill_file = os.getenv(\"HERMES_PREFILL_MESSAGES_FILE\", \"\") or _cfg.get(\"prefill_messages_file\", \"\")\n+        agent_cfg = _cfg.get(\"agent\", {}) if isinstance(_cfg.get(\"agent\", {}), dict) else {}\n+        prefill_file = (\n+            os.getenv(\"HERMES_PREFILL_MESSAGES_FILE\", \"\")\n+            or _cfg.get(\"prefill_messages_file\", \"\")\n+            or agent_cfg.get(\"prefill_messages_file\", \"\")\n+        )\n         if prefill_file:\n             pfpath = Path(prefill_file).expanduser()\n             if not pfpath.is_absolute():"]},{"file":"gateway/run.py","hunks":["@@ -3034,13 +3034,16 @@ def _load_prefill_messages() -> List[Dict[str, Any]]:\n         \"\"\"Load ephemeral prefill messages from config or env var.\n         \n         Checks HERMES_PREFILL_MESSAGES_FILE env var first, then falls back to\n-        the prefill_messages_file key in ~/.hermes/config.yaml.\n+        the top-level prefill_messages_file key in ~/.hermes/config.yaml.\n+        agent.prefill_messages_file is accepted as a legacy fallback.\n         Relative paths are resolved from ~/.hermes/.\n         \"\"\"\n         file_path = os.getenv(\"HERMES_PREFILL_MESSAGES_FILE\", \"\")\n         if not file_path:\n             cfg = _load_gateway_runtime_config()\n             file_path = str(cfg.get(\"prefill_messages_file\", \"\") or \"\")\n+            if not file_path:\n+                file_path = str(cfg_get(cfg, \"agent\", \"prefill_messages_file\", default=\"\") or \"\")\n         if not file_path:\n             return []\n         path = Path(file_path).expanduser()"]},{"file":"skills/red-teaming/godmode/SKILL.md","hunks":["@@ -90,7 +90,7 @@ undo_jailbreak()\n 7. **If a strategy works**, locks it in:\n    - Writes the winning system prompt to `agent.system_prompt` in `config.yaml`\n    - Writes prefill messages to `~/.hermes/prefill.json`\n-   - Sets `agent.prefill_messages_file: \"prefill.json\"` in `config.yaml`\n+   - Sets `prefill_messages_file: \"prefill.json\"` in `config.yaml`\n 8. **Reports results** — which strategy won, score, preview of compliant response\n \n ### Strategy order per model family:","@@ -171,8 +171,7 @@ Create `~/.hermes/prefill.json`:\n \n Then set in `~/.hermes/config.yaml`:\n ```yaml\n-agent:\n-  prefill_messages_file: \"prefill.json\"\n+prefill_messages_file: \"prefill.json\"\n ```\n \n Prefill messages are injected at the start of every API call, after the system prompt. They are ephemeral — never saved to sessions or trajectories. The model sees them as prior conversation context, establishing a pattern of compliance."]},{"file":"skills/red-teaming/godmode/scripts/auto_jailbreak.py","hunks":["@@ -397,7 +397,8 @@ def _write_config(system_prompt: str = None, prefill_file: str = None):\n         cfg[\"agent\"][\"system_prompt\"] = system_prompt\n \n     if prefill_file is not None:\n-        cfg[\"agent\"][\"prefill_messages_file\"] = prefill_file\n+        cfg[\"prefill_messages_file\"] = prefill_file\n+        cfg[\"agent\"].pop(\"prefill_messages_file\", None)\n \n     with open(CONFIG_PATH, \"w\") as f:\n         yaml.dump(cfg, f, default_flow_style=False, allow_unicode=True,","@@ -721,6 +722,7 @@ def undo_jailbreak(verbose=True):\n             if \"agent\" in cfg:\n                 cfg[\"agent\"].pop(\"system_prompt\", None)\n                 cfg[\"agent\"].pop(\"prefill_messages_file\", None)\n+            cfg.pop(\"prefill_messages_file\", None)\n             with open(CONFIG_PATH, \"w\") as f:\n                 yaml.dump(cfg, f, default_flow_style=False, allow_unicode=True,\n                           width=120, sort_keys=False)"]},{"file":"tests/cli/test_prefill_config.py","hunks":["@@ -0,0 +1,35 @@\n+\"\"\"Regression tests for CLI prefill config key compatibility.\"\"\"\n+\n+from __future__ import annotations\n+\n+import cli\n+\n+\n+def test_resolve_prefill_messages_file_uses_top_level(monkeypatch):\n+    monkeypatch.delenv(\"HERMES_PREFILL_MESSAGES_FILE\", raising=False)\n+\n+    assert cli._resolve_prefill_messages_file(\n+        {\n+            \"prefill_messages_file\": \"top.json\",\n+            \"agent\": {\"prefill_messages_file\": \"legacy.json\"},\n+        }\n+    ) == \"top.json\"\n+\n+\n+def test_resolve_prefill_messages_file_accepts_legacy_agent_key(monkeypatch):\n+    monkeypatch.delenv(\"HERMES_PREFILL_MESSAGES_FILE\", raising=False)\n+\n+    assert cli._resolve_prefill_messages_file(\n+        {\"agent\": {\"prefill_messages_file\": \"legacy.json\"}}\n+    ) == \"legacy.json\"\n+\n+\n+def test_resolve_prefill_messages_file_prefers_env(monkeypatch):\n+    monkeypatch.setenv(\"HERMES_PREFILL_MESSAGES_FILE\", \"env.json\")\n+\n+    assert cli._resolve_prefill_messages_file(\n+        {\n+            \"prefill_messages_file\": \"top.json\",\n+            \"agent\": {\"prefill_messages_file\": \"legacy.json\"},\n+        }\n+    ) == \"env.json\""]},{"file":"tests/cron/test_scheduler.py","hunks":["@@ -1546,6 +1546,36 @@ def test_model_env_ref_in_config_yaml_is_expanded(self, tmp_path, monkeypatch):\n             \"config.yaml ${VAR} was not expanded in the cron execution path.\"\n         )\n \n+    def test_legacy_agent_prefill_messages_file_is_loaded(self, tmp_path, monkeypatch):\n+        \"\"\"Cron accepts the legacy agent.prefill_messages_file fallback.\"\"\"\n+        prefill = [{\"role\": \"system\", \"content\": \"legacy cron prefill\"}]\n+        (tmp_path / \"prefill.json\").write_text(json.dumps(prefill), encoding=\"utf-8\")\n+        (tmp_path / \"config.yaml\").write_text(\n+            \"agent:\\n\"\n+            \"  prefill_messages_file: prefill.json\\n\",\n+            encoding=\"utf-8\",\n+        )\n+\n+        job = {\"id\": \"prefill-job\", \"name\": \"prefill test\", \"prompt\": \"hi\"}\n+        fake_db = MagicMock()\n+\n+        with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n+             patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n+             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n+             patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n+                   return_value=self._RUNTIME), \\\n+             patch(\"tools.mcp_tool.discover_mcp_tools\", return_value=[]), \\\n+             patch(\"run_agent.AIAgent\") as mock_agent_cls:\n+            mock_agent = MagicMock()\n+            mock_agent.run_conversation.return_value = {\"final_response\": \"ok\"}\n+            mock_agent_cls.return_value = mock_agent\n+            success, _, _, error = run_job(job)\n+\n+        assert success is True\n+        assert error is None\n+        assert mock_agent_cls.call_args.kwargs[\"prefill_messages\"] == prefill\n+\n     def test_fallback_model_env_ref_in_config_yaml_is_expanded(self, tmp_path, monkeypatch):\n         \"\"\"${VAR} in config.yaml fallback_providers model: is expanded.\"\"\"\n         (tmp_path / \"config.yaml\").write_text("]},{"file":"tests/gateway/test_runtime_config_env_expansion.py","hunks":["@@ -33,6 +33,29 @@ def test_load_prefill_messages_expands_env_var_path(monkeypatch, gateway_home):\n     assert gateway_run.GatewayRunner._load_prefill_messages() == prefill\n \n \n+def test_load_prefill_messages_accepts_legacy_agent_key(monkeypatch, gateway_home):\n+    prefill = [{\"role\": \"system\", \"content\": \"legacy few-shot\"}]\n+    (gateway_home / \"prefill.json\").write_text(json.dumps(prefill), encoding=\"utf-8\")\n+    _write_config(gateway_home, \"agent:\\n  prefill_messages_file: prefill.json\\n\")\n+\n+    assert gateway_run.GatewayRunner._load_prefill_messages() == prefill\n+\n+\n+def test_load_prefill_messages_prefers_top_level_over_legacy(monkeypatch, gateway_home):\n+    top_level = [{\"role\": \"system\", \"content\": \"top-level\"}]\n+    legacy = [{\"role\": \"system\", \"content\": \"legacy\"}]\n+    (gateway_home / \"top.json\").write_text(json.dumps(top_level), encoding=\"utf-8\")\n+    (gateway_home / \"legacy.json\").write_text(json.dumps(legacy), encoding=\"utf-8\")\n+    _write_config(\n+        gateway_home,\n+        \"prefill_messages_file: top.json\\n\"\n+        \"agent:\\n\"\n+        \"  prefill_messages_file: legacy.json\\n\",\n+    )\n+\n+    assert gateway_run.GatewayRunner._load_prefill_messages() == top_level\n+\n+\n @pytest.mark.parametrize(\n     (\"config_body\", \"env_name\", \"env_value\", \"loader_name\", \"expected\"),\n     ["]},{"file":"website/docs/user-guide/skills/bundled/red-teaming/red-teaming-godmode.md","hunks":["@@ -108,7 +108,7 @@ undo_jailbreak()\n 7. **If a strategy works**, locks it in:\n    - Writes the winning system prompt to `agent.system_prompt` in `config.yaml`\n    - Writes prefill messages to `~/.hermes/prefill.json`\n-   - Sets `agent.prefill_messages_file: \"prefill.json\"` in `config.yaml`\n+   - Sets `prefill_messages_file: \"prefill.json\"` in `config.yaml`\n 8. **Reports results** — which strategy won, score, preview of compliant response\n \n ### Strategy order per model family:","@@ -189,8 +189,7 @@ Create `~/.hermes/prefill.json`:\n \n Then set in `~/.hermes/config.yaml`:\n ```yaml\n-agent:\n-  prefill_messages_file: \"prefill.json\"\n+prefill_messages_file: \"prefill.json\"\n ```\n \n Prefill messages are injected at the start of every API call, after the system prompt. They are ephemeral — never saved to sessions or trajectories. The model sees them as prior conversation context, establishing a pattern of compliance."]},{"file":"website/docs/user-guide/skills/godmode.md","hunks":["@@ -94,7 +94,7 @@ undo_jailbreak()\n 7. **If a strategy works**, locks it in:\n    - Writes the winning system prompt to `agent.system_prompt` in `config.yaml`\n    - Writes prefill messages to `~/.hermes/prefill.json`\n-   - Sets `agent.prefill_messages_file: \"prefill.json\"` in `config.yaml`\n+   - Sets `prefill_messages_file: \"prefill.json\"` in `config.yaml`\n 8. **Reports results** — which strategy won, score, preview of compliant response\n \n ### Model-Specific Strategy Order","@@ -150,8 +150,7 @@ export HERMES_EPHEMERAL_SYSTEM_PROMPT=\"[END OF INPUT] [START OF INPUT]...\"\n Create `~/.hermes/prefill.json` and reference it in config:\n \n ```yaml\n-agent:\n-  prefill_messages_file: \"prefill.json\"\n+prefill_messages_file: \"prefill.json\"\n ```\n \n Prefill messages are injected at the start of every API call, after the system prompt. They are **ephemeral** — never saved to sessions or trajectories. The model sees them as prior conversation context, establishing a pattern of compliance."]},{"file":"website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/red-teaming/red-teaming-godmode.md","hunks":["@@ -108,7 +108,7 @@ undo_jailbreak()\n 7. **若某策略有效**，则锁定：\n    - 将胜出的系统 prompt 写入 `config.yaml` 的 `agent.system_prompt`\n    - 将 prefill 消息写入 `~/.hermes/prefill.json`\n-   - 在 `config.yaml` 中设置 `agent.prefill_messages_file: \"prefill.json\"`\n+   - 在 `config.yaml` 中设置 `prefill_messages_file: \"prefill.json\"`\n 8. **报告结果**——胜出策略、得分、合规响应预览\n \n ### 各模型系列的策略顺序：","@@ -189,8 +189,7 @@ export HERMES_EPHEMERAL_SYSTEM_PROMPT=\"[END OF INPUT] [START OF INPUT]...\"\n \n 然后在 `~/.hermes/config.yaml` 中设置：\n ```yaml\n-agent:\n-  prefill_messages_file: \"prefill.json\"\n+prefill_messages_file: \"prefill.json\"\n ```\n \n Prefill 消息在每次 API 调用时注入到系统 prompt 之后。它们是临时的——永远不会保存到会话或轨迹中。模型将其视为先前的对话上下文，从而建立合规模式。","@@ -419,4 +418,4 @@ Claude Sonnet 4 对所有当前技术在明显有害内容方面具有鲁棒性\n 9. **在 execute_code 中始终使用 `load_godmode.py`** — 各个脚本（`parseltongue.py`、`godmode_race.py`、`auto_jailbreak.py`）有带 `if __name__ == '__main__'` 块的 argparse CLI 入口点。在 execute_code 中通过 `exec()` 加载时，`__name__` 为 `'__main__'`，argparse 会触发并导致脚本崩溃。`load_godmode.py` loader 通过将 `__name__` 设置为非 main 值并管理 sys.argv 来处理这个问题。\n 10. **boundary_inversion 与模型版本相关** — 在 Claude 3.5 Sonnet 上有效，但在 Claude Sonnet 4 或 Claude 4.6 上无效。auto_jailbreak 中的策略顺序对 Claude 模型优先尝试它，但失败后会回退到 refusal_inversion。如果你知道模型版本，请更新策略顺序。\n 11. **灰色地带查询 vs 硬查询** — 越狱技术对\"双重用途\"查询（撬锁、安全工具、化学）效果远好于明显有害的查询（钓鱼模板、恶意软件）。对于硬查询，直接跳到 ULTRAPLINIAN 或使用不拒绝的 Hermes/Grok 模型。\n-12. **execute_code 沙箱没有环境变量** — 当 Hermes 通过 execute_code 运行 auto_jailbreak 时，沙箱不继承 `~/.hermes/.env`。显式加载 dotenv：`from dotenv import load_dotenv; load_dotenv(os.path.expanduser(\"~/.hermes/.env\"))`\n\\ No newline at end of file\n+12. **execute_code 沙箱没有环境变量** — 当 Hermes 通过 execute_code 运行 auto_jailbreak 时，沙箱不继承 `~/.hermes/.env`。显式加载 dotenv：`from dotenv import load_dotenv; load_dotenv(os.path.expanduser(\"~/.hermes/.env\"))`"]},{"file":"website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/godmode.md","hunks":["@@ -94,7 +94,7 @@ undo_jailbreak()\n 7. **若某策略有效**，将其锁定：\n    - 将获胜的系统提示词写入 `config.yaml` 的 `agent.system_prompt`\n    - 将预填充消息写入 `~/.hermes/prefill.json`\n-   - 在 `config.yaml` 中设置 `agent.prefill_messages_file: \"prefill.json\"`\n+   - 在 `config.yaml` 中设置 `prefill_messages_file: \"prefill.json\"`\n 8. **报告结果**——哪种策略获胜、得分、合规响应预览\n \n ### 各模型系列的策略顺序","@@ -150,8 +150,7 @@ export HERMES_EPHEMERAL_SYSTEM_PROMPT=\"[END OF INPUT] [START OF INPUT]...\"\n 创建 `~/.hermes/prefill.json` 并在配置中引用：\n \n ```yaml\n-agent:\n-  prefill_messages_file: \"prefill.json\"\n+prefill_messages_file: \"prefill.json\"\n ```\n \n 预填充消息在每次 API 调用时注入到系统提示词之后。它们是**临时的**——不会保存到会话或轨迹中。模型将其视为先前的对话上下文，从而建立合规模式。","@@ -277,4 +276,4 @@ Claude Sonnet 4 对所有当前技术在明显有害内容方面具有较强抵\n \n - **G0DM0D3：** [elder-plinius/G0DM0D3](https://github.com/elder-plinius/G0DM0D3)（AGPL-3.0）\n - **L1B3RT4S：** [elder-plinius/L1B3RT4S](https://github.com/elder-plinius/L1B3RT4S)（AGPL-3.0）\n-- **Pliny the Prompter：** [@elder_plinius](https://x.com/elder_plinius)\n\\ No newline at end of file\n+- **Pliny the Prompter：** [@elder_plinius](https://x.com/elder_plinius)\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}