
Fifteen years building engineering platforms, currently focused on advanced AI infrastructure at yeet. I love turning the deeply complex topics into something everyone can understand. I relate deeply with the core yeet philosophy that you can just build things.
Last updated: October 2026
Quick answer. Read it at the kernel, because the agent's transcript records what it believes it did rather than what its process actually opened.
yeet run gh:yeet-src/dialoutprints every outbound connection with the process that opened it, andyeet run gh:yeet-src/agentwireadds the SNI hostname so TLS destinations are named rather than numbered. Expect four categories on a working agent host: the model API, MCP servers, package registries from commands the agent ran, and whatever those commands fetched. One measured caveat before you plan a DNS-based approach: on a glibc host, name resolution goes through NSS over a Unix socket, so DNS taps see almost nothing while SNI names almost everything.
I build eBPF tooling at yeet, and this is the question underneath most agent security conversations once you strip away the framing. Not "is it sandboxed" but "what left the machine, and which process sent it".
Because a transcript is a record of the agent's intentions, while connections are made by processes. Those two things overlap most of the time and diverge exactly when it matters, which is what makes the transcript misleading rather than merely incomplete.
An agent logs the tool calls it decided to make, which is also what an audit of agent activity reads. It does not log what those tools did underneath: an MCP server opening a connection to a remote API, a npm install reaching a registry and its CDN, a test suite calling a service, a binary phoning home on first run. All of that is real network activity initiated by the agent's work, and none of it appears as a line in the transcript because the agent never saw it as a decision.
The gap widens with autonomy. An agent running a build touches more hosts than its transcript names, and every process it spawns can open its own connections. The ones it does not name are precisely the ones nobody chose deliberately. That is the set worth knowing about.
There is also the structural limit of self-reporting: a transcript is written by the thing being audited. For most purposes that is fine. For the question "what did this process actually connect to", it is the wrong instrument regardless of how good the agent is.
Read connections at the point the kernel establishes them, so the answer never depends on a process agreeing to report on itself. The mechanism is the same whether the process is the agent, a tool it loaded, or something a build script spawned three levels down:
curl -fsSL https://yeet.cx | sh # install yeet, once
yeet run gh:yeet-src/dialout # outbound connections, with the process
dialout reads the kernel's connect path, and the same technique applied to any process covers the non-agent case.
Every outbound TCP connection passes through the kernel's connect(2) path, and a probe there sees the destination address, the port, and the PID and command name that opened it. Nothing is installed into the agent, no proxy sits in the path, and no configuration on the agent's side is involved, which also means it works for an agent you did not configure.
Addresses are only half useful, because a destination behind a CDN tells you little. The name is in the TLS handshake:
yeet run gh:yeet-src/agentwire # SNI hostnames per process
agentwire reads the ClientHello directly, which is the same cleartext field a failing TLS handshake exposes.
SNI appears in the ClientHello in cleartext, before encryption, so the hostname is readable without touching keys. RFC 6066 defines the extension, and the ClientHello carrying it is the first message of the handshake, which is why the name is visible to anything watching the socket. Measured on a real agent workload, SNI names close to every connection while DNS fires close to never, because glibc's resolver hands the query to an NSS module rather than putting it on the wire. Where nss-resolve is configured, the lookup travels over the /run/systemd/resolve/io.systemd.Resolve AF_UNIX socket to systemd-resolved, and a wire tap never sees it. If you were planning to answer this question with DNS logs, that is the thing to know first.
Four categories cover a normal host, and knowing that expected set in advance is the thing that makes an unexpected entry meaningful. Without it every capture is just a list of hostnames you have no basis for judging.
npm, pip, cargo and friends, pulled by commands the agent ran rather than by the agent directly. A single npm install reaches the registry and then whatever object storage it redirects to, which is more hosts than the command suggests.The useful exercise is subtraction. Capture a normal session, write down the set, and then look at what a later session adds. A new destination that does not fall into one of those four categories is worth a question, and that is a far cheaper signal than trying to classify every connection in the abstract. That baseline is also what an egress allowlist should be written from, because a policy built on assumptions either breaks the agent or permits more than intended.
Yes, when it runs as its own process, which is the common case and the main reason per-process attribution earns its keep here. Without it every connection collapses into one undifferentiated pile attributed to the agent.
An MCP server started by the agent is a separate process with its own PID and command name, so a connection it opens is attributed to it rather than to the agent. That distinction is what lets you answer "the agent connected to this" versus "a tool the agent loaded connected to this", which are very different findings when something unexpected appears.
Two cases complicate it. A server communicating with the agent over a Unix socket produces no TCP for that leg, so you see only its outbound connections and not its conversation with the agent. And a server implemented as a library inside the agent process has no separate PID, so its traffic attributes to the agent itself. The first is common and fine; the second is worth knowing about before you conclude the agent made a call directly.
No, because the probe reads events the kernel already generates rather than sitting anywhere in the data path. Nothing is intercepted, buffered or re-encrypted, so there is no mechanism by which it could add latency to a request.
A connect-path probe fires once per connection, not once per byte, so the cost scales with how often the agent opens sockets rather than with how much it transfers. The kernel metrics an APM agent misses covers why this vantage point differs from an in-process one. There is no proxy to route through, no TLS termination, and no added hop, which also means nothing breaks if the capture stops.
This is the practical difference from the obvious alternative. Putting a proxy in front of an agent gives you the same visibility and makes the proxy a dependency: it has to handle every protocol the agent uses, and when it fails the agent fails with it. Reading at the kernel is strictly observational, so the failure mode is losing visibility rather than losing the agent.
Three limits apply, and the first of them decides whether this approach answers your actual question or a neighbouring one. Checking it before you build around the technique saves discovering the gap later.
Payloads. This shows that a connection happened and where it went, not what crossed it. For request and response contents you need to read inside TLS at the library boundary, which is a different mechanism with different constraints, and httpscope is the tool for it.
Unix sockets. Probes on the TCP path do not see AF_UNIX traffic, so a local MCP server or a database on a Unix socket is invisible on that leg. Connect over TCP if it needs to be visible.
Anything before the probe attached. Capture sees traffic while it is attached, so an existing long-lived connection does not reappear until it reconnects. Start the capture before the session you want to observe.
The question "what is my agent connecting to" has a precise answer available on any Linux host, and the transcript is not where it lives. Read connections at the kernel with the process attached, name the TLS destinations by SNI, and compare a session against the four categories you expect. The value is not a complete list of hosts, which changes constantly, but the ability to tell a model API call from an MCP server's onward connection from something nobody chose.
Read outbound connections at the kernel with the sending process attached. That single vantage point captures the agent, any MCP servers it started and every subprocess it spawned, without configuring anything on the agent's side or putting a proxy in the path. It also works on an agent you did not set up, which matters when you are auditing someone else's machine.
Yes, through the SNI field in the TLS ClientHello, which is sent in cleartext before encryption begins and is therefore readable without any private key. Because the probe fires inside the sending process, each hostname arrives attached to a PID, so you can tell the agent's own requests from those of a tool it loaded.
Because on a glibc host, name resolution goes through NSS to a local stub over a Unix socket, so the query frequently never reaches the wire at all for a DNS tap to see. This was measured rather than assumed: SNI named close to every connection in a real agent workload while DNS fired close to never, so the two are not co-equal sources.
No. A transcript records the tool calls the agent decided to make, which is a record of intent rather than of network activity. Connections opened underneath by MCP servers, package managers, test suites or any binary the agent executed never appear, because the agent never experienced them as a decision.
By PID. An MCP server running as its own process opens connections under its own command name, so per-process attribution separates them cleanly from the agent's own traffic. The exception is a server implemented as a library inside the agent process, which has no separate PID and whose connections therefore attribute to the agent itself.
No, and that is the main practical advantage over the obvious alternative. A kernel probe reads connection events the kernel already generates, so there is no additional hop, no TLS termination and no component that can fail closed and take the agent down with it. If the capture stops, you lose visibility rather than losing the agent.
Four categories cover a normal host: the model API, any MCP servers it started, package registries and their CDNs pulled by commands it ran, and whatever the actual work touched such as a staging API or a webhook. Knowing that expected set in advance is what makes a fifth category meaningful rather than just another hostname in a list.
Not from connection-level monitoring, which answers where traffic went rather than what crossed. Reading contents means attaching at the TLS library boundary instead, where the data is plaintext inside the process. That is a different mechanism with its own limits, including that Go and Java clients over TLS cannot be read that way.
Yes. A kernel probe observes sockets regardless of which network namespace opened them, so an agent running inside a container is covered by the same capture as one on the host. The PID identifies which container the connection came from, though you resolve that to a container name through its cgroup rather than getting it directly.
It shows you an unexpected destination and the process that reached it, which is the detection half of the problem. It does not block the connection and it does not reveal what was sent, so it belongs alongside egress policy rather than instead of it: the policy bounds what is reachable, the capture tells you something tried.
server_name extension, the source for SNI travelling in the ClientHello and therefore being readable before encryption begins.nss-resolve(8). Source for glibc resolution reaching systemd-resolved over the /run/systemd/resolve/io.systemd.Resolve AF_UNIX socket, which is why a wire-level DNS tap sees so little.nsswitch.conf rather than the resolver decides where a lookup goes.connect(2) and unix(7). The syscall the probe attaches to, and the socket family it cannot see, which together define the coverage boundary described here.dialout. The outbound-connection monitor used here, which attaches the sending process to every connection.agentwire. The SNI reader, and the source of the measured coverage finding that SNI names nearly every connection while DNS fires almost never.httpscope. For the payload question this post does not answer, reading HTTP at the TLS library boundary rather than at the connection.