# Crash report sensitive data: stop agent context leaks

Crash reporting can quietly become the widest data-export path in an agent-enabled application. Teams lock down production credentials, then let an exception SDK collect breadcrumbs, HTTP context, process details, and copied tool input before shipping the bundle to a vendor account. The crash was local. The evidence did not stay local.

The fix is not to abandon crash reports. You need them when an agent integration fails in a way that ordinary logs cannot explain. The job is to decide which debugging facts leave the machine, stop collecting raw request material by default, and prove that the collector obeys those limits. I have seen incident reviews derail because the stack trace was harmless but a "useful" context field contained the entire failing request.

## Crash reports are evidence packages, not just stack traces

A crash report is often a bundle assembled across several layers. The operating system may create a native diagnostic report. Your application may attach exception metadata. A crash SDK may add device data, breadcrumbs, session history, logs, tracing fields, and user-defined tags. An agent framework may have already serialized its prompt, tool input, response, or retry state into one of those fields.

This distinction matters because developers tend to audit the visible stack trace and call the result clean. The sensitive material usually sits beside it.

A report can expose data through:

- exception messages that interpolate a URL, command, or response body
- breadcrumbs made from request logs or tool-call events
- custom fields that receive a whole request object
- command-line arguments, temporary-file paths, and environment-derived values
- attached files, screenshots, session replay, or support feedback

A memory address in a native report does not normally reveal an API key by itself. A string copied into a panic message absolutely can. Do not blur those two cases. Native diagnostics need access controls; application telemetry needs deliberate data minimization.

Apple's documentation for diagnostic reports describes them as records used to diagnose application problems, with process and thread information. That is useful and usually narrower than an application monitoring event. Once an SDK enriches that report with your own runtime context, however, the data boundary changes. Treat the enriched event as application data headed to another system.

## Agent failures create unusually rich diagnostic context

An ordinary request handler might fail with a route and a status code. An agent action can carry a user instruction, repository paths, command output, arguments for a remote API, an SSH destination, and the previous tool results that led to the failure. That context helps reproduce the bug. It also makes careless collection expensive.

The risky path is predictable. A developer wraps every tool invocation in a convenience logger. The logger emits the entire argument object. The observability SDK converts log records into breadcrumbs. A network timeout triggers an exception. The report now contains the tool input, including headers or a pasted token, and the vendor receives it.

The same failure appears when code does this:

```js
try {
  await runTool(toolName, input);
} catch (error) {
  throw new Error(`Tool failed: ${toolName} input=${JSON.stringify(input)} error=${error.message}`);
}
```

That message looks helpful during a local test. In production, it creates an unbounded data field that every error collector, log sink, and alert integration may copy. Replace it with stable identifiers and an allowlisted summary:

```js
try {
  await runTool(toolName, input);
} catch (error) {
  throw new Error(`Tool failed: name=${toolName} request_id=${requestId} input_shape=${inputShape}`);
}
```

`inputShape` can mean a list of approved field names and sizes, never field values. If you cannot explain why a field belongs in an error event, leave it out. Reproduction should begin with a correlation ID that points to a protected local record, not a full request pasted into a hosted dashboard.

## Raw request capture is a bad default

Raw request capture remains popular because it makes the first debugging session fast. It is wrong as a default for services that handle agent actions. Authorization headers, cookies, signed URLs, query parameters, request bodies, and custom headers all routinely contain material that has no business in a crash system.

A safe event has enough facts to group, triage, and route the incident:

```json
{
  "request_id": "rq_8c2f1a",
  "channel": "http",
  "method": "POST",
  "route": "/v1/issues/{issue_id}/comments",
  "status_class": "5xx",
  "duration_ms": 8120,
  "attempt": 2,
  "error_kind": "upstream_timeout"
}
```

The route uses a template, not the literal path. The event says the request retried, not what it sent. The opaque ID lets an authorized responder inspect the local source record if they need it.

Do not hash secrets and call them redacted. A deterministic hash can still identify a repeated bearer token and can be vulnerable when the original has a small search space. Replace sensitive values with a constant marker or omit them. If you need to know whether a credential was present, record `auth_present: true`, not its type, length, prefix, or fingerprint.

Also review URL handling. Many libraries record full URLs automatically. A route such as `/callback?code=...` or a signed download URL can leak through a field the developer never manually added. Remove query strings before the event enters the SDK, rather than hoping a later processor recognizes every variation.

## Redaction must run before storage and before export

A scrubber is a backstop, not a permission slip to collect everything. SDK hooks differ: some process the final event, some only process selected fields, and some do not cover native crash attachments or breadcrumbs produced by a separate integration. A rule that redacts `Authorization` may miss `authorization`, `x-api-token`, a URL parameter, or a JSON string embedded in an exception.

Build the boundary in layers. First, turn off automatic capture you do not need. Then construct events from allowlisted fields. Then run a defensive scrubber over all remaining strings. Finally, test the serialized data received by the collector.

This pseudocode shows the order that prevents the most trouble:

```text
request arrives
  -> derive route template and request ID
  -> retain protected local diagnostic record if policy permits
  -> create minimal crash context from allowlisted fields
  -> scrub all residual strings
  -> send minimized event
```

Do not pass the original request object to a "before send" callback. Once a library has inspected that object, plugins may have already turned it into breadcrumbs or spans. Give the telemetry code a small object that cannot contain sensitive fields in the first place.

Sentry documents data scrubbing and event processors, while Firebase Crashlytics documents custom keys and log collection. Read those settings as collection controls, not as a generic privacy guarantee. In both cases, custom context and logs are the places where application teams most often defeat their own default protections.

## SDK defaults change when integrations change

A crash SDK upgrade can add tracing, console capture, breadcrumbs, session replay, performance instrumentation, or a framework integration that sees more request state. A security review performed when the app had only stack traces does not cover the new data path.

Maintain a small collection inventory for each runtime. Record the native crash collector, exception SDK, logging bridge, tracing package, feedback widget, and support bundle mechanism. For each one, answer four mundane questions: what triggers collection, which fields it collects automatically, where it stores data before upload, and who can read it after upload.

Do not forget side channels. Developers often disable request bodies in the crash product but forward the same exception to a log aggregator. Alerting rules may copy the exception text into chat notifications. A support button may attach a local log archive. You need one data map for the incident path, not a separate optimistic story for each vendor.

On macOS, inspect both app-level reporting and operating-system diagnostics. Apple crash reports can remain local or be shared through system reporting choices, while third-party SDKs follow their own configuration and network path. A desktop app should make this distinction clear to the people operating it. "Crash reports are disabled" is meaningless if a separate error-monitoring client still uploads enriched events.

## Put credentials outside the agent process

Redaction reduces exposure after code has handled a secret. The stronger move is to prevent the agent process from holding the secret at all. Then an agent crash, a copied environment, and an accidental debug dump have less sensitive material available to capture.

This does not mean a crash report becomes harmless. The agent may still hold private source, prompts, or tool arguments. It does mean that a bearer token cannot leak from a process that never received it.

Sallyport follows this boundary for HTTP APIs and SSH actions: the app keeps credentials in its encrypted vault, executes the action, and returns the result to the agent rather than giving the agent credential material. That removes one recurring crash-telemetry failure mode, but you still need to minimize the request data and result text your agent handles.

Avoid putting secrets in command arguments as well. Process arguments appear in diagnostic tools more often than developers expect, and shell history or process inspection can expose them independently of crash reporting. Pass secret material through a protected broker or a carefully managed local mechanism, not through a visible argument such as `--token=...`.

## Local retention needs a retention rule too

Keeping full reports on the machine can be appropriate for difficult failures, especially during controlled development. It is not automatically safe. A local spool can outlive the incident, get copied into a support archive, or sit in a shared user profile where another process can read it.

Separate two records. Send a minimized remote event for grouping and alerting. Keep any fuller reproduction record locally in a protected location with a short retention period and an explicit deletion path. The remote event needs the local record's opaque ID, not its contents.

A useful local record includes the exact build identifier, a sanitized configuration fingerprint, timing, and a reference to the action attempt. It should not be a loose text file that concatenates environment variables, request bodies, and terminal output. Structured local storage lets you apply the same allowlist and deletion rules that you use for exports.

For agent action gateways, audit records deserve special care. They help establish what the agent tried to do, but they are not a license to mirror raw credentials or entire prompts into every diagnostic subsystem. Sallyport's audit trail separates action records from the agent itself, and its `sp audit verify` command verifies the encrypted hash chain offline without a vault key. Verification tells you whether the trail changed; data minimization still decides what diagnostic context belongs elsewhere.

## Prove the collector with planted secrets

Configuration review catches obvious mistakes. A planted-data test catches the mistakes that matter. Run it before launch, after telemetry SDK upgrades, and whenever an agent framework gains a new tool or logging integration.

Use unique marker strings that cannot be mistaken for real credentials. Put different markers in the places teams routinely forget: an authorization header, a query parameter, a JSON body field, an agent instruction, an environment variable, a command argument, and a tool result. Trigger a controlled exception, then search every destination.

Check the vendor event view, raw event export if available, local crash spool, application logs, tracing events, alert payloads, support attachments, and any queue between the app and the collector. Search for the exact markers and common transformations such as URL encoding or escaped JSON. Inspecting only the web dashboard is how teams miss a leak in a breadcrumb or attached log.

Write down the expected result. For example, a report may contain `route=/v1/files/{file_id}` and `request_id=rq_test_01`, but it must not contain `MARKER_HEADER_7`, `MARKER_PROMPT_7`, or the literal query string. Treat a failed test like a security defect: disable the offending capture path, add a regression test, and retest the serialized event.

## A small review catches most accidental exports

Review crash telemetry whenever somebody changes error handling, observability, an agent tool, or support diagnostics. The reviewer should ask whether the new code can serialize a request-shaped object, not merely whether it added a new secret field.

Use this short checklist during review:

- Error messages contain identifiers and categories, not serialized inputs or outputs.
- Telemetry receives allowlisted context, never a raw request or agent state object.
- URLs lose query strings and headers never enter breadcrumbs or custom fields.
- Full local diagnostics have protected storage, deletion rules, and a named reason to exist.
- A planted-marker test covers every configured export destination.

The uncomfortable part is that good debugging habits often cause the leak. Developers add context because the last incident lacked it. Keep the context that classifies the failure, preserve fuller evidence under local control when justified, and stop exporting raw material just because a crash SDK offers a field for it.
