AI Coding Agents Are a New Attack Surface – Here's How to Monitor Them
A privileged process with real credentials, real source code, and real secrets – and most security tooling is blind to it.

Lion Grizotsky
Threat Hunter at Mars Security
July 2, 2026
AI coding agents have quietly become one of the most privileged processes on a developer's machine. Claude Code reads and writes files, runs shell commands, installs plugins, and connects to external tool servers – all under a real, authenticated employee identity, inside a real corporate environment with real source code and real secrets.
That is a remarkable productivity unlock. It is also, viewed from a security desk, a new attack surface and a new insider-risk surface at the same time. A poisoned instruction file in a cloned repo, a malicious plugin, an unsanctioned tool server, or a single careless "stop asking me for permission" session can quietly turn a coding assistant into an execution and exfiltration channel – and it happens with the developer's own credentials, on the developer's own laptop, where most security tooling has limited visibility.
The good news: Claude Code can emit a rich, structured account of everything it does over OpenTelemetry. Once that telemetry lands in Mars ISE, the agent stops being a blind spot and becomes just another hunting ground – queried with the same hypotheses, the same workflow, and the same analysts you already point at your identity provider, your endpoints, and your SaaS audit logs.
This post walks through what that telemetry contains and three concrete hunts you can run against it: privilege and permission abuse, risky command execution, and software-supply-chain risk from plugins and tool servers.
First, an honest caveat: none of this is on by default
Claude Code does not collect or ship any of this telemetry unless you turn it on. There is no passive, default-on data collection to rely on. An organization has to deliberately enable it and point it at a collector:
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector.example.com:4317
In practice you don't ask every developer to set these by hand – you push them centrally through Claude Code's managed settings, distributed via MDM, so the configuration can't be overridden by users. Distributed tracing (the spans we lean on later) is a separate, beta opt-in on top of that (CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1). The practical takeaway: the visibility described below is something you choose to deploy as a control, not something that exists automatically. Treating your fleet's Claude Code telemetry configuration as a security decision – enabled, centrally enforced, and shipped to your security platform – is step zero for everything that follows.
What the telemetry actually contains
Claude Code exports three kinds of signal. Usage metrics (sessions, tokens, cost) are great for FinOps dashboards but are not our focus here. The two that matter for hunting are:
- Events – discrete log records, one per meaningful thing that happens: a prompt is submitted, a tool finishes, a permission decision is made, a plugin is installed, a tool server connects. Every event from a single user prompt shares a
prompt.id, and anevent.sequencecounter preserves their order within a session. Each event also carries the authenticated identity (user.email, organization and account identifiers) and host context (terminal type, OS). - Spans – the optional, beta distributed-tracing view. Where events are a flat stream, spans capture cause and effect: a single
trace_idties one prompt to the model calls, tool runs, and permission waits it triggered, connected byspan_id/parent_span_id. This is what lets you reconstruct the full chain of a single turn rather than guessing how scattered events relate.
The most important nuance: redaction is the default
This is the detail that determines whether a hunt is possible at all, so it's worth stating plainly: the sensitive content is redacted unless you opt in. Out of the box, prompts, Bash command strings, tool-server and plugin names, file paths, and tool input/output are not present in the telemetry. They only appear when specific gates are set:
| What you want to see | Gate that reveals it |
|---|---|
| User prompt text | OTEL_LOG_USER_PROMPTS=1 |
| Bash commands, tool parameters, plugin/server names | OTEL_LOG_TOOL_DETAILS=1 |
| Full tool input/output content (spans only) | OTEL_LOG_TOOL_CONTENT=1 |
| Raw API request/response bodies | OTEL_LOG_RAW_API_BODIES |
This cuts both ways, and a good hunter holds both sides in their head at once. It is a genuine privacy control – you can run the agent's telemetry without ever capturing prompt text or command contents. But every gate you leave off is also a hunting blind spot. The same hunt can be high-fidelity in one fleet and nearly blind in another depending purely on configuration. So for each hunt below, we'll be explicit about which signals survive full redaction and which require a gate – because knowing your own configuration is the difference between a finding and a false sense of security.
How we hunt this on Mars
Mars ingests Claude Code's events and spans via your existing telemetry architecture (OTel collector to SIEM / data lake) and normalizes them into the same place as the rest of your security telemetry.
The point of this architecture is that Claude Code becomes just another telemetry source: you ship it through the same centrally managed pipeline you trust for everything else, land it in the same environment as identity/endpoint/SaaS logs, and then apply the same detection engineering and hunting workflows to it.
Our hunting approach is behavior-driven and hypothesis-led rather than indicator-driven. Instead of maintaining a brittle list of "bad" strings, a hunt starts from a falsifiable claim about adversary behavior – what we'd expect to see if it were happening, what would rule it out, and which benign activities look similar in this specific environment. Evidence is then gathered to confirm or disprove the claim, with the benign look-alikes spelled out up front so the hunt can terminate cleanly instead of drowning in noise. The three hunts below are written in exactly that shape.
Hunt 1 – Privilege and permission escalation
The behavior. Claude Code's permission model is the safety rail: by default, sensitive actions pause for a human to approve. The risky move is when the agent – or a user driving it – steps out of that guard-railed mode into unattended operation and then does sensitive work with no per-action approval. An attacker who lands an instruction into the agent's context wants exactly this: get to a mode where actions execute without a prompt.
The logs. Mode changes are first-class events: claude_code.permission_mode_changed, carrying from_mode, to_mode, and the trigger that caused it. The transition that should always draw a look is to_mode = bypassPermissions (with acceptEdits and auto as softer relatives). Read those alongside the permission-decision signal on claude_code.tool_decision and claude_code.tool_result, where source / decision_source explains how each action was allowed:
configmeans the action was auto-allowed by settings or the active mode – i.e. no human was prompted. A surge ofconfig-sourced approvals right after an escalation is the tell.user_permanentmeans the user chose "don't ask again," writing a durable allow-rule. That's not a one-time decision – it's a lasting weakening of the guard rail that outlives the session.
If you've enabled tracing, the same story shows up structurally: a claude_code.tool span normally has a claude_code.tool.blocked_on_user child recording the permission wait and its decision / source. When that child is absent, no human was ever in the loop for that action.
The hunt. Find sessions that escalate to bypassPermissions, then follow the prompt.id and user.email to the tool activity that ran under that mode, and judge whether the escalation was justified by what the user was actually doing. Benign look-alikes to account for: CI and other non-interactive claude -p automation legitimately runs without prompts, and a trusted developer doing a big mechanical refactor may turn off prompting on purpose.
Coverage. This is the hunt that works even in a fully-redacted fleet. Mode transitions and decision sources are structural – they're present without OTEL_LOG_TOOL_DETAILS or any prompt logging. You may not see what was run, but you will always see that the rails came down and that actions were auto-approved afterward, which is often enough to prioritize a closer look.
Hunt 2 – Risky command execution
The behavior. The highest-consequence thing the agent can do is run a shell command. The patterns worth hunting are the classic ones, now issued by an AI: fetch-and-execute (curl ... | sh), decode-and-run of an encoded payload, deliberately disabling the execution sandbox, or piping local data to an external host. The twist with an agent is provenance – the dangerous command may originate not from the developer, but from a prompt injection buried in a repo file, a README, or a malicious skill the model decided to follow.
The logs. Bash executions surface as claude_code.tool_result events with tool_name = Bash. The substance lives in tool_parameters (gated by OTEL_LOG_TOOL_DETAILS), which carries the command itself – bash_command / full_command – along with timeout and, crucially, a dangerouslyDisableSandbox flag. That flag set to true is high-signal on its own and worth an alert regardless of the command text.
Tracing makes the provenance question answerable. The full_command on a claude_code.tool span, read together with the user_prompt attribute on its parent claude_code.interaction span, lets you compare what was run against what was asked. The injection tell is a command that has no plausible relationship to the user's actual request. Two more patterns to watch within a single trace_id: large reads (tool_result_size_bytes) followed by an outbound network command – the read-a-secret-then-exfiltrate shape – and commands issued by a subagent rather than the main session, which the agent_id / parent_agent_id attributes make visible.
The hunt. Search Bash tool events for fetch-pipe-execute, encoded, and network-egress command shapes, plus any use of the sandbox-disable flag. Then follow the trace_id / prompt.id to reconstruct the prompt-to-command chain and to attribute commands to a specific session or subagent. Benign look-alikes: legitimate dependency installers, build and setup scripts, and infrastructure tooling all run shell commands that can superficially resemble the bad patterns.
Coverage. This hunt leans on OTEL_LOG_TOOL_DETAILS – without it, the command text and the sandbox flag simply aren't in the data. But it doesn't go fully dark: even redacted, you still see that a Bash tool ran, whether it succeeded, its input/output sizes, and its timing. That supports a weaker but real volume-and-anomaly version of the hunt – an unusual burst of Bash activity, or large results moving at odd hours, still stands out without ever seeing a command string.
Hunt 3 – Supply chain: plugins and tool servers
The behavior. Plugins and external tool servers (MCP servers) extend what the agent can do – and in doing so, they extend its trust boundary. A third-party plugin can contribute its own automated hooks and its own tool servers; an unsanctioned tool server can feed the model both new capabilities and new data. This is classic software-supply-chain risk, relocated into the developer's AI assistant: the question is which untrusted extensions have entered the environment, and what they brought with them.
The logs. Installation is recorded by claude_code.plugin_installed, with marketplace.is_official (a false here means it did not come from an official source) and install.trigger. Session startup emits claude_code.plugin_loaded for each active plugin, which is your fleet-wide inventory signal – it reports enabled_via, plugin.scope, and, importantly, has_hooks and has_mcp. A non-official plugin that brings both automated hooks and its own tool servers is a high-interest combination. Even when names are redacted, a plugin_id_hash lets you count and track distinct third-party plugins across the fleet.
Tool-server connections show up as claude_code.mcp_server_connection, with status, transport_type, server_scope, and error_code. Two things to flag: network transports (transport_type = http or sse) and user/local-scoped servers reaching endpoints that aren't on your allowlist, and repeated failed connections, which can indicate probing or a misconfigured rogue server.
The hunt. Build a baseline of the sanctioned plugins and tool servers across your fleet from claude_code.plugin_loaded and claude_code.mcp_server_connection, then compare current activity against it to surface what's new, rare, or non-official. Trace a suspicious plugin's installation back to the user and to any hooks or tool servers it introduced, and check unfamiliar server names and domains against reputation data. Benign look-alikes: plugins enabled by org policy or shipped in the default bundle, and sanctioned internal tool servers, will all appear as "extensions" and should be filtered out by the baseline rather than alerted on.
Coverage. The inventory version of this hunt works ungated – has_hooks, has_mcp, marketplace.is_official, and plugin_id_hash are structural and present even when names are redacted, so you can always answer "how many unofficial, hook-and-server-bearing plugins are out there." Pinning a finding to a specific plugin or server name is what requires OTEL_LOG_TOOL_DETAILS.
What the trace view adds
Each hunt above can be run on events alone. Turning on tracing is what elevates them from enumeration to investigation. A single trace_id reconstructs the full chain of one turn – claude_code.interaction to claude_code.llm_request to claude_code.tool to claude_code.tool.blocked_on_user to claude_code.tool.execution – and folds in any work done by subagents through the agent_id / parent_agent_id attributes. That structure is what lets a hunter answer the cause-and-effect question that actually matters: did this risky command originate from the user's own prompt, from an instruction injected into the agent's context, or from a subagent the model spawned? Events tell you what happened; the trace tells you why.
Tracing is beta and a separate opt-in (CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1), and the hook span in particular needs detailed beta tracing enabled – but where you've turned it on, it is the difference between a list of events and a reconstructed incident.
Closing: telemetry is also posture
The same data that powers these hunts answers the governance questions a security leader should be asking about AI tooling. Which permission modes is the fleet actually running in? What unofficial plugins and tool servers exist out there? And – the meta-question – are the logging gates configured so that defenders even have the fields they'd need when something goes wrong? That last one is a real policy decision: enough detail to investigate, scoped tightly enough to respect developer privacy. Make it deliberately, and revisit it as your usage grows.
AI agents are becoming privileged actors in the software development lifecycle, with standing access that rivals the engineers using them. The way you keep that power accountable is the same way you've always kept privileged access accountable: capture what it does as first-class security telemetry, normalize it next to everything else, and hunt it with falsifiable hypotheses. Claude Code gives you the signal. Enabling it, shipping it to Mars ISE, and pointing your hunters at it turns your AI coding assistant from a blind spot into one of the best-instrumented actors in your environment.