{"record":{"id":"e2897970ca8f5e67","repo":"NousResearch/hermes-agent","slug":"trace-upload-blocked-secret-redaction-failed-so","errorCode":null,"errorMessage":"Trace upload blocked: secret redaction failed, so the transcript may still contain credentials or other sensitive data. Fix the redactor or rerun with --no-redact only after manually reviewing the transcript.","messagePattern":"Trace upload blocked: secret redaction failed, so the transcript may still contain credentials or other sensitive data\\. Fix the redactor or rerun with --no-redact only after manually reviewing the transcript\\.","errorType":"exception","errorClass":"TraceRedactionError","httpStatus":null,"severity":"error","filePath":"agent/trace_upload.py","lineNumber":72,"sourceCode":"def _now_iso() -> str:\n    return datetime.now(timezone.utc).strftime(\"%Y-%m-%dT%H:%M:%S.%f\")[:-3] + \"Z\"\n\n\ndef _redact(text: Any, enabled: bool) -> Any:\n    \"\"\"Redact secrets from a string body when redaction is enabled.\n\n    Non-strings pass through untouched. Uses Hermes' shared redactor with\n    ``force=True`` so an upload always scrubs known secret shapes even if\n    the user disabled log redaction globally.\n    \"\"\"\n    if not enabled or not isinstance(text, str) or not text:\n        return text\n    try:\n        from agent.redact import redact_sensitive_text\n        return redact_sensitive_text(text, force=True)\n    except Exception as exc:\n        logger.warning(\"Trace upload redaction failed; refusing upload\", exc_info=True)\n        raise TraceRedactionError(_REDACTION_BLOCKED_MESSAGE) from exc\n\n\ndef _content_to_blocks(content: Any, redact: bool) -> List[Dict[str, Any]]:\n    \"\"\"Normalize a message ``content`` field into Anthropic content blocks.\"\"\"\n    if content is None:\n        return []\n    if isinstance(content, str):\n        return [{\"type\": \"text\", \"text\": _redact(content, redact)}]\n    if isinstance(content, list):\n        blocks: List[Dict[str, Any]] = []\n        for part in content:\n            if isinstance(part, dict):\n                ptype = part.get(\"type\")\n                if ptype == \"text\":\n                    blocks.append({\"type\": \"text\", \"text\": _redact(part.get(\"text\", \"\"), redact)})\n                elif ptype in (\"image_url\", \"image\"):\n                    # Keep a placeholder; the viewer renders text turns and we\n                    # don't want to inline base64 blobs into a trace.","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/trace_upload.py#L54-L90","documentation":"Raised as TraceRedactionError from agent/trace_upload.py:72 when the shared redactor (agent.redact.redact_sensitive_text with force=True) throws while scrubbing trace text before upload. The upload is deliberately refused (fail-closed) because the transcript may still contain credentials. The underlying exception is logged with exc_info before the re-raise.","triggerScenarios":"Calling the trace-upload path (build_trace_jsonl / upload helpers that call _redact(text, redact=True)) when redact_sensitive_text raises for any reason: a broken regex/pattern in agent/redact.py, an unexpected input type inside the redactor, or a partially broken install where agent.redact imports but fails at runtime.","commonSituations":"A change to the redaction patterns introduces an exception on certain payloads; a new secret shape (e.g. unusual token format) hits an untested branch; environment differences (locale, missing dependency used by the redactor) make the redactor crash only in CI or on another machine.","solutions":["Read the WARNING log line 'Trace upload redaction failed; refusing upload' — the chained exception (exc_info=True) names the exact redactor failure; fix that in agent/redact.py.","Reproduce standalone: from agent.redact import redact_sensitive_text; redact_sensitive_text(<failing text>, force=True) and fix whatever raises.","Add a regression test for the failing input shape so the redactor stays exception-free.","Only after manually reviewing the transcript for secrets, rerun with --no-redact as the message instructs — this is the documented escape hatch, not the default."],"exampleFix":"// before (redactor crashes on some input -> upload refused)\nreturn redact_sensitive_text(text, force=True)\n\n// after (redactor itself must be fixed; keep fail-closed upload behavior)\n// e.g. in agent/redact.py, guard the pattern application:\nfor pattern, repl in _SECRET_PATTERNS:\n    try:\n        text = pattern.sub(repl, text)\n    except re.error:\n        logger.exception(\"bad redaction pattern %s\", pattern)\n        raise","handlingStrategy":"try-catch","validationCode":"from agent.redact import redact_sensitive_text\n\ndef redaction_is_healthy(sample: str) -> bool:\n    \"\"\"Run the redactor over representative text before starting a run.\"\"\"\n    try:\n        redact_sensitive_text(sample, force=True)\n        return True\n    except Exception:\n        return False","typeGuard":null,"tryCatchPattern":"from agent.trace_upload import TraceRedactionError\n\ntry:\n    upload_trace(messages, redact=True)\nexcept TraceRedactionError:\n    # upload was refused; never fall back to uploading unredacted text here.\n    log.error(\"trace upload skipped: redaction failed\")\n    # surface to the user; only a human may decide on --no-redact after review","preventionTips":["Keep the redactor unit-tested against the secret shapes your traces actually contain.","Never catch TraceRedactionError and retry with redact=False automatically — the block is a security control.","Log the chained exception (exc_info) so redactor bugs are diagnosable from the warning line."],"tags":["security","redaction","trace-upload","fail-closed"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}