rickvanderzwet.nl

Personal notes on technology, sustainability and DIY projects

When the measurement lies: debugging with an AI agent

16-10-2026 · author: rick · ai, process

Introduction

I maintain a small FreeBSD infrastructure: a home router, a backup server on a remote site behind NAT, reverse SSH tunnels to reach it, and port knocking in front of the tunnel endpoint - the setup described in my previous post. Most of the day-to-day work on it I now do together with an AI coding agent: Mistral Vibe 2.26.1, the CLI coding agent of Mistral AI (a European initiative I am happy to support), running its default model configuration. This evening produced a bug hunt with a twist that is worth writing down, because the most interesting failure was not in the network. It was in how an AI agent measures.

The short version of the hunt: after a configuration change, the agent's port-knock over IPv6 no longer worked. The agent ran ten-plus tcpdump experiments, concluded that the packets "never arrive", went through several wrong theories about the ISP, and only stopped when I ran one command myself, interactively, on the console - and the packets showed up instantly. Same interface, same filter, same seconds. The agent's measurement pipeline had been lying to it, four different ways.

If you build or operate AI agents, this is the post for you. The checklist below now lives in the AGENTS.md of the repository that the agent reads at the start of every session - which is exactly how operational experience should flow back into agent behavior.

The hunt, compressed

The knocking daemon on the router (knockd) is supposed to open the firewall for a client that hits five ports in the right order. IPv4 worked; the IPv6 variant did not. From the agent's measurements:

Then, at my own terminal:

router# tcpdump -ni ng0 'ip6 and tcp dst port 54321'
21:23:01.218943 IP6 2001:db8:2877:1::1.43336 > 2001:db8:7ae:154::1.54321: Flags [S], seq ...
Note

the port and addresses in this example are generic; the port in particular is not part of the real knock sequence.

There they were. Not only did the packets arrive - they passed the exact same pcap filter expression the agent had used. After that single data point, the real cause (a userspace parsing bug in the knocking daemon for IPv6 on loopback-type interfaces, while the kernel-side filter matched fine) was found in an hour.

Every "zero" the agent had measured was an artifact of its own measurement pipeline. Four distinct ways:

Lesson 1: stdio buffering eats evidence

The general rule: stdout to a terminal is line-buffered; stdout to a file or a pipe is block-buffered (typically 4-64 KiB). An agent does not run commands on a terminal. It redirects to files, it kills processes with timeouts, and it reads the files afterwards. When a timeout 40 tcpdump ... > /tmp/x.log is killed by SIGTERM, the packet lines and even the closing statistics can still be sitting in the stdio buffer - the file says "nothing", the packet was there.

This is not a tcpdump quirk; it hits any tool. The fixes are per tool:

ToolUnbuffered
tcpdumptcpdump -l
grepgrep --line-buffered
sedsed -u
awkfflush() after each print
pythonpython -u or PYTHONUNBUFFERED=1

On Linux, stdbuf -oL is a generic wrapper; FreeBSD base does not have it, so there you use the tool's own flag. And when you run ssh host cmd > file, remember that the buffering of the remote command counts.

The rule that saved us: never trust an empty result unless the output contains the closing footer or statistics that prove the tool actually finished its accounting. A tcpdump log without a "N packets captured" line is not a measurement; it is a buffer.

Lesson 2: pretty-filtering hid the errors

The agent read its own capture files with grep -v '^tcpdump:' ... to strip the noise - and thereby also stripped the error lines from its reading. Every subsequent conclusion was built on output it had pre-filtered. Rule: read raw first, filter for presentation second. Errors live exactly in the lines you did not want to see.

Lesson 3: filter terms and negative greps

Two smaller but equally effective traps:

Rule: grep for what you want, not against what you do not want, and validate every filter expression against a known-positive case before you build on its output.

Lesson 4: cross the timing, and the interface

A capture that returns zero proves nothing unless you also prove that the probe was sent and arrived within the capture window. The ground truth here was the firewall log (pflog): it showed the knock packets arriving in windows where the agent's captures were not even running yet, or had already been killed. Timestamps on both ends, every time.

And: the WAN traffic rides a VLAN subinterface. Filters on the parent NIC see only tagged frames and never match. Measure at the level where the packets actually appear.

The process rules

The network bug is fixed. What I want to keep is the process that replaced the fumbling. All of it is now in the repository's AGENTS.md, so the agent starts every future session with it:

The rules, ready to copy

The list above in copy-paste form, generalized from my repository's AGENTS.md. Drop it into yours - any agent that reads instructions at session start will benefit on the first run:

# Measurement and process hygiene

- stdout to a file or pipe is block-buffered. Use the tool's unbuffered
  flag (tcpdump -l, grep --line-buffered, sed -u, awk fflush(), python -u)
  and only trust output if the tool's closing footer or statistics is
  actually present. A log without the final stats line is a buffer, not
  a measurement.
- Read raw output first; apply presentation filters second. Error lines
  are exactly the lines you filtered away.
- Grep for what you want, never against what you do not want, and
  validate every filter expression against a known-positive case before
  building conclusions on it.
- A negative result only counts if you can prove the probe was sent and
  arrived within the measurement window. Cross-check timestamps against
  ground truth on the receiving side (firewall logs).
- Measure at the network level where the packets actually appear (VLAN,
  tunnel, sub-interface), not at an arbitrary parent interface.
- Helper commands inside a ProxyCommand need their stdin redirected (-n).
- Never restart a service over its own transport path: the stop step
  kills the transport that carries the start step. Detach the restart,
  or use a second path.
- Before every mutation on a production system: present the files to
  change, the exact commands, the expected output and the rollback; wait
  for explicit approval.
- Record fixes for methodology errors immediately in AGENTS.md, not at
  the end of the session.
- Commit per topic, during the session.

What agent builders can take from this

Two things, I think.

First: agent runtimes should make these failure modes impossible or at least loud. A process runner that keeps stdout line-buffered, always reads the final buffer after a process is killed, and warns when an expected footer or statistics line is missing, would have prevented three of the four false negatives outright. Cheaper agents are nice; honester agents are better.

Second: the loop back into the model is text, so publish the text. The lessons above only help future sessions because they live in a file the agent reads. The AGENTS.md convention (operational instructions checked into the repository) is a surprisingly effective channel to feed field experience back into agent behavior - and anything published openly feeds the next generation of models, European or otherwise. That is why this post exists.

And the most valuable data point of the evening was the human one: an operator looking over the agent's shoulder and saying your measurement must be wrong - check the instrument. The best division of labor I have found so far: the agent runs the experiments, the human guards the method.

Wrap-up

Final score: one real bug (the knocking daemon's IPv6 parsing on loopback-typed interfaces), four measurement artifacts that hid it, one expensive self-inflicted outage, and a checklist that turns all of it into something the next session - and hopefully the next agent - starts with. The full checklist lives in the repository; consider this post the long-form advertisement for keeping one yourself.

Download as: plain text