Mind the Gap: Closing Claude's Compliance API Blind Spots with OpenTelemetry

Gain visibility for Claude beyond the Compliance API. Using OpenTelemetry to get logs that include tool calls, MCP activity and file access.

Mind the Gap: Closing Claude's Compliance API Blind Spots with OpenTelemetry

In previous posts I've said how Claude Compliance API telemetry has limits. You can see user's prompts and Claude's reply, but not which tools Claude called, what an MCP server handed back, or which files it read off their laptop. In this post I show how to close that gap and get more Claude coverage using OpenTelemetry.

This is the third post in this Claude Enterprise monitoring series. I'd recommend reading the posts before if you haven't already:

Auditing Claude Enterprise: Shipping the Compliance API into Your SIEM
In this post I show how to use Anthropic’s Compliance API to stream Claude Enterprise audit events into your SIEM, and introduce claude-compliance-sdk, a Python SDK I built to make interacting with the API easier. Why bother? You don’t need me to tell you that AI

Part 1: How to ingest Claude Compliance logs into your SIEM

Detecting Misuse with the Claude Compliance API: The Threat Is in the Content
Detections for Claude Enterprise built on Compliance API content: a prefilter and LLM judge that catch prompt injection, jailbreaks and data exfiltration.

Part 2: Writing detections using the Compliance logs and introducing a prefilter -> LLM judge pipeline for scanning chat content

Why bother?

If you've been playing along at home, you'll now have Claude Compliance API logs flowing into your SIEM, mapped to your data models and picked up by your existing detections, with a prefilter -> LLM judge pipeline assessing message content and flagging potential violations.

These are detections are valuable and catch real threats, but there is still a blind spot. They only reason over what was said. The true "agentic" attacks are about what was done, and the doing happens in the gaps the Compliance API cannot see. The execution layer is where the worst things happen.

  • A poisoned MCP response tricks Claude into a sensitive action. The conversation feed records jon.snow's innocuous prompt and Claude's reply; the instruction that actually drove the action arrived in the MCP server's return, which the Compliance API never captures.
  • Claude reads a file outside the project scope on jon.snow's machine, a directory it had no business touching. There is no conversation artefact for this at all. A file was read, and the only place that fact exists is the execution layer.
  • A tool action jon.snow rejected, followed moments later by a near-identical one that was auto-approved. Approval decisions live nowhere in the conversation; from the content feed, both the rejection and the approval are invisible.

The conversation tells you what was said and the execution layer tells you what was done. A detection for an agentic platform needs both.

Reviewing the coverage matrix

AI moves quickly, and since I mapped Claude Enterprise coverage in the first post, the landscape has already shifted. Here is the most up-to-date version:

Layer Claude.ai Chat Claude API Claude Code Cowork Files Cowork Chrome Cowork MCP Cowork Tasks
Audit log export βœ… 🟑 🟑 ❌ ❌ ❌ ❌
Compliance API Β· events βœ… 🟑 🟑 ❌ ❌ ❌ ❌
Compliance API Β· content βœ… 🟑 ❌ ❌ ❌ ❌ ❌
OpenTelemetry Β· Code ❌ ❌ βœ… ❌ ❌ ❌ ❌
OpenTelemetry Β· Cowork ❌ ❌ ❌ βœ… 🟑 βœ… 🟑
On-device proxy / LLM gateway 🟑 🟑 βœ… ❌ βœ… 🟑 🟑
About OTel: OpenTelemetry (OTel) is an open-source, vendor-neutral standard for emitting telemetry from software, logs, metrics and traces, in a common format (OTLP) that you ship to a collector or to a SIEM that speaks it. It's not Claude-specific. What is new is that Claude's surfaces now emit their execution-layer activity over it, so tool calls, calls out to MCP servers (Model Context Protocol, the open standard Claude uses to reach external tools and data) and file access all arrive as ordinary log events. Two shapes show up in this post: event-style logs from Cowork and Claude Code, and traces (span trees) from the Office agents.

OpenTelemetry for Cowork wasn't available when I wrote Part 1. It's now native and admin-configurable (Claude Desktop 1.1.4173 and later), set in Org settings under Cowork with an OTLP endpoint, protocol and headers.

OpenTelemetry for Claude Code was available, but you had to rely on users setting their own environment variables. These variables are now enforceable: pushed through managed-settings.json by your MDM, where the values cannot be overridden locally.

Additionally, there are the Office agents (Excel, Word, PowerPoint and Outlook), which now emit trace-based telemetry to a per-organisation collector.

Cowork activity is still not in the Compliance API; OTel is the only centralised record of it. The two sources complement each other, and they share a user account identifier that lets you join them. Full coverage relies on you ingesting both sources.

Cowork: telemetry by flipping a switch

An admin can now turn on OTel logging for Cowork organisation-wide, under Organization settings > Cowork. You'll need to enter the OTLP endpoint, protocol and headers. Once those are populated, logs start flowing straight away in any new session (existing sessions pick it up once they're restarted).

You get five event types:

  • user_prompt
  • tool_result
  • tool_decision
  • api_request
  • api_error

The full prompt text arrives in user_prompt. There is no client-side redaction toggle, so what jon.snow typed is what lands in your collector. We'll see how this can be a double-edged sword later on.

Tool and MCP invocations arrive in tool_result: tool_name, success, duration_ms, and tool_parameters, which for an MCP call includes mcp_server_name and mcp_tool_name. The tool_input contains the file paths, URLs and arguments the tool was given. This is the record of what Claude actually did.

Approval decisions arrive twice over, which is convenient for detection. There are dedicated tool_decision events (decision, source), and the same outcome is duplicated onto tool_result (decision_type, decision_source). The useful part is the source field, which tells you who or what made the decision: config means a policy decided it, hook means an automation did, and user_permanent, user_temporary, user_abort or user_reject mean the person at the keyboard did.

Correlation is handled by prompt.id, a UUID that comes with every event a single prompt triggered, so you can reconstruct everything Claude did in response to one input. session.id groups a working session, and event.sequence orders events within it.

Identity is richer than you might expect: user.email, user.account_uuid, user.account_id (in the tagged format the admin APIs use) and organization.id. You can use those account identifiers to join back to your Compliance API records, which is how the Cowork feed and the conversation feed become one picture. Workspace context comes through workspace.host_paths, the folders jon.snow mounted into the session.

Let's review a single prompt.id end to end. jon.snow asks Cowork to "tidy up the Q3 numbers". The event stream for that one prompt reads:

user_prompt    prompt.id=8f3c…  "tidy up the Q3 numbers"
tool_result    prompt.id=8f3c…  mcp_server_name=ledgers, mcp_tool_name=read_sheet
tool_result    prompt.id=8f3c…  tool_name=read_file, tool_input=~/Documents/winterfell-ledgers/q3.xlsx
tool_decision  prompt.id=8f3c…  decision=allow, source=config
tool_result    prompt.id=8f3c…  tool_name=write_file, tool_input=~/Documents/winterfell-ledgers/q3.xlsx

These events tell you that an MCP server was queried, a file under winterfell-ledgers was read, and the write that followed was auto-approved by policy rather than by jon.snow.

Note: the exporter runs inside the Cowork VM, and traffic to non-allowlisted domains is silently dropped, so your collector's domain has to be added under Admin settings > Capabilities > Network egress. Miss that and you get a telemetry gap with no error to tell you about it.

Claude Code: the same but different

Claude Code's OTel mechanism is much like Cowork's; the main difference is how you configure it to send events. A user can turn it on with environment variables, but for enterprise-wide enforcement you push a managed-settings.json with the environment variables set through your MDM, where the values sit at high precedence and users can't override them. A minimal configuration looks like this:

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_LOG_TOOL_DETAILS": "1",
    "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "https://collector.westeros.example:4318"
  }
}

Claude's monitoring documentation lists all the available settings.

The event set is richer than Cowork's, more than twenty types, so rather than enumerate them I will table the security-relevant ones against the question each answers:

Question Event Useful attributes
Was a tool allowed or denied? tool_decision decision, source, tool_parameters
Did someone escalate permissions? permission_mode_changed new mode, including bypassPermissions
Did a policy hook block something? hook_execution_complete num_blocking
Did an MCP server connect or fail? mcp_server_connection server name, status
Was a plugin installed? plugin_installed plugin name
What ran, and what did it touch? tool_result tool name, command, paths (needs OTEL_LOG_TOOL_DETAILS=1)

Claude Code's defaults are the opposite of Cowork's, which matters when you bring the two feeds together. Code redacts by default and you opt content in.

  • OTEL_LOG_USER_PROMPTS controls whether prompt text is recorded at all (off by default, only the length is kept)
  • OTEL_LOG_TOOL_DETAILS adds Bash commands, MCP server and tool names, and skill names
  • OTEL_LOG_TOOL_CONTENT and OTEL_LOG_RAW_API_BODIES sit further up the exposure curve
    Cowork ships everything and Code ships nothing, so your collector-side policy has to bring them in line. For a SOC, prompts-off is too blind and raw API bodies are too much (and a data-handling problem in their own right), so events plus OTEL_LOG_USER_PROMPTS and OTEL_LOG_TOOL_DETAILS, with OTEL_LOG_TOOL_CONTENT and raw bodies left off, is probably the happy medium.

Two things to note:

  • Identity is only populated with OAuth: there you get user.email and user.account_uuid on every event, but on a direct API key, Bedrock or Vertex deployment you get only an anonymous user.id and session.id, and you attach identity yourself through OTEL_RESOURCE_ATTRIBUTES (for example enduser.id=jon.snow@westeros.example).
  • Claude Code does not pass its OTEL_* variables down to the subprocesses it spawns, so Bash commands, hooks and MCP servers are not themselves traced; you see that Claude ran them, not what they did once running. This is where EDR or process-level logging earns its keep: it picks up where Claude's telemetry leaves off on the endpoint.

Office agents: traces, not logs

Claude also provides Office agents, and they are the odd one out. Excel, Word, PowerPoint and Outlook emit OTLP/HTTP traces, not logs, to a per-organisation custom collector set under Organization settings > Office agents. Each user turn (one prompt and the agent's reply) produces a nested set of spans, where a span is one recorded step of work. The top-level span is agent.query; nested under it are agent.stream, agent.tool_execution, and, where they apply, file.upload and agent.compaction.

The span to care about is agent.tool_execution, the action record. It contains:

  • tool.input
  • tool.output (the first 4000 characters)
  • tool.read_write to tell a read from a write
  • tool.accept_decision, which is manual, auto_accept or deferred, so you can audit approval habits the same way you do in the other two surfaces

A few things worth knowing before you build on it:

  • No attributes are redacted or filtered, so prompts, tool input and output, document URLs and filenames all arrive in the clear, but the assistant's response text is not included. This is the opposite of the Compliance API, whose strength is precisely the response. Each source sees what the other cannot.
  • When you set a custom endpoint, telemetry goes exclusively to your collector with no dual-send to Anthropic, and gRPC is rejected because the add-in lives in an Office WebView that only speaks HTTP.
  • As with Code, a direct-provider deployment (Bedrock, Vertex or a gateway) keeps the core audit payload but loses user identity, MCP metadata and the file-upload spans, so attribution there means joining session.id against your IdP logs.

Boundaries and gateways

The MCP boundary

The execution telemetry tells you a tool was called, with which parameters, and whether it succeeded. It still does not give you the full content of what an MCP server returned, and tool_input is truncated (strings over 512 characters are cut, the whole payload limited to around 4K), so long commands and large payloads are partially invisible at the point they get interesting.

One structural fix is to own the boundary. In an ideal world, you would host or proxy the MCP servers your organisation allows, and log requests and responses server-side, where nothing is truncated and the schema is yours. OTel watches the client; the proxy watches the wire. Pair this with allowlisting the servers Claude is permitted to reach, and use the mcp_server_connection events to tell you when someone wires up one you did not sanction.

An LLM gateway

You should also consider locking down the model boundary. As AI usage evolves and formalises, the emerging best practice is to route every request to any model through an LLM gateway you control. The rationale is the same as for any other third-party API: you wouldn't let arbitrary egress leave the estate unwatched.

A gateway in front of the provider logs each request and response at full fidelity: no truncation or redaction wrangling, and no dependence on the endpoint being a managed one. It is also the one vantage point that sees full prompt and response content whichever surface produced it, which is what the execution telemetry does not give you, and what the Compliance API gives you only for the surfaces it covers.

The limitation is that you can only do this where you control the path to the model. jon.snow's Claude Desktop, Cowork and the Office add-ins talk to Anthropic's endpoints directly, behind pinned certificates and, for Cowork, an embedded VM, so there is nothing to transparently intercept there. In practice the gateway covers your API-based and direct-provider workloads while OTel covers the first-party apps; neither replaces the other, and "route everything through a gateway" is often out of reach.

What's still missing?

Shadow AI is still an issue. Cowork exports only when an admin sets the endpoint, and Code exports only where managed settings reach. A personal Claude account on an unmanaged laptop logs nothing at all. It's a tale as old as time: your telemetry coverage will only be as good as the amount of your estate you administer.

The telemetry is itself a data-handling problem. Cowork logs full prompt text, tool inputs and user emails with no client-side toggle, and the Office spans are unredacted. You are building a second copy of sensitive conversational data inside the SIEM, so retention, access control and collector-side redaction are now your responsibility, and the judge-pipeline lesson from Part 2 (treat the content as hostile, fence it, limit who can read it) applies to this feed too. There are possible solutions to this: OTel collectors (and some SIEMs if data is being sent to them directly) allow for filtering and redacting data as it passes through them, meaning you can drop the most sensitive data types that aren't needed for detections.

The failure modes are quiet. The Cowork egress allowlist drops collector traffic without an error, Code's subprocesses go untraced, and truncation hides the tails of long inputs.

You can't fall back on intercepting these apps' traffic. Claude Desktop, Cowork and the Office add-ins pin their certificates, and Cowork runs inside its own VM, so a proxy that tries to read their encrypted traffic just gets refused. It's the same wall the gateway ran into, and it's why the supported telemetry above, rather than a proxy added underneath the apps, is the only real option for these surfaces.

Pulling the formats together is your job. You've now got four to reconcile: Cowork events, Code events, Office spans and the Compliance API's records. They share enough common fields to link records across them (user.account_uuid and user.account_id, organization.id, session.id, and prompt.id within a single stream), but getting them into the one consistent shape your detections can run against is work you do at the collector. The Part 2 lesson still holds: convert everything to one common format as it arrives, then write your rules against that.

Now what?

You can now see the execution layer across all three surfaces: every tool call, MCP invocation, file path and approval decision, joined to the conversation feed you were already ingesting after Part 2. The coverage matrix has materially fewer gaps than it did, and the agentic activity that used to be invisible is now landing in your collector.

This Claude Enterprise series is building to its climax. In the next post I will write the detections this execution-layer data unlocks: tool-decision escalation patterns, unapproved MCP servers, permission-mode bypass, hook failures, and the one correlation the Compliance API could never support, an MCP response landing immediately before a sensitive action.

Resources

Share this article