[BUG] claude subprocess does not inherit active OTel span as parent — requires manual OTEL_TRACEPARENT injection

Resolved 💬 4 comments Opened Apr 27, 2026 by steveyackey Closed May 30, 2026

Summary

When the claude CLI is invoked as a subprocess from within an active OTel span, it does not automatically inherit that span as its parent, and its spans do not appear in the calling process's trace. Callers must manually inject both OTEL_TRACEPARENT (for parent context) and OTel exporter configuration (endpoint + auth headers) into the subprocess environment — none of which is documented.

This is inconsistent with how every other instrumented outbound call works: HTTP clients, gRPC stubs, etc. propagate context automatically. The claude subprocess is an outbound call that the SDK should instrument transparently.

Environment

  • Claude Code CLI v2.1.112 / headless claude subprocess (invoked programmatically)
  • OTel backend: Honeycomb (OTLP HTTP); reproducible with any backend
  • OS: Linux

Reproduction

Script run on 2026-04-27. Full repro at bottom of this issue.

Orchestrator opens a span, then calls claude twice — pass 1 with OTEL_TRACEPARENT set, pass 2 without:

[repro] trace_id = 4f31c28b55b70c30ae966a94f9a81944

[repro] Pass 1: calling claude WITH OTEL_TRACEPARENT set
[repro] Pass 1 exit code: 0

[repro] Pass 2: calling claude WITHOUT OTEL_TRACEPARENT
[repro] Pass 2 exit code: 0

Result in Honeycomb (trace 4f31c28b55b70c30ae966a94f9a81944, local environment):

  • 1 span total — only orchestrator.run
  • 0 children, 0 descendants
  • Neither pass 1 nor pass 2 subprocess spans appear anywhere in the trace

The subprocess spans are completely missing from the orchestrator trace — not orphaned within it, but absent entirely. They either land in a disconnected root trace or are dropped, depending on whether the subprocess has its own exporter configured.

Root Cause

The claude subprocess has its own OTel initialization that is independent of the calling process's OTel setup. It does not:

  1. Inherit the caller's active span as its parent (no automatic OTEL_TRACEPARENT injection)
  2. Export to the caller's OTel endpoint (needs its own exporter config)

For subprocess spans to appear in the orchestrator's trace, callers must manually inject all of the following into the subprocess environment before each invocation:

  • OTEL_TRACEPARENT — W3C traceparent of the currently active orchestrator span
  • OTEL_EXPORTER_OTLP_ENDPOINT — the OTLP endpoint to export to
  • OTEL_EXPORTER_OTLP_HEADERS — auth headers (e.g. x-honeycomb-team=<key>)

None of this is documented. There is no SDK-level warning when these are absent.

Expected Behavior

The SDK should snapshot the active OTel span context at subprocess launch time and inject it as OTEL_TRACEPARENT into the subprocess environment automatically — the same way an HTTP client injects traceparent into outbound request headers. No manual injection should be required.

Exporter configuration is a harder problem (the subprocess may need different auth), but at minimum the parent context propagation should be automatic.

Workaround

Manually inject the active span context and exporter config before each invocation:

from opentelemetry import propagate

carrier = {}
propagate.inject(carrier)  # reads the currently active span

subprocess_env = {
    **base_env,
    **carrier,                                           # OTEL_TRACEPARENT, OTEL_TRACESTATE
    "OTEL_EXPORTER_OTLP_ENDPOINT": "https://...",
    "OTEL_EXPORTER_OTLP_HEADERS": "x-honeycomb-team=...",
}
result = run_claude("do some work", env=subprocess_env)

Impact

  • Severity: Medium (telemetry correctness, no runtime impact)
  • Who is affected: Any orchestrator that invokes claude as a subprocess from within a traced context. Subprocess spans are completely absent from the calling trace without the workaround.

Repro Script

#!/usr/bin/env python3
"""
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
"""
import os, subprocess, sys, time

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator

OTLP_ENDPOINT = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")
provider = TracerProvider(resource=Resource.create({"service.name": "orphan-repro-test"}))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=f"{OTLP_ENDPOINT}/v1/traces")))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("orphan-repro")

def traceparent(span):
    ctx = span.get_span_context()
    return f"00-{ctx.trace_id:032x}-{ctx.span_id:016x}-{'01' if ctx.trace_flags else '00'}"

with tracer.start_as_current_span("orchestrator.run") as root:
    tid = f"{root.get_span_context().trace_id:032x}"
    print(f"trace_id: {tid}")

    # Pass 1: OTEL_TRACEPARENT injected
    env1 = {**os.environ, "OTEL_TRACEPARENT": traceparent(root),
            "OTEL_EXPORTER_OTLP_ENDPOINT": OTLP_ENDPOINT}
    subprocess.run(["claude", "-p", "say hello", "--output-format", "text"], env=env1)

    time.sleep(2)

    # Pass 2: no OTEL_TRACEPARENT (bug trigger)
    env2 = {**os.environ, "OTEL_EXPORTER_OTLP_ENDPOINT": OTLP_ENDPOINT}
    subprocess.run(["claude", "-p", "say goodbye", "--output-format", "text"], env=env2)

provider.force_flush(timeout_millis=15000)
print(f"\nCheck trace {tid} in your OTel backend — expect 0 children under orchestrator.run")

Related

  • #48862 — missing docs for inbound TRACEPARENT/TRACESTATE env var support
  • #50776 — non-standard token attribute names on claude_code.llm_request spans

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗