Privacy: captureContent¶
The captureContent plugin option controls whether prompt, completion, and tool content text is recorded on spans. It accepts either:
- a single boolean —
trueturns every category on,falseturns every category off (legacy shape), or - a granular
ContentCapturePolicyobject with the per-category flagsinputMessages,outputMessages,toolInputs,toolOutputs,systemPrompt(ISI-1000).
Default: every flag is false (privacy-first).
What captureContent controls¶
captureContent now controls two surfaces: the Traceloop LLM-client spans and the plugin's own hook-surface spans. The granular policy lets you turn each category on or off independently.
Traceloop LLM-client spans (via the preload)¶
Traceloop instrumentations (@traceloop/instrumentation-anthropic, @traceloop/instrumentation-openai) accept a single traceContent boolean. The plugin derives it as inputMessages || outputMessages || systemPrompt — the three categories that map to prompt/completion text. When the derived boolean is true, Traceloop LLM-client spans include:
| Span attribute | Contents |
|---|---|
gen_ai.prompt.N.role |
Role for message N (e.g., user, assistant, system). |
gen_ai.prompt.N.content |
Full message text for message N. |
gen_ai.completion.N.role |
Role of the model's response. |
gen_ai.completion.N.content |
Full generated text from the model. |
Token counts, model identifiers, latency, cost, and error state are recorded regardless of the flag — only the prompt/completion text is gated.
⚠️ Traceloop is all-or-nothing across directions. Traceloop exposes a single
traceContentboolean and does not distinguish input from output. The plugin's policy unifies the three LLM-content categories withinputMessages || outputMessages || systemPrompt, so enabling any one of them flips Traceloop on and Traceloop then records bothgen_ai.prompt.*.contentandgen_ai.completion.*.contenton LLM-client spans. Concretely,{ inputMessages: true }does not record prompts only — completions land on the LLM-client span too. If you need true one-direction capture, leave every LLM-content flag (inputMessages,outputMessages,systemPrompt) atfalseand rely on the plugin's per-flag-gatedopenclaw.content.*attributes on the hook-surface spans, which do honor the requested direction in isolation.
Plugin hook-surface spans (openclaw.content.*)¶
Each policy flag also gates one or more openclaw.content.* attributes on the plugin's own spans:
| Flag | Span attribute(s) | What is captured |
|---|---|---|
inputMessages |
openclaw.content.input_message on openclaw.request; openclaw.content.prompt / openclaw.content.messages on openclaw.agent.turn |
Inbound user message text + the prompt and message history passed to the LLM |
outputMessages |
openclaw.content.output_message on openclaw.message.sent |
Outbound assistant reply text |
toolInputs |
openclaw.content.tool_input on execute_tool <tool> |
Full tool-call input arguments (JSON-stringified) |
toolOutputs |
openclaw.content.tool_output on execute_tool <tool> |
Tool-call result text (text parts only) |
systemPrompt |
openclaw.content.system_prompt on openclaw.agent.turn |
System prompt text |
All openclaw.content.* attributes are truncated to 8192 UTF-16 code units per value (a JavaScript string .length measurement, not bytes — CJK and emoji content can occupy 2–4 bytes per code point on the wire). Larger payloads get an inline …(truncated, N more chars) marker, and the cut point is adjusted by one code unit when it would otherwise split a surrogate pair so the prefix stays valid UTF-16.
What captureContent does not affect¶
captureContent only gates the prompt/completion/tool content attributes above. Independent of the policy, hook spans still emit metadata such as:
openclaw.request— session/channel identifiers, no message bodyopenclaw.agent.turn— token counts, model, durationexecute_tool *— tool name, duration,openclaw.tool.input_preview(1 KB truncated preview), result chars/parts countsopenclaw.message.sent— channel, recipient, char count
The openclaw.tool.input_preview preview attribute is always emitted, independent of toolInputs. It is a 1 KB capped preview meant for debugging and security correlation; the granular openclaw.content.tool_input is the larger 8192-code-unit capture that the policy gates.
Gateway-launch setting (not hot-reloadable)¶
captureContent is read by the ESM preload (instrumentation/preload.mjs) at gateway launch time, before OpenClaw parses plugin config. The Traceloop instrumentations are constructed once, at preload, and the traceContent option is fixed for the lifetime of the process.
Consequence: changing captureContent in openclaw.json mid-run has no effect until the next gateway restart.
Bridge mechanism¶
The preload reads two environment variables and prefers the granular one:
OPENCLAW_OTEL_CONTENT_POLICY— JSON encoding of theContentCapturePolicyobject. When set and parseable, Traceloop'straceContentis derived asinputMessages || outputMessages || systemPrompt. This var takes precedence.OPENCLAW_OTEL_CAPTURE_CONTENT— legacy single-boolean flag. The string value must be exactlytrueto enable; anything else (including1,yes,True, unset) resolves tofalse. Strict match keeps the privacy default unambiguous.
The plugin's start() phase re-exports both env vars from the parsed plugin config so subprocesses inherit the intended values, and warns if the preload-resolved traceContent does not match the policy.
How to enable¶
All categories on (legacy boolean)¶
OPENCLAW_OTEL_CAPTURE_CONTENT=true \
NODE_OPTIONS="--import /path/to/openclaw-observability-plugin/instrumentation/preload.mjs" \
openclaw gateway start
Granular (recommended)¶
Capture only specific categories — e.g., tool I/O for debugging without recording user prompts:
OPENCLAW_OTEL_CONTENT_POLICY='{"toolInputs":true,"toolOutputs":true}' \
NODE_OPTIONS="--import /path/to/openclaw-observability-plugin/instrumentation/preload.mjs" \
openclaw gateway start
Via systemd:
[Service]
Environment=OPENCLAW_OTEL_CONTENT_POLICY={"toolInputs":true,"toolOutputs":true}
Environment=NODE_OPTIONS=--import /path/to/openclaw-observability-plugin/instrumentation/preload.mjs
ExecStart=/usr/bin/openclaw gateway start
Plugin config for parity (so the otel_status tool and the mismatch warning agree):
{
"plugins": {
"entries": {
"otel-observability": {
"enabled": true,
"config": {
"captureContent": {
"toolInputs": true,
"toolOutputs": true
}
}
}
}
}
}
When to enable content capture¶
Enabling any captureContent flag is a deliberate tradeoff. The granular policy lets you trade off per-category instead of all-or-nothing: for example, enabling only toolInputs/toolOutputs records tool-call I/O for debugging while leaving user prompts and assistant replies out of spans. Useful when:
- You are debugging prompt engineering and need to see actual prompt/completion pairs alongside token counts.
- You operate the OTLP backend and downstream storage yourself.
- You have reviewed the backend's retention and access controls against the sensitivity of the prompts your agents handle.
Avoid enabling content capture when:
- Users can pass arbitrary data into the agent (free-form chat, uploaded documents, customer PII).
- Your backend retention is long, retention controls are weak, or access is broad.
- You have regulatory obligations around prompt/completion text (HIPAA, PCI, GDPR right-to-erasure).
Sensitive data reaching spans¶
Whenever a flag is true, the following can land in span storage:
inputMessages/systemPrompt: user chat input (including PII, credentials, proprietary data), document contents summarized into prompts, system prompts that may encode prompt-engineering IP.outputMessages: assistant replies, which can mirror or reveal sensitive input data.toolInputs: tool-call arguments — file paths, command lines, query strings, API request bodies. Note that even withtoolInputs: false, the smalleropenclaw.tool.input_previewattribute is still emitted.toolOutputs: tool-call results fed back to the model — file contents fromtool.Read, shell command output, database query results.
The plugin does not scrub Traceloop or openclaw.content.* span attributes before export. If you need redaction, apply it at the OTel Collector (transform / attributes processors) or at the backend.
Complementary detections¶
Whether or not you capture content, the real-time detection module runs on the hook-surface path and flags:
- Sensitive file access (
tool.Readon.env, SSH keys, cloud creds) - Dangerous command execution
- Prompt-injection patterns on inbound messages
These alerts work without content capture — they operate on paths, command strings, and matched patterns, and emit only the patterns and a short preview on the span.
See also¶
- Real-Time Detection — application-layer security events
- Configuration — plugin config reference
- github issue #15 — original report motivating this wiring