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.
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",
"value_digest": "sha256:4a91...",
"classification": "credential"
}
The reference lets an authorised incident responder retrieve the original from a restricted store. The digest lets the control plane determine whether two findings contain the same value. 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.
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.
Claude Code: Building Production Agents That Actually Scale covers MCP boundaries, permissions, observability, evals, rollback, cost control, and review packets for production coding agents.