Skip to content

redact encrypted_content in traffic captures - #153

Merged
raine merged 3 commits into
raine:mainfrom
rikbrown:rik/redact-blob
Sep 14, 2026
Merged

raine merged 3 commits into
raine:mainfrom
rikbrown:rik/redact-blob

Conversation

@rikbrown

Copy link
Copy Markdown
Contributor

Problem

Codex server-side compaction returns an opaque encrypted_content blob, which the proxy replays as a compaction input item on every later turn. With CCP_TRAFFIC_LOG=1 that blob is written to disk verbatim.

To be clear about severity: the blob is ciphertext we cannot read locally, so this is not a plaintext leak. But it is a large, replayable handle to a whole conversation's context, and because it is re-sent every turn it accumulates across a session's captures.

image_url and Anthropic source.data already have explicit arms in redact_traffic for the same reason — bulk conversation payload does not belong in a debug capture. This applies the existing precedent to encrypted_content.

Fix

A new arm in redact_traffic_with_depth, next to the source.data case, routing the value through the existing redact_traffic_value so it becomes [redacted len=N]. Blob size stays visible, since that is genuinely useful diagnostic information.

Not added to REDACT_KEYS, for two reasons:

  1. REDACT_KEYS is shared with the structured logger via logging::redact_value, which is only ever called on the fields map in Logger::emit. No body, input item, or event value reaches the logger, so adding the key there would be a no-op for logging.
  2. REDACT_KEYS is a credential list. This is bulk payload, which belongs with the image_url / source.data arms.

Reasoning items carry the same key and are covered by the same arm.

Scope — what this does not cover

This covers 020-upstream-request (write_json) and the 040/050 per-event JSON captures (write_json_event).

It does not cover the raw 032-upstream-response-body.sse capture on the Codex HTTP and WebSocket paths (client.rs, websocket.rs), which is written with write_bytes and bypasses redaction entirely. The blob still persists there. Fixing that means either re-serialising parsed events or a text-level scrub of the SSE, which felt like a separate change — happy to follow up if you'd prefer it in one go.

Providers that assemble upstream_sse from StreamTrafficCapture::upstream_event (grok, opencode) are already redacted, since those frames go through write_json_event. Codex is the one path that dumps raw bytes.

Verification

  • redact_traffic_strips_compaction_encrypted_content — a compaction item in a request body; asserts the blob is replaced by [redacted len=N], the gAAAAA prefix is gone, and type, message text and model survive
  • redact_traffic_strips_reasoning_encrypted_content_in_events — same for a reasoning item inside response.output_item.done
  • cargo clippy --all-targets -- -D warnings clean, cargo fmt --check clean, full suite green

🤖 Generated with Claude Code

rikbrown and others added 3 commits September 13, 2026 16:03
Codex server-side compaction returns an opaque encrypted_content blob
that the proxy replays as a compaction input item on every later turn.
With CCP_TRAFFIC_LOG=1 that blob was written to disk verbatim in the
captured upstream request body and in the per-event response captures.
The blob is ciphertext, so this is not a plaintext leak, but it is a
large, replayable handle to a whole conversation's context and it bloats
captures considerably. image_url and Anthropic source.data are already
redacted for the same reason: bulk conversation payload does not belong
in a debug capture.

Add encrypted_content to the bulk-payload arms of redact_traffic rather
than to REDACT_KEYS. REDACT_KEYS is shared with the structured logger,
which only ever receives request metadata, never bodies, so the key
cannot reach it; keeping the change in the traffic path avoids widening
a credential list with something that is not a credential. The value is
replaced with the existing "[redacted len=N]" form so blob size remains
visible for diagnostics. Reasoning items carry the same key and are
covered by the same arm.

This covers 020-upstream-request and the 040/050 JSON event captures.
The raw 032-upstream-response-body.sse capture on the Codex HTTP and
WebSocket paths is written with write_bytes and bypasses redaction
entirely, so the blob still persists there; that path is left as is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The encrypted_content redaction arm only recognizes the key
`encrypted_content`. The same opaque conversation handle also reaches
traffic captures inside the Anthropic thinking signature the Codex
translator emits, `ccp:codex:v1:<encoded-id>:<encrypted_content>`:
streaming writes it under `delta.signature` in 050 downstream events,
buffered translation writes it under `content[].signature`, and the
next turn replays the same value through the 010 incoming Anthropic
request capture. A capture could therefore still contain the full
replay blob while the key-based redaction passed its tests.

Add a `signature` arm that redacts the whole value only when it is
proxy-owned. Ownership is decided by `is_proxy_reasoning_signature`,
added beside the encoder so the `ccp:codex:v1:` format has a single
source of truth instead of a copied prefix. Foreign signatures, such
as real Anthropic signatures, are left untouched. The value is
replaced with the existing `[redacted len=N]` form so blob size stays
visible, matching the neighboring bulk payload arms.

Tests cover a 050 signature delta, buffered and incoming thinking
blocks, unrelated signature preservation, and the actual capture
boundary through write_json_event and write_json. Raw SSE and native
raw response bytes still bypass redaction; that pre-existing gap is
unchanged and remains separately tracked.
Bring the prepared raine#153 branch onto local main
0e9a6aa, which carries the merged
grok streaming corrections and the cold smoke timeout fix. The original
PR raine#153 head d4452cb and the traffic
correction 48adf80 remain ancestors,
so the contributor branch still fast-forwards without a rebase or force
push.

The merge is clean and drops nothing from either side. Main did not
touch the files this branch changed, so the JSON signature redaction
correction is unchanged and the combined tree adds everything main
gained after the branch base bcfa0a4.
@raine
raine merged commit b642a6e into raine:main Sep 14, 2026
2 checks passed
@raine

raine commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Thanks for the redaction fix! Merged, including the reasoning-signature capture case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants