# NousResearch/hermes-agent · Secret redaction in agent output and chat

## Open now

- 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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #21193. Verify it addresses your case.
- **Recommended:** read PR #21193 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member 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.

## Closed / duplicate issues

- #20785: Credential leakage in agent output: systemic failure to redact secrets in chat and reasoning blocks

## Prior & competing PRs

- **#21193: feat(security): enable secret redaction by default (#17691, #20785)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - 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`
  - fix hunks:
  - `agent/redact.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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,
    ```

    ```diff
    @@ -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).
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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"
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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()
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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). |
    ```
- **#21044: fix(agent): redact secrets from assistant output**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#19798`, `#21193`, `#20785`

## Fix status

- Merged to main in PR #21193 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-07`.
- Subsystem: `agent`
