[Bug] OTel metrics: skills from non-official plugin marketplaces report skill.name="third-party" with no opt-out — teams lose analytics for their own skills

Status Open
Reported on v2.1.209
Maintainer reply ✓ Yes — bcherny
Activity 3 comments · opened Jul 14, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Environment

  • Claude Code 2.1.209, macOS (behaviour observed across 2.1.206–2.1.209 in live telemetry)
  • OTEL metrics enabled via managed settings, exported OTLP → Grafana Cloud

What happens

On the claude_code.token.usage and claude_code.cost.usage metrics, the skill.name attribute is reported inconsistently depending on where the skill is installed from:

| Skill source | skill.name on metrics |
| --- | --- |
| Built-in / official marketplace (anthropics/claude-plugins-official) | verbatim |
| User skills (~/.claude/skills) and project skills (repo .claude/skills) | verbatim |
| Any other plugin marketplace — including one the team publishes itself | the literal string "third-party" |

The related attributes are masked on the same series, so the bucket cannot be split downstream: plugin_name is also "third-party", and MCP attribution is masked to "custom".

There is no control that changes this. OTEL_LOG_TOOL_DETAILS=1 unmasks skill names on skill_activated events only; no environment variable or managed setting affects the metrics-side mask.

Why this matters

Plugin marketplaces are the recommended way to distribute a team's shared skills. A team that follows that recommendation loses exactly the telemetry that skill.name exists to provide: every one of their skills collapses into a single "third-party" series. In our live capture, a 30-day window shows ~97% of all skill-attributed tokens (~147M of ~152M) in the one "third-party" bucket, because our own plugins (published from our own GitHub repos, installed via our own marketplace) are treated identically to genuinely third-party code.

The same skill copied into ~/.claude/skills reports verbatim — so the masking rule keys on the distribution channel, not on anything about the skill itself.

The mask is not protective

As documented in #76957 (event-side masking regression, filed with OTLP captures), api_request events in the same default export already carry skill.name verbatim. Masking the metrics therefore breaks analytics without hiding the names.

Steps to reproduce

  1. Publish a minimal plugin marketplace repo containing one skill; install and enable it.
  2. Enable OTEL metrics export (any backend; a local OTLP dump listener suffices).
  3. Invoke the skill; observe claude_code.token.usage reports skill.name="third-party", plugin_name="third-party".
  4. Copy the same skill folder into ~/.claude/skills; invoke it; observe skill.name verbatim.

Ask

Either:

  1. Report plugin skill names verbatim on cost/token metrics (matching user/project/official skills), or
  2. Provide a managed-settings control — e.g. an allowlist of trusted marketplaces or plugins whose skills report verbatim — so organisations can opt their own plugins into the telemetry they already operate.

Related

  • #76957 — skill_activated events mask skill.name to "custom_skill" while api_request events and metrics export it verbatim (regression since ~2.1.126)
  • #58674 — earlier report of the event-side regression, closed by stale-bot

View original on GitHub ↗

3 Comments

zifter · 27 days ago

Adding an independent reproduction on 2.1.220, plus one finding I don't think is in the thread yet: the mask also holds on the cost-bearing event, with OTEL_LOG_TOOL_DETAILS=1 enabled. Enabling that flag is therefore not a workaround for cost/token attribution — only for call counting.

Setup

  • Claude Code 2.1.220, macOS arm64
  • Metrics: OTLP → VictoriaMetrics → Grafana
  • Separate probe run with OTEL_LOGS_EXPORTER=console and OTEL_LOG_TOOL_DETAILS=1 to read the events directly

What the flag does fix (confirming #17046)

tool_result / tool_decision carry real names in tool_parameters:

body: "claude_code.tool_result"
  tool_name: "mcp_tool"
  tool_parameters: "{\"mcp_server_name\":\"<our-server>\",\"mcp_tool_name\":\"list_datasources\"}"
body: "claude_code.tool_result"
  tool_name: "Skill"
  tool_parameters: "{\"skill_name\":\"<our-plugin>:<our-skill>\"}"

plugin_loaded goes further and emits the verbatim plugin name and the marketplace identity:

body: "claude_code.plugin_loaded"
  "plugin.name": "<our-plugin>"
  "marketplace.name": "<our-marketplace>"
  "plugin.scope": "user-local"

What it does not fix

In the same session, with the flag on, api_request — the only event carrying cost_usd — is still masked:

body: "claude_code.api_request"
  model: "claude-opus-5"
  cost_usd: 0.0232505
  cache_read_tokens: 26406
  "skill.name": "third-party"
  "plugin.name": "third-party"

Across the whole probe the verbatim skill name appeared exactly once — in tool_parameters, where there is no cost — while the costed event said third-party.

So three signals disagree inside one process and one turn: plugin_loaded knows the plugin and its marketplace, tool_result knows the skill name, and api_request, emitted milliseconds later, reports third-party.

Impact

30-day window, ~136 distinct named skills visible:

  • skill.name="third-party"39.8% of all skill-attributed cost
  • mcp_server.name="custom"85.7% of all MCP-attributed cost

The MCP figure is the worse one: every self-hosted server collapses into a single series, so "which internal MCP server is costing us" has no answer at all.

One consequence worth spelling out: because the mask keys on the distribution channel rather than on the skill, the same skill lands in different buckets depending on how each developer installed it. We have skills present both in our own marketplace and as loose directories in ~/.claude/skills, and their usage splits between a verbatim series and the third-party bucket. That makes the named series unreliable too, not only the opaque one — a per-skill number silently undercounts by however many people installed it the supported way.

Workaround, and why it isn't enough

All events in a turn share prompt.id, so a log pipeline can join the verbatim name from tool_result onto cost_usd from api_request. Two problems: attribution is ambiguous when several skills or subagents fire in the same turn, and it requires standing up an OTLP logs receiver purely to recover an attribute the metrics pipeline already holds. Teams running only a metrics backend — ours included — cannot use it at all.

Ask

plugin_loaded already carries a verbatim plugin.name and marketplace.name, so the identity is demonstrably available in-process. The gap is purely on the metrics side.

What would help is a metrics-side control, separate from OTEL_LOG_TOOL_DETAILS. That variable governs the logs signal, and making it the switch for a metric label would force operators who run only a metrics backend to enable full tool-parameter logging — tool_input, including complete bash commands — just to recover a plugin name. That is a far larger exposure than the label being requested, and it is the wrong signal to reach for.

Either shape would solve it:

  • a trusted-marketplace allowlist, as proposed in #70547, so an organisation can un-mask marketplaces it owns; or
  • a dedicated opt-in in the OTEL_METRICS_* family, applying to skill.name / plugin.name on cost.usage and token.usage, to the same attributes on api_request, and to mcp_server.name / mcp_tool_name.

Related, asking for the same thing: #70547 (open), and #59951 — auto-closed by the stale bot on 2026-08-02 rather than declined on the merits.

bcherny collaborator · 13 days ago

Thanks for the detailed report — reproduced on 2.1.233.

With OTel metrics enabled, a skill from a non-official (local) marketplace plugin emits skill.name: "third-party" and plugin.name: "third-party" on the cost/token counters, while a user-local skill reports its real name verbatim. No hashed identifier or marketplace attribute accompanies the redacted value, so distinct third-party skills are indistinguishable in metrics.

This redaction is intentional and documented: the monitoring docs state that third-party plugin skill and plugin names are replaced with "third-party" for privacy, while built-in, bundled, user-defined, and official-marketplace names appear verbatim. So this is working as designed today, and we're treating the ask — a stable hashed per-skill identifier (or an opt-in to emit the real name, as some log events already support via OTEL_LOG_TOOL_DETAILS=1) — as a feature request rather than a bug.

It's a reasonable ask: a salted hash would keep names private while letting deployments distinguish and cost-attribute their third-party skills. Leaving this open as an enhancement request.

🤖 Generated with Claude Code

sammcj · 13 days ago

To give another data point - we're hitting this with a client we're working with on adopting Claude Code, they have a private / internal self-hosted marketplace and want to measure skill adoption over time but skill.name collapsing to third-party makes every one of our skills indistinguishable, so per-skill adoption is unmeasurable.

OTEL_LOG_TOOL_DETAILS=1 is dangerous as it leaks other information due to enabling full_command, file_path, tool_parameters and tool_input across all tools. Enabling fleet-wide command and prompt logging to recover a skill name isn't a trade we can make, so we have no path to attribution at all.

The redaction is also asymmetric: an official-marketplace skill is named verbatim, the same skill vendored into an internal marketplace is not - both names are our own strings going only to our own collector, which the docs confirm is never sent to Anthropic.

Either fix works for us:

  • A marketplace allowlist, or a names-only flag, so an operator can opt their own marketplace in without enabling command and prompt logging.
  • #85555 - surfacing skill frontmatter on skill_activated.

Related, all closed by the stale bot: #70547, #59951, #71639.