From 947faed3bc3961f6d6f6a3af4cf8dc2424a92877 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:02:02 -0700 Subject: [PATCH] feat(approvals): make dangerous command approval timeout configurable (#3886) * feat(approvals): make dangerous command approval timeout configurable Read `approvals.timeout` from config.yaml (default 60s) instead of hardcoding 60 seconds in both the fallback CLI prompt and the TUI prompt_toolkit callback. Follows the same pattern as `clarify.timeout` which is already configurable via CLI_CONFIG. Closes #3765 * fix: add timeout default to approvals section in DEFAULT_CONFIG --------- Co-authored-by: acsezen --- hermes_cli/callbacks.py | 3 ++- hermes_cli/config.py | 1 + tools/approval.py | 28 ++++++++++++++++++++++------ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/hermes_cli/callbacks.py b/hermes_cli/callbacks.py index fa51ee157..87f86b84d 100644 --- a/hermes_cli/callbacks.py +++ b/hermes_cli/callbacks.py @@ -241,7 +241,8 @@ def approval_callback(cli, command: str, description: str) -> str: lock = cli._approval_lock with lock: - timeout = 60 + from cli import CLI_CONFIG + timeout = CLI_CONFIG.get("approvals", {}).get("timeout", 60) response_queue = queue.Queue() choices = ["once", "session", "always", "deny"] if len(command) > 70: diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 4cdbb0af9..69530761d 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -407,6 +407,7 @@ DEFAULT_CONFIG = { # off — skip all approval prompts (equivalent to --yolo) "approvals": { "mode": "manual", + "timeout": 60, }, # Permanently allowed dangerous command patterns (added via "always" approval) diff --git a/tools/approval.py b/tools/approval.py index e7313b002..95011173f 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -241,7 +241,7 @@ def save_permanent_allowlist(patterns: set): # ========================================================================= def prompt_dangerous_approval(command: str, description: str, - timeout_seconds: int = 60, + timeout_seconds: int | None = None, allow_permanent: bool = True, approval_callback=None) -> str: """Prompt the user to approve a dangerous command (CLI only). @@ -256,6 +256,9 @@ def prompt_dangerous_approval(command: str, description: str, Returns: 'once', 'session', 'always', or 'deny' """ + if timeout_seconds is None: + timeout_seconds = _get_approval_timeout() + if approval_callback is not None: try: return approval_callback(command, description, @@ -336,15 +339,28 @@ def _normalize_approval_mode(mode) -> str: return "manual" -def _get_approval_mode() -> str: - """Read the approval mode from config. Returns 'manual', 'smart', or 'off'.""" +def _get_approval_config() -> dict: + """Read the approvals config block. Returns a dict with 'mode', 'timeout', etc.""" try: from hermes_cli.config import load_config config = load_config() - mode = config.get("approvals", {}).get("mode", "manual") - return _normalize_approval_mode(mode) + return config.get("approvals", {}) or {} except Exception: - return "manual" + return {} + + +def _get_approval_mode() -> str: + """Read the approval mode from config. Returns 'manual', 'smart', or 'off'.""" + mode = _get_approval_config().get("mode", "manual") + return _normalize_approval_mode(mode) + + +def _get_approval_timeout() -> int: + """Read the approval timeout from config. Defaults to 60 seconds.""" + try: + return int(_get_approval_config().get("timeout", 60)) + except (ValueError, TypeError): + return 60 def _smart_approve(command: str, description: str) -> str: