Safeguard
Supply Chain Security•

OpenTelemetry GenAI Semantic Conventions: Tracing LLM Calls and Agents

The OpenTelemetry GenAI conventions name the spans, attributes and metrics for LLM calls, agents, tools and MCP. What they record and how to redact it.

Safeguard Security Team
47 min read

The OpenTelemetry GenAI semantic conventions are a shared vocabulary for telemetry about generative AI: the span names, attribute keys, metrics and events that describe a model call, an agent run, a tool execution or an MCP request. Every part is still marked Development. Prompts and completions are not captured by default, and that default is the most important security control in the whole set.

This post goes one level deeper than our pieces on what Claude Code logs through OpenTelemetry by default, the collector's redaction and filter processors, distributed tracing and trace context and agent planning patterns. Those explained the plumbing and the agent loop. This one explains the GenAI vocabulary itself, attribute by attribute, and then shows how to keep the sensitive parts out of your trace store. Everything below was checked against the GenAI conventions repository on 24 October 2026, and the walkthrough code was run against the OpenTelemetry Python SDK 1.45.1.

Key takeaways

  • The GenAI conventions cover inference, embeddings, retrieval, memory, agents, workflows, tools, MCP, metrics and events, all at Development stability.
  • In semantic conventions v1.42.0 (released 12 June 2026) the GenAI, OpenAI and MCP definitions moved out of the core repository into a dedicated semantic-conventions-genai repository. Old links still work but point to a "Moved" notice.
  • A model call is a span named {gen_ai.operation.name} {gen_ai.request.model}, for example chat my-model, with gen_ai.provider.name, token usage, response model and finish reasons as attributes.
  • Agents get invoke_agent, invoke_workflow, plan and execute_tool spans. Tool calls that run shell commands or agent skills have their own refinements.
  • Across MCP hops, trace context travels inside each JSON-RPC message in params._meta, not only in HTTP headers.
  • Instructions, input messages, output messages, tool definitions, tool arguments and tool results are all Opt-In attributes. Instrumentation should not capture them unless you ask.
  • Once content is on, your trace store holds prompts, personal data and pasted secrets. Redact in the SDK first and the collector second, keep retention short, and keep tenants apart.

What are OpenTelemetry semantic conventions?

OpenTelemetry gives you the plumbing: an API, SDKs, a wire protocol (OTLP) and the Collector. Semantic conventions are the agreed names and meanings for spans, attributes, metrics and events, so spans from different libraries can be queried the same way.

The HTTP conventions are the familiar example. Any HTTP client span records http.request.method, server.address and http.response.status_code, whatever the language or library. A dashboard that groups by status code works on all of them.

Generative AI needed the same thing. Every model SDK and agent framework had invented its own fields for model name, token counts and prompt text. The GenAI conventions give them one shape, so "input tokens per model per service" is one query across languages and frameworks.

Each convention defines a few properties that matter in practice:

  • Span name. A low cardinality name, such as chat my-model or execute_tool get_weather.
  • Span kind. CLIENT for a call to another process, INTERNAL for work inside the same process, SERVER for the receiving side.
  • Attributes with requirement levels. Required, Conditionally Required, Recommended or Opt-In. Opt-In attributes are the ones an instrumentation must not record unless the user turns them on.
  • Stability. Development, then (eventually) Stable. Stable conventions come with compatibility promises. Development ones do not.

What does the GenAI set cover?

The GenAI index page in the new repository lists these documents, each with status Development:

DocumentSignalsWhat it describes
Client InferenceSpans, events, metricsA call to a model that generates a response or asks for a tool call
Model spansSpansEmbeddings, retrieval, fetch response, memory operations and the execute tool span
Agent spansSpansCreating and invoking agents, workflows, planning, skills and command execution
EventsEventsEvaluation results; inference detail events live in Client Inference
ExceptionsEventsExceptions raised during GenAI operations
MetricsMetricsClient and server durations, agent, workflow and tool durations
Inference token metricsMetricsInput, output, cached and reasoning token counts
Model Context ProtocolSpans, metricsMCP client and server operations and sessions, and how context propagates
Provider specific pagesVariesExtra attributes for a few model providers and cloud platforms

The well known values for gen_ai.operation.name show how wide the scope is: chat, text_completion, generate_content, embeddings, retrieval, fetch_response, create_agent, invoke_agent, invoke_workflow, plan, execute_tool, and seven memory operations (create_memory, update_memory, upsert_memory, delete_memory, search_memory, create_memory_store, delete_memory_store).

Stability status and the 2026 move to a separate repository

Two facts here trip people up.

DateEventSource
23 November 2021W3C Trace Context Level 1 becomes a W3C RecommendationW3C
30 May 2024W3C Baggage published as a Candidate Recommendation SnapshotW3C
24 January 2025Semantic conventions v1.30.0 released; the Python OpenAI instrumentation still defaults to this GenAI shape unless you opt in to newer conventionssemantic-conventions releases; instrumentation README
25 August 2025v1.37.0: gen_ai.system renamed to gen_ai.provider.name; per message events replaced by gen_ai.system_instructions, gen_ai.input.messages and gen_ai.output.messagessemantic-conventions changelog
5 May 2026open-telemetry/semantic-conventions-genai repository createdGitHub API
12 June 2026v1.42.0: all gen_ai.*, OpenAI and MCP definitions deprecated in the core repository and moved to the GenAI repositorysemantic-conventions changelog
4 August 2026Core semantic conventions v1.44.0, the latest core release at the time of writingsemantic-conventions releases
23 October 2026opentelemetry-instrumentation-genai-openai 1.3b0 published to PyPIPyPI

What this means for you:

  1. Nothing in the GenAI set is Stable. Every span, attribute and metric carries the Development badge. Expect renames. The v1.37.0 change from gen_ai.system to gen_ai.provider.name broke many dashboards, and the move to new message attributes broke anything that parsed the old per message events.
  2. Instrumentations pin to a version. The Python instrumentation README says that without an opt in it aligns with v1.30.0. To get the current shape, set OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental. Two services with different settings will send different attribute names for the same thing.
  3. The schema URL is not settled. At the time of writing, the new repository's README lists the schema URL as "TODO", and the v1.42.0 changelog tells instrumentations to refer to the new repository for the schema_url to use.
  4. Old links mislead. The docs/gen-ai folder in the core repository now holds short "Moved" stubs. If a blog post or internal wiki links there, it is pointing at a frozen copy.

The practical rule: record which conventions version each service emits, and write queries that tolerate old and new names until every service has moved.

Which attributes describe a model call?

The core span is gen_ai.client.inference. It represents a client call to a model or service that generates a response or requests a tool call. It covers streaming and non streaming variants of chat, completions, responses and multimodal generation APIs.

  • Span name: {gen_ai.operation.name} {gen_ai.request.model}, for example chat my-model-1.
  • Span kind: CLIENT. It may be INTERNAL when the model runs in the same process.
  • Duration: from when the operation starts until the response is fully received or the call fails or is cancelled. Automatic retries belong inside the same span.

The attributes, grouped by purpose

PurposeAttributeRequirement levelNotes
What kind of callgen_ai.operation.nameRequiredchat, generate_content, text_completion and so on
Who served itgen_ai.provider.nameRequiredSet from the instrumentation's best knowledge; a proxy may hide the real upstream
What was asked forgen_ai.request.modelConditionally Required, if availableThe exact model name requested
What answeredgen_ai.response.modelRecommendedOften a dated or versioned name, different from the request
Response identitygen_ai.response.idRecommendedLets you join the span to provider side logs
Why it stoppedgen_ai.response.finish_reasonsRecommendedArray; well known values include stop, length, content_filter, tool_call, compaction, error
Request parametersgen_ai.request.max_tokens, temperature, top_p, top_k, stop_sequences, frequency_penalty, presence_penalty, seed, choice.count, stream, reasoning.levelMostly RecommendedSome are Conditionally Required when present in the request
Token usagegen_ai.usage.input_tokens, gen_ai.usage.output_tokensRecommendedPlus cache read, cache write, reasoning, and per modality (text, image, audio) breakdowns
Streaminggen_ai.response.time_to_first_chunkRecommended for streaming requestsSeconds to the first chunk
Conversationgen_ai.conversation.idConditionally RequiredOnly when a real conversation identifier exists
Prompt templatesgen_ai.prompt.name, gen_ai.prompt.versionConditionally Required when a named template is usedgen_ai.prompt.variable is Opt-In
Output typegen_ai.output.typeConditionally Requiredtext, json, image
Endpointserver.address, server.portRecommended / Conditionally RequiredShould show the server behind any proxy, if known
Failureerror.typeConditionally Required on errorA low cardinality error class or provider error code
Contentgen_ai.system_instructions, gen_ai.input.messages, gen_ai.output.messages, gen_ai.tool.definitionsOpt-InNot recorded unless the user turns content capture on

Three details in the specification are worth reading twice.

Sampling attributes come first. The specification lists gen_ai.operation.name, gen_ai.provider.name, gen_ai.request.model, server.address and server.port as attributes that should be set at span creation time, because head samplers only see what exists when the span starts. If you add the model name after the call, a sampler that keeps "all calls to the expensive model" cannot see it.

The provider attribute is a discriminator, not proof. The specification says gen_ai.provider.name should be set from the instrumentation's best knowledge and may differ from the real upstream, for example when a client SDK talks to a proxy that relays to another provider. It also decides which provider specific attributes may appear. A span with gen_ai.provider.name set to aws.bedrock should carry aws.bedrock.* attributes and not openai.* ones. For security reviews, this means a trace tells you which SDK your code used, not which company processed the data. Data residency questions still need your gateway or proxy logs.

Conversation IDs must be real. The specification says that when no conversation identifier exists, instrumentations should not invent one. A new UUID, the trace ID or a hash of the request content are all named as things that should not be used as a fallback. This matters for privacy: a hash of the prompt is a fingerprint of the prompt.

Worked example: token usage arithmetic

The specification gives an example for a request with 100 text tokens, 40 of them cached, and 200 image tokens. The totals are supersets of the details:

AttributeValueWhy
gen_ai.usage.input_tokens300100 text + 200 image, cached tokens included
gen_ai.usage.cache_read.input_tokens40The cached part, already inside the 300
gen_ai.usage.text.input_tokens100Text only
gen_ai.usage.text.cache_read.input_tokens40Cached text
gen_ai.usage.image.input_tokens200Image only

Two consequences follow. First, never add cache_read to input_tokens when you compute cost; it is already counted. Second, when a provider reports both billed units and raw model tokens, the specification says to report the billed count so the number matches what you are charged for.

A second example from the walkthrough later in this post: an agent makes two chat calls. The first uses 412 input and 38 output tokens and ends with finish reason tool_call. The second uses 471 input and 22 output tokens and ends with stop. The agent span sums them: 883 input tokens and 60 output tokens. The second call's input is larger because it carries the first question, the model's tool call and the tool result. That growth per turn is why long agent runs get expensive, and why token metrics per agent invocation are worth having.

Metrics and events for model calls

Spans describe one call; metrics describe all of them cheaply. The metrics documents define, among others:

MetricWhat it measures
gen_ai.client.inference.durationDuration of inference calls from the client side
gen_ai.client.inference.time_to_first_chunkTime to the first streamed chunk
gen_ai.client.inference.time_per_output_chunkTime between streamed chunks
gen_ai.client.inference.usage.input_tokens and .output_tokensToken counts, with separate cache read, cache write and reasoning variants
gen_ai.client.inference.operation.input_tokens and .output_tokensPer operation token counts, recorded for every operation including failed ones (0 when no response arrived)
gen_ai.client.operation.durationDuration of other client GenAI operations
gen_ai.server.request.duration, gen_ai.server.time_to_first_token, gen_ai.server.time_per_output_tokenThe same view from a model server you run yourself
gen_ai.invoke_agent.duration, gen_ai.invoke_agent.inference_calls, gen_ai.invoke_agent.tool_callsHow long agent invocations take and how many model and tool calls each makes
gen_ai.invoke_workflow.duration, gen_ai.execute_tool.durationWorkflow and tool durations

Duration histograms use explicit bucket boundaries from 0.01 seconds doubling up to 81.92 seconds. That range fits model calls, which can take tens of seconds, much better than default HTTP buckets.

The main event is gen_ai.client.inference.operation.details. It carries the request parameters and, when content capture is on, the chat history and output. The specification says it can store input and output details independently from traces and should be logged at DEBUG severity (severity number 5). There is also gen_ai.evaluation.result for quality scores from an evaluator, parented to the span being evaluated, and gen_ai.client.operation.exception for errors.

The events route matters for security. Events travel through your logs pipeline, which may have different retention, different access controls and a different backend from your traces. Decide on purpose where content goes.

How are agent and tool executions represented?

An agent is a loop: think, call a model, maybe call a tool, feed the result back, repeat. The agent spans document gives each part of that loop a span.

SpanOperation nameKindSpan nameWhen it is used
Create agentcreate_agentCLIENTcreate_agent {gen_ai.agent.name}Registering an agent with a remote agent service
Invoke agent (client)invoke_agentCLIENTinvoke_agent {gen_ai.agent.name}Calling an agent that runs as a remote service
Invoke agent (internal)invoke_agentINTERNALinvoke_agent {gen_ai.agent.name}Running an agent inside your own process, through a framework
Invoke workflowinvoke_workflowINTERNALinvoke_workflow {gen_ai.workflow.name}A multi step or multi agent workflow
PlanplanINTERNALplan {gen_ai.agent.name}A planning or task decomposition phase
Execute toolexecute_toolINTERNALexecute_tool {gen_ai.tool.name}One tool call
Load skill, read skill resourceexecute_tool refinementsINTERNALexecute_tool {gen_ai.tool.name} {gen_ai.skill.name} and variantsAgent skills loaded or read as tools
Command executionexecute_tool refinementINTERNALexecute_tool {gen_ai.tool.name} {process.executable.name} and variantsA tool that runs shell commands or skill scripts

When the agent name is not available, the invoke and plan span names fall back to the bare operation name.

The invoke agent spans carry the same request and usage attributes as an inference span, plus agent identity: gen_ai.agent.name, gen_ai.agent.id, gen_ai.agent.description and gen_ai.agent.version. Like the inference span, they list gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions and gen_ai.tool.definitions as Opt-In.

The execute tool span in detail

The execute tool span is where agents touch the real world, so it is the span security teams care about most.

AttributeRequirement levelWhat it holds
gen_ai.operation.nameRequiredexecute_tool
gen_ai.tool.nameRequiredFor example get_weather
gen_ai.tool.call.idConditionally Required, only if the model supplied oneJoins the tool span to the model's tool call
gen_ai.tool.typeRecommendedfunction (client side), extension (agent side call to external APIs) or datastore (retrieval)
gen_ai.tool.descriptionRecommendedMarked in the specification as possibly sensitive
gen_ai.agent.name, gen_ai.conversation.idConditionally RequiredWhich agent ran it, in which conversation
gen_ai.tool.call.argumentsOpt-InThe arguments the model chose, marked as possibly sensitive
gen_ai.tool.call.resultOpt-InThe tool's return value, marked as possibly sensitive
error.typeConditionally Required on errorWhy the tool failed

The specification asks framework instrumentations to trace tool calls, and encourages application developers to follow the same convention when their own code runs tools. It also warns against two spans for one call: if a framework and an MCP instrumentation both see the same tool call, only one should record it as the tool span.

The command execution refinement is new and important for coding agents. It applies when a tool runs one or more commands, including a skill's script. The specification gives examples from several agent SDKs, including a client bash tool in Anthropic's tool use documentation. It also says instrumentations may add CLI client spans for each observed command or process. That gives you a trace record of which commands an agent ran, which is useful for incident review and very sensitive in its own right, since command lines often contain file paths, hostnames and sometimes credentials.

What a full agent trace looks like

A simple run of an in-process agent that answers one question with one tool call produces a tree like this:

invoke_agent weather-agent          INTERNAL   gen_ai.usage.input_tokens=883
├── chat demo-model-1               CLIENT     finish_reasons=["tool_call"]  in=412 out=38
├── execute_tool get_weather        INTERNAL   gen_ai.tool.call.id=call_001
└── chat demo-model-1               CLIENT     finish_reasons=["stop"]       in=471 out=22

If the HTTP client library is also instrumented, each chat span gets a child HTTP span for the actual request. If the tool is an MCP tool, the execute_tool span (or the MCP client span, but not both) leads to an MCP server span in another process, joined by trace context.

Claude Code is a real example of this structure, with its own span names. Its monitoring documentation describes a claude_code.interaction root span for each user prompt, with claude_code.llm_request, claude_code.hook and claude_code.tool children. Each tool span has two children: claude_code.tool.blocked_on_user for the time spent waiting on a permission decision and claude_code.tool.execution for the work itself. Subagent spans nest under the parent's tool span. Tracing there is a beta feature and is off by default: you enable it with CLAUDE_CODE_ENABLE_TELEMETRY=1 and CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 and choose an exporter with OTEL_TRACES_EXPORTER.

How do traces link across MCP hops?

Trace context normally travels in HTTP headers. The W3C Trace Context traceparent header carries a version, a 16 byte trace ID, an 8 byte parent ID and trace flags, such as 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01. A server that receives it creates its span as a child.

MCP breaks that model in two ways. It runs over stdio as well as HTTP, and stdio has no headers. And with Streamable HTTP, one HTTP request can carry several MCP messages, while one MCP request can span several HTTP requests if it is retried. The MCP conventions therefore say:

  • Instrumentations should inject trace context into each MCP request or notification, in the params._meta property bag.
  • The keys are written unprefixed: traceparent, tracestate and baggage, even though MCP normally expects DNS prefixed keys in _meta. The MCP proposal SEP-414 defines this handling.
  • The MCP server should use the context from params._meta as the parent of its server span, and should link (not parent) any ambient transport context, such as the HTTP request's own trace.
  • The MCP client span becomes the parent of the MCP server span regardless of transport.

A tool call request with context looks like this:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get-weather",
    "arguments": { "city": "Lisbon" },
    "_meta": {
      "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
    }
  }
}

MCP spans are named {mcp.method.name} {target}, with kind CLIENT on the client and SERVER on the server. The conventions recommend these MCP spans over the generic RPC conventions, because they record message exchanges that RPC and HTTP spans miss. There are also four metrics: mcp.client.operation.duration, mcp.server.operation.duration, mcp.client.session.duration and mcp.server.session.duration.

Claude Code follows the same idea outward. Its documentation says outbound HTTP MCP requests carry traceparent, model requests to the Anthropic API carry a traceparent header set to the claude_code.llm_request span's context, and commands Claude Code runs receive a TRACEPARENT environment variable so their own spans join the same trace. In Agent SDK and non interactive -p sessions it also reads TRACEPARENT and TRACESTATE from its environment, while interactive sessions ignore inbound TRACEPARENT to avoid inheriting stray values from CI or container environments. That last choice is a good pattern to copy.

And across A2A hops?

The Agent2Agent (A2A) protocol runs over HTTP based transports. Its specification lists tracing among the enterprise practices it aligns with, but defines no protocol field for trace context, and the GenAI repository has no A2A document today. In practice you use standard W3C Trace Context on the HTTP request: the caller's invoke_agent client span is the parent of the remote agent's server spans. If a hop strips headers, agree on your own carrier and document it.

How are prompts and completions captured, and why is content capture opt in?

The specification is blunt: model instructions, user messages and model outputs are considered sensitive and are often large. It names three problems with recording them in telemetry: storage cost, regulatory requirements, and the need for different access models for operational data and user data. So instrumentations should not capture content by default and should offer an opt in.

It then describes three patterns, in order of maturity:

PatternWhat is recordedWhere the specification says it fits
1. DefaultNo instructions, inputs or outputsEverywhere, unless you have a reason
2. Content on spansgen_ai.system_instructions, gen_ai.input.messages, gen_ai.output.messages as span attributesWhen volume is manageable and privacy rules do not apply or the store complies with them, for example pre-production
3. External storage with referencesContent uploaded elsewhere; the span records a referenceRecommended for production when volume is a concern or sensitive data must be handled securely, because external storage allows separate access controls

The message format

When content is captured, it follows JSON schemas published with the conventions. Messages have a role (system, user, assistant, tool) and a list of parts. Part types include text, tool_call, tool_call_response, server_tool_call, server_tool_call_response, reasoning, compaction, blob, file and uri. A user turn and a tool round trip look like this:

[
  {"role": "user", "parts": [{"type": "text", "content": "What is the weather in Lisbon?"}]},
  {"role": "assistant", "parts": [{"type": "tool_call", "id": "call_001",
    "name": "get_weather", "arguments": {"city": "Lisbon"}}]},
  {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_001",
    "response": {"temp_c": 21, "conditions": "sunny"}}]}
]

Note the reasoning part type. If your model returns reasoning text and content capture is on, that text lands in telemetry too. Reasoning often restates the user's data in new words, which defeats redaction rules that only look for the original strings.

Structured attribute values are supported on events (logs) but may not yet be supported on spans in every language. In that case the specification says to serialize the value to a JSON string on spans and keep the structured form on events. It also allows a truncation option that shortens individual message contents while keeping the JSON structure.

The upload hook

For pattern 3, instrumentations may support an in-process hook. The details matter:

  • The hook works independently of the content capture flags.
  • If configured, it is called regardless of the span sampling decision, with the instructions, inputs and outputs before they are serialized, plus the span.
  • The hook may modify the span and the messages. If content attributes are also enabled, they are recorded after the hook runs, using the modified values.

That last point makes the hook the right place for in-process redaction: replace an email address in the message objects, and both the uploaded copy and any span attribute get the redacted version. The "regardless of sampling" point cuts the other way: a 1 percent trace sample does not mean 1 percent of prompts are uploaded. Every prompt goes to the upload target.

How real instrumentations expose the opt in

The OpenTelemetry Python GenAI instrumentations use one environment variable, OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT. In the current opentelemetry-instrumentation-genai-openai package the values are no_content (the default), span_only, event_only and span_and_event. The upload hook is enabled with OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload and a destination in OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH, which accepts a local path or an fsspec compatible URI.

Packaging has moved too. The older opentelemetry-instrumentation-openai-v2 package is marked deprecated and only receives security patches; its README points to opentelemetry-instrumentation-genai-openai, in a separate opentelemetry-python-genai repository, with breaking changes. In the older package, true was a legacy value that put content on the old per message events, so an upgrade can move content between your logs and traces pipelines.

Claude Code has its own switches, which its documentation describes in detail. By default, user prompt events record only the prompt length. OTEL_LOG_USER_PROMPTS=1 adds prompt text, and the documentation notes that the text then appears in two attributes, prompt and prompt_text, so a collector rule must drop both. OTEL_LOG_ASSISTANT_RESPONSES adds response text, OTEL_LOG_TOOL_DETAILS adds tool parameters such as Bash commands and MCP tool names, and OTEL_LOG_RAW_API_BODIES emits full API request and response bodies, which the documentation says include the entire conversation history. The documentation also states that this telemetry goes only to the OpenTelemetry endpoint you configure, never to Anthropic.

What are the privacy and security risks?

Turning content capture on changes what your observability stack is. It stops being a store of timings and counters and becomes a store of conversations. OWASP's 2025 Top 10 for LLM applications lists sensitive information disclosure as LLM02 and names personal data, financial details, health records, confidential business data, security credentials and legal documents among the information at risk. Every one of those can arrive in a prompt.

Personal data in prompts and tool results

Users paste names, addresses, account numbers and support histories into assistants. Retrieval tools pull customer records into context. Tool results return database rows. With content capture on, all of it is copied into spans or events and shipped to wherever your telemetry goes. The OpenTelemetry project's own guidance on handling sensitive data starts with data minimisation: only collect data that serves an observability purpose, avoid personal information unless it is truly necessary, and regularly review collected attributes.

Secrets in prompts, arguments and command lines

Developers paste API keys and connection strings into coding assistants to ask why a call fails. Agents read configuration files and environment variables into context. The command execution span and tool argument attributes can hold full command lines. None of this is hypothetical; it is how people use these tools. Treat any content attribute as able to contain credentials, and make sure your trace store is not readable by more people than the systems those credentials unlock.

Retention and copies

Telemetry is copied often: SDK buffers, the collector's sending queue, debug file exporters, hot and cold backend storage, and analysis exports. Each copy has its own retention, so a prompt your application deletes after 30 days may live for a year in a trace archive. If you have deletion obligations, map every place a span can land.

Multi tenant collectors and backends

A shared collector that serves many teams or many customers is a mixing point. Without care, one tenant's prompts are visible in another tenant's trace search, or a single debug exporter writes everyone's content to one file. Keep content bearing pipelines separate, tag every span with a tenant attribute at the source, route by it, and check that your backend's access controls actually filter on it.

Trace context as an information channel

Trace context itself carries some risk, and the W3C specification is explicit about it:

  • Correlation. traceparent lets a downstream service correlate requests in one transaction. The specification calls this risk theoretical, but allows services to restart the trace at a boundary.
  • Information in tracestate. The specification warns that tracestate may reveal things like the monitoring software version, and says application owners should either keep confidential information out of it or not send it to external systems.
  • Baggage. W3C Baggage propagates key value pairs to every downstream service, including third party model and tool endpoints. The MCP conventions' own example shows userId=alice in baggage. That is fine inside your network and a leak when the next hop is someone else's MCP server.
  • Denial of service. The specification notes that a public API which naively continues every incoming trace with the sampled flag set lets an attacker overwhelm it with tracing overhead. Agents that accept requests from the internet should not trust incoming sampling decisions blindly.

Telemetry as an injection surface

Content attributes hold text that a user or a model wrote. Dashboards and trace viewers render it. Treat that text as untrusted: a trace viewer that renders message content as HTML has the same problems as any other page that renders user input. And anything that reads traces back into a model, such as an "explain this trace" feature, is reading attacker controllable text. That is prompt injection by way of your observability stack.

How do redaction and filtering work in the collector?

The specification's ordering is the right one: do not capture what you do not need, redact in process where you can, and use the collector as a second line. The Collector's own redaction processor README says the processor is intended as "one line of defence rather than the only compliance measure in place."

There are five processors to know. All are in the contrib distribution.

ProcessorStability (traces)What it doesUse it for
redactionBeta (logs and metrics alpha)Deletes attributes not on an allowlist; masks or hashes values that match blocked patternsFail closed allowlists; masking card numbers, emails, key shaped strings
transformBetaRuns OTTL statements on spans, logs and metricsDeleting specific keys, regex replacement, truncation, conditional edits
attributesBetaInsert, update, delete or hash attributes by key or regexSimple deletes and hashing
filterAlphaDrops whole spans, logs or metric points that match conditionsDropping noisy or content heavy records entirely
tail_samplingBetaBuffers whole traces and decides which to keepKeeping errors and slow runs, sampling the rest

The redaction processor

Its design is allowlist first. If allowed_keys is empty and allow_all_keys is false, every attribute is removed. The README describes this as designed to fail closed. On the attributes that remain, blocked_values regular expressions mask matching parts of values, and blocked_key_patterns mask whole values whose keys match. allowed_values takes precedence over blocked_values, which is useful and also easy to get wrong.

Instead of masking with asterisks, you can set hash_function to md5, sha1, sha3, hmac-sha256 or hmac-sha512. The README recommends HMAC for low entropy data such as IP addresses, because a plain hash of a small input space can be reversed by trying every value. The same applies to email addresses and user IDs.

The summary option controls the diagnostic attributes the processor adds about what it removed. The README points out that in some contexts a list of redacted attribute names leaks information, so silent or info is safer in production than debug.

One detail for the logs pipeline: the processor also handles log records whose body is a map, and reports how many body keys it removed or masked. If an instrumentation puts content in a plain string body instead, test the processor against your real records, or handle the body with the transform processor.

The transform processor

OTTL statements are the precise tool for GenAI content. To drop all content attributes from spans:

processors:
  transform/drop-genai-content:
    error_mode: ignore
    trace_statements:
      - delete_key(span.attributes, "gen_ai.system_instructions")
      - delete_key(span.attributes, "gen_ai.input.messages")
      - delete_key(span.attributes, "gen_ai.output.messages")
      - delete_key(span.attributes, "gen_ai.tool.definitions")
      - delete_matching_keys(span.attributes, "^gen_ai\\.tool\\.call\\.(arguments|result)$")

To keep content but mask email addresses and shorten long values:

processors:
  transform/mask-genai-content:
    error_mode: ignore
    trace_statements:
      - replace_pattern(span.attributes["gen_ai.input.messages"], "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}", "[email]")
      - replace_pattern(span.attributes["gen_ai.output.messages"], "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}", "[email]")
      - truncate_all(span.attributes, 4096)

Be careful with truncate_all on JSON strings: it cuts at a byte count and can leave invalid JSON. The specification's own truncation option works inside the instrumentation and preserves structure, so prefer that where it exists. Also note that error_mode: ignore (the default when unset) means a broken statement is skipped with a log line rather than stopping the pipeline. That keeps telemetry flowing, and it also means a typo in a redaction rule fails open. Test rules with sample data before you rely on them.

What should be sampled or dropped?

DataKeep at full rateSampleDrop or keep out of the pipeline
GenAI metrics (durations, token counts)Yes, they are cheap and aggregateNoNo
Inference and tool spans without contentErrors, slow runs, unusual finish reasons (content_filter, length, error)Successful routine runsNo
Content attributes in productionOnly with a written purpose and short retentionNot a substitute for redactionBy default
Content in pre-productionAcceptable with synthetic or test dataOptionalReal customer data
Tool arguments and resultsWhen needed for incident review, behind restricted accessOptionalArguments from tools that touch credentials or files
Raw API bodies and full command linesRarely; time boxed debugging onlyNoBy default
tracestate and baggage to external endpointsNoNoStrip at the boundary

Tail sampling is the natural fit for agents, because you only know whether a run was interesting at the end. The tail sampling processor buffers spans per trace (decision_wait defaults to 30 seconds and num_traces to 50,000) and then applies policies such as status_code, latency and probabilistic. Two cautions: agent runs that last longer than decision_wait get decided before they finish, and the buffer holds content in memory if content capture is on. Redact before tail sampling, not after.

Step by step: instrument a small agent, export to a local collector and redact content

This walkthrough builds a tiny agent with one tool, instruments it by hand with the GenAI conventions, sends traces to a local Collector over OTLP, and removes content in the Collector. The model call is a stand-in function, so it runs offline without an API key. When you use a real provider SDK, an instrumentation package can create the inference spans for you; the attributes are the same.

Step 1: create an environment and install the SDK

python3 -m venv .venv
. .venv/bin/activate
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
pip list | grep opentelemetry

We ran this with Python 3.14 and got opentelemetry-sdk 1.45.1. Pin exact versions in a lock file for anything you deploy.

Step 2: write the agent

Save this as agent.py. Content capture is controlled by AGENT_CAPTURE_CONTENT and is off unless you set it, which mirrors the convention's default.

import json
import os
import time

from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.trace import SpanKind, Status, StatusCode

CAPTURE = os.getenv("AGENT_CAPTURE_CONTENT", "false").lower() == "true"

provider = TracerProvider(resource=Resource.create({"service.name": "weather-agent"}))
if os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
    from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
    provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
else:
    provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("weather-agent", "0.1.0")

MODEL = "demo-model-1"

def fake_model(messages):
    """Stand-in for a provider SDK call, so the example runs offline."""
    time.sleep(0.05)
    if messages[-1]["role"] == "user":
        return {"id": "resp_001", "model": MODEL + "-2026-01",
                "finish_reason": "tool_call", "usage": {"in": 412, "out": 38},
                "tool_call": {"id": "call_001", "name": "get_weather",
                              "arguments": {"city": "Lisbon"}}}
    return {"id": "resp_002", "model": MODEL + "-2026-01",
            "finish_reason": "stop", "usage": {"in": 471, "out": 22},
            "text": "It is 21 C and sunny in Lisbon."}

def chat(messages):
    with tracer.start_as_current_span(
        f"chat {MODEL}",
        kind=SpanKind.CLIENT,
        attributes={
            "gen_ai.operation.name": "chat",
            "gen_ai.provider.name": "demo",
            "gen_ai.request.model": MODEL,
            "gen_ai.request.max_tokens": 512,
            "server.address": "llm.internal.example",
            "server.port": 443,
        },
    ) as span:
        if CAPTURE:
            span.set_attribute("gen_ai.input.messages", json.dumps(messages))
        r = fake_model(messages)
        span.set_attribute("gen_ai.response.id", r["id"])
        span.set_attribute("gen_ai.response.model", r["model"])
        span.set_attribute("gen_ai.response.finish_reasons", [r["finish_reason"]])
        span.set_attribute("gen_ai.usage.input_tokens", r["usage"]["in"])
        span.set_attribute("gen_ai.usage.output_tokens", r["usage"]["out"])
        if CAPTURE and "text" in r:
            span.set_attribute("gen_ai.output.messages", json.dumps(
                [{"role": "assistant", "parts": [{"type": "text", "content": r["text"]}]}]))
        return r

def get_weather(call):
    with tracer.start_as_current_span(
        "execute_tool get_weather",
        kind=SpanKind.INTERNAL,
        attributes={
            "gen_ai.operation.name": "execute_tool",
            "gen_ai.tool.name": "get_weather",
            "gen_ai.tool.type": "function",
            "gen_ai.tool.call.id": call["id"],
        },
    ) as span:
        if CAPTURE:
            span.set_attribute("gen_ai.tool.call.arguments", json.dumps(call["arguments"]))
        result = {"temp_c": 21, "conditions": "sunny"}
        if CAPTURE:
            span.set_attribute("gen_ai.tool.call.result", json.dumps(result))
        return result

def run(question):
    with tracer.start_as_current_span(
        "invoke_agent weather-agent",
        kind=SpanKind.INTERNAL,
        attributes={
            "gen_ai.operation.name": "invoke_agent",
            "gen_ai.provider.name": "demo",
            "gen_ai.agent.name": "weather-agent",
            "gen_ai.request.model": MODEL,
        },
    ) as agent_span:
        user = {"role": "user", "parts": [{"type": "text", "content": question}]}
        try:
            first = chat([user])
            call = first["tool_call"]
            result = get_weather(call)
            final = chat([user,
                          {"role": "assistant", "parts": [{"type": "tool_call", "id": call["id"],
                                                           "name": call["name"],
                                                           "arguments": call["arguments"]}]},
                          {"role": "tool", "parts": [{"type": "tool_call_response",
                                                      "id": call["id"], "response": result}]}])
            agent_span.set_attribute("gen_ai.usage.input_tokens",
                                     first["usage"]["in"] + final["usage"]["in"])
            agent_span.set_attribute("gen_ai.usage.output_tokens",
                                     first["usage"]["out"] + final["usage"]["out"])
            return final["text"]
        except Exception as exc:
            agent_span.set_status(Status(StatusCode.ERROR))
            agent_span.set_attribute("error.type", type(exc).__name__)
            raise

if __name__ == "__main__":
    print(run("What is the weather in Lisbon? My email is jane@example.com"))
    provider.shutdown()

A few choices in this code follow the specification directly:

  • The sampling relevant attributes (gen_ai.operation.name, gen_ai.provider.name, gen_ai.request.model, server.address, server.port) are passed when the span is created, not set afterwards.
  • gen_ai.provider.name is demo because the stand-in is not a real provider. Use a well known value such as openai or anthropic only when that is the SDK you are calling.
  • Messages use the role plus parts shape from the conventions' JSON schemas, serialized to a JSON string on spans.
  • The tool span records gen_ai.tool.call.id only because the model supplied one.
  • gen_ai.conversation.id is not set, because this agent has no real conversation identifier.

Step 3: run it with the console exporter

python agent.py

With no OTLP endpoint set, the script prints spans to the console. You should see four spans: one invoke_agent weather-agent root, two chat demo-model-1 children of kind CLIENT, and one execute_tool get_weather child. In our run, the first chat span showed 412 input and 38 output tokens with finish reason tool_call, the second 471 and 22 with stop, and the agent span 883 input tokens in total. No message content appears.

Now turn capture on and look again:

AGENT_CAPTURE_CONTENT=true python agent.py | grep -E 'input.messages|output.messages|tool.call'

The gen_ai.input.messages attribute now contains the full question, including the email address the user typed. The tool arguments and result appear as well. This is exactly the data the rest of the walkthrough removes.

Step 4: write a collector configuration that redacts

Save this as otelcol.yaml. It receives OTLP over HTTP, deletes content attributes, masks email addresses in anything that survives, and prints the result.

receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
  transform/drop-genai-content:
    error_mode: ignore
    trace_statements:
      - delete_key(span.attributes, "gen_ai.system_instructions")
      - delete_key(span.attributes, "gen_ai.input.messages")
      - delete_key(span.attributes, "gen_ai.output.messages")
      - delete_key(span.attributes, "gen_ai.tool.definitions")
      - delete_matching_keys(span.attributes, "^gen_ai\\.tool\\.call\\.(arguments|result)$")
  redaction/genai:
    allow_all_keys: true
    blocked_values:
      - "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
    summary: info
  batch: {}

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, transform/drop-genai-content, redaction/genai, batch]
      exporters: [debug]

allow_all_keys: true is used here so the redaction processor only masks values; the transform processor has already removed the content keys. For a stricter setup, set allow_all_keys: false and list every attribute you want to keep in allowed_keys. That fails closed when a new instrumentation version adds an attribute you have not reviewed.

Step 5: run the collector

Run the contrib distribution, which includes the transform and redaction processors. With a container runtime:

docker run --rm -p 127.0.0.1:4318:4318 \
  -v "$PWD/otelcol.yaml:/etc/otelcol-contrib/config.yaml:ro" \
  otel/opentelemetry-collector-contrib:0.163.2

The receiver listens on all interfaces inside the container, and the 127.0.0.1: prefix on -p keeps the port reachable only from your own machine. If you run the Collector binary directly instead, change the receiver endpoint to 127.0.0.1:4318 so it is not exposed to your network. Version 0.163.2 was the latest Collector release on 21 October 2026. Pin by digest in real deployments. If you installed the otelcol-contrib binary instead, validate and run with:

otelcol-contrib validate --config=otelcol.yaml
otelcol-contrib --config=otelcol.yaml

Step 6: send traces with content capture on, and check they are clean

export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
AGENT_CAPTURE_CONTENT=true python agent.py

The Python OTLP HTTP exporter appends /v1/traces to the base endpoint. In the Collector's debug output you should see the same four spans with token counts, finish reasons and tool names, and no gen_ai.input.messages, gen_ai.output.messages or tool argument attributes. If the user had typed an email address into an attribute you kept on purpose, it would show as asterisks.

Test the failure case too: remove the transform processor from the pipeline, restart, and run again. The content returns, which proves the rule is doing the work. Turn that into an automated test.

Step 7: move redaction into the process

The Collector is the second line. The first is the application. In this example, the simplest in-process control is the flag itself. With an instrumentation package, keep OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT at no_content in production, and if you need content for evaluation, use the upload hook to send redacted copies to a separate store with its own access policy. Content that never leaves the process cannot leak from the pipeline.

GenAI spans, HTTP client spans and vendor specific LLM logs compared

Many teams already have some LLM telemetry before they adopt the GenAI conventions. Here is how the three common sources compare by what they record.

What you want to knowOrdinary HTTP client spanVendor specific LLM log or SDK eventGenAI semantic convention spans
That a call happened, its duration and statusYesYesYes
Endpoint host and portYesUsuallyYes, server.address and server.port
Which model was requested and which answeredNo, unless parsed from the bodyYes, in the vendor's field namesYes, gen_ai.request.model and gen_ai.response.model
Token usage with cache and reasoning breakdownNoYes, vendor specificYes, standard names and arithmetic rules
Why generation stoppedNoYesYes, gen_ai.response.finish_reasons
Agent, workflow and tool structureNo, just a list of HTTP callsSometimes, framework specificYes, invoke_agent, plan, execute_tool
Cross process linkage over MCP stdioNoNoYes, context in params._meta
Prompt and completion textNo (bodies are not recorded by HTTP conventions)Often on by default or by a single switchOpt-In only, with a defined schema
Portable across providers and backendsYesNoYes, but at Development stability
Main riskToo little contextContent captured by default; lock inVersion churn; content captured once someone flips the flag

Keep HTTP spans (they show retries, TLS errors and proxies), add GenAI spans as their parents, and stop relying on vendor specific logs for cross provider queries.

Common mistakes

  • Treating Development as Stable. Building alerts on attribute names without recording which convention version each service emits. The gen_ai.system to gen_ai.provider.name rename and the message attribute change will not be the last.
  • Linking to the old docs. The core repository's docs/gen-ai pages are now "Moved" stubs. Use the GenAI repository or the opentelemetry.io pages generated from it.
  • Turning content capture on in production to debug one issue, and leaving it on. Time box it, scope it to one service, and put an expiry in the ticket.
  • Dropping only one of two copies. Content may be on spans, on events, or both, depending on span_only, event_only or span_and_event. Claude Code puts prompt text in both prompt and prompt_text. Redact every copy.
  • Assuming sampling limits content exposure. Upload hooks run regardless of the sampling decision, and tail samplers buffer everything before they decide.
  • Hashing low entropy values with plain hashes. A SHA hash of an email address or IP is reversible by guessing. Use HMAC with a secret key, or drop the value.
  • Shipping redaction rules without a test. allowed_values overrides blocked_values, and error_mode: ignore skips a broken OTTL statement while data keeps flowing. Send a known sensitive string through the pipeline and fail the build if it comes out.
  • Inventing conversation IDs. Using the trace ID or a hash of the prompt as gen_ai.conversation.id is explicitly discouraged, and the hash is a fingerprint of the content.
  • Propagating baggage to third party tools. User identifiers in baggage travel to every MCP server and model endpoint downstream. Strip it at the boundary.
  • Reading gen_ai.provider.name as proof of where data went. It reflects the SDK, not the upstream behind a proxy.
  • Rendering message content as HTML in internal tools. Trace viewers and evaluation dashboards should treat content as untrusted text.

FAQ

Are the OpenTelemetry GenAI semantic conventions stable?

No. As of 24 October 2026, every GenAI document, span, metric and attribute is marked Development. Instrumentations usually need OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental to emit the newest shape, and the Python OpenAI instrumentation README warns that experimental features will bring breaking changes.

Where do the GenAI semantic conventions live now?

In the open-telemetry/semantic-conventions-genai GitHub repository. Semantic conventions v1.42.0 deprecated the GenAI, OpenAI and MCP definitions in the core repository and moved them there. The opentelemetry.io specification pages for GenAI are still the readable entry point.

What replaced gen_ai.system?

gen_ai.provider.name, from semantic conventions v1.37.0. Well known values include openai, anthropic, aws.bedrock, azure.ai.openai and gcp.vertex_ai, among others.

Does OpenTelemetry record my prompts by default?

Instrumentations that follow the conventions should not. Instructions, input messages, output messages, tool definitions, tool arguments and tool results are Opt-In. Check each library you use, because a library that does not follow the conventions may behave differently.

How do I turn on prompt capture in the Python GenAI instrumentation?

Set OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT to span_only, event_only or span_and_event. The default is no_content. For production, the conventions recommend storing content externally with references on spans, which the upload hook supports.

How does trace context pass through an MCP server?

The client injects traceparent, tracestate and baggage into the request's params._meta, unprefixed. The server uses that context as the parent of its server span and links any transport level context. This works over stdio, where there are no HTTP headers.

How do I remove prompts from traces in the OpenTelemetry Collector?

Use the transform processor with delete_key on gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions and gen_ai.tool.definitions, and delete_matching_keys for tool arguments and results. Add the redaction processor with an allowlist for a fail closed second layer, and remember to handle events in the logs pipeline as well.

Should I record token usage on spans or metrics?

Both, for different jobs. Metrics are cheap and good for cost and capacity dashboards. Span attributes let you find the single run that used 200,000 tokens. Remember that gen_ai.usage.input_tokens already includes cached tokens.

Can I trace Claude Code with OpenTelemetry?

Yes. Its documentation describes metrics and events, and a beta tracing mode enabled with CLAUDE_CODE_ENABLE_TELEMETRY=1, CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 and OTEL_TRACES_EXPORTER. Prompt text, response text, tool details and raw API bodies each have their own switch and are off by default.

Where Safeguard fits

Safeguard is not an observability product. It does not collect traces, run a collector or redact telemetry, and nothing in this post depends on it. The fit is with the software that does that work. OpenTelemetry SDKs, GenAI instrumentation packages, exporters and Collector images are open source dependencies, and in the GenAI area they are young, versioned as betas and changing quickly: the Python OpenAI instrumentation has already been deprecated in favour of a new package with breaking changes. Safeguard scans repositories, container images and SBOM files (safeguard scan --dir ., --image <name>, --sbom <file>), shows which projects carry a given component and version, matches them against vulnerability data and applies policies and gates. That tells you which services still run the deprecated instrumentation, or an old Collector image, when a fix ships. Remediation pull requests are available and switched off by default.

Fix it without waiting for upstream

Observability pipelines are built from open source parts: SDKs, instrumentation libraries, exporters and Collector distributions, all pulled in as dependencies and container images and rebuilt on every release. When one of those parts has a vulnerability, the fix path is the same as for any other dependency, so the habits below apply to your telemetry stack as much as to your application.

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.

3. Gold Registry: start from components that are already fixed

registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

The first demo is a real session with the Safeguard CLI. It shows the assistant reading a finding, asking before each command, upgrading a dependency and running the tests, without pushing anything. The same flow applies when an OpenTelemetry instrumentation package or exporter in your agent service needs an upgrade.

Click through it at your own pace, or open it full screen.

The second demo sets up the local runner on your own machine, approves it with a device code and scans from the terminal. It is a good fit for checking the dependencies and images that make up a self hosted Collector before you deploy them.

Click through it at your own pace, or open it full screen.

The third demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. Those tool calls are MCP requests, so they are exactly the kind of hop the MCP conventions above describe, and choosing a small tool set is the same least privilege habit that keeps tool arguments out of places they should not be.

Click through it at your own pace, or open it full screen.

Download the apps at safeguard.sh/download and read the CLI docs at docs.safeguard.sh/docs/cli.

Quick check

Your agent's traces are sampled at 1 percent and the upload hook is configured to send content to object storage. What share of prompts reaches the storage bucket?

All of them. The specification says the upload hook should be invoked regardless of the span sampling decision. Sampling controls how many spans you keep, not how much content you upload. Redact inside the hook, and give the bucket its own retention and access policy.

A collector rule deletes gen_ai.input.messages from spans, but prompt text still shows up in your logging backend. What is the likely cause?

Content is also being captured on events. With event_only or span_and_event, or with an event such as gen_ai.client.inference.operation.details, the same messages travel through the logs pipeline. Add matching rules to the logs pipeline, or set capture to no_content at the source.

Sources

Last reviewed: 24 October 2026. The GenAI semantic conventions are at Development stability and change often; check the GenAI repository and your instrumentation's changelog before you build dashboards or redaction rules on a specific attribute name.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.