
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: September 2026
Quick answer. Yes, and the mechanism is boring: the agent has your source on disk and an allowed connection to a host that accepts uploads. A domain allowlist decides whether a connection may open, not what travels through it, and Anthropic's own docs warn that allowing a broad domain such as
github.com"can create paths for data exfiltration" because the proxy makes its decision from the client-supplied hostname "without inspecting TLS." The routes that actually constrain egress are a TLS-terminating proxy, a network namespace with a default-deny firewall, a container with no route out, or an air-gapped run. To see what your agent is dialing right now, one command:yeet run github:yeet-src/claudefeed -- --only=exec,conn.
I build eBPF tooling at yeet, where thousands of engineers run these scripts on their own machines, and the network side of agent confinement is the half I keep having to tell people is not solved. I am not the person who will tell you whether your legal team considers a model provider a third-party processor, and that question matters more than anything here. What I keep seeing is a specific misread: an engineer configures a domain allowlist, watches the prompts stop, and concludes the agent can no longer send anything out. Those are different claims. The allowlist governs which hosts a connection may reach, and it is silent on the bytes, which is why a post about egress has to start with what a hostname decision can and cannot express.
Yes, and it does not require a compromised agent or a malicious model. The agent reads your files as part of ordinary work, holds their contents in a context window, and makes outbound HTTPS calls to its model provider on every turn. Those two facts are the whole capability: your code is already leaving the machine, by design, to the endpoint that generates the completions. That is the arrangement you accepted when you installed it, and for most teams it is fine. The question worth asking is narrower and rarely asked: which other hosts can it reach, and what would tell you if the volume going to any of them changed.
The uncomfortable part is that "exfiltration" and "normal operation" are the same syscalls, the same sockets, and often the same destination. A git push to a repository you do not own looks exactly like a git push to one you do. A curl posting a build log to a pastebin is the same connect() and write() as a curl fetching a dependency. There is no packet flag for intent. This is why egress control for agents ends up being about destinations and volumes instead of about detecting bad behavior, and why the tools that work are the ones that refuse a connection, not the ones that try to classify it.
It stops a connection to a host you did not list, and that is a narrower guarantee than it sounds. The distinction is between a hostname decision and a content decision, and an allowlist only makes the first. Claude Code's sandboxing documentation is direct about the consequence in its Security limitations section: "By default the built-in proxy does not terminate or inspect TLS on outbound traffic, so the contents of encrypted connections are not examined." The warning that follows is the sentence to take to your threat model: "Allowing broad domains such as github.com can create paths for data exfiltration. Because the proxy makes its allow decision from the client-supplied hostname without inspecting TLS, code running inside the sandbox can potentially use domain fronting or similar techniques to reach hosts outside the allowlist."
Read that twice, because it names two separate gaps. The first is that an allowed domain is a permitted channel: github.com is on nearly every allowlist because npm, pip and cargo need it, and a repository you control is reachable at that same hostname. The second is domain fronting, where the hostname the proxy inspects and the host that actually serves the request are not the same, so even the hostname decision can be sidestepped. Both are documented by the vendor, not discovered by a critic, which is the strongest possible provenance for a limitation. The docs also say the direction of travel: "Stronger TLS-aware network isolation is an active area of development."
There is a second scope limit worth pairing with it, because it explains why the allowlist feels tighter than it is. The sandbox governs Bash subprocesses. Anthropic's Scope section states it plainly, and the network half inherits that boundary: strictAllowlist, the setting that denies unlisted hosts outright instead of prompting, is "enforced for sandboxed commands only; in-process tools such as WebFetch still follow their permission rules." So the strictest egress setting available applies to the shell and not to the agent's own fetching. That is a defensible design (the tool calls have their own permission layer) and it means "egress is locked down" is two configurations, not one.
Six routes, and they split on one question: does the mechanism decide by hostname, or does it decide by route and content. Everything in the first group is a policy about names, which a determined process can work around and an ordinary process will simply exceed. Everything in the second group is a property of the network the process lives in, which it cannot argue with. The table leads on decision criteria because the choice is nearly always driven by whether you can tolerate a CA certificate in your toolchain or a lost internet connection in your dev loop.
| Route | Decides by | Reads payload contents | Survives domain fronting | Agent keeps working normally |
|---|---|---|---|---|
| claudefeed (eBPF, observe) | Nothing; it reports every TCP connect | No, addresses and ports only | Records the real IP regardless of SNI | Yes, it does not block |
| conntrack and nftables counters (observe) | Nothing; it accounts per flow | No, byte counts only | Yes, it counts real flows | Yes, it does not block |
| Built-in domain allowlist | Client-supplied hostname | No, not by default | No, per vendor docs | Yes |
| TLS-terminating proxy with a custom CA | Hostname plus inspected content | Yes, that is the point | Yes, it sees the real request | Mostly, until a tool pins certificates |
| Network namespace plus default-deny firewall | Route, address and port | No, but nothing unlisted routes | Yes, DNS and IP are both constrained | Only for hosts you allowed |
| Container or VM with no egress route | Route; there is no path out | Not applicable | Yes, there is nothing to front | No, offline dev loop only |
claudefeed is the observation half, and it is where to start because you almost certainly do not know your agent's current egress footprint. It attaches a kprobe to tcp_connect alongside tracepoints on execve, openat and inet_listen, gates all of them on a tracked hash map of session tgids, and streams one line per event. The gate matters for cost: a system-wide firehose of openat never crosses into userspace, because the filter runs in the kernel and drops anything outside the session subtree after a single hash lookup. The tracked set self-propagates across exec, so a git three levels deep in the tree stays in scope without you naming it.
For an egress question the interesting class is conn. The probe reads the destination address and port straight off struct sock with BPF_CORE_READ, which means it records where the connection actually went, not what a hostname claimed, and a domain-fronted request shows its real destination IP. What it does not do is read bytes. conn events carry family, address and port, and there is no payload capture and no byte accounting, by design: copying connection contents through a ring buffer is a different tool with a very different cost profile. So claudefeed answers "which hosts did this session dial" precisely and answers "how much went to each" not at all.
Two more limits belong right here rather than in a footnote, because both fail in the direction that matters for security. It is TCP only, and connect initiation rather than completion, so UDP and raw sockets are not hooked and DNS over UDP is invisible; a peer that refuses or times out still produces a conn line. And it is observe-only: it tells you what a session did and does not stop, hold or modify anything. For a boundary that actually refuses, that is a firewall or a namespace, not this.
Where a connection probe tells you a socket opened, the kernel's connection tracker tells you how much went through it, and that is the one question the eBPF tools in this post do not answer. conntrack -L lists live flows with per-direction packet and byte counts, and an nftables rule with a counter statement accumulates the same totals for any traffic you can express as a match. Neither needs a program written, both are present on essentially every modern distribution, and together they turn "the agent connected to this host" into "the agent sent 340MB to this host."
What you give up is attribution, and it is a real loss for this question. Conntrack keys on the flow tuple, not on the process, so a busy machine gives you volumes with no reliable way to say which process produced them. That is the exact inverse of the probe's trade: the kernel probe knows the process and not the bytes, and conntrack knows the bytes and not the process. Running both and correlating by destination address and port is the practical answer today, and it is a correlation you do by eye.
If your question is about content and not destination, this is the answer and there is no substitute for it. You run a proxy that terminates TLS, install its CA certificate inside the environment the agent runs in, and inspect or block requests on what they actually contain. mitmproxy is the usual choice for interactive work and Squid with SSL-bump for something more permanent. Claude Code supports pointing at one: the docs describe custom proxy configuration for exactly this case and say to use it "if your threat model requires TLS inspection," and the experimental built-in network.tlsTerminate setting terminates TLS for credential masking but "does not add content filtering."
The costs are real and land on your toolchain rather than on the agent. Certificate-pinning clients break, and you will find out which of your tools pin by watching them fail. Every inspected request is a place your proxy now holds plaintext source code, which is a new secret to protect and a new log to govern. And content inspection at scale is a classification problem with no clean answer: a rule that blocks large POST bodies also blocks a legitimate large diff. Use it when you need to prove what left, not to catch a determined adversary.
This is the strongest route that still lets the agent work, and it is underused because it sounds harder than it is. Put the agent in its own network namespace, give it a veth pair to a bridge, and write iptables or nftables rules that drop everything except the specific addresses and ports it needs. Now the decision is about routes rather than names, so domain fronting has nothing to front: a connection to an address you did not allow has no path out of the namespace regardless of what SNI it presented. Pin DNS to a resolver you control and the name-to-address mapping stops being attacker-influenced too.
What you give up is convenience, and specifically the convenience of not knowing what your toolchain talks to. Every registry, every package mirror, every telemetry endpoint your language server phones home to is now something you enumerate or something that breaks. This is where the observation tools pay for themselves: run claudefeed for a normal working session first, take the list of hosts it produces, and build your allow rules from measured reality instead of a guess. That sequence, observe then constrain, is the difference between a firewall that lasts a week and one you disable on Wednesday.
Cut the route entirely and there is no exfiltration channel, which makes this the only route on the list that is a guarantee rather than a policy. It is also the only one that stops the agent from working in the usual sense, because a coding agent with no network cannot reach its model. It becomes viable in two shapes: a local model served from the host with the guest allowed to reach only that one address, or a batch pattern where the agent runs offline against a checkout and you review the diff afterward. A VM adds a hypervisor boundary, which is the only thing on this list that also contains a kernel exploit.
The reason to know this route exists even if you will not use it is calibration. When someone says an agent sandbox prevents exfiltration, this is what actually preventing exfiltration costs. Anything cheaper is a reduction in exposure, and calling a reduction a prevention is how teams end up surprised.
Run an observation pass for one normal working session and read the list, because the gap between what you think your agent talks to and what it talks to is where every later decision goes wrong. The fastest version is one command, and it needs no configuration because the tool finds live sessions by program name on its own:
curl -fsSL https://yeet.cx | sh # install the yeet daemon, once
yeet run github:yeet-src/claudefeed -- --only=exec,conn # what ran, and what it dialed
--only=exec,conn drops the file-open class, which is by far the noisiest, and leaves a readable narrative of commands and the connections they opened. Flags go after -- so the runtime routes them to the script rather than consuming them itself, and getting that wrong is the most common first-run mistake. Add --secs=120 for a bounded run you can diff later, and --match=<name> if your agent's program basename is not claude. The feed is append-only with one line per event and no full-screen repaint, so unlike most terminal tools it is safe to pipe or redirect, and colors no-op to plain text off a TTY.
To pair a destination with a volume, read the connection tracker for the same peer while the session runs:
conntrack -L -p tcp 2>/dev/null | grep ESTABLISHED # live flows with per-direction byte counts
-L lists the current table and -p tcp narrows it to TCP. The bytes= fields are what a connect probe cannot give you, and the address is what you match back against the conn lines.
Read the output for three things specifically. Hosts you cannot explain are the obvious one, and the ASN column is what makes them stand out. The count of distinct destinations matters more than any single row, because that number is what your firewall rules will have to cover and it is usually larger than people guess. Connections from processes you did not expect to have network access are the third: a language server, a test runner, or a package manager reaching out mid-session is normal, and knowing which ones do it is the difference between an allowlist that works and one that breaks your build.
Yes, in specific and predictable places, and knowing them in advance is most of the work. The failures cluster because agent toolchains all reach for the same handful of services, and each one fails differently under a default-deny policy. Ranked by how often they bite:
npm, pip, cargo and go each hit a registry plus a CDN plus, frequently, github.com for a git-sourced dependency. Blocking one of the three produces an error that names only the first, so the fix looks incomplete when it is actually three separate allow rules.No, and this is the most common mix-up in agent confinement, because both live under the word sandbox while governing unrelated subsystems. A filesystem boundary decides which paths a process may open. An egress boundary decides where its packets may go. Neither implies the other, and a tool that does one well will say nothing about the other. agent-lock is the case in point: it attaches a BPF LSM program to lsm/file_open and returns -EPERM for anything outside your project directory, which is a kernel-enforced boundary on reads, and its own documentation states that it governs opens and not sockets and that the agent's calls to its model provider keep working. That is not an oversight, it is the tool's scope.
The two boundaries do compose usefully, and the composition is the actual recommendation. Confine the filesystem and the agent cannot read ~/.ssh to exfiltrate it in the first place, which shrinks what an egress channel is worth. Constrain egress and the material the agent legitimately reads has fewer places to go. Anthropic's docs make the same point from the other direction in a warning worth quoting because it names both failure modes at once: "Effective sandboxing requires both filesystem and network isolation. Without network isolation, a compromised agent could exfiltrate sensitive files like SSH keys. Without filesystem isolation... a compromised agent could backdoor system resources to gain network access." Either half alone has a documented bypass through the other.
If your interest is the filesystem side specifically, that is covered in depth in how to sandbox an AI coding agent on Linux, including the hardlink gap in path-based enforcement and the process-name enrollment failure that makes an unconfined agent look identical to a quiet one. This post deliberately does not re-answer it.
Not from the tools in this post, and the distinction is worth stating precisely because it is the difference between an audit trail and a volume alarm. claudefeed records connection initiation from kprobe/tcp_connect, which gives you destination and timing with no byte accounting at all. It answers "where did it go" and does not answer "how much."
Byte volumes come from a different hook, and this is a gap in the current tooling rather than a solved problem with a flag. Per-connection accounting means hooking the send path (tcp_sendmsg or a socket-level LSM hook) and accumulating per-socket counters, which is a straightforward eBPF program and simply is not what these two scripts do. If that is the tool you need, the runtime underneath it is yeet, where a probe plus a ring buffer plus a table is a readable script instead of a compiled product, and describing the program you want to a coding agent pointed at the yeet docs is a reasonable way to get a first version. Meanwhile conntrack byte counters and nftables accounting rules give you per-flow volumes today without writing anything, at the cost of per-process attribution.
When the question is organizational rather than per-machine, which is a different problem than the one this post solves. Four situations where the tools here are the wrong shape:
auditd and Falco are built for it.If you have a domain allowlist configured and believe your agent cannot send your code out, read Anthropic's Security limitations section and adjust the belief, not the configuration: the proxy decides on a client-supplied hostname without inspecting TLS by default, a broad domain such as github.com is a documented exfiltration path, and strictAllowlist covers sandboxed commands rather than in-process tools. If you want to know your actual exposure before changing anything, run claudefeed for one working session and read the conn lines, then pair its destinations with conntrack -L byte counts to see volume as well as host. If you need to prove what left the machine, only a TLS-terminating proxy with its own CA reads the payload, and it will break your pinned tools and hold your plaintext. If you need a boundary the process cannot talk its way past, a network namespace with default-deny is the answer and the host list from your observation pass is how you write its rules without breaking the build. If you need a guarantee rather than a reduction, that is a container or VM with no egress route and a local model, and the reason to price it out is to see what prevention actually costs.
The part worth carrying away is that a filesystem boundary and an egress boundary are separate subsystems that share a word, and each has a documented bypass through the other. Most teams have configured one and believe they have both.
Yes, and it already does in normal operation: the agent reads your files and transmits their contents to its model provider on every turn. The security question is which other hosts it can reach and whether you would notice a change. Because reading code and sending it are both ordinary behavior, egress control works by restricting destinations instead of detecting intent.
No, it restricts which hosts a connection may reach. Anthropic's sandboxing documentation warns that allowing broad domains such as github.com "can create paths for data exfiltration," because the proxy makes its allow decision from the client-supplied hostname without inspecting TLS, so domain fronting can reach hosts outside the allowlist. An allowlist reduces exposure; it does not read what you send.
Hook the kernel's connect path and filter by process subtree. claudefeed attaches a kprobe to tcp_connect gated on a map of session process ids, so it reports every TCP connection a session and its descendants open, with the real destination address in place of a claimed hostname.
Not from a socket-level hook, which sees the ciphertext after TLS. Reading plaintext requires attaching to the TLS library's own functions, typically uprobes on SSL_write and SSL_read in OpenSSL, which works for dynamically linked processes and fails on statically linked or custom TLS stacks. Connection-level tools such as claudefeed record addresses and ports only, never payloads.
A network namespace with a default-deny nftables or iptables ruleset, because the decision is made on routes and addresses instead of hostnames, so nothing unlisted has a path out regardless of what SNI it presents. It needs no CA certificate and no proxy, and the practical prerequisite is knowing which hosts your toolchain actually needs.
No. A filesystem boundary such as agent-lock's BPF LSM hook on file_open decides which paths may be opened and says nothing about sockets, which is why a confined agent still talks to its model provider normally. The two boundaries compose well and neither substitutes for the other; Anthropic's docs warn that each has a bypass through the other's absence.
Yes. Kernel probes are passive observers with no stop imposed on the traced process, unlike ptrace, and a well-built tool filters in the kernel so events outside the watched subtree are dropped after one hash lookup and never cross into userspace. The cost then scales with the watched session's activity, not the machine's total syscall volume.
Some of them, and predictably. Tools that pin a certificate authority reject a proxy's CA no matter how correctly it is installed, and there is no configuration that fixes it; those tools get excluded from the proxy. The proxy also holds plaintext source code for every inspected request, which becomes a new secret to protect and a new log to govern.
It depends on the layer. Claude Code's documentation states that network restrictions "apply to all scripts, programs, and subprocesses spawned by commands," but its strictAllowlist setting is "enforced for sandboxed commands only" while in-process tools follow their own permission rules. So subprocess coverage and agent-tool coverage are two configurations rather than one.
No. A kprobe on tcp_connect fires only for TCP, so UDP and raw sockets are invisible and ordinary DNS lookups do not appear at all. That is a real gap for egress monitoring, because a resolver can carry data in query names. Covering it means hooking the UDP send path or constraining DNS to a resolver you control.
Scope and durability. A mesh egress rule is a platform artifact that applies across services, persists, and produces an auditable log, which is what an organizational policy needs. A per-session script or namespace governs one process tree on one machine, which suits debugging and evaluation. They answer the same question at different altitudes.
The list of destinations it actually reached, which almost nobody has recorded in advance. Kernel connection probes give it only from the moment they attach, so the practical answer is conntrack and firewall logs for the past, and starting an observation pass now for the future. Recording a normal session's host list is also how you write firewall rules that do not break your build.
github.com "can create paths for data exfiltration" via domain fronting because the proxy decides "from the client-supplied hostname without inspecting TLS"; that by default the proxy "does not terminate or inspect TLS on outbound traffic"; that network.tlsTerminate is experimental and "does not add content filtering"; that strictAllowlist is "enforced for sandboxed commands only"; that Claude Code "pre-allows no domains by default"; the Scope section limiting the sandbox to Bash subprocesses; and the warning that filesystem and network isolation each have a bypass through the other's absence.kprobe/tcp_connect probe that reads destination address and port from struct sock, the tracked tgid hash map that gates the file and network probes in the kernel so a system-wide openat firehose never reaches userspace, the self-propagating tracked set across exec, the five event classes (exec, exit, open, conn, listen), the --only, --except, --match and --secs flags, and the append-only pipe-safe output.tcp_connect fires on initiation rather than completion so a refused peer still logs; that it observes and "does not stop, hold, or modify anything"; that open events carry paths and never contents; the openat-only coverage gap; and that nothing is retained across runs.conntrack -L lists live flows with per-direction packet and byte counters. It is the route to the volume question a tcp_connect probe cannot answer, and it keys on the flow tuple rather than the process, which is why per-process attribution has to come from the probe and be correlated by address and port.lsm/file_open returning -EPERM outside a project directory, and its own statement that a hook on file opens governs the filesystem rather than sockets, so a confined agent's calls to its model provider keep working and nothing there prevents exfiltration. The reason a filesystem boundary and an egress boundary have to be chosen separately.-EPERM denies an operation and 0 allows it, and the framing of BPF LSM as a way to implement system-wide MAC and audit policies using eBPF. The contrast with a passive kprobe, which observes without a verdict.Built with yeet, a JS runtime for writing eBPF programs on Linux machines. Join us on discord.