Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes

Redaction policy

Harn writes operational data to several places that an outside party might eventually see — .harn-runs/* transcripts, Receipt envelopes, the JSONL event log, the portal API, connector status snapshots, crystallized workflow bundles. Each of those surfaces was previously responsible for its own ad-hoc scrubbing of HTTP headers, URL query parameters, and tokens, which meant the same secret could leak through one path while being scrubbed on another.

The harn_vm::redact module collapses that into a single RedactionPolicy that every persistence path consults, so a string treated as sensitive in one place is treated as sensitive everywhere.

Default categories#

The default policy redacts:

  • Auth headersAuthorization, Cookie, Set-Cookie, Proxy-Authorization, X-API-Key, X-Auth-Token, X-CSRF-Token, X-XSRF-Token, plus any header whose name (case- insensitively) contains authorization, cookie, secret, token, or key. Common debugging headers (User-Agent, Content-Type, X-Request-Id, GitHub/Slack/Linear/Notion signature headers) are retained on a built-in safe-list.
  • URLs with credentials in userinfohttps://user:pw@host/... loses both user and pw. The username and password fields are cleared rather than replaced so downstream URL parsers continue to function.
  • Sensitive URL query parametersapi_key, apikey, access_token, refresh_token, id_token, client_secret, password, secret, token, auth, bearer, sig, signature, and anything ending in _token, _secret, or _password.
  • JSON object fields — recursive walk of any persisted JSON value swaps the value for [redacted] whenever the key matches the default-sensitive set above (or the additions a host installs via with_extra_field).
  • Free-form string contents — strings are scanned for high- confidence secret patterns: AWS access key ids, GitHub OAuth/PAT/ fine-grained tokens, GitLab glpat-, npm npm_, OpenAI/Anthropic sk-, Slack xox*-, Stripe sk_live_/sk_test_/rk_…, RFC 6750 Bearer … tokens, and PEM -----BEGIN … PRIVATE KEY----- blocks. These regexes are the same set used by the secret_scan builtin — there is one definition, not two.
  • URL-shaped strings — when a JSON string value is itself a single http(s) URL, the policy applies userinfo and query-param redaction to it before the secret-pattern scanner runs.

The redaction marker is the literal "[redacted]". Receipts, log events, portal DTOs, transcript JSONL, and connector envelopes all use the same placeholder, which makes downstream parsers and humans grepping logs trivial to write.

Where the policy is applied#

SurfaceEntry pointNotes
LLM transcript JSONLcrate::llm::agent_observeEvery provider_call_request / provider_call_response / message event is scrubbed before it is appended to llm_transcript.jsonl and the agent.transcript.llm topic.
Receipt envelopesReceipt::redact_in_placeHosts wrap their ReceiptSink with RedactingReceiptSink to redact every persisted receipt without touching individual receipt-builder call sites. Schema-required identifiers (id, trace_id, persona, status, digests) remain stable for query and replay.
LogEvent payloadsLogEvent::redact_in_placeHeaders are scrubbed in-place; the JSON payload is walked recursively. Backends are intentionally unaware of redaction so emitters that need it call this before appending.
Observability eventsstd/observability processors: [obs().Processor.redaction]The processor walks the enriched event once before routing, so every configured backend receives the same scrubbed payload.
Portal transcript projectionportal::transcriptDefense in depth: even transcripts written before the unified policy landed are redacted at portal read time.
Workflow artifactsArtifactRecord::redact_in_placetext is run through the secret-pattern scanner; data and metadata go through the JSON walk.
Session bundlesharn_vm::session_bundleSanitized exports walk the whole bundle and emit a redaction manifest; replay-only exports additionally withhold prompt and tool payload fields. Import and validation reject high-confidence secret markers unless explicitly allowed.
Connector inbound headersHeaderRedactionPolicy (alias for RedactionPolicy)Each connector strips inbound HTTP headers before they reach the durable inbox.
HTTP egress diagnosticscrate::egressURLs reported in EgressBlocked errors and http_mock calls are redacted via the same policy.

Host extension points#

The default policy is not the only policy. Hosts (IDE hosts, the Harn portal, third-party orchestrators) may want to:

  • Mark an additional header as safe. A bespoke webhook can opt back into raw delivery by adding it to the safe-list:

    use harn_vm::redact::{push_policy, RedactionPolicy};
    push_policy(RedactionPolicy::default().with_safe_header("X-My-Trace-Id"));
    
  • Mark an additional header as sensitive. Host-explicit deny substrings override the built-in safe-list — that is how a host says "treat my own delivery header as sensitive even though Harn would normally keep it for debugging":

    push_policy(RedactionPolicy::default().with_deny_header_substring("delivery"));
    
  • Add an internal field name to the redaction set. with_extra_field("internal_audit_token") ensures that any JSON walk through receipts, event payloads, or artifacts replaces the field's value with [redacted] regardless of how the value was serialized.

  • Add an internal URL parameter. with_extra_url_param("session") scrubs ?session=… everywhere redact_url runs.

  • Disable the heuristic free-form scanner. Performance-sensitive paths that have already been audited can opt out with RedactionPolicy::default().disable_string_scan().

The active policy is a thread-local stack mirroring the existing approval / capability policy stacks in crate::orchestration::policy. Push a host policy at orchestrator startup with push_policy(...), or scope it to a single execution with the RAII PolicyGuard. The stack is cleared by harn_vm::reset_thread_local_state(), so test runs that share a thread cannot leak overrides into each other.

What we do not redact#

  • Schema-required identifier fields on receiptsid, trace_id, persona, status, started_at, cost_usd, and digests are part of the receipt envelope contract. Replay and reconciliation expect them to round-trip stable.
  • Connector metric counters — the ConnectorMetricsSnapshot is numeric and carries no caller-supplied data.
  • Public artifact metadata fields with non-sensitive nameskind, title, created_at, lineage carry routing/replay information rather than secrets.

If a host's product domain has additional fields it considers sensitive, the right answer is with_extra_field / with_extra_url_param, not editing the default deny list. The default list is the floor, not the ceiling.

Verification#

End-to-end fixture coverage lives in crates/harn-vm/tests/harn_vm/redaction_fixtures.rs. Each test stages a representative secret (Stripe, GitHub PAT, AWS access key, Bearer token, URL with userinfo) through one persistence surface and asserts the secret never appears in the rendered JSON. Adding a new persistence surface that should respect the policy means adding a fixture there, not a new redaction helper.