hermes - ✅(Solved) Fix [Bug]: [Bug]: terminal() tool leaks "declare -x" state-sync output into LLM context window (macOS) [3 pull requests, 2 comments, 3 participants]

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…
GitHub stats
NousResearch/hermes-agent#15459Fetched 2026-04-25 06:22:38
View on GitHub
Comments
2
Participants
3
Timeline
14
Reactions
0
Timeline (top)
labeled ×5referenced ×4cross-referenced ×3commented ×2

Error Message

The parser designed to strip this state-sync output before sending it back to the LLM is failing on macOS. This causes the LLM to read 60+ lines of environment variables on every single terminal tool call. In our case, this wasted so many context tokens that it caused our provider API (Ollama Cloud / DeepSeek-V3.2) to throw an HTTP 400 Bad Request error because the tool payload rapidly exceeded maximum context limits.

Additional Logs / Traceback (optional)

Root Cause

When using the terminal tool with TERMINAL_PERSISTENT_SHELL=True, every command's output is prepended with a massive declare -x environment variable dump (the output of export -p). The parser designed to strip this state-sync output before sending it back to the LLM is failing on macOS. This causes the LLM to read 60+ lines of environment variables on every single terminal tool call. In our case, this wasted so many context tokens that it caused our provider API (Ollama Cloud / DeepSeek-V3.2) to throw an HTTP 400 Bad Request error because the tool payload rapidly exceeded maximum context limits.

Fix Action

Fix / Workaround

Update the regex or string-splitting logic that handles TERMINAL_PERSISTENT_SHELL state syncing so it correctly identifies and strips the declare -x block on macOS environments. Temporary Workaround for other users: Set TERMINAL_PERSISTENT_SHELL=False in .env or config.yaml to bypass the buggy parser entirely.

PR fix notes

PR #15469: fix: strip leaked state-sync output from terminal (defense-in-depth for #15459)

Description (problem / solution / changelog)

Problem

When the persistent-shell snapshot redirect (export -p > file) fails — permissions, deleted temp dir, race conditions — the full environment dump (declare -x, export, alias lines) leaks into stdout and enters LLM context. No output-side stripping existed anywhere in the pipeline.

Fix

Add _strip_state_sync_leak() as a defense-in-depth filter in BaseEnvironment:

  • Strips lines starting at column 0 with declare -x , declare -f , export , alias — the patterns produced by export -p, declare -f, and alias -p
  • Preserves legitimate output — embedded occurrences mid-line are NOT stripped
  • Called in _update_cwd() before CWD marker extraction, so every execute() call is covered
  • Handles None/empty/missing output keys gracefully

Testing

13 new tests in TestStripStateSyncLeak:

  • declare -x with and without =
  • export VAR=value (zsh/POSIX)
  • declare -f function definitions
  • alias lines
  • Mixed leak + real output
  • Empty / missing output
  • Embedded declare -x mid-line preserved
  • CWD marker preserved after stripping

All 30 environment tests pass.

Closes

Fixes #15459

Changed files

  • cli-config.yaml.example (modified, +10/-1)
  • run_agent.py (modified, +85/-3)
  • tests/run_agent/test_glm_stop_heuristic.py (added, +393/-0)
  • tests/run_agent/test_run_agent.py (modified, +22/-2)
  • tests/tools/test_base_environment.py (modified, +203/-0)
  • tools/environments/base.py (modified, +48/-0)

PR #15472: fix: strip leaked declare-x env dump from terminal output on macOS (#15459)

Description (problem / solution / changelog)

Fixes #15459

When using the terminal tool with TERMINAL_PERSISTENT_SHELL=True on macOS, every command's output is prepended with ~60 lines of declare -x environment variable declarations from the session snapshot. This wastes context tokens and can trigger HTTP 400 errors on providers with smaller context windows.

Root Cause

On macOS (bash 3.2 and certain Homebrew bash builds), source-ing a file containing declare -x VAR="value" lines can emit those declarations to stdout, unlike on Linux where they execute silently.

Fix (2-layer approach)

Layer 1: tools/environments/base.py (root cause)

Wrap the snapshot source command in a group with stdout redirected to /dev/null:

# Before:
source {snapshot} 2>/dev/null || true

# After:
{ source {snapshot} 2>/dev/null || true; } >/dev/null

This prevents any stdout from the snapshot sourcing from leaking into the command output.

Layer 2: tools/terminal_tool.py (defense-in-depth)

Add a regex post-processing step that strips any contiguous block of declare -x lines at the start of output, catching leaks from other code paths.

Testing

  • Verified syntax with ast.parse() on both modified files
  • The regex only matches declare -x lines at the START of output (using ^ anchor), so it never strips user command output that might legitimately contain declare -x

Fixes NousResearch/hermes-agent#15459

Changed files

  • tools/environments/base.py (modified, +11/-2)
  • tools/terminal_tool.py (modified, +12/-1)

PR #15474: fix: suppress declare -x stdout leak when sourcing snapshot on macOS

Description (problem / solution / changelog)

Summary

The terminal() tool leaks 60+ lines of declare -x environment variable output into the LLM context window on macOS when TERMINAL_PERSISTENT_SHELL=True.

Root Cause

In _wrap_command() (tools/environments/base.py), the session snapshot is sourced before each command:

parts.append(f"source {self._snapshot_path} 2>/dev/null || true")

The snapshot file contains declare -x statements (written by export -p). On macOS, sourcing a file with declare -x prints each declaration to stdout. The 2>/dev/null only redirects stderr — stdout leaks directly into the command output that gets sent back to the LLM.

On Linux, declare -x during source is silent, which is why this was never caught before.

Fix

One-line change:

# Before (only suppresses stderr)
parts.append(f"source {self._snapshot_path} 2>/dev/null || true")

# After (suppresses both stdout and stderr)
parts.append(f"source {self._snapshot_path} > /dev/null 2>&1 || true")

The source builtin still sets all variables in the current shell — only the print output is suppressed. The redirect is harmless on Linux where it's already silent.

Fixes #15459

Changed files

  • tools/environments/base.py (modified, +7/-26)

Code Example

Report       https://paste.rs/f1MAU
  agent.log    https://paste.rs/eLAoz
  gateway.log  https://paste.rs/V9xaO

---
RAW_BUFFERClick to expand / collapse

Bug Description

When using the terminal tool with TERMINAL_PERSISTENT_SHELL=True, every command's output is prepended with a massive declare -x environment variable dump (the output of export -p). The parser designed to strip this state-sync output before sending it back to the LLM is failing on macOS. This causes the LLM to read 60+ lines of environment variables on every single terminal tool call. In our case, this wasted so many context tokens that it caused our provider API (Ollama Cloud / DeepSeek-V3.2) to throw an HTTP 400 Bad Request error because the tool payload rapidly exceeded maximum context limits.

Steps to Reproduce

Run Hermes Agent on macOS. Ensure TERMINAL_PERSISTENT_SHELL=True is enabled in .env or config.yaml. Have the LLM execute a simple terminal command via the terminal() tool (e.g., echo "Hello World"). Observe the raw output parsed back to the LLM. It will contain the full environment dump rather than just the command output.

Expected Behavior

The LLM should only see the standard output of the command (e.g., Hello World). The background state-syncing output (declare -x...) should be completely filtered out by the backend parser before the string is passed back to the agent.

Actual Behavior

The LLM receives ~63 lines of declare -x environment variable declarations before the actual command output. Example: code Bash declare -x BROWSER_INACTIVITY_TIMEOUT="120" declare -x COLORTERM="truecolor" ... [60 more lines] ... declare -x TERMINAL_PERSISTENT_SHELL="True" Hello World

Affected Component

Agent Core (conversation loop, context compression, memory)

Messaging Platform (if gateway-related)

N/A (CLI only)

Debug Report

Report       https://paste.rs/f1MAU
  agent.log    https://paste.rs/eLAoz
  gateway.log  https://paste.rs/V9xaO

Operating System

macOS

Python Version

Python 3.14.2

Hermes Version

2.32.0

Additional Logs / Traceback (optional)

Root Cause Analysis (optional)

The persistent shell backend runs export -p (or similar) to sync directory and variable state between tool calls. However, the stdout parser/regex in terminal_tool.py (or the underlying shell handler) is failing to filter this output out of the response string returned to the LLM on macOS. This might be due to macOS bash/zsh newline handling differences or how macOS formats export -p compared to Linux.

Proposed Fix (optional)

Update the regex or string-splitting logic that handles TERMINAL_PERSISTENT_SHELL state syncing so it correctly identifies and strips the declare -x block on macOS environments. Temporary Workaround for other users: Set TERMINAL_PERSISTENT_SHELL=False in .env or config.yaml to bypass the buggy parser entirely.

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

extent analysis

TL;DR

Update the regex or string-splitting logic in terminal_tool.py to correctly filter out the declare -x block on macOS environments.

Guidance

  • Investigate the differences in newline handling and export -p formatting between macOS and Linux to understand why the current parser is failing.
  • Review the terminal_tool.py code to identify the specific regex or string-splitting logic that needs to be updated.
  • Test the updated parser with various terminal commands and environment variables to ensure it correctly filters out the declare -x block.
  • Consider implementing a temporary workaround by setting TERMINAL_PERSISTENT_SHELL=False in .env or config.yaml to bypass the buggy parser.

Example

No code snippet is provided as the issue does not contain sufficient information to create a specific example.

Notes

The root cause of the issue is likely due to differences in macOS bash/zsh newline handling or export -p formatting, which is causing the parser to fail. The proposed fix involves updating the regex or string-splitting logic to correctly identify and strip the declare -x block on macOS environments.

Recommendation

Apply the temporary workaround by setting TERMINAL_PERSISTENT_SHELL=False in .env or config.yaml to bypass the buggy parser, as this is a safer and more immediate solution until a permanent fix can be implemented.

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 - ✅(Solved) Fix [Bug]: [Bug]: terminal() tool leaks "declare -x" state-sync output into LLM context window (macOS) [3 pull requests, 2 comments, 3 participants]