aftertrace

Redaction

Private data has to pass two gates before anything is written. The first is in Claude Code, which decides what it sends. The second is in Aftertrace, which deletes identity always and content unless you ask to keep it.

Press P for the prompt valve, T for the tool details valve, C for the content shutter, or click them. The readout follows one key at a time.

Gate 1: what Claude Code sends

Claude Code keeps prompt text, responses and tool content to itself unless you switch them on. The switches are environment variables, all off by default:

Switch What it adds Aftertrace's advice
OTEL_LOG_TOOL_DETAILS=1 Bash commands (full_command), file paths, skill, agent, plugin and MCP names, tool_parameters Turn on. Several findings need real names.
OTEL_LOG_USER_PROMPTS=1 The prompt text you typed (user_prompt, prompt, prompt_text) Leave off.
OTEL_LOG_ASSISTANT_RESPONSES=1 Response text in events Leave off.
OTEL_LOG_TOOL_CONTENT=1 tool.output span events: file text, command output, fetch results Leave off.
OTEL_LOG_RAW_API_BODIES=1 Every request and response body, the whole conversation Never for Aftertrace.

With prompts off, Claude Code still sends the user_prompt key, with the value <REDACTED>, and a user_prompt_length that is just a number. The figure shows that as a dim indigo token. Details and caveats per switch: Connect Claude Code.

Gate 2: what Aftertrace deletes

Every OTLP batch is decoded, then converted to rows, then written. Redaction runs in the middle step, in internal/otlp/convert.go, on each attribute map before it becomes a row:

That is seven calls to redact.Attrs in all. Each call deletes keys by exact name, plus any key that starts with a content prefix:

Every other key is kept. That includes session.id, model names, token counts, costs, durations, and the tool details from gate 1. Tool details are not content keys, so when Claude Code sends them, Aftertrace stores them.

A log record's body is never stored. Aftertrace only reads it to name the event when the record has no event.name attribute.

The same gate runs for aftertrace import. aftertrace demo loads its synthetic sessions with content kept, because nothing in them is real.

Before and after

A claude_code.llm_request span as Claude Code sends it, and as it is stored. Values are made up.

sent                                        stored
session.id         "4f1c9e…"                session.id         "4f1c9e…"
user.email         "you@example.com"        ·
user.account_uuid  "8a2e51…"                ·
organization.id    "c71b02…"                ·
terminal.type      "vscode"                 terminal.type      "vscode"
model              "claude-opus-5-5"        model              "claude-opus-5-5"
input_tokens       1840                     input_tokens       1840
output_tokens      412                      output_tokens      412
cache_read_tokens  52010                    cache_read_tokens  52010
ttft_ms            1430                     ttft_ms            1430
request_id         "req_01…"                request_id         "req_01…"

The api_request event for the same call. Only identity goes.

sent                                        stored
event.name         "api_request"            event.name         "api_request"
session.id         "4f1c9e…"                session.id         "4f1c9e…"
user.email         "you@example.com"        ·
organization.id    "c71b02…"                ·
model              "claude-opus-5-5"        model              "claude-opus-5-5"
cost_usd           0.0412                   cost_usd           0.0412
duration_ms        6120                     duration_ms        6120
request_id         "req_01…"                request_id         "req_01…"

The claude_code.interaction span that opened the turn, with OTEL_LOG_USER_PROMPTS=1 and without --store-content. Claude Code sent the text, and Aftertrace deleted it.

sent                                        stored
user_prompt        "fix the flaky test…"    ·
user_prompt_length 31                       user_prompt_length 31

The four settings

Claude Code switch --store-content Prompt text in the store Identity in the store
off (default) off (default) no: it was never sent no
off on no: it was never sent; user_prompt is kept as <REDACTED> no
OTEL_LOG_USER_PROMPTS=1 off no: Aftertrace deletes it no
OTEL_LOG_USER_PROMPTS=1 on yes no

The same holds for every other key in the content list, such as tool.output with OTEL_LOG_TOOL_CONTENT=1. OTEL_LOG_TOOL_DETAILS=1 is different: commands and paths are stored whatever --store-content says, because they are not content keys. A command line can hold a secret, so leave that switch off if yours do.

Checking it

The serve log prints the gate when it starts:

redact=on store_content=false  (no telemetry)

To confirm nothing private got in, ask the store. The raw SQL box on the Query page runs this:

SELECT
  count(*) FILTER (WHERE json_extract_string(attrs, '$."user.email"') IS NOT NULL) AS emails,
  count(*) FILTER (WHERE json_extract_string(attrs, '$.user_prompt') <> '<REDACTED>') AS prompt_texts
FROM spans

emails is always 0. prompt_texts is 0 unless you switched prompts on in Claude Code and also ran with --store-content.

Next

Edit this page on GitHub