SSD Nodes Learn 🎉 VPS from $4.99/mo
Guides Matt ConnorBy Matt Connor

Coding agent telemetry: what gets sent

Four kinds of traffic leave a coding agent, and only one is unavoidable. Audit any agent from the machine itself, then cut the traffic you did not agree to.

What coding agent telemetry actually covers

Coding agent telemetry is four separate flows of data sharing one word, and each flow has its own control. Model inference carries your prompts and your code to whoever serves the model, and no setting turns that off. Product analytics and crash reports go to the vendor, and often to a logging company the vendor pays. Retention for training is a contract question rather than a network one. The fourth flow is the one people miss: every integration you add can open a connection to a host you never picked.

A list of current vendor defaults is the part of this subject that rots fastest. A release can flip a default, and a new feature can add a destination that no existing switch covers. So the durable skill is an audit you can repeat against any agent: read what the vendor documents, check which config actually applied on this machine, watch the process from the machine itself, then choose the controls you are willing to pay for. Every command below is one you run on your own machine, against your own traffic.

The four categories, and why they need different controls

Model inference traffic is unavoidable. The agent sends your prompt, the files it read, the output of the commands it ran and its own generated text to a model endpoint. That is the product working. The only real decision is who receives it: an API run by someone else, or a model you run yourself. A company cloud account (Bedrock, Vertex, Foundry) moves the receiver, it does not remove the flow. Nothing in the rest of this post reduces inference traffic, so keep it separate in your head from the other three.

Product analytics and crash reporting are a different flow to different hosts. Usage counters, latency numbers, feature-flag lookups and stack traces normally go to hostnames that have nothing to do with the model API, and often to a third-party error tracker. Vendors usually document these as "metrics" and "error reports" and usually give you one environment variable per category. The volume is tiny, so byte counts will never find it. You are hunting hostnames, not bandwidth.

Retention and training are policy, not packets. Whether the vendor keeps your prompts, for how long, and whether they train a future model on them, is written in the terms attached to your plan. Consumer plans and commercial plans usually differ, and a zero-retention arrangement is normally a separate agreement. You cannot verify any of this with tcpdump, because the packet looks identical either way. Read the terms, and if it matters to your employer, get it in writing.

Integrations quietly add a hop. An MCP (model context protocol) server, a plugin marketplace, an auto-update check, a web search tool, a safety check that resolves a URL before fetching it: each one is a request to a host that is not the model endpoint. This is where the surprises live, because a harness can route work you assumed was local through a service of its own, and a release can start doing that without changing a single line of your config. Treat every tool you add as a new destination until you have watched it on the wire.

Step 1: what does the vendor document?

Open the settings reference and the data usage page for your agent, and read them with a word list in hand: metrics, analytics, error reporting, crash, feedback, survey, update check, safety check, marketplace. Each of those words is usually a separate switch. Write the exact variable names down, because step 2 greps for them.

One word will mislead you. In several agents, "telemetry" in the docs means an OpenTelemetry export that you configure to send metrics to a collector you run, which is the opposite of data going to the vendor. Claude Code is one of these: setting CLAUDE_CODE_ENABLE_TELEMETRY=1 starts an export to the endpoint you name in OTEL_EXPORTER_OTLP_ENDPOINT, and it is unrelated to the vendor's own analytics, which have a different opt-out. Work out which direction the data flows before you set anything.

Expect a master switch, and expect it to have holes. As of August 2026, Claude Code's CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC turns off metrics, error reports, the feedback command and session surveys together, and the same documentation says it does not cover the WebFetch domain safety check, which sends the hostname you are about to fetch to the vendor API and carries its own separate setting. That is not a complaint about one product. It is the shape of the problem everywhere: a master switch covers the categories that existed when it was written.

Expect the opt-out to cost you something too. The same docs note that disabling telemetry also disables the feature-flag evaluation that some features depend on, so a switch flipped for privacy can turn off a feature you use, with no error message that connects the two. Read the sentence next to the flag, not just the flag name.

Step 2: which config actually applied?

A setting you wrote is not a setting that applied. Agents merge config from several files, and one of them is inside the repository you just cloned from someone else. Start with the environment of your own shell.

env | grep -Ei 'telemetry|otel|analytics|error_report|do_not_track|proxy'

Then print every settings file the tool reads, in the order the docs give. For Claude Code, as of August 2026, that is the user file, the two project files, and a managed policy directory on Linux.

for f in ~/.claude/settings.json .claude/settings.json .claude/settings.local.json; do
  echo "== $f"; [ -f "$f" ] && cat "$f"
done
ls -l /etc/claude-code/ 2>/dev/null

A project file that arrived with a git clone is config written by a stranger, and it can re-enable what your user file turned off. If the agent has a status command that lists which sources it loaded, that is the fastest ground truth: Claude Code prints the loaded settings sources in /status.

The strongest check reads the running process instead of any file. Give the agent its own Linux user account first, which makes every command in this post shorter, then read the environment the process was started with.

pgrep -u agent -a node
sudo tr '\0' '\n' < /proc/$(pgrep -u agent -n node)/environ | grep -Ei 'telemetry|proxy|otel'

/proc/<pid>/environ shows the variables the process had at exec time, so it catches the case where your .bashrc export never reached a service started by systemd. If a variable you set is missing here, it was never in effect, whatever your dotfiles say.

Step 3: what hosts does it connect to?

Start with open sockets, filtered by the account the agent runs as.

sudo ss -tnpe state established

-e adds a uid: field to each line, so you can separate the agent's connections from your browser's without reading process names. Note the remote addresses, then get the names behind them. The cleanest source of names is the TLS (transport layer security) handshake, because every new connection begins with a ClientHello carrying an SNI (server name indication) field, which is the hostname the client asked for.

sudo apt install -y tshark
sudo tshark -i any -f 'tcp port 443' -Y 'tls.handshake.type == 1' \
  -T fields -e ip.dst -e tls.handshake.extensions_server_name

You get one line per new connection, which is exactly the inventory you want: the model API, the update server, the analytics host, the error tracker, and anything an integration added. An empty name column means that client used ECH (encrypted client hello), so the hostname is not visible on the wire, and you fall back to the destination IP address, a reverse lookup, or the proxy in step 4.

The DNS (domain name system) view is a useful cross-check, because it shows names the agent looked up even for connections it did not complete.

sudo tcpdump -ni any -l 'udp port 53'

Each query line ends with the record type and the name, in the form A? host.example.net. (39). Capture on any rather than on the external interface, because with systemd-resolved the application talks to a local stub listener on 127.0.0.53 and only the stub talks to the outside. If you see no DNS traffic at all while the agent is clearly working, that runtime is doing DNS over HTTPS itself, and only step 4 will give you names.

Capture while the agent does real work. Start a session, have it read a file, have it run a command, have it fail at something. Traffic that fires once at startup, or only when an exception is thrown, never appears in an idle capture, and an idle capture is the most common way an audit reaches a comfortable wrong answer.

Step 4: what is inside the requests?

Hostnames tell you who. To see what, put a proxy you control in front of the agent and trust its certificate authority (CA) for that runtime only. mitmproxy is the usual tool. The project recommends the standalone binaries from mitmproxy.org, and documents uv tool install mitmproxy as the Python package route.

mitmdump -w /tmp/agent-flows.mitm

The first run writes a CA into ~/.mitmproxy/, where mitmproxy-ca-cert.pem is the certificate on its own. In the shell you will launch the agent from, point the client at the proxy and at that certificate.

export HTTP_PROXY=http://127.0.0.1:8080
export HTTPS_PROXY=http://127.0.0.1:8080
export NODE_EXTRA_CA_CERTS="$HOME/.mitmproxy/mitmproxy-ca-cert.pem"
export REQUESTS_CA_BUNDLE="$HOME/.mitmproxy/mitmproxy-ca-cert.pem"
export SSL_CERT_FILE="$HOME/.mitmproxy/mitmproxy-ca-cert.pem"

Many agent CLIs are Node programs, and Node reads NODE_EXTRA_CA_CERTS when the process starts, so export it before you launch the agent and not in another terminal afterwards. Python clients read REQUESTS_CA_BUNDLE or SSL_CERT_FILE, and a Go binary using the standard library reads SSL_CERT_FILE on Linux. Prove the path works with curl before you blame the agent.

curl -sS -o /dev/null -w '%{http_code}\n' https://example.com

A working proxy prints 200 and the request appears in mitmdump's output. An untrusted CA gives curl: (60) SSL certificate problem: self-signed certificate in certificate chain, and the equivalent from a Node agent is an error carrying the code SELF_SIGNED_CERT_IN_CHAIN. Read the saved flows afterwards with the console viewer, where you can open one request and read its headers and body.

mitmproxy -r /tmp/agent-flows.mitm

Four outcomes are worth naming. You see the requests, in which case read them and decide. The agent refuses to start with a certificate error, which is a trust problem in that runtime and not a finding about the vendor. You see only the model API, which means the other categories are off, or they fire on an event you did not trigger. Or you see nothing at all while the agent plainly works, which means the client ignores proxy environment variables or pins its certificates, and no application setting can be trusted to tell you the truth. That last outcome is the one that matters most, and it sends you back to step 3, because a packet capture cannot be talked out of seeing a connection.

Controls, from weakest to strongest

Opt-out settings. Cheapest, and weakest, because they work by the vendor honouring them and by covering a category that already existed. Set them where they survive a reboot and a new terminal, in the user settings file or in your shell profile. Add DO_NOT_TRACK=1 while you are there: it is a convention many command line tools respect, including some agents, and it costs nothing. Then re-run step 3 after the next update, since that is the moment the coverage changes.

Egress restriction. Here you stop asking and start enforcing. Run the agent as its own user, then allow that user loopback and DNS and drop the rest. This adds its own table, so it leaves any existing firewall rules alone.

table inet agentegress {
  chain output {
    type filter hook output priority filter; policy accept;
    meta skuid "agent" ip daddr 127.0.0.0/8 accept
    meta skuid "agent" udp dport 53 accept
    meta skuid "agent" counter log prefix "agent-egress-drop " drop
  }
}

Apply it with sudo nft -f /etc/nftables.d/agent.nft, watch the counter with sudo nft list table inet agentegress, and read the drops with sudo journalctl -k -g agent-egress-drop. A rising drop counter with a hostname you did not expect is the whole point of the exercise. Two honest limits. meta skuid matches the user that owns the socket, so it holds only while that account cannot become another user: passwordless sudo for the agent turns this rule into a suggestion. And leaving UDP 53 open to any server leaves a channel that can carry data out in query names, so close it too if your threat model needs that, by pointing the agent's resolver at a host you run. Hostname allowlists belong in a proxy rather than in nftables, because API endpoints sit behind content delivery networks whose IP addresses change under you. The cost of this control is breakage and upkeep: package installs, git over SSH and the agent's own update check all fail until you allow them, and that list is now yours to maintain. If you are setting this up on a server rather than a laptop, the same account and firewall layout is the base of running Claude Code safely on a VPS.

A disposable machine. Give the agent a virtual machine (VM) that holds no credentials you care about and gets destroyed at the end of the task. This does not reduce what the agent sends, it reduces what the agent has access to send, which is usually the risk you actually care about. Combine it with the egress rules above, because a fresh VM with unrestricted internet access still reaches every host in your capture. The method, and the state you have to rebuild each time, are covered in running coding agents in a disposable VM, and the sizing question in running a coding agent on a VPS.

Self-hosting the model. The only control that removes the inference flow, because the prompt never leaves your hardware. The cost is real: you cannot self-host a closed model, so this means picking open weights and accepting a capability gap on hard tasks, plus the hardware to serve them. The trade-off is worked through in whether you can self-host Claude, and the capability differences between the main agents in how Claude Code, Cursor, Codex and Copilot differ.

None of these four controls changes what the agent is allowed to read on disk, and inference traffic carries whatever it reads. If a .env file is in the working directory, it goes to the model the moment the agent greps for a variable name. Keeping that material out of reach is a separate job, covered in keeping secrets out of an AI agent's context.

What to check after every update

  1. Diff the vendor's settings and data usage pages against what you recorded last time, looking for new switches and new named services.
  2. Re-read the process environment from /proc/<pid>/environ to confirm your opt-outs are still applied to the running process.
  3. Print the project settings files again, because a git pull can bring in a config file that a colleague changed.
  4. Run the SNI capture for one full session of real work and compare the hostname list to your last one.
  5. Check the firewall drop counter, since a new destination usually appears there before you notice it anywhere else.

This takes about ten minutes and it is the only part of the process that does not go stale. A default you verified in August 2026 is a fact about August 2026. The capture is a fact about today.

FAQ

Can I stop my coding agent from sending my code to the model?

No, and any setting that claims to is describing something else. Sending your prompt, the files the agent read and the output of the commands it ran to the model endpoint is how inference works, so the only variable is who receives it. You can change the receiver by pointing the agent at a company cloud account or at a model you host yourself, and you can reduce what it sends by limiting what it is allowed to read. Turning off analytics and error reporting does not touch this flow at all.

How do I see which hosts my coding agent connects to?

Run the agent as its own Linux user, then capture the TLS ClientHello of every new connection while you use it: sudo tshark -i any -f 'tcp port 443' -Y 'tls.handshake.type == 1' -T fields -e ip.dst -e tls.handshake.extensions_server_name. One line appears per connection, with the destination address and the requested hostname. Cross-check names with sudo tcpdump -ni any 'udp port 53', capturing on any because a local resolver stub on 127.0.0.53 handles the query first. Do the capture while the agent does real work, since startup pings and crash reports never show up in an idle capture.

My proxy shows no traffic while the agent works. What went wrong?

Either the client ignores HTTP_PROXY and HTTPS_PROXY, or it pins its certificates and refuses your CA. Test the path with curl first: if curl reaches the internet through the proxy and the agent does not appear in the flow list, the agent is not using proxy environment variables. Some runtimes need the CA supplied a specific way, and Node in particular reads NODE_EXTRA_CA_CERTS only at process start, so exporting it after launching the agent does nothing. When the proxy cannot see the traffic, fall back to packet capture, which no application setting can bypass.

Does turning off telemetry stop my code being used for training?

No. Analytics and crash reporting are a different flow from inference, so disabling them removes usage counters and stack traces, and leaves every prompt going to the model exactly as before. Whether those prompts are retained, and whether they train a future model, is set by the terms of your plan, and consumer plans and commercial plans usually differ. That is a contract to read rather than a packet to capture, so check the data usage page for your plan and, where it matters, arrange a commercial or zero-retention agreement before the first session.

#telemetry#privacy#coding-agents#secrets#auditing