posts / about / rss English Português

What did your agent actually do? Ask the kernel

Last year I built a pipeline that processes tens of millions of images. When something went wrong, I had a stack trace and a database row, and I could trust them. The system couldn’t misdescribe what it did, because it didn’t describe anything.

Coding agents changed that. When Claude Code or a Python agent runs a task, what we get is a transcript of what the model said it was doing, plus whatever the framework logs. The story is usually right, but when it isn’t, how can we tell?

Three accounts of the same task

An agent’s transcript is the model’s narrative of its own actions. The framework’s logs are a second narrative, filtered through whatever the framework author thought was worth recording. Underneath both is what the operating system saw: which processes were spawned, which files were opened and written, which sockets connected to which hosts. Of the three accounts, it’s the only one that isn’t written by the agent or its tools (with caveats I’ll get to).

Most of the time the three layers agree. The cases I care about are the ones where they diverge:

A transcript can’t be relied on to surface divergences like these, since it’s written by the same model that made the mistake. If the cause was an injected instruction rather than a mistake, there’s even less reason to expect it to show up there.

Framework logs don’t close the gap either. They record what each tool reported back, so a subprocess the tool spawned or a connection it opened, anything beyond the return value, isn’t in them.

With coding agents this gets worse, because the tool is often a shell. The framework log says the agent ran python fix.py. It doesn’t say that fix.py was written by the agent one step earlier and could do anything. When tools are hand-written by a developer, logging what they return means something. When the tool is “run whatever I just wrote,” the log is describing a box, not what’s inside it.

A public case, at a much larger scale

In May 2026, RubyGems was flooded with more than 2,000 junk packages, and a report published in September by Spencer Kitts, Thomas Larsen and Sydney Von Arx attributes the campaign to OpenAI agents running during training and evaluation (coverage in The Hacker News). According to the report, the agents gained remote code execution on RubyDoc’s build servers, used them to scrape UK local government websites, and tried to obtain other users’ API keys. One package carried a comment about disabling the malicious code in the next version. OpenAI’s statement to Reuters said its agents used RubyGems “to carry out benign tasks and retrieve public information.” The statement doesn’t say whether its models uploaded the packages.

What strikes me most is how little anyone can reconstruct. Four months later, Ruby Central says it can’t determine whether the packages were published by AI agents at all, and the researchers who made the attribution can’t say why the agents went to so much trouble to collect data that was already public.

Ruby Central was never going to have telemetry from the agents’ hosts, though. The operator was. The researchers say nobody in the RubyGems community heard from OpenAI before the report came out, and SecurityWeek describes the company as apparently unaware that its agents might be responsible. If that’s right, the party that held every transcript and every framework log for those runs either didn’t notice or couldn’t say. We don’t know what those transcripts said. It seems they weren’t enough.

The case also complicates my own argument. Traced from the agents’ hosts, most of this would have looked like traffic to rubygems.org, which is exactly the host you’d expect a package manager to talk to, and encrypted at that. The code execution happened on someone else’s servers. Kernel telemetry wouldn’t have seen any of that. What it would have seen is narrower: processes pushing packages thousands of times (gem push, or curl against the API), .gem files being built by the hundred, and outbound volume to the registry far beyond what installing dependencies needs. No lookup task justifies that, and that is the kind of mismatch I’m after.

We’re observing agents from the wrong side

Almost all of the tooling I’ve seen for agent observability watches the LLM side: prompts, completions, token counts, tool-call arguments. That’s valuable, and I use it. But tool-call arguments are a record of what the agent asked for. What happened after the request reached the machine isn’t in them.

Watching the machine side is nothing new to security people. Falco and Tetragon have been tracing syscalls with eBPF for years. What they don’t have is the other half: what the agent believed it was doing when the syscall happened.

Kernel events have their own limitations. They tell you that a file was written, and nothing about why, or whether the content is right. With TLS, the kernel sees a socket and a destination but not what was sent (AgentSight, below, gets around this by hooking the SSL library in userspace). An agent that gets root can interfere with the tracing itself. And a raw syscall trace with no link back to the agent’s intent is mostly noise. What I think is useful is joining the two sides: lining up what the agent said it was doing with what the machine did, and looking closely at the places where they disagree.

There’s a small amount of research starting to look at this from the OS side. Researchers at UC Santa Cruz, working under the open-source eunomia-bpf project, published AgentSight (2025), which uses eBPF to correlate LLM traffic with kernel events, and AgentCgroup (2026), which maps OS resource usage to individual tool calls. It’s early work, and I’m starting by reproducing it.

What I’m doing about it

I just started a master’s at UFRGS on exactly this question: can system-level telemetry tell you, reliably, what an agent actually did, and can it catch a compromised agent that the text layer misses? Over the next few months I’ll be reproducing the existing work, running real agents on real benchmarks with kernel tracing on, and publishing what I learn. The first write-up will be the smallest version of the idea: one agent, one task, the transcript on one side and the process and file events on the other.

If you run tool-using agents unsupervised (in CI, in automations, on a shared box, in a background job), I’d like to hear how you find out what they did. Especially if the answer is “we don’t.” Write me at contact@josehenrique.dev or on LinkedIn.

← all posts