# NousResearch/hermes-agent · Redact API credentials from request debug dump files

## Open now

- Nothing open on GitHub.

Multiple reports and fixes address plaintext API keys and Authorization headers being written to request_dump_*.json and session_*.json files, with proposed solutions involving applying existing redaction logic to this code path.

## TL;DR

- 12 member(s): 0 open issue(s), 5 closed issue(s), 0 open PR(s), 1 merged PR(s), 6 PR(s) closed without merging.
- A fix was merged to main in PR #46637. Verify it addresses your case.
- **Recommended:** read PR #46637 (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_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.20`: 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

- #8518: \[Security\] API key exposed in request debug dumps via incomplete masking
- #18707: \[Feature\]: request_dump_*.json files in ~/.hermes/sessions/ contain plaintext Authorization headers — agent/redact.py exists but isn't applied on this path
- #19202: \[Feature\]: Failed-request dumps write complete Authorization headers (containing API keys) to ~/.hermes/sessions/request_dump_*.json in plaintext — agent/redact.py exists but isn't invoked on this path
- #19845: Session capture leaks API credentials in plaintext to session_*.json
- #46583: Hardening: redact secrets in API request debug dumps (unredacted request body, written unconditionally on API errors)

## Prior & competing PRs

- **#46637: fix(agent): redact secrets in request debug dumps**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/agent_runtime_helpers.py`, `agent/redact.py`, `scripts/release.py`, `tests/agent/test_redact.py`, `tests/run_agent/test_run_agent_codex_responses.py`
  - fix hunks:
  - `agent/agent_runtime_helpers.py`

    ```diff
    @@ -1217,12 +1217,23 @@ def dump_api_request_debug(
     
             timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
             dump_file = agent.logs_dir / f"request_dump_{agent.session_id}_{timestamp}.json"
    -        atomic_json_write(dump_file, dump_payload, default=str)
    +
    +        # Redact secrets before persisting/printing. This dump captures the
    +        # full request body (system prompt, tool defs, context-embedded
    +        # values), and this path fires unconditionally on API errors — so it
    +        # otherwise lands any context-embedded secret in cleartext on disk.
    +        # Run the serialized dump through the same scrubber used for logs/tool
    +        # output, then hand the resulting payload back to the shared atomic
    +        # JSON writer so request dumps keep the same write semantics as before.
    +        from agent.redact import redact_sensitive_text
    +        _serialized = json.dumps(dump_payload, ensure_ascii=False, indent=2, default=str)
    +        _redacted_payload = json.loads(redact_sensitive_text(_serialized, force=True))
    +        atomic_json_write(dump_file, _redacted_payload, default=str)
     
             agent._vprint(f"{agent.log_prefix}🧾 Request debug dump written to: {dump_file}")
     
             if env_var_enabled("HERMES_DUMP_REQUEST_STDOUT"):
    -            print(json.dumps(dump_payload, ensure_ascii=False, indent=2, default=str))
    +            print(json.dumps(_redacted_payload, ensure_ascii=False, indent=2, default=str))
     
             return dump_file
         except Exception as dump_error:
    ```
  - `agent/redact.py`

    ```diff
    @@ -104,6 +104,7 @@
         r"mem0_[A-Za-z0-9]{10,}",           # Mem0 Platform API key
         r"brv_[A-Za-z0-9]{10,}",            # ByteRover API key
         r"xai-[A-Za-z0-9]{30,}",            # xAI (Grok) API key
    +    r"ntn_[A-Za-z0-9]{10,}",            # Notion internal integration token
     ]
     
     # ENV assignment patterns: KEY=value where KEY contains a secret-like name
    ```
  - `scripts/release.py`

    ```diff
    @@ -46,6 +46,7 @@
     # Auto-extracted from noreply emails + manual overrides
     AUTHOR_MAP = {
         "[redacted:email]": "Kenmege",
    +    "[redacted:email]": "xtymac",
         "[redacted:email]": "Diyoncrz18",
         "[redacted:email]": "ali-nld",
         "[redacted:email]": "necoweb3",
    ```
  - `tests/agent/test_redact.py`

    ```diff
    @@ -53,6 +53,10 @@ def test_fal_key(self):
             result = redact_sensitive_text("fal_abc123def456ghi789jkl")
             assert "abc123def456" not in result
     
    +    def test_notion_internal_integration_token(self):
    +        result = redact_sensitive_text("ntn_abc123def456ghi789jkl")
    +        assert "abc123def456" not in result
    +
         def test_short_token_fully_masked(self):
             result = redact_sensitive_text("key=sk-short1234567")
             assert "***" in result
    ```
  - `tests/run_agent/test_run_agent_codex_responses.py`

    ```diff
    @@ -1913,6 +1913,58 @@ def test_dump_api_request_debug_uses_chat_completions_url(monkeypatch, tmp_path)
         assert payload["request"]["url"] == "http://127.0.0.1:9208/v1/chat/completions"
     
     
    +def test_dump_api_request_debug_redacts_request_and_error_secrets(monkeypatch, tmp_path, capsys):
    +    """Request debug dumps should redact secrets before disk/stdout output."""
    +    import json
    +
    +    _patch_agent_bootstrap(monkeypatch)
    +    monkeypatch.setenv("HERMES_DUMP_REQUEST_STDOUT", "1")
    +    agent = run_agent.AIAgent(
    +        model="gpt-4o",
    +        base_url="http://127.0.0.1:9208/v1",
    +        api_key="[redacted:secret-assignment]",
    +        quiet_mode=True,
    +        max_iterations=1,
    +        skip_context_files=True,
    +        skip_memory=True,
    +    )
    +    agent.logs_dir = tmp_path
    +
    +    notion_token = "[redacted:secret-assignment]"
    +    error_secret = "[redacted:secret-assignment]"
    +    response_secret = "[redacted:secret-assignment]"
    +    response = SimpleNamespace(status_code=400, text=f"provider echoed {response_secret}")
    +
    +    class ProviderError(RuntimeError):
    +        body: object
    +        response: object
    +
    +    error = ProviderError(f"bad token {error_secret}")
    +    error.body = {"message": f"bad token {error_secret}"}
    +    error.response = response
    +
    +    dump_file = agent._dump_api_request_debug(
    +        {
    +            "model": "gpt-4o",
    +            "messages": [{"role": "user", "content": f"use {notion_token}"}],
    +            "metadata": {"NOTION_API_KEY": notion_token},
    +        },
    +        reason="provider_error",
    +        error=error,
    +    )
    +
    +    assert dump_file is not None
    +    dumped_text = dump_file.read_text()
    +    stdout_text = capsys.readouterr().out
    +    for raw in (notion_token, error_secret, response_secret, "providersecret1234567890"):
    +        assert raw not in dumped_text
    +        assert raw not in stdout_text
    +
    +    payload = json.loads(dumped_text)
    +    assert payload["request"]["headers"]["Authorization"].startswith("Bearer sk-ant-p...")
    +    assert "***" in dumped_text or "..." in dumped_text
    +
    +
     # --- Reasoning-only response tests (fix for empty content retry loop) ---
     
     
    ```
- **#8533: fix: strip API keys from request debug dumps (#8518)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#8768`
- **#8768: fix: omit authorization from request debug dumps**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#8518`, `#46637`
- **#18750: fix(agent): redact secrets in request_dump_*.json files**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#8518`, `#46637`
- **#18909: fix(security): strip credentials from request debug dumps**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#8533`, `#8768`, `#8518`, `#8533`, `#8768`
- **#33322: \[codex\] Redact request debug dumps**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#18707`, `#19202`, `#8518`, `#46637`, `#46637`, `#46583`, `#18707`, `#19202`
- **#46595: fix(agent): redact secrets in API request debug dumps**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#18750`

## Fix status

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