fix(cli): safe mode also skips shell-hook registration

--safe-mode promised to disable ALL customizations, but shell hooks
declared in config.yaml's hooks: block registered anyway —
register_from_config() runs independently of plugin discovery and
load_config() does not honor HERMES_IGNORE_USER_CONFIG. Gate it on
HERMES_SAFE_MODE at the single chokepoint so troubleshooting runs fire
zero user-configured code (plugins, MCP, and hooks).

Docs (en + zh) updated; positive + negative tests added.
This commit is contained in:
Teknium 2026-07-07 02:21:16 -07:00
parent fc02b1c276
commit 299d5c6603
5 changed files with 49 additions and 3 deletions

View file

@ -224,6 +224,15 @@ def register_from_config(
if not isinstance(cfg, dict):
return []
# Safe mode (--safe-mode / HERMES_SAFE_MODE=1): shell hooks are user
# customizations too — skip registration entirely so a troubleshooting
# run fires zero user-configured code (plugins, MCP, AND hooks).
from utils import env_var_enabled
if env_var_enabled("HERMES_SAFE_MODE"):
logger.info("HERMES_SAFE_MODE=1 — shell-hook registration skipped")
return []
effective_accept = _resolve_effective_accept(cfg, accept_hooks)
specs = _parse_hooks_block(cfg.get("hooks"))

View file

@ -130,3 +130,40 @@ def test_parser_accepts_safe_mode_on_root_and_chat():
assert parser.parse_args(["--safe-mode"]).safe_mode is True
assert parser.parse_args(["chat", "--safe-mode"]).safe_mode is True
assert parser.parse_args(["chat"]).safe_mode is False
def test_shell_hooks_skipped(monkeypatch):
monkeypatch.setenv("HERMES_SAFE_MODE", "1")
from agent.shell_hooks import register_from_config
cfg = {
"hooks": {
"pre_tool_call": [{"command": "echo hooked"}],
},
"hooks_auto_accept": True,
}
assert register_from_config(cfg, accept_hooks=True) == []
def test_shell_hooks_register_without_safe_mode(monkeypatch):
import agent.shell_hooks as sh
cfg = {
"hooks": {
"pre_tool_call": [{"command": "echo hooked"}],
},
"hooks_auto_accept": True,
}
manager = types.SimpleNamespace(_hooks={})
plugins = types.ModuleType("hermes_cli.plugins")
setattr(plugins, "get_plugin_manager", lambda: manager)
setattr(plugins, "VALID_HOOKS", {"pre_tool_call"})
monkeypatch.setitem(sys.modules, "hermes_cli.plugins", plugins)
monkeypatch.setattr(sh, "_registered", set())
registered = sh.register_from_config(cfg, accept_hooks=True)
assert len(registered) == 1
assert "pre_tool_call" in manager._hooks

View file

@ -117,7 +117,7 @@ Common options:
| `--pass-session-id` | Pass the session ID into the system prompt. |
| `--ignore-user-config` | Ignore `~/.hermes/config.yaml` and use built-in defaults. Credentials in `.env` are still loaded. Useful for isolated CI runs, reproducible bug reports, and third-party integrations. |
| `--ignore-rules` | Skip auto-injection of `AGENTS.md`, `SOUL.md`, `.cursorrules`, persistent memory, and preloaded skills. Combine with `--ignore-user-config` for a fully isolated run. |
| `--safe-mode` | Troubleshooting mode: disable ALL customizations — user config, rules/memory injection, plugins, and MCP servers (implies `--ignore-user-config` and `--ignore-rules`). Use to isolate whether a problem comes from your setup or from Hermes itself. |
| `--safe-mode` | Troubleshooting mode: disable ALL customizations — user config, rules/memory injection, plugins, shell hooks, and MCP servers (implies `--ignore-user-config` and `--ignore-rules`). Use to isolate whether a problem comes from your setup or from Hermes itself. |
| `--source <tag>` | Session source tag for filtering (default: `cli`). Use `tool` for third-party integrations that should not appear in user session lists. |
| `--max-turns <N>` | Maximum tool-calling iterations per conversation turn (default: 90, or `agent.max_turns` in config). |

View file

@ -716,7 +716,7 @@ Advanced per-platform knobs for throttling the outbound message batcher. Most us
| `HERMES_ACCEPT_HOOKS` | Auto-approve any unseen shell hooks declared in `config.yaml` without a TTY prompt. Equivalent to `--accept-hooks` or `hooks_auto_accept: true`. |
| `HERMES_IGNORE_USER_CONFIG` | Skip `~/.hermes/config.yaml` and use built-in defaults (credentials in `.env` still load). Equivalent to `--ignore-user-config`. |
| `HERMES_IGNORE_RULES` | Skip auto-injection of `AGENTS.md`, `SOUL.md`, `.cursorrules`, memory, and preloaded skills. Equivalent to `--ignore-rules`. |
| `HERMES_SAFE_MODE` | Troubleshooting mode: disable ALL customizations — skips plugin discovery and MCP server loading. Set automatically by `--safe-mode` (which also sets the two flags above). |
| `HERMES_SAFE_MODE` | Troubleshooting mode: disable ALL customizations — skips plugin discovery, MCP server loading, and shell-hook registration. Set automatically by `--safe-mode` (which also sets the two flags above). |
| `HERMES_MD_NAMES` | Comma-separated list of rules-file names to auto-inject (default: `AGENTS.md,CLAUDE.md,.cursorrules,SOUL.md`). |
| `HERMES_TOOL_PROGRESS` | Deprecated compatibility variable for tool progress display. Prefer `display.tool_progress` in `config.yaml`. |
| `HERMES_TOOL_PROGRESS_MODE` | Deprecated compatibility variable for tool progress mode. Prefer `display.tool_progress` in `config.yaml`. |

View file

@ -532,7 +532,7 @@ Graph 事件Teams 会议、日历、聊天等)的入站变更通知监听
| `HERMES_ACCEPT_HOOKS` | 无需 TTY 提示自动批准 `config.yaml` 中声明的任何未见过的 shell hook。等同于 `--accept-hooks``hooks_auto_accept: true`。 |
| `HERMES_IGNORE_USER_CONFIG` | 跳过 `~/.hermes/config.yaml` 并使用内置默认值(`.env` 中的凭证仍会加载)。等同于 `--ignore-user-config`。 |
| `HERMES_IGNORE_RULES` | 跳过 `AGENTS.md``SOUL.md``.cursorrules`、记忆和预加载技能的自动注入。等同于 `--ignore-rules`。 |
| `HERMES_SAFE_MODE` | 故障排查模式:禁用**所有**自定义项——跳过插件发现和 MCP 服务器加载。由 `--safe-mode` 自动设置(同时也会设置上面两个 flag。 |
| `HERMES_SAFE_MODE` | 故障排查模式:禁用**所有**自定义项——跳过插件发现、MCP 服务器加载和 shell hook 注册。由 `--safe-mode` 自动设置(同时也会设置上面两个 flag。 |
| `HERMES_MD_NAMES` | 自动注入的规则文件名逗号分隔列表(默认:`AGENTS.md,CLAUDE.md,.cursorrules,SOUL.md`)。 |
| `HERMES_TOOL_PROGRESS` | 工具进度显示的已弃用兼容变量。优先使用 `config.yaml` 中的 `display.tool_progress`。 |
| `HERMES_TOOL_PROGRESS_MODE` | 工具进度模式的已弃用兼容变量。优先使用 `config.yaml` 中的 `display.tool_progress`。 |