On this page
- Define what the app considers secret
- Redaction and minimization are different
- Redact centrally
- Use structured fields instead of interpolated dumps
- Treat authorization headers as always sensitive
- Custom headers need explicit secret metadata
- Sanitize URLs before logging
- Avoid full request and response bodies
- Log shape, not content
- Errors can contain secrets too
- Provider error bodies deserve special care
- Tool arguments and results are frequently sensitive
- Hashing is not always anonymization
- Stable identifiers should be deliberate
- Crash reports are another log sink
- Support bundles need the same policy
- Development logging is still dangerous
- Build a sensitive-field denylist and a safe-field allowlist
- Detect obvious leaks in tests
- Fuzz the sanitizer
- Avoid accidental logging through Codable / reflection
- Model logging as a security boundary
- Where BYOKchat fits
- 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:
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.