# NousResearch/hermes-agent · Secret redaction gaps in logs, chat, and debug uploads

## Open now

- 2 issues, 2 PRs open on GitHub.
- #46006: \[Bug\]: Security Issue when following the outlined Bug Report Procedure.
- #64351: \[Bug\]: Gateway logs inbound message bodies verbatim; \`hermes debug share\` then uploads them to a public paste (leaked a live OAuth client_secret)
- PR #64376: fix(gateway): redact credentials from inbound message logging
- PR #67969: fix(config): mask inline fallback provider keys in hermes dump

Multiple reports and fixes address credentials and API keys leaking through outbound chat messages, gateway logs, and the debug-share upload feature, alongside work to enable redaction by default and require user consent before uploading logs.

## TL;DR

- 24 member(s): 2 open issue(s), 7 closed issue(s), 2 open PR(s), 4 merged PR(s), 9 PR(s) closed without merging.
- A fix was merged to main in PR #19318. Verify it addresses your case.
- **Recommended:** read PR #19318 (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: `partially_resolved` · deciding PR(s): `pr-19318,pr-21193,pr-53907,pr-56125`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.07`: 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.

## Open related issues

- #46006: \[Bug\]: Security Issue when following the outlined Bug Report Procedure.
- #64351: \[Bug\]: Gateway logs inbound message bodies verbatim; \`hermes debug share\` then uploads them to a public paste (leaked a live OAuth client_secret)

## Closed / duplicate issues

- #17691: Security: HERMES_REDACT_SECRETS off-by-default exposes API keys in user-visible Telegram/Discord chat output
- #19316: \[Bug\] hermes debug share uploads logs with credentials to public paste service
- #19897: Security: HERMES_REDACT_SECRETS off-by-default exposes API keys in user-visible Telegram/Discord chat output
- #20785: Credential leakage in agent output: systemic failure to redact secrets in chat and reasoning blocks
- #22016: \[Bug\]: SECURITY FLAW: \`hermes debug share\` exposes private data
- #23810: Security: outbound chat messages bypass redact_sensitive_text in v0.13.0 (gateway/platforms/*.py)
- #31423: \[Security\] Default secret-pattern redaction on outbound agent messages / dispatch logs

## Prior & competing PRs

- **#19318: fix(debug): redact log content at upload time in hermes debug share**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/debug.py`, `hermes_cli/main.py`, `scripts/release.py`, `tests/hermes_cli/test_debug.py`
  - fix hunks:
  - `hermes_cli/debug.py`

    ```diff
    @@ -1,12 +1,19 @@
    -"""``hermes debug`` — debug tools for Hermes Agent.
    +"""``hermes debug`` debug tools for Hermes Agent.
     
     Currently supports:
         hermes debug share    Upload debug report (system info + logs) to a
                               paste service and print a shareable URL.
    +                          By default, log content is run through
    +                          ``agent.redact.redact_sensitive_text`` with
    +                          ``force=True`` before upload so credentials in
    +                          ``~/.hermes/logs/*.log`` are not leaked into
    +                          the public paste service. Pass ``--no-redact``
    +                          to disable.
     """
     
     import io
     import json
    +import logging
     import sys
     import time
     import urllib.error
    ```

    ```diff
    @@ -19,6 +26,16 @@
     from hermes_constants import get_hermes_home
     from utils import atomic_replace
     
    +logger = logging.getLogger(__name__)
    +
    +# Banner prepended to upload-bound log content when redaction is enabled.
    +# Visible in the public paste so reviewers know the content was sanitized.
    +# Kept short; the trailing newline guarantees the banner sits on its own line.
    +_REDACTION_BANNER = (
    +    "[hermes debug share: log content redacted at upload time. "
    +    "run with --no-redact to disable]\n"
    +)
    +
     
     # ---------------------------------------------------------------------------
     # Paste services — try paste.rs first, dpaste.com as fallback.
    ```

    ```diff
    @@ -368,17 +385,40 @@ def _resolve_log_path(log_name: str) -> Optional[Path]:
         return None
     
     
    +def _redact_log_text(text: str) -> str:
    +    """Run ``redact_sensitive_text`` with ``force=True`` over upload-bound text.
    +
    +    Uses ``force=True`` so redaction fires regardless of the operator's
    +    ``security.redact_secrets`` setting. The local on-disk log file is
    +    not modified; only the in-memory copy headed for the public paste
    +    service is sanitized. Returns the redacted text (or the original
    +    when empty / non-string).
    +    """
    +    if not text:
    +        return text
    +    from agent.redact import redact_sensitive_text
    +
    +    return redact_sensitive_text(text, force=True)
    +
    +
     def _capture_log_snapshot(
         log_name: str,
         *,
         tail_lines: int,
         max_bytes: int = _MAX_LOG_BYTES,
    +    redact: bool = True,
     ) -> LogSnapshot:
         """Capture a log once and derive summary/full-log views from it.
     
         The report tail and standalone log upload must come from the same file
         snapshot. Otherwise a rotation/truncate between reads can make the report
         look newer than the uploaded ``agent.log`` paste.
    +
    +    When ``redact`` is True (the default), both ``tail_text`` and
    +    ``full_text`` are run through ``_redact_log_text`` so the snapshot
    +    returned is upload-safe. The on-disk log file is never modified.
    +    Pass ``redact=False`` to capture original log content (used by
    +    ``hermes debug share --no-redact``).
         """
         log_path = _resolve_log_path(log_name)
         if log_path is None:
    ```

    ```diff
    @@ -438,18 +478,34 @@ def _capture_log_snapshot(
             if truncated:
                 full_text = f"[... truncated — showing last ~{max_bytes // 1024}KB ...]\n{full_text}"
     
    +        if redact:
    +            tail_text = _redact_log_text(tail_text)
    +            full_text = _redact_log_text(full_text)
    +
             return LogSnapshot(path=log_path, tail_text=tail_text, full_text=full_text)
         except Exception as exc:
             return LogSnapshot(path=log_path, tail_text=f"(error reading: {exc})", full_text=None)
     
     
    -def _capture_default_log_snapshots(log_lines: int) -> dict[str, LogSnapshot]:
    -    """Capture all logs used by debug-share exactly once."""
    +def _capture_default_log_snapshots(
    +    log_lines: int, *, redact: bool = True
    +) -> dict[str, LogSnapshot]:
    +    """Capture all logs used by debug-share exactly once.
    +
    +    ``redact`` is forwarded to each ``_capture_log_snapshot`` call so all
    +    captured logs share the same redaction policy for a given run.
    +    """
         errors_lines = min(log_lines, 100)
         return {
    -        "agent": _capture_log_snapshot("agent", tail_lines=log_lines),
    -        "errors": _capture_log_snapshot("errors", tail_lines=errors_lines),
    -        "gateway": _capture_log_snapshot("gateway", tail_lines=errors_lines),
    +        "agent": _capture_log_snapshot(
    +            "agent", tail_lines=log_lines, redact=redact
    +        ),
    +        "errors": _capture_log_snapshot(
    +            "errors", tail_lines=errors_lines, redact=redact
    +        ),
    +        "gateway": _capture_log_snapshot(
    +            "gateway", tail_lines=errors_lines, redact=redact
    +        ),
         }
     
     
    ```

    ```diff
    @@ -532,6 +588,7 @@ def run_debug_share(args):
         log_lines = getattr(args, "lines", 200)
         expiry = getattr(args, "expire", 7)
         local_only = getattr(args, "local", False)
    +    redact = not getattr(args, "no_redact", False)
     
         if not local_only:
             print(_PRIVACY_NOTICE)
    ```

    ```diff
    @@ -539,8 +596,16 @@ def run_debug_share(args):
         print("Collecting debug report...")
     
         # Capture dump once — prepended to every paste for context.
    +    # The dump is already redacted at extract time via dump.py:_redact;
    +    # log_snapshots are redacted by _capture_default_log_snapshots when
    +    # redact=True so credentials never reach the public paste service.
         dump_text = _capture_dump()
    -    log_snapshots = _capture_default_log_snapshots(log_lines)
    +    log_snapshots = _capture_default_log_snapshots(log_lines, redact=redact)
    +
    +    if redact:
    +        logger.info(
    +            "hermes debug share: applied force-mode redaction to log snapshots before upload"
    +        )
     
         report = collect_debug_report(
             log_lines=log_lines,
    ```

    ```diff
    @@ -556,6 +621,15 @@ def run_debug_share(args):
         if gateway_log:
             gateway_log = dump_text + "\n\n--- full gateway.log ---\n" + gateway_log
     
    +    # Visible banner so reviewers reading the public paste know redaction
    +    # was applied at upload time. Banner is omitted under --no-redact.
    +    if redact:
    +        report = _REDACTION_BANNER + report
    +        if agent_log:
    +            agent_log = _REDACTION_BANNER + agent_log
    +        if gateway_log:
    +            gateway_log = _REDACTION_BANNER + gateway_log
    +
         if local_only:
             print(report)
             if agent_log:
    ```

    ```diff
    @@ -666,6 +740,7 @@ def run_debug(args):
             print("  --lines N    Number of log lines to include (default: 200)")
             print("  --expire N   Paste expiry in days (default: 7)")
             print("  --local      Print report locally instead of uploading")
    +        print("  --no-redact  Disable upload-time secret redaction (default: redact)")
             print()
             print("Options (delete):")
             print("  <url> ...    One or more paste URLs to delete")
    ```
  - `hermes_cli/main.py`

    ```diff
    @@ -8891,6 +8891,7 @@ def main():
         hermes debug share --lines 500  Include more log lines
         hermes debug share --expire 30  Keep paste for 30 days
         hermes debug share --local      Print report locally (no upload)
    +    hermes debug share --no-redact  Disable upload-time secret redaction
         hermes debug delete <url>       Delete a previously uploaded paste
     """,
         )
    ```

    ```diff
    @@ -8916,6 +8917,16 @@ def main():
             action="store_true",
             help="Print the report locally instead of uploading",
         )
    +    share_parser.add_argument(
    +        "--no-redact",
    +        action="store_true",
    +        help=(
    +            "Disable upload-time secret redaction (default: redact). Logs "
    +            "are normally run through agent.redact.redact_sensitive_text "
    +            "with force=True before upload so credentials are not leaked "
    +            "into the public paste service."
    +        ),
    +    )
         delete_parser = debug_sub.add_parser(
             "delete",
             help="Delete a paste uploaded by 'hermes debug share'",
    ```
  - `scripts/release.py`

    ```diff
    @@ -678,6 +678,8 @@
         "[redacted:email]": "chengoak",  # PR #17467
         "[redacted:email]": "YuShu",  # co-author only
         "[redacted:email]": "CharlieKerfoot",  # PR #18951
    +    # Debug share upload-time redaction (May 2026)
    +    "[redacted:email]": "GodsBoy",  # PR #19318
     }
     
     
    ```
  - `tests/hermes_cli/test_debug.py`

    ```diff
    @@ -273,6 +273,101 @@ def test_falls_back_when_primary_empty(self, hermes_home):
             assert "rotated agent data" in snap.full_text
     
     
    +# ---------------------------------------------------------------------------
    +# Capture log redaction (force=True applies regardless of HERMES_REDACT_SECRETS)
    +# ---------------------------------------------------------------------------
    +
    +# A vendor-prefixed token used across redaction tests. Long enough to clear
    +# the redactor's `floor` parameter so it actually masks rather than fully blanks.
    +_REDACT_FIXTURE_TOKEN = "[redacted:secret-assignment]"
    +
    +
    +class TestCaptureLogSnapshotRedaction:
    +    """Pin upload-time redaction at the _capture_log_snapshot boundary."""
    +
    +    @pytest.fixture
    +    def hermes_home_with_secret(self, tmp_path, monkeypatch):
    +        """Isolated HERMES_HOME whose agent.log contains a vendor-prefixed token."""
    +        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.
    +        monkeypatch.delenv("HERMES_REDACT_SECRETS", raising=False)
    +
    +        logs_dir = home / "logs"
    +        logs_dir.mkdir()
    +        (logs_dir / "agent.log").write_text(
    +            f"2026-04-12 17:00:00 INFO config: api_key=[redacted:secret-assignment] loaded\n"
    +        )
    +        (logs_dir / "errors.log").write_text("")
    +        (logs_dir / "gateway.log").write_text("")
    +        return home
    +
    +    def test_default_redacts_tail_and_full_text(self, hermes_home_with_secret):
    +        from hermes_cli.debug import _capture_log_snapshot
    +
    +        snap = _capture_log_snapshot("agent", tail_lines=10)
    +
    +        # Both views the upload uses must be sanitized.
    +        assert _REDACT_FIXTURE_TOKEN not in snap.tail_text
    +        assert snap.full_text is not None
    +        assert _REDACT_FIXTURE_TOKEN not in snap.full_text
    +
    +    def test_redact_false_passes_through(self, hermes_home_with_secret):
    +        from hermes_cli.debug import _capture_log_snapshot
    +
    +        snap = _capture_log_snapshot("agent", tail_lines=10, redact=False)
    +
    +        # Original token survives when the caller opts out.
    +        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):
    +        """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.
    +        """
    +        import os
    +
    +        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", "") == ""
    +
    +        snap = _capture_log_snapshot("agent", tail_lines=10)
    +
    +        assert _REDACT_FIXTURE_TOKEN not in snap.tail_text
    +        assert snap.full_text is not None
    +        assert _REDACT_FIXTURE_TOKEN not in snap.full_text
    +
    +    def test_capture_default_log_snapshots_threads_redact(
    +        self, hermes_home_with_secret
    +    ):
    +        from hermes_cli.debug import _capture_default_log_snapshots
    +
    +        snaps = _capture_default_log_snapshots(50)
    +
    +        # Default threads redact=True to all three captured logs.
    +        assert _REDACT_FIXTURE_TOKEN not in snaps["agent"].tail_text
    +        assert _REDACT_FIXTURE_TOKEN not in (snaps["agent"].full_text or "")
    +
    +    def test_capture_default_log_snapshots_no_redact_passes_through(
    +        self, hermes_home_with_secret
    +    ):
    +        from hermes_cli.debug import _capture_default_log_snapshots
    +
    +        snaps = _capture_default_log_snapshots(50, redact=False)
    +
    +        assert _REDACT_FIXTURE_TOKEN in snaps["agent"].tail_text
    +        assert _REDACT_FIXTURE_TOKEN in (snaps["agent"].full_text or "")
    +
    +
     # ---------------------------------------------------------------------------
     # Debug report collection
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -556,6 +651,124 @@ def test_share_exits_on_report_upload_failure(self, hermes_home, capsys):
             assert "all failed" in out.err
     
     
    +# ---------------------------------------------------------------------------
    +# Share-time redaction wiring + visible banner
    +# ---------------------------------------------------------------------------
    +
    +class TestRunDebugShareRedaction:
    +    """End-to-end: --no-redact flag, banner injection, default behavior."""
    +
    +    @pytest.fixture
    +    def hermes_home_with_secret(self, tmp_path, monkeypatch):
    +        """Isolated HERMES_HOME whose agent.log contains a vendor-prefixed token."""
    +        home = tmp_path / ".hermes"
    +        home.mkdir()
    +        monkeypatch.setenv("HERMES_HOME", str(home))
    +        monkeypatch.delenv("HERMES_REDACT_SECRETS", raising=False)
    +
    +        logs_dir = home / "logs"
    +        logs_dir.mkdir()
    +        (logs_dir / "agent.log").write_text(
    +            f"2026-04-12 17:00:00 INFO config: api_key=[redacted:secret-assignment] loaded\n"
    +        )
    +        (logs_dir / "errors.log").write_text("")
    +        (logs_dir / "gateway.log").write_text(
    +            f"2026-04-12 17:00:01 INFO gateway.run: token {_REDACT_FIXTURE_TOKEN}\n"
    +        )
    +        return home
    +
    +    def test_default_share_redacts_uploaded_content(
    +        self, hermes_home_with_secret, capsys
    +    ):
    +        """The uploaded report and full-log pastes do not contain the raw token."""
    +        from hermes_cli.debug import run_debug_share
    +
    +        args = MagicMock()
    +        args.lines = 50
    +        args.expire = 7
    +        args.local = False
    +        args.no_redact = False
    +
    +        captured: list[str] = []
    +
    +        def fake_upload(content, expiry_days=7):
    +            captured.append(content)
    +            return f"https://paste.rs/{len(captured)}"
    +
    +        with patch("hermes_cli.dump.run_dump"), \
    +             patch("hermes_cli.debug._sweep_expired_pastes", return_value=(0, 0)), \
    +             patch("hermes_cli.debug.upload_to_pastebin", side_effect=fake_upload):
    +            run_debug_share(args)
    +
    +        # At least the report plus one full log paste reached the upload path.
    +        assert len(captured) >= 2
    +        for content in captured:
    +            assert _REDACT_FIXTURE_TOKEN not in content, (
    +                "raw token leaked into upload-bound content"
    +            )
    +
    +    def test_default_share_includes_redaction_banner(
    +        self, hermes_home_with_secret, capsys
    +    ):
    +        """Each upload-bound paste carries the visible redaction banner."""
    +        from hermes_cli.debug import run_debug_share
    +
    +        args = MagicMock()
    +        args.lines = 50
    +        args.expire = 7
    +        args.local = False
    +        args.no_redact = False
    +
    +        captured: list[str] = []
    +
    +        def fake_upload(content, expiry_days=7):
    +            captured.append(content)
    +            return f"https://paste.rs/{len(captured)}"
    +
    +        with patch("hermes_cli.dump.run_dump"), \
    +             patch("hermes_cli.debug._sweep_expired_pastes", return_value=(0, 0)), \
    +             patch("hermes_cli.debug.upload_to_pastebin", side_effect=fake_upload):
    +            run_debug_share(args)
    +
    +        for content in captured:
    +            assert "redacted at upload time" in content, (
    +                "redaction banner missing from upload-bound content"
    +            )
    +
    +    def test_no_redact_flag_disables_redaction_and_banner(
    +        self, hermes_home_with_secret, capsys
    +    ):
    +        """--no-redact preserves original log content and omits the banner."""
    +        from hermes_cli.debug import run_debug_share
    +
    +        args = MagicMock()
    +        args.lines = 50
    +        args.expire = 7
    +        args.local = False
    +        args.no_redact = True
    +
    +        captured: list[str] = []
    +
    +        def fake_upload(content, expiry_days=7):
    +            captured.append(content)
    +            return f"https://paste.rs/{len(captured)}"
    +
    +        with patch("hermes_cli.dump.run_dump"), \
    +             patch("hermes_cli.debug._sweep_expired_pastes", return_value=(0, 0)), \
    +             patch("hermes_cli.debug.upload_to_pastebin", side_effect=fake_upload):
    +            run_debug_share(args)
    +
    +        # The agent.log paste should now contain the raw token.
    +        assert any(_REDACT_FIXTURE_TOKEN in c for c in captured), (
    +            "expected raw token in --no-redact upload"
    +        )
    +        # No banner anywhere when redaction is disabled.
    +        for content in captured:
    +            assert "redacted at upload time" not in content, (
    +                "banner present with --no-redact"
    +            )
    +
    +
     # ---------------------------------------------------------------------------
     # run_debug router
     # ---------------------------------------------------------------------------
    ```
- **#21193: feat(security): enable secret redaction by default (#17691, #20785)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - 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: [redacted:secret-assignment] in config.yaml
    +# (bridged to this env var in hermes_cli/main.py, gateway/run.py, and
    +# cli.py) or `HERMES_REDACT_SECRETS=[redacted:secret-assignment] 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=[redacted:secret-assignment] "
    +                    "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": False,
    +        "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: [redacted:secret-assignment] 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). |
    ```
- **#53907: fix(gateway): redact full credential set on outbound chat path (#23810)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/run.py`, `tests/gateway/test_telegram_noise_filter.py`
- **#56125: fix(security): require explicit consent before uploading debug logs**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/cli_commands_mixin.py`, `hermes_cli/debug.py`, `hermes_cli/subcommands/debug.py`, `tests/hermes_cli/test_debug.py`
  - fix hunks:
  - `hermes_cli/cli_commands_mixin.py`

    ```diff
    @@ -2593,7 +2593,12 @@ def _handle_debug_command(self, cmd_original: str = ""):
             words = {w.lower() for w in cmd_original.split()[1:]}
             local = "local" in words
             nous = "nous" in words and not local
    -        args = SimpleNamespace(lines=200, expire=7, local=local, nous=nous)
    +        # Typing the /debug slash command is itself the explicit consent to
    +        # upload, so we pass yes=True to skip run_debug_share's [y/N] prompt.
    +        # input() would hang inside prompt_toolkit's event loop anyway.
    +        args = SimpleNamespace(
    +            lines=200, expire=7, local=local, nous=nous, yes=True
    +        )
             run_debug_share(args)
     
         def _handle_update_command(self) -> bool:
    ```
  - `hermes_cli/debug.py`

    ```diff
    @@ -196,15 +196,19 @@ def _best_effort_sweep_expired_pastes() -> None:
     # ---------------------------------------------------------------------------
     
     _PRIVACY_NOTICE = """\
    -⚠️  This will upload the following to a public paste service:
    -  • System info (OS, Python version, Hermes version, provider, which API keys
    -    are configured — NOT the actual keys)
    -  • Recent log lines (agent.log, errors.log, gateway.log, gui.log, desktop.log
    -    — may contain conversation fragments and file paths)
    -  • Full agent.log, gateway.log, gui.log, and desktop.log (up to 512 KB each —
    -    likely contains conversation content, tool outputs, and file paths)
    -
    -Pastes auto-delete after 6 hours.
    +⚠️  This will upload system info + logs to a PUBLIC paste service.
    +
    +Cryptographic secrets (API keys, tokens, passwords) are redacted before
    +upload, but the following personal data is NOT redacted and will be public:
    +  • Your display name and persistent platform user ID
    +  • Verbatim content of your recent messages (prompts, responses, tool output)
    +  • Local filesystem paths
    +  • Any other PII present in the logs
    +
    +The resulting URL is public to anyone who has the link. Pastes auto-delete
    +after 6 hours, but may be archived by third parties in the meantime.
    +
    +Use --local to view the report without uploading.
     """
     
     _GATEWAY_PRIVACY_NOTICE = (
    ```

    ```diff
    @@ -774,6 +778,38 @@ def build_debug_share(
         )
     
     
    +def _confirm_upload(args) -> bool:
    +    """Require explicit consent before any debug-share upload.
    +
    +    The privacy notice is printed by the caller. This gates the actual
    +    upload: with ``--yes`` (or ``-y``) we proceed unprompted; otherwise we
    +    ask an interactive ``[y/N]`` question. In a non-interactive context
    +    (no TTY on stdin — scripts, CI, piped input) we refuse rather than
    +    hang or upload silently, so debug data can't be exposed without a
    +    deliberate ``--yes``.
    +
    +    Returns True to proceed with the upload, False to abort.
    +    """
    +    if bool(getattr(args, "yes", False)):
    +        return True
    +    if not sys.stdin.isatty():
    +        print(
    +            "ERROR: Non-interactive mode requires --yes to confirm upload.\n"
    +            "       This prevents accidental exposure of personal data.\n"
    +            "       Use --local to view the report without uploading.",
    +            file=sys.stderr,
    +        )
    +        sys.exit(1)
    +    try:
    +        answer = input("Upload debug report? [y/N] ").strip().lower()
    +    except (EOFError, KeyboardInterrupt):
    +        answer = ""
    +    if answer not in ("y", "yes"):
    +        print("Aborted.")
    +        return False
    +    return True
    +
    +
     def run_debug_share(args):
         """Collect debug report + full logs, upload each, print URLs."""
         log_lines = getattr(args, "lines", 200)
    ```

    ```diff
    @@ -805,10 +841,12 @@ def run_debug_share(args):
             return
     
         if nous:
    -        _run_debug_share_nous(log_lines=log_lines, redact=redact)
    +        _run_debug_share_nous(args, log_lines=log_lines, redact=redact)
             return
     
         print(_PRIVACY_NOTICE)
    +    if not _confirm_upload(args):
    +        return
         print("Collecting debug report...")
         print("Uploading...")
     
    ```

    ```diff
    @@ -856,7 +894,7 @@ def run_debug_share(args):
     """
     
     
    -def _run_debug_share_nous(*, log_lines: int, redact: bool) -> None:
    +def _run_debug_share_nous(args, *, log_lines: int, redact: bool) -> None:
         """Handle ``hermes debug share --nous``: upload the bundle to Nous-S3.
     
         Collects the same force-redacted bundle as the paste path, gzips it into
    ```

    ```diff
    @@ -867,6 +905,8 @@ def _run_debug_share_nous(*, log_lines: int, redact: bool) -> None:
         from hermes_cli.diagnostics_upload import share_to_nous
     
         print(_NOUS_PRIVACY_NOTICE)
    +    if not _confirm_upload(args):
    +        return
         if not redact:
             print(
                 "⚠️  --no-redact is set: secrets in your logs will NOT be redacted "
    ```
  - `hermes_cli/subcommands/debug.py`

    ```diff
    @@ -24,7 +24,8 @@ def build_debug_parser(subparsers, *, cmd_debug: Callable) -> None:
             formatter_class=argparse.RawDescriptionHelpFormatter,
             epilog="""\
     Examples:
    -    hermes debug share              Upload debug report and print URL
    +    hermes debug share              Upload debug report (asks for confirmation)
    +    hermes debug share --yes        Skip confirmation (for scripts/CI)
         hermes debug share --lines 500  Include more log lines
         hermes debug share --expire 30  Keep paste for 30 days
         hermes debug share --local      Print report locally (no upload)
    ```

    ```diff
    @@ -55,6 +56,16 @@ def build_debug_parser(subparsers, *, cmd_debug: Callable) -> None:
             action="store_true",
             help="Print the report locally instead of uploading",
         )
    +    share_parser.add_argument(
    +        "-y",
    +        "--yes",
    +        action="store_true",
    +        help=(
    +            "Skip the confirmation prompt and upload immediately. Required "
    +            "in non-interactive contexts (scripts/CI); without it, and with "
    +            "no TTY on stdin, the command refuses rather than upload silently."
    +        ),
    +    )
         share_parser.add_argument(
             "--no-redact",
             action="store_true",
    ```
  - `tests/hermes_cli/test_debug.py`

    ```diff
    @@ -1289,7 +1289,8 @@ def test_share_shows_privacy_notice(self, hermes_home, capsys):
                 run_debug_share(args)
     
             out = capsys.readouterr().out
    -        assert "public paste service" in out
    +        assert "PUBLIC paste service" in out
    +        assert "NOT redacted" in out
     
         def test_local_no_privacy_notice(self, hermes_home, capsys):
             from hermes_cli.debug import run_debug_share
    ```

    ```diff
    @@ -1304,7 +1305,7 @@ def test_local_no_privacy_notice(self, hermes_home, capsys):
                 run_debug_share(args)
     
             out = capsys.readouterr().out
    -        assert "public paste service" not in out
    +        assert "PUBLIC paste service" not in out
     
     
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -1519,6 +1520,7 @@ class _A:
                 local = False
                 nous = True
                 no_redact = False
    +            yes = True
     
             a = _A()
             for k, v in over.items():
    ```

    ```diff
    @@ -1602,6 +1604,9 @@ def test_bare_debug_defaults_to_paste(self):
             c = self._captured("/debug")
             assert c["nous"] is False and c["local"] is False
             assert c["lines"] == 200 and c["expire"] == 7
    +        # The slash command IS the consent action → skip the [y/N] prompt
    +        # (input() would hang inside prompt_toolkit's event loop).
    +        assert c["yes"] is True
     
         def test_nous_word_sets_nous(self):
             c = self._captured("/debug nous")
    ```

    ```diff
    @@ -1629,3 +1634,132 @@ def test_no_arg_default_keyword(self):
             c = self._captured("")
             assert c["nous"] is False and c["local"] is False
     
    +
    +class TestShareConsentGate:
    +    """`hermes debug share` requires explicit consent before uploading.
    +
    +    Uses SimpleNamespace rather than MagicMock so ``args.yes`` is a real
    +    ``False`` — a MagicMock auto-provides a truthy ``.yes`` and would silently
    +    bypass the very gate under test.
    +    """
    +
    +    def _args(self, **over):
    +        from types import SimpleNamespace
    +
    +        base = dict(lines=50, expire=7, local=False, nous=False,
    +                    no_redact=False, yes=False)
    +        base.update(over)
    +        return SimpleNamespace(**base)
    +
    +    def test_aborts_on_user_decline(self, hermes_home, capsys, monkeypatch):
    +        """Interactive user typing anything but y/yes → no upload."""
    +        from hermes_cli.debug import run_debug_share
    +
    +        monkeypatch.setattr("sys.stdin.isatty", lambda: True)
    +        monkeypatch.setattr("builtins.input", lambda _: "n")
    +
    +        with patch("hermes_cli.dump.run_dump"), \
    +             patch("hermes_cli.debug.upload_to_pastebin") as mock_upload:
    +            run_debug_share(self._args())
    +
    +        mock_upload.assert_not_called()
    +        assert "Aborted" in capsys.readouterr().out
    +
    +    def test_proceeds_on_user_accept(self, hermes_home, capsys, monkeypatch):
    +        """Interactive user typing 'y' → upload proceeds."""
    +        from hermes_cli.debug import run_debug_share
    +
    +        monkeypatch.setattr("sys.stdin.isatty", lambda: True)
    +        monkeypatch.setattr("builtins.input", lambda _: "y")
    +
    +        with patch("hermes_cli.dump.run_dump"), \
    +             patch("hermes_cli.debug._sweep_expired_pastes", return_value=(0, 0)), \
    +             patch("hermes_cli.debug.upload_to_pastebin",
    +                   return_value="https://paste.rs/test"), \
    +             patch("hermes_cli.debug._schedule_auto_delete"):
    +            run_debug_share(self._args())
    +
    +        out = capsys.readouterr().out
    +        assert "Debug report uploaded" in out
    +        assert "Aborted" not in out
    +
    +    def test_yes_flag_skips_prompt(self, hermes_home, capsys, monkeypatch):
    +        """--yes uploads without ever calling input()."""
    +        from hermes_cli.debug import run_debug_share
    +
    +        def _boom(_):
    +            raise AssertionError("input() must not be called with --yes")
    +
    +        monkeypatch.setattr("builtins.input", _boom)
    +
    +        with patch("hermes_cli.dump.run_dump"), \
    +             patch("hermes_cli.debug._sweep_expired_pastes", return_value=(0, 0)), \
    +             patch("hermes_cli.debug.upload_to_pastebin",
    +                   return_value="https://paste.rs/test"), \
    +             patch("hermes_cli.debug._schedule_auto_delete"):
    +            run_debug_share(self._args(yes=True))
    +
    +        assert "Debug report uploaded" in capsys.readouterr().out
    +
    +    def test_non_interactive_requires_yes(self, hermes_home, capsys, monkeypatch):
    +        """No TTY + no --yes → exit(1), never upload silently."""
    +        from hermes_cli.debug import run_debug_share
    +
    +        monkeypatch.setattr("sys.stdin.isatty", lambda: False)
    +
    +        with patch("hermes_cli.dump.run_dump"), \
    +             patch("hermes_cli.debug.upload_to_pastebin") as mock_upload:
    +            with pytest.raises(SystemExit) as exc:
    +                run_debug_share(self._args())
    +
    +        assert exc.value.code == 1
    +        mock_upload.assert_not_called()
    +        err = capsys.readouterr().err
    +        assert "Non-interactive mode requires --yes" in err
    +        assert "personal data" in err
    +
    +    def test_non_interactive_with_yes_succeeds(self, hermes_home, capsys, monkeypatch):
    +        """No TTY but --yes present → upload proceeds."""
    +        from hermes_cli.debug import run_debug_share
    +
    +        monkeypatch.setattr("sys.stdin.isatty", lambda: False)
    +
    +        with patch("hermes_cli.dump.run_dump"), \
    +             patch("hermes_cli.debug._sweep_expired_pastes", return_value=(0, 0)), \
    +             patch("hermes_cli.debug.upload_to_pastebin",
    +                   return_value="https://paste.rs/test"), \
    +             patch("hermes_cli.debug._schedule_auto_delete"):
    +            run_debug_share(self._args(yes=True))
    +
    +        assert "https://paste.rs/test" in capsys.readouterr().out
    +
    +    def test_nous_path_also_gated(self, hermes_home, capsys, monkeypatch):
    +        """The --nous S3 path enforces the same consent gate (sibling site)."""
    +        from hermes_cli.debug import run_debug_share
    +
    +        monkeypatch.setattr("sys.stdin.isatty", lambda: True)
    +        monkeypatch.setattr("builtins.input", lambda _: "n")
    +
    +        with patch("hermes_cli.dump.run_dump"), \
    +             patch("hermes_cli.diagnostics_upload.share_to_nous") as mock_nous:
    +            run_debug_share(self._args(nous=True))
    +
    +        mock_nous.assert_not_called()
    +        assert "Aborted" in capsys.readouterr().out
    +
    +    def test_local_never_prompts(self, hermes_home, capsys, monkeypatch):
    +        """--local renders to stdout and must not prompt or upload."""
    +        from hermes_cli.debug import run_debug_share
    +
    +        def _boom(_):
    +            raise AssertionError("input() must not be called for --local")
    +
    +        monkeypatch.setattr("builtins.input", _boom)
    +
    +        with patch("hermes_cli.dump.run_dump"), \
    +             patch("hermes_cli.debug.upload_to_pastebin") as mock_upload:
    +            run_debug_share(self._args(local=True))
    +
    +        mock_upload.assert_not_called()
    +        assert "Aborted" not in capsys.readouterr().out
    +
    ```
- **#19349: fix(debug): redact log content at upload time in hermes debug share**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#19318`
- **#21044: fix(agent): redact secrets from assistant output**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#19798`, `#21193`, `#20785`
- **#22139: fix(cli): require user confirmation before uploading debug share data**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#47952`, `#47952`, `#22016`
- **#22210: fix(security): require explicit consent before uploading debug logs**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#56125`
- **#23821: Fix/gateway redact outbound chat**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `1207d81ee`, `#53907`, `#23810`, `#53907`
- **#23822: fix(gateway): redact outbound replies before delivery**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `1207d81ee`, `#53907`, `#23810`, `#53907`
- **#24303: fix(security): Apply secret redaction to all outbound platform messages**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `1207d81ee`, `#53907`, `#23810`, `#53907`
- **#29521: feat(gateway): Layer-2 credential redaction on outbound platform messages**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `1207d81ee`, `#53907`, `#23810`, `#53907`
- **#64376: fix(gateway): redact credentials from inbound message logging**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `gateway/run.py`, `tests/gateway/test_inbound_message_redaction.py`
- **#67969: fix(config): mask inline fallback provider keys in hermes dump**. open
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `78ba04f21`
  - files: `agent/redact.py`, `hermes_cli/dump.py`, `tests/agent/test_redact_query_param_policy.py`, `tests/hermes_cli/test_dump_fallback_key_redaction.py`
- **#69561: fix(debug): redact PII from public debug shares**. closed without merge
  - maintainer direction: category `partial_fix`; implication `needs_llm`; refs `7c82d9710ccb`, `#22139`, `#22210`, `#47952`, `#56125`, `#69561`, `#69561`, `#22139`, `#56125`, `#22210`, `#22139`, `#56125`, `#47952`, `#47952`, `#56125`, `#56125`, `#22016`, `#46006`, `#69561`, `#22016`, `#46006`, `#22139`, `#22210`, `#56125`, `#47952`, `#69561`, `#69561`, `#22139`, `#22210`, `#56125`, `#56125`, `#47952`, `#1`, `#1`, `#15803`, `#14532`, `#6`, `#1`, `#9`, `#374151`, `#4338`, `#22016`, `#46006`, `#69561`

## Fix status

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