NousResearch/hermes-agent

Secret redaction in agent output and chat

Cluster 20785 · agent. Data as of

Nothing open on GitHub. Fix merged in #21193 on 7 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #21193. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
Nothing open on GitHub.

The members share a theme of preventing secrets or credentials from appearing in agent output: one proposes enabling secret redaction by default, another redacts secrets from assistant output, and a third reports credential leakage in chat and reasoning blocks. The titles alone do not show how these relate in implementation or what their current status is.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #21193 feat(security): enable secret redaction by default (#17691, #20785)

    merged to main

    files: agent/redact.py, cli.py, gateway/run.py, hermes_cli/config.py, tests/hermes_cli/test_debug.py, tests/hermes_cli/test_redact_config_bridge.py, website/docs/reference/environment-variables.md

    Show the diff
    • agent/redact.py
      @@ -56,12 +56,15 @@
       })
       
       # Snapshot at import time so runtime env mutations (e.g. LLM-generated
      -# `export HERMES_REDACT_SECRETS=[redacted:secret-assignment] cannot enable/disable redaction
      -# mid-session.  OFF by default — user must opt in via
      -# `security.redact_secrets: true` in config.yaml (bridged to this env var
      -# in hermes_cli/main.py and gateway/run.py) or `HERMES_REDACT_SECRETS=true`
      -# in ~/.hermes/.env.
      -_REDACT_ENABLED = os.getenv("HERMES_REDACT_SECRETS", "").lower() in ("1", "true", "yes", "on")
      +# `export HERMES_REDACT_SECRETS=[redacted:secret-assignment] cannot disable redaction
      +# mid-session.  ON by default — secure default per issue #17691. Users who
      +# need raw credential values in tool output (e.g. working on the redactor
      +# itself) can opt out via `security.redact_secrets: false` in config.yaml
      +# (bridged to this env var in hermes_cli/main.py, gateway/run.py, and
      +# cli.py) or `HERMES_REDACT_SECRETS=false` in ~/.hermes/.env. An opt-out
      +# warning is logged at gateway and CLI startup so operators see the
      +# downgrade — see `_log_redaction_status()` in gateway/run.py and cli.py.
      +_REDACT_ENABLED = os.getenv("HERMES_REDACT_SECRETS", "true").lower() in ("1", "true", "yes", "on")
       
       # Known API key prefixes -- match the prefix + contiguous token chars
       _PREFIX_PATTERNS = [
    • cli.py
      @@ -10213,6 +10213,24 @@ def run(self):
                   _welcome_text = "Welcome to Hermes Agent! Type your message or /help for commands."
                   _welcome_color = "#FFF8DC"
               self._console_print(f"[{_welcome_color}]{_welcome_text}[/]")
      +
      +        # Redaction opt-out warning (#17691): ON by default, loud when off.
      +        # The redactor snapshots its state at import time so any toggle now
      +        # won't affect the running process — we just want the operator to
      +        # see that they're running without the safety net.
      +        try:
      +            _redact_raw = os.getenv("HERMES_REDACT_SECRETS", "true")
      +            if _redact_raw.lower() not in ("1", "true", "yes", "on"):
      +                self._console_print(
      +                    "[bold red]⚠  Secret redaction is DISABLED[/] "
      +                    f"(HERMES_REDACT_SECRETS={_redact_raw}). "
      +                    "API keys and tokens may appear verbatim in chat output, "
      +                    "session JSONs, and logs. Set "
      +                    "[cyan]security.redact_secrets: [redacted:secret-assignment] in config.yaml "
      +                    "to re-enable."
      +                )
      +        except Exception:
      +            pass
               # First-time OpenClaw-residue banner — fires once if ~/.openclaw/ exists
               # after an OpenClaw→Hermes migration (especially migrations done by
               # OpenClaw's own tool, which doesn't archive the source directory).
    • gateway/run.py
      @@ -2763,6 +2763,29 @@ async def start(self) -> bool:
                   )
               except Exception:
                   pass
      +        # Redaction status: ON by default (#17691). Surface a prominent
      +        # warning if an operator has explicitly opted out so they don't
      +        # forget the downgrade is active — the redactor snapshots its
      +        # state at import time, so this log line is the source of truth
      +        # for this process's lifetime.
      +        try:
      +            _redact_raw = os.getenv("HERMES_REDACT_SECRETS", "true")
      +            _redact_on = _redact_raw.lower() in ("1", "true", "yes", "on")
      +            if _redact_on:
      +                logger.info(
      +                    "Secret redaction: ENABLED (tool output, logs, and chat "
      +                    "responses are scrubbed before delivery)"
      +                )
      +            else:
      +                logger.warning(
      +                    "Secret redaction: DISABLED (HERMES_REDACT_SECRETS=%s). "
      +                    "API keys and tokens may appear verbatim in chat output, "
      +                    "session JSONs, and logs. Set security.redact_secrets: true "
      +                    "in config.yaml to re-enable.",
      +                    _redact_raw,
      +                )
      +        except Exception:
      +            pass
               try:
                   from hermes_cli.profiles import get_active_profile_name
                   _profile = get_active_profile_name()
    • hermes_cli/config.py
      @@ -1191,7 +1191,7 @@ def _ensure_hermes_home_managed(home: Path):
           # Pre-exec security scanning via tirith
           "security": {
               "allow_private_urls": False,  # Allow requests to private/internal IPs (for OpenWrt, proxies, VPNs)
      -        "redact_secrets": [redacted:secret-assignment]
      +        "redact_secrets": True,
               "tirith_enabled": True,
               "tirith_path": "tirith",
               "tirith_timeout": 5,
      @@ -3974,10 +3974,10 @@ def load_config() -> Dict[str, Any]:
       
       _SECURITY_COMMENT = """
       # ── Security ──────────────────────────────────────────────────────────
      -# Secret redaction is OFF by default — tool output (terminal stdout,
      -# read_file results, web content) passes through unmodified. Set
      -# redact_secrets to true to mask strings that look like API keys, tokens,
      -# and passwords before they enter the model context and logs.
      +# Secret redaction is ON by default — strings that look like API keys,
      +# tokens, and passwords are masked in tool output, logs, and chat
      +# responses before the model or user ever sees them. Set redact_secrets
      +# to false to disable (e.g. when developing the redactor itself).
       # tirith pre-exec scanning is enabled by default when the tirith binary
       # is available. Configure via security.tirith_* keys or env vars
       # (TIRITH_ENABLED, TIRITH_BIN, TIRITH_TIMEOUT, TIRITH_FAIL_OPEN).
      @@ -4017,8 +4017,8 @@ def load_config() -> Dict[str, Any]:
       
       _COMMENTED_SECTIONS = """
       # ── Security ──────────────────────────────────────────────────────────
      -# Secret redaction is OFF by default. Set to true to mask strings that
      -# look like API keys, tokens, and passwords in tool output and logs.
      +# Secret redaction is ON by default. Set to false to pass tool output,
      +# logs, and chat responses through unmodified (e.g. for redactor dev).
       #
       # security:
       #   redact_secrets: true
    • tests/hermes_cli/test_debug.py
      @@ -291,9 +291,11 @@ def hermes_home_with_secret(self, tmp_path, monkeypatch):
               home = tmp_path / ".hermes"
               home.mkdir()
               monkeypatch.setenv("HERMES_HOME", str(home))
      -        # Critical: ensure the user has NOT opted in to redaction. The whole
      -        # point of this PR is that share-time redaction works for users who
      -        # never set this env var.
      +        # Baseline fixture: no explicit env-var opinion. With the post-#17691
      +        # default of ON, the default-path tests below exercise the
      +        # secure-default behaviour. The `force=True` regression test
      +        # setenvs to "false" inline to prove force=True works even when
      +        # the runtime flag is disabled.
               monkeypatch.delenv("HERMES_REDACT_SECRETS", raising=False)
       
               logs_dir = home / "logs"
      @@ -324,21 +326,26 @@ def test_redact_false_passes_through(self, hermes_home_with_secret):
               assert _REDACT_FIXTURE_TOKEN in snap.tail_text
               assert _REDACT_FIXTURE_TOKEN in (snap.full_text or "")
       
      -    def test_force_true_overrides_unset_env_var(self, hermes_home_with_secret):
      +    def test_force_true_works_when_redaction_disabled(
      +        self, hermes_home_with_secret, monkeypatch
      +    ):
               """Regression test: redact_sensitive_text short-circuits without force=True.
       
               If a future refactor drops `force=True` from `_redact_log_text`, this
               test fails immediately. Without `force=True`, the redactor returns the
      -        input unchanged when HERMES_REDACT_SECRETS is unset, and the feature
      -        ships silently broken for its target audience.
      +        input unchanged when HERMES_REDACT_SECRETS=[redacted:secret-assignment] and the share-time
      +        redaction feature ships silently broken for users who opted out of
      +        runtime redaction (e.g. developers working on the redactor itself).
               """
               import os
       
      +        # Force the runtime flag off so we're exercising the force=True path,
      +        # not the default-on path.
      +        monkeypatch.setenv("HERMES_REDACT_SECRETS", "false")
      +
               from hermes_cli.debug import _capture_log_snapshot
       
      -        # Belt-and-suspenders: confirm the env var is genuinely unset for this
      -        # test so we know we're exercising the force=True path.
      -        assert os.environ.get("HERMES_REDACT_SECRETS", "") == ""
      +        assert os.environ.get("HERMES_REDACT_SECRETS", "") == "false"
       
               snap = _capture_log_snapshot("agent", tail_lines=10)
       
    • tests/hermes_cli/test_redact_config_bridge.py
      @@ -72,11 +72,13 @@ def test_redact_secrets_false_in_config_yaml_is_honored(tmp_path):
           assert "ENV_VAR=false" in result.stdout
       
       
      -def test_redact_secrets_default_false_when_unset(tmp_path):
      -    """Without the config key, redaction stays OFF by default.
      +def test_redact_secrets_default_true_when_unset(tmp_path):
      +    """Without the config key or env var, redaction is ON by default (#17691).
       
      -    Secret redaction is opt-in — users who want it must set
      -    `security.redact_secrets: true` explicitly (or HERMES_REDACT_SECRETS=[redacted:secret-assignment]
      +    Secret redaction is a secure default — users who need raw credential
      +    values in tool output (e.g. working on the redactor itself) must set
      +    `security.redact_secrets: false` explicitly (or
      +    `HERMES_REDACT_SECRETS=[redacted:secret-assignment]
           """
           hermes_home = tmp_path / ".hermes"
           hermes_home.mkdir()
      @@ -107,7 +109,7 @@ def test_redact_secrets_default_false_when_unset(tmp_path):
               timeout=30,
           )
           assert result.returncode == 0, f"probe failed: {result.stderr}"
      -    assert "REDACT_ENABLED=False" in result.stdout
      +    assert "REDACT_ENABLED=True" in result.stdout
       
       
       def test_redact_secrets_true_in_config_yaml_is_honored(tmp_path):
    • website/docs/reference/environment-variables.md
      @@ -456,7 +456,7 @@ Advanced per-platform knobs for throttling the outbound message batcher. Most us
       | `HERMES_EPHEMERAL_SYSTEM_PROMPT` | Ephemeral system prompt injected at API-call time (never persisted to sessions) |
       | `HERMES_PREFILL_MESSAGES_FILE` | Path to a JSON file of ephemeral prefill messages injected at API-call time. |
       | `HERMES_ALLOW_PRIVATE_URLS` | `true`/`false` — allow tools to fetch localhost/private-network URLs. Off by default in gateway mode. |
      -| `HERMES_REDACT_SECRETS` | `true`/`false` — control secret redaction in logs and shareable outputs (default: `true`). |
      +| `HERMES_REDACT_SECRETS` | `true`/`false` — control secret redaction in tool output, logs, and chat responses (default: `true`). |
       | `HERMES_WRITE_SAFE_ROOT` | Optional directory prefix that restricts `write_file`/`patch` writes; paths outside require approval. |
       | `HERMES_DISABLE_FILE_STATE_GUARD` | Set to `1` to turn off the "file changed since you read it" guard on `patch`/`write_file`. |
       | `HERMES_CORE_TOOLS` | Comma-separated override for the canonical core tool list (advanced; rarely needed). |

1 PR closed without merging

From the close comments: done elsewhere 1

  • #21044 fix(agent): redact secrets from assistant output · done elsewhere → #19798, #21193, #20785
Closed issues · 1
  • Closed#20785Credential leakage in agent output: systemic failure to redact secrets in chat and reasoning blocks
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#21193
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
7 May 2026

For your agent

The same facts, typed.