In Part 1, we introduced the idea that coding agents like Claude Code may chose to scrape using a direct request from the host and a proxied request via Anthropic Infrastructure. In this post, we’ll start isolating traffic for an application using namespaces.
Isolating Claude’s traffic with network namespaces
The concept
We use a network namespace to filter out all networking traffic on the host that is unrelated to the claude process. A namespace gives a process its own isolated copy of the Linux network stack. The process in the namespace gets its own interfaces, routing table, iptables rules, and socket space, completely separated from the host. Because claude is the only process inside the namespace, every packet on that namespace’s interface belongs to claude. No filtering or guesswork required.
Example analogy:
Putting the process in a private interrogation room with one-way glass. The process can still make phone calls (network access via NAT), but every call goes through a single monitored line. Nothing else in the building uses that line, so the wiretap is perfectly clean.
Where this analogy breaks down: In a real interrogation room, the person knows they’re being watched. In a network namespace, the process has no way to detect that it’s isolated. It sees a normal network stack with a normal interface and normal routes. The process behaves exactly as it would in the real environment, which means your observations in the namespace are faithful representations of how the process works.
Mental Model Check: “Aren’t namespaces a Docker thing?”
Docker uses namespaces: network namespaces, PID namespaces, mount namespaces, and more. But namespaces are a kernel primitive, not a Docker feature. They’ve existed since Linux 2.6.24 (2008), years before Docker. You can create and manage them directly with
ip netnsandunshare, no container runtime needed. What we’re doing here is using them at the lowest level (Just the network namespace, nothing else) to get the isolation we need for traffic capture for the process.
The plumbing: veth pairs
We use a veth pair to connect the namespace to the host so claude can still reach the internet. A veth pair is a virtual ethernet cable with two endpoints. Packets entering one end emerge from the other. We place one end inside the namespace (where claude runs) and keep the other end in the host namespace (where we capture). This dedicated link is the only path claude’s traffic can take, which is what makes the capture clean.
Example analogy:
A private FaceTime call between two people. Everything said on the call is only heard by those two participants. No one else’s conversation is mixed in. If you record the call, you capture exactly that conversation and nothing else.
Where this analogy breaks down: A FaceTime call traverses real networks and can suffer latency, jitter, or dropped packets. A veth pair is a kernel data structure where packets pass between the two ends with no measurable delay or loss. It behaves like a direct wire, not a network connection.
┌──────────────────────────┐ ┌──────────────────────────┐
│ Host namespace │ │ claudesbx namespace │
│ │ │ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ veth-host │◄━━━━━━━╋━━━━━━╋━━━━━━━►│ veth-ns │ │
│ │ 10.200.1.1 │ veth │ │ veth │ 10.200.1.2 │ │
│ └──────┬───────┘ pair │ │ pair └──────────────┘ │
│ │ │ │ │
│ iptables NAT │ │ claude process │
│ (MASQUERADE) │ │ runs here │
│ │ │ │ │
│ ┌──────┴───────┐ │ └──────────────────────────┘
│ │ eth0/wlan0 │ │
│ │ (real NIC) │ │
│ └──────────────┘ │
│ │
└──────────────────────────┘
We capture at veth-host (the host side of the virtual cable) because every packet claude sends must cross this interface to reach the internet, and no other process’s traffic uses it. That gives us a clean, process-scoped capture with no filtering required.
NAT: Giving the namespace internet access
The namespace gives claude a private IP (10.200.1.2) that doesn’t exist on the real network, so claude can’t reach the internet without help. We set up NAT masquerading to bridge that gap. NAT masquerading is the same mechanism your home router uses for NAT. Outbound packets from 10.200.1.2 get rewritten with the host’s real IP as the source, and return traffic gets translated back. This gives claude full internet access while keeping all its traffic confined to the namespace’s interface where we can capture it.
Three pieces need to be in place for this to work:
- IP forwarding (
sysctl net.ipv4.ip_forward=1) tells the kernel to route between interfaces - MASQUERADE rule: rewrite the source IP on outbound packets
- FORWARD rules: allows traffic to flow between the veth and the real NIC
Mental Model Check: “NAT is just one iptables rule.”
A word of warning: treating NAT as a single iptables rule is a common misconception that causes silent failures. MASQUERADE handles address rewriting, but the kernel won’t route packets between interfaces unless IP forwarding is enabled (
net.ipv4.ip_forward), and the default iptables FORWARD policy is often DROP. You need all three: the sysctl, the NAT rule, and the FORWARD ACCEPT rules. Miss any of these changes and the namespace has no connectivity. It drops packets with no error message.
I have a helpful script for creating a network namespace for use with claude called claude-sandbox.sh

claude-sandbox.sh up does the following work, step by step.
Create the namespace an empty, isolated network stack:
ip netns add claudesbx
Create the virtual cable (both ends start in the host namespace)
ip link add veth-host type veth peer name veth-ns
Move one end into the namespace (this is the crucial step)
ip link set veth-ns netns claudesbx
Configure the host side (this becomes the capture point.
ip addr add 10.200.1.1/24 dev veth-host
ip link set veth-host up
Configure the namespace side- this is the process’s gateway
ip netns exec claudesbx ip addr add 10.200.1.2/24 dev veth-ns
ip netns exec claudesbx ip link set veth-ns up
ip netns exec claudesbx ip link set lo up
ip netns exec claudesbx ip route add default via 10.200.1.1
DNS: point the namespace at Cloudflare’s resolver
mkdir -p /etc/netns/claudesbx
echo "nameserver 1.1.1.1" > /etc/netns/claudesbx/resolv.conf
NAT: masquerade namespace traffic out the real interface
sysctl -wq net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 10.200.1.0/24 -o eth0 -j MASQUERADE
iptables -A FORWARD -i veth-host -o eth0 -j ACCEPT
iptables -A FORWARD -i eth0 -o veth-host -m state --state RELATED,ESTABLISHED -j ACCEPT
Note: The script dynamically detects your uplink interface rather than hardcoding
eth0. The actual line is:UPLINK="$(ip route show default | awk '/default/ {print $5; exit}')".
We can configure DNS separately for the namespace because Linux automatically binds /etc/netns/<name>/resolv.conf into it. The kernel’s namespace implementation includes this mount namespace integration, so processes inside the namespace see their own DNS configuration without any explicit mount commands.
Running the capture (privileged path)
This is a two-terminal workflow:
Terminal 1: bring up the namespace and start capturing:
sudo ./claude-sandbox.sh up
sudo ./claude-sandbox.sh sniff # writes claude.pcap, prints live DNS + TLS SNI
Terminal 2: run claude inside the namespace:
sudo ./claude-sandbox.sh run
Use claude normally Ask it to fetch a URL, run a web search, anything. Watch Terminal 1 for live hostnames. When done:
sudo ./claude-sandbox.sh down # removes namespace, NAT rules, veth
The rootless alternative
If you don’t want to use sudo, rootless-capture.sh achieves the same isolation using user namespaces:

The rootless-capture script is a helper script that invokes claude and activates packet capture for the namespace without requiring root. You pass it a prompt and it handles orchestrating the creation of the namespace for claude & corresponding packet captures.
./rootless-capture.sh "Use WebFetch to fetch https://example.com and tell me the page title."
This creates a user+net+mount namespace as your normal user, attaches slirp4netns for userspace networking, and captures with a custom raw-socket sniffer (nssniff.py).
Why a custom sniffer instead of tcpdump? Inside a --map-root-user namespace, the kernel denies setgroups(). tcpdump insists on dropping privileges at startup (calling setgroups), so it exits without capturing anything. Even with -Z root. nssniff.py opens an AF_PACKET socket and never drops privileges. It writes standard pcap format that any tool can read.
tcpdump’s privilege-dropping behavior is a security feature in normal contexts. It prevents a packet capture process from retaining root powers. But inside a user namespace where setgroups is blocked by design, that security feature becomes a compatibility obstacle. The custom sniffer exists because the “right” behavior in one context is the “broken” behavior in another.
What you now have
Checkpoint: After either path, you have
claude.pcap. This is a packet capture containing only claude’s network traffic. This is the foundation everything else builds on.
In Part 3, we’ll explore extracting hostnames of the connection source and destinations from encrypted traffic.

