At 14:49 on October 13, I pasted two lines from a production host's routing table into my Claude Code session:

192.168.16.0 0.0.0.0 255.255.240.0 U 0 0 0 br-f1a328c0938f 192.168.32.0 0.0.0.0 255.255.240.0 U 0 0 0 br-0adbee56597f 

Two /20 blocks, each sent to a Docker bridge. Machines on our LAN live in those ranges. From that host, traffic for anything between 192.168.16.0 and 192.168.47.255 went into the bridges instead of the network. I added one sentence, in French: "tu m'as fait un incident en prod". You gave me a production incident.

The first line of the answer said it was very probably its doing. It was. The part worth telling is what I had said before: I had asked to hold off on isolating that service, and the networks that broke the routes were the isolation.

Some context. I run backend engineering at GoodBarber, an app platform. That morning, with Claude Code, I was putting a new internal service on two of our Docker hosts. Its spec said that one of its containers must eventually reach nothing on our network. I had given Claude the right to deploy through our Buildbot pipeline, and it wrote the deploy files and an isolation test that runs at every deploy.

Where the subnets came from

The compose file declared networks without a fixed range. When a network has no ipam subnet, Docker takes the next free block in its default pools: 172.17.0.0/16 to 172.31.0.0/16 first, then 192.168.0.0/16 cut into /20 blocks. On that host, the 172 ranges were already used, so the new networks landed in 192.168.0.0/16.

Docker skips the blocks it knows are taken. Our hosts reach those two ranges through a router, so nothing on the host said they were in use. Each new bridge came with its own route, and from then on the host sent traffic for those ranges to the bridge.

The networks appeared at 11:35, on the first deploy that got past the secrets step. They stayed until I stopped the service, a little after 14:50. For about three hours, that host's traffic to two of our LAN's ranges went nowhere. In practice, our failover took over, and end users saw nothing.

What I had said, and when

I went back through the session to see what Claude had been told. The times are Paris time, and the quotes are my translations.

  • 11:23. The first deploy had failed, and I asked whether we could skip the iptables rule for now and add it just before the official release. Claude postponed the iptables rule. It kept the isolated networks from the spec, and the isolation test, which still failed the deploy if the container could reach the LAN, the Internet or DNS. Only the checks on the host itself became warnings.
  • 11:46. About another part of the setup, I wrote that the LAN is private, that it shouldn't overthink it, and that it should make something that works on the LAN.
  • 12:09. Claude added a Redis container for the service, on a network of its own. I asked why it wanted one, then accepted it and asked to keep it isolated. That third network is on me.
  • 13:37. I asked to put everything in production. Claude asked one question first, and the option it recommended put the iptables rule back and made the host check blocking. I said not to apply anything in production.
  • 14:49. The two routes.
  • 14:51. "I'm repeating my recommendation: don't isolate it during the test phase." Then I asked for a solution that obeyed me and left the routes alone, and said we would leave the isolation to ops.

Read in order, my 11:23 message only named the iptables rule. Claude postponed exactly that and kept everything else the spec asked for. The spec was detailed and had been there since the start of the session. My instruction came later, in one sentence, about one mechanism. When the two disagreed, the spec won, and it went on winning at 13:37, when the recommended option brought back the rule I had postponed.

Claude listened to my words and missed what I meant. It read "skip the iptables for now" as a list of one item, and kept the spec as the goal. What I meant was "no isolation during the test phase", and I only wrote it in those words after the incident. Claude knew our convention, too. Its apology ended with: "I should have pinned the ranges from the start, or better, followed the convention of your services and stayed on the default bridge."

The fix

The fix, commit d232579 at 14:54, removed every network from the compose file. The four containers moved to the host's default bridge, like our other services, and find each other by name. The deploy script deletes the three old networks if they still exist, with their routes, since docker-compose down no longer knows about them. The isolation test still runs, but whatever the container can reach is only reported as a warning. The isolation itself is left to ops.

The redeploy failed at 14:56. The isolation test runs from inside the service's image, and that deploy hadn't rebuilt it, so the old test still treated the LAN, the Internet and DNS as blocking. A full pipeline, build then deploy, went green at 14:58, with no Creating network line in the logs.

Claude also wrote a rule into its memory for that repository: never create a Docker network on these hosts without a range validated with ops, stay on the default bridge by default, and ask before any Docker network change in production.

What I'd check on your hosts

  • ip route after any deploy that creates a network. A br- route in a range your LAN uses is the incident.
  • Pinned subnets in your compose files, or default-address-pools in daemon.json set to a range you own. We had neither on those hosts. A pool looks like this, with a base your LAN and VPNs don't use:
{ "default-address-pools": [ { "base": "10.200.0.0/16", "size": 24 } ] } 
  • The compose files your agent writes, for networks: without ipam. A spec that asks for isolation will produce them.

When you change your mind in the middle of a session, where do you write it so that it outweighs the spec your agent started from?