BYOKchat Blog

How to Redact Secrets From AI App Logs

Design AI app logging that preserves useful diagnostics while removing API keys, tokens, protected headers, private URLs, prompt content, and other sensitive data before it leaves the process.

· 5 min read

On this page
  1. Define what the app considers secret
  2. Redaction and minimization are different
  3. Redact centrally
  4. Use structured fields instead of interpolated dumps
  5. Treat authorization headers as always sensitive
  6. Custom headers need explicit secret metadata
  7. Sanitize URLs before logging
  8. Avoid full request and response bodies
  9. Log shape, not content
  10. Errors can contain secrets too
  11. Provider error bodies deserve special care
  12. Tool arguments and results are frequently sensitive
  13. Hashing is not always anonymization
  14. Stable identifiers should be deliberate
  15. Crash reports are another log sink
  16. Support bundles need the same policy
  17. Development logging is still dangerous
  18. Build a sensitive-field denylist and a safe-field allowlist
  19. Detect obvious leaks in tests
  20. Fuzz the sanitizer
  21. Avoid accidental logging through Codable / reflection
  22. Model logging as a security boundary
  23. Where BYOKchat fits
  24. Further reading

Logs are one of the easiest places for a well-designed AI app to leak secrets accidentally.

The dangerous pattern is usually not malicious code.

It is useful debugging code such as:

logger.debug("request = \(request)")

or:

print("headers: \(headers)")

or:

Provider failed: URL=https://example.com?token=SECRET

A reliable logging design starts with this rule:

Sensitive data must be removed before it reaches the logging sink, not after logs have already been collected.

Define what the app considers secret

For an AI client, obvious secrets include:

API keys
OAuth access tokens
OAuth refresh tokens
Authorization headers
protected custom headers
private keys
session secrets

But privacy-sensitive values often extend further:

prompt text
model output
reasoning
attachment contents
tool arguments
tool results
private endpoint hostnames
file paths
user-entered project instructions

Not every item needs identical handling, but every category should be classified.

Redaction and minimization are different

Redaction means:

Authorization: Bearer SECRET

becomes:

Authorization: [REDACTED]

Minimization means you never log the field at all.

For many AI payloads, minimization is better.

For example, instead of:

{
  "prompt": "Summarize my private contract..."
}

record:

{
  "input_chars": 1840,
  "attachments": 2,
  "request_kind": "chat"
}

Redact centrally

Do not rely on code like:

logger.debug("apiKey = [redacted]")

at every call site.

Someone will eventually forget.

Prefer a structured logger with a sanitizer pipeline:

Diagram illustrating the surrounding section

The sink should receive only sanitized data.

Use structured fields instead of interpolated dumps

Bad:

logger.error("Request failed \(request) \(error)")

Better:

event=request_failed
provider=custom
status=401
method=POST
route=/v1/chat/completions
request_id=local-123

Structured events let you control exactly which values are permitted.

Treat authorization headers as always sensitive

Headers commonly carrying secrets include:

Authorization
Proxy-Authorization
x-api-key
api-key
x-goog-api-key
provider-specific token headers
user-marked protected custom headers

A sanitizer should match header names case-insensitively.

Example output:

Authorization: [REDACTED]
Content-Type: application/json

Custom headers need explicit secret metadata

A multi-provider client may let users configure headers such as:

X-Workspace-ID
X-Internal-Token
X-Gateway-Key

The logger cannot know from the name alone which are sensitive.

Store configuration like:

header name
value reference
isProtected

If isProtected is true, never emit the value.

Sanitize URLs before logging

Even if credentials should never be in URLs, defensive redaction still matters.

For a URL such as:

https://example.com/v1?token=SECRET&page=2

log:

https://example.com/v1?token=[REDACTED]&page=2

or better, log only:

scheme=https
host_class=custom
path=/v1

For privacy-sensitive custom endpoints, you may not want to log the hostname at all.

Avoid full request and response bodies

AI request bodies often contain the most private data in the application.

A request body may include:

  • system instructions;
  • full conversation history;
  • retrieved documents;
  • file contents;
  • tool schemas;
  • user prompts.

A response body can include:

  • model text;
  • reasoning summaries;
  • tool calls;
  • citations;
  • provider metadata.

The default production logger should not dump these.

Log shape, not content

Useful diagnostics can still be rich.

Example:

{
  "event": "provider_request_finished",
  "provider": "anthropic",
  "model_family": "configured",
  "status": "success",
  "input_tokens": 2120,
  "output_tokens": 506,
  "ttft_ms": 910,
  "duration_ms": 6840,
  "tool_calls": 2
}

This is enough to investigate many performance/reliability issues without copying conversation text.

Errors can contain secrets too

A common mistake is to sanitize the request but log the raw error.

Errors can embed:

URL
headers
request body snippets
server response body
filesystem path
user-entered value

Treat error.localizedDescription and equivalent strings as untrusted diagnostic input.

Prefer normalized errors:

network.timeout
http.401
http.429
tls.untrusted_certificate
provider.invalid_response
stream.disconnected

Then optionally attach a sanitized safe message.

Provider error bodies deserve special care

Providers sometimes return error payloads containing request identifiers or echoed configuration values.

Do not assume a remote error body is safe because it came from a known API.

Store only fields you need.

For example:

{
  "status": 429,
  "provider_code": "rate_limit_exceeded",
  "request_id": "..."
}

rather than the full raw body.

Tool arguments and results are frequently sensitive

Tool calls can contain:

email recipients
calendar titles
file paths
search queries
private document text
shell-like arguments
URLs

Logging them by default undermines the privacy model of a local-first client.

A safer tool event:

{
  "tool": "calendar.create_event",
  "result": "success",
  "duration_ms": 83,
  "approval": "user_approved"
}

without the arguments/result body.

Hashing is not always anonymization

Developers sometimes replace a secret with:

SHA256(secret)

and assume the result is safe.

That still creates a stable identifier tied to the secret.

For low-entropy values, hashes can sometimes be guessed or correlated.

Only hash when there is a clear purpose, such as deduplicating an opaque local identifier, and understand the privacy tradeoff.

Stable identifiers should be deliberate

If you need to correlate events for one provider connection, generate an app-local opaque ID:

connection_7D3A...

Do not derive it from:

API key
hostname
email address
account ID

unless there is a documented reason.

Crash reports are another log sink

Crash-reporting systems can receive:

  • breadcrumbs;
  • console logs;
  • custom metadata;
  • exception messages.

The redaction boundary must happen before those values are attached.

Do not build a sanitized local log and then separately attach raw request metadata to crash reports.

Support bundles need the same policy

A “Export diagnostics” feature can be extremely useful.

It can also become a one-click privacy leak.

A safe support bundle might include:

app version
OS version
provider kind
sanitized connection settings
request timing
normalized error codes
feature flags
model capability snapshot

and exclude:

API keys
protected headers
prompt/response/reasoning
attachments
tool arguments/results
raw private URLs

Development logging is still dangerous

A debug build may run on a real account with a real API key.

“Only in debug” does not make credential leakage harmless.

Use fake keys and provider simulators where possible.

If verbose network logging is available, require an explicit developer toggle and sanitize it too.

Build a sensitive-field denylist and a safe-field allowlist

A denylist catches known secret names:

authorization
api_key
access_token
refresh_token
password
secret

But a safe-field allowlist is stronger for structured analytics/logging.

For example, define exactly which properties ProviderRequestFinished may contain.

Unknown fields are rejected rather than logged automatically.

Detect obvious leaks in tests

Use sentinel values:

TEST_SECRET_DO_NOT_LOG_9B7C
TEST_PROMPT_PRIVATE_4A11
TEST_TOOL_RESULT_PRIVATE_22D0

Run representative workflows, collect logs, and assert those strings are absent.

This catches regressions that code review may miss.

Fuzz the sanitizer

Useful test cases include:

authorization header with odd capitalization
secret in query parameter
secret inside nested JSON
secret in error description
secret in redirect URL
secret in custom header
secret in tool argument
secret in provider response error

The goal is not perfect arbitrary-secret detection.

The goal is to prove your known sensitive paths are sanitized.

Avoid accidental logging through Codable / reflection

Convenience code such as:

String(describing: connection)

or automatic JSON encoding of a whole state object can bypass field-level policy.

Sensitive types should either omit secrets from their description or never be logged wholesale.

Model logging as a security boundary

A good architecture looks like:

application state
   -> structured safe event
   -> sanitizer
   -> local log / crash system / telemetry

not:

application state
   -> stringify everything
   -> hope downstream filters catch it

Where BYOKchat fits

A BYOK client needs enough diagnostics to debug provider compatibility, streaming, tools, and local endpoints without collecting the conversation itself.

That means logs should focus on:

  • normalized events;
  • timing;
  • token usage;
  • capability decisions;
  • status/error categories;
  • tool names and outcomes;
  • local opaque identifiers.

Secrets and user content should remain outside the logging pipeline.

Further reading

Keep reading