Claude Code investigates a failed production deployment. It queries an MCP log tool and receives a stack trace containing a bearer token. The agent spots the token and removes it from the answer before handing the work to a reviewer.
The final answer is clean. The token is not.
It may now exist in the model request, the MCP gateway log, the tracing platform, a prompt cache, a tool error, and the review packet draft. Redacting the last copy does nothing to the copies already made.
This is an awkward failure because the agent can appear to behave responsibly. It notices the secret. It warns the operator. It prints [REDACTED]. Meanwhile, the observability system has preserved the original tool response for thirty days.
Updated 4 October 2026 with an executable trace-access test and observed role-by-surface results. The YAML records below illustrate a control design; they are not built-in Claude Code settings or evidence from a production run.
Stop sensitive values before the model sees them
The strongest redaction point is the first trusted boundary after retrieval. For an MCP call, that is usually the gateway or tool adapter. Inspect the structured response there, before it enters the model context.
response_policy:
tool: deployment_logs.read
allowed_fields:
- timestamp
- service
- error_code
- sanitized_message
blocked_classes:
- credential
- session_token
- customer_personal_data
replacement: safe_reference
raw_retention: restricted_incident_store
Do not ask the model to decide whether a string is safe after you have already sent it the string. Pattern matching helps with familiar token formats, but classification should also use field names, source metadata, and data contracts. A value returned in authorization_header is sensitive even if it does not match the scanner’s current regular expressions.
The safe response can preserve useful evidence without exposing the value:
{
"error_code": "DEPLOY_AUTH_401",
"sanitized_message": "Bearer credential rejected",
"sensitive_reference": "secret-event:7f92",
"classification": "credential"
}
The reference lets an authorised incident responder retrieve the original from a restricted store. A digest can help the restricted control plane compare findings, but do not expose a plain hash of a guessable secret: it can enable offline guessing. Use a keyed fingerprint inside the trusted boundary when comparison is necessary, and omit it from ordinary telemetry. Claude Code gets enough information to diagnose the fault without receiving reusable authority.
Treat every copy as a separate disclosure surface
A production run has more data surfaces than the visible transcript. Inventory them before deciding that redaction works:
- model request and response logs
- MCP request and response logs
- distributed traces and span attributes
- prompt and semantic caches
- retry queues and dead letter records
- terminal output and shell history
- review packets, tickets, and chat notifications
- evaluator inputs and failure reports
Each surface needs an owner, a data policy, and a retention rule. Turning off prompt logging at the model provider does not protect a raw MCP response stored by your tracing library. Masking trace attributes does not remove a secret copied into an evaluator failure message.
This is why MCP tool output must remain evidence rather than authority. Tool output can contain hostile instructions, but it can also contain credentials and customer data. Both problems need enforcement before the content reaches later stages.
Carry a data label through the run
Once the gateway replaces a sensitive value, keep its classification attached to the safe reference. Downstream systems should make decisions from that label rather than rescanning a flattened string.
sensitive_item:
reference: secret-event:7f92
class: credential
source: mcp:deployment_logs.read
run_id: cc-run-4938
first_seen_at: 2026-08-28T09:37:14Z
allowed_destinations:
- restricted_incident_store
prohibited_destinations:
- model_context
- review_packet
- analytics_export
expires_at: 2026-08-29T09:37:14Z
A label can survive tool calls, retries, and handoffs. If a later component tries to place secret-event:7f92 in an analytics export, the policy engine can stop it even though the raw token is no longer visible.
Keep the raw store separate from ordinary agent telemetry. Access should use a named incident role, record the reason, and expire. The agent should never turn a safe reference back into the original value by itself.
Can your debugger read the restricted evidence?
A reference can keep a token out of the trace while leaving it one click away. If the resolver accepts every signed-in engineer, ordinary debugging access has become incident access. Test the resolver as well as the redactor.
The downloadable trace-access fixture uses a synthetic secret and two identities: an ordinary debugger and an incident responder with a time-limited grant. It exercises trace search, export and raw retrieval without contacting any services:
python3 trace-access-boundary.py
Six local tests passed when preparing this update. The access matrix below describes the fixture’s observed behaviour, not your tracing platform’s defaults.
| Identity and request | Observed result |
|---|---|
| Same-tenant debugger searches traces or exports them | Sanitized message and reference; no synthetic secret |
| Debugger resolves a known reference to raw data | Denied |
| Same-tenant responder with the matching active incident grant retrieves raw data | Allowed; positive control |
| Responder uses an expired grant or a different incident | Denied |
| Responder from another tenant uses search, export or raw retrieval | Denied on every surface |
| Debugger requests a missing reference versus a forbidden one | Same public error message |
The positive control matters. Blocking everybody would satisfy the denial tests while making incident response unusable. The expiry test uses the boundary itself: at the expiry time, access has ended.
In the model, search and export both construct an allowlisted response. Neither serializes the stored record and then tries to remove the sensitive fields. The raw retrieval path checks tenant, incident and grant expiry separately. Its audit entry records the decision without copying the secret.
A reference is a locator, not a credential
Keep these decisions at the service that returns the evidence. A long random reference makes guessing harder; possession of it must not authorize retrieval. A debugger can learn a valid reference from an ordinary trace, so an unguessable ID does not address that path.
The fixture gives missing and forbidden raw references the same error text. That limits one disclosure channel. It does not prove resistance to enumeration: response timing, status codes, search counts and cache behaviour can still reveal whether a record exists. Rate-limit resolution attempts and test those channels in your deployed service.
This is an executed in-memory teaching model, not production authentication code. The caller constructs the identity object and supplies the clock. A real service must derive identity from a verified session, obtain trusted time, and validate incident grants against current server-side authority. Do not paste this fixture behind an HTTP endpoint and treat its Identity values as proof of access.
Repeat the matrix through your actual search UI, export worker and payload resolver using disposable canaries. Include expired sessions, cached results and asynchronous exports that finish after a grant expires. Those integration cases have not been executed here. If your export service uses a shared powerful account, verify that it enforces the requesting user’s scope rather than inheriting the worker’s broader access.
Produce a propagation receipt
Before the run closes, reconcile where the sensitive item could have travelled. The receipt should name every expected surface and record whether it received raw, sanitized, or no data.
sensitive_data_propagation_receipt:
run_id: cc-run-4938
item_reference: secret-event:7f92
classification: credential
interception_point: mcp_gateway
raw_value_reached_model: false
surfaces:
mcp_gateway_restricted_log: raw_encrypted
model_context: safe_reference_only
trace_platform: safe_reference_only
prompt_cache: safe_reference_only
review_packet: safe_reference_only
evaluator: safe_reference_only
deletion_actions: []
unresolved_surfaces: []
canary_leaks: 0
status: verified
This artifact is more useful than secret redacted. It distinguishes prevention from cleanup. If the raw value reached a trace before the filter ran, record that surface under deletion_actions and keep the run open until the owner confirms removal or containment.
Some systems cannot guarantee selective deletion. Say so. Rotate the credential, shorten the affected retention period where possible, restrict access to the contaminated store, and preserve the incident record. A neat receipt must not pretend that a copy disappeared when the platform cannot prove it.
Add the receipt to the same review packet that carries the patch and test evidence. The packet should contain the safe reference and status, never the raw value.
Test with canaries, including the error paths
Seed disposable canary values into test tool responses. Give each surface a distinct canary so you can tell where a leak began.
fixtures:
- name: token_in_structured_field
expected: blocked_before_model
- name: token_inside_stack_trace
expected: replaced_with_safe_reference
- name: scanner_times_out
expected: fail_closed
- name: retry_queue_copies_raw_response
expected: test_failure
- name: evaluator_prints_failed_input
expected: sanitized_failure_record
- name: review_packet_renders_tool_error
expected: safe_reference_only
Test failures as aggressively as successful responses. Scanners time out. Parsers reject malformed JSON. Trace exporters retry old payloads. Debug modes get enabled during incidents, which is exactly when production logs are most likely to contain sensitive material.
Search every downstream test store for each canary after the run. Zero hits outside the restricted fixture store is the release condition. A final answer containing no canary proves only that one output path was clean.
My rule is simple: once Claude Code sees a raw secret, you have a containment problem rather than a formatting problem. Intercept sensitive values before model context, replace them with controlled references, and require a propagation receipt that accounts for every copy.
For the wider operating workflow, see what is inside the Claude Code production-agents book.