Full disclosure: I used both Claude Desktop and Gemini Antigravity to troubleshoot this issue. I told Claude that I thought the problem was MTU-related. It asked me to run some tests and concluded I was wrong. I wasn't so sure, so went to Gemini for a second opinion. What follows is a write-up by Gemini, and for some reason it decided to write it up as a multi-act murder mystery:
If you have spent any time writing code, you know that git push is supposed to be the most boring part of your day. You commit your work, you push, and you move on.
Until one morning, a routine push to GitHub fails. Not with a permissions error, and not with a merge conflict. Instead, you get this:
Writing objects: 100% (3/3), 3.00 MiB | 2.56 MiB/s, done.
remote: error: inflate: data stream error (incorrect data check)
remote: fatal: pack has bad object at offset 264: inflate returned -3
error: remote unpack failed: index-pack failed
To github.com:heyjudeuk/ballroomwithcarole.git
! [remote rejected] main -> main (failed)
GitHub was accusing my local repository of sending corrupted objects.
What followed was a descent into one of the most delightfully devious networking mysteries I have ever encountered—involving SSH cryptographic integrity, deceptive ICMP ping results, a hidden Path MTU black hole, and router-level TCP MSS clamping quirks.
Here is the full story of how it broke, why every standard diagnostic lied, and how we solved it.
The Crime Scene
The symptoms started while pushing commits containing image assets to GitHub from a Mac mini on a Toob Full Fibre (FTTP) broadband connection.
The behaviour was maddeningly specific:
- Small commits pushed effortlessly. A one-line README fix or a 10KB commit sailed straight through every single time.
- Commits over ~1MB failed consistently.
- Over HTTPS: Git would churn for a couple of seconds and then abort:
fatal: unexpected disconnect while reading sideband packet - Over SSH: The tunnel connected cleanly, but GitHub rejected the payload:
remote: error: inflate: data stream error (incorrect data check) remote: fatal: pack has bad object at offset 264: inflate returned -3 - The offset varied: One run would fail at offset
264, the next at263, another at312.
- Over HTTPS: Git would churn for a couple of seconds and then abort:
- Switching networks fixed it instantly.
- Pushing the exact same repository over a 5G phone hotspot? Succeeded.
- Plugging an Ethernet cable from the Mac mini directly into the Toob router? Succeeded.
The fault was undeniably pinned to a single variable: the Mac mini communicating over the Toob router’s Wi-Fi.
Act I: The Red Herrings
When an upload fails with “corrupt object” and “unexpected disconnect”, your first instinct is to assume software or hardware corruption. We methodically worked through the suspects:
| Hypothesis | Test | Result | Verdict |
|---|---|---|---|
| Local Git repository is corrupt | git fsck --full & local clone |
0 errors | Innocent |
| Defective Git binary | Homebrew Git vs Apple system Git | Both failed identically | Innocent |
| Git pack compression bug | Single-threaded pack, core.compression 0 |
Still failed | Innocent |
| Malicious Wi-Fi proxy / MITM | Verify GitHub SSH key fingerprints | Exact match to GitHub’s public keys | Innocent |
| macOS Network filtering | Check VPNs, Little Snitch, Network Extensions | None installed | Innocent |
| MTU issue | Ping Cloudflare with 1500-byte packets (ping -D -s 1472) |
0% packet loss |
“Innocent” (The Trap!) |
Every conventional check told us the system was healthy. The commits were pristine, Git was fine, the Mac was fine, and even pinging Cloudflare at 1500 bytes passed without a single dropped packet.
Yet large Wi-Fi pushes continued to die.
Act II: The Cryptographic Paradox
To crack the case, we had to step back and ask a fundamental computer science question:
Why would a Wi-Fi fault produce corrupted data instead of a dropped connection?
Wi-Fi uses an 802.11 Frame Check Sequence (a 32-bit CRC) at Layer 2. If atmospheric noise flips a bit in transit, the frame is silently dropped and retransmitted over the air. Even if that somehow failed, TCP’s checksum would catch it.
More importantly, both SSH and TLS (HTTPS) are cryptographically authenticated. Modern SSH connections negotiate Authenticated Encryption with Associated Data (AEAD), such as chacha20-poly1305@openssh.com or aes128-gcm@openssh.com.
If a single bit of encrypted data was altered between the Mac’s antenna and GitHub’s server, the SSH decryption engine on GitHub would immediately throw a cryptographic MAC verification error:
Corrupted MAC on input
SSH terminates the connection on the spot. It never decrypts garbage and hands it over to Git.
So how on earth was GitHub’s git-receive-pack process seeing incorrect data check inside zlib?
The Revelation: Truncation Masquerading as Corruption
When you push a commit over SSH, Git packages the commits into a compressed stream (a “packfile”) and pipes it directly into the SSH process’s standard input. On the other end, GitHub pipes the incoming stream straight into git-receive-pack, which uses zlib to inflate and index the objects on the fly.
Now, imagine what happens if the network connection abruptly stalls or drops while GitHub is midway through decompressing a packfile object:
- GitHub’s SSH server notices the socket has died, closes its pipe, and sends an unexpected End-of-File (EOF) to
git-receive-pack. - Inside
zlib, an object stream ends with a 4-byte Adler-32 checksum. - When
git-receive-packreaches an unexpected EOF while in the middle of an object,inflate()attempts to evaluate whatever partial trailing bytes it received against its running checksum. - The Adler-32 check fails, and
zlibreports:strm->msg = "incorrect data check"; return Z_DATA_ERROR; /* -3 */ - And Git faithfully takes that
-3and logs:remote: fatal: pack has bad object at offset 264: inflate returned -3
The data was never corrupted in transit. The connection was being choked to death mid-stream. Because the drop happened at slightly different byte boundaries depending on packet timing, the reported byte offset fluctuated on every run.
And that sudden disconnect at ~1MB under HTTPS? It was identical: Git’s HTTPS sideband demuxer directly reported unexpected disconnect while reading sideband packet. Both protocols were dying of the exact same network choke.
Act III: The PMTUD Black Hole
Why would the connection choke only on large transfers over Wi-Fi?
This brought us back to the MTU (Maximum Transmission Unit)—and why our earlier ping test had deceived us.
The Ping Illusion: Path MTU is Destination-Specific
Earlier, we ran a ping test to Cloudflare:
ping -D -s 1472 1.1.1.1
It passed with 1500 bytes (1472 payload + 28 bytes ICMP/IP headers). We concluded: “The MTU is fine.”
That conclusion was a mistake. Path MTU is strictly destination-specific.
- Cloudflare (
1.1.1.1) has direct peering at UK Internet Exchange Points (LINX and LONAP in London) with clean, unencapsulated 1500-byte paths. - The path from your Mac through Toob across transit backbones to GitHub’s infrastructure (hosted across Azure/AWS/Fastly) traverses completely different Autonomous Systems (ASNs), encapsulation tunnels, and middleboxes. An MTU of 1500 can be completely fine to Cloudflare while being fatal to GitHub.
How Real TCP Streams Behave
When Git streams an upload, TCP operates under strict rules:
- TCP sets the DF (Don’t Fragment) bit on every packet so that it can measure Path MTU dynamically.
- For small commits (< a few kilobytes), packets are small and fit well under 1500 bytes. They fly right through.
- For large commits (> 1MB), TCP opens up its Congestion Window (CWND) and starts blasting maximum-sized 1500-byte packets at wire speed.
Enter the Toob FTTP & Linksys Velop Wi-Fi Mesh
Toob’s broadband infrastructure uses Carrier-Grade NAT (CGNAT) and tunnelling protocols (such as MAP-T or DS-Lite), which consume byte overhead for encapsulation. On top of that, the standard-issue Linksys Velop mesh nodes add wireless encapsulation when bridging traffic from Wi-Fi to the WAN.
As a result, the actual maximum packet size over the Toob Wi-Fi link is less than 1500 bytes.
When our Mac mini hurled a 1500-byte packet with DF=1 at the router:
- The router could not send the packet over the link because it was too large.
- Because
DF=1, the router was forbidden from fragmenting it. - Standard networking protocol dictates that the router must drop the packet and send back an ICMP Type 3, Code 4 notification: “Fragmentation Needed / Packet Too Big”.
- But consumer routers and ISP middleboxes routinely drop these ICMP control messages.
This is the dreaded Path MTU Discovery (PMTUD) Black Hole.
[Mac Mini] ─ 1500B (DF=1) ─► [Linksys Wi-Fi]
▲ │
│ (Too big!)
│ │
│ DROPPED
│ │
│ x (ICMP dropped)
│ │
└── Mac never notified ◄───────┘
↳ Retransmits 1500B into void
↳ Connection freezes
The Mac mini kept shouting 1500-byte packets into the void. The router quietly swallowed them. The TCP socket stalled, the keepalives expired, the connection collapsed, and GitHub saw an incomplete stream.
Act IV: The Million-Dollar Question: Why Did Ethernet at 1500 Work When Wi-Fi Failed?
If the path to GitHub was the bottleneck, why did plugging an Ethernet cable into the exact same router with the exact same 1500 MTU work flawlessly?
This is where consumer router architecture gets fascinating. There are three key reasons:
1. Router-Side “TCP MSS Clamping” (Active on Ethernet, Bypassed on Wi-Fi)
When an ISP connection requires an MTU smaller than 1500, home routers handle this automatically using a feature called TCP MSS Clamping (TCPMSS --clamp-mss-to-pmtu).
When your Mac opens a connection to GitHub, it sends a TCP SYN packet proposing an MSS based on its local MTU: 1500 - 40 bytes (IP+TCP headers) = 1460 bytes. The router intercepts that packet, crosses out 1460, and rewrites it to match the WAN limit (e.g., 1452 or 1420). Both sides agree to send smaller packets, and MTU 1500 “just works.”
The catch? In home mesh routers like the Linksys Velop, Ethernet ports and Wi-Fi radios use completely different processing pipelines:
- Ethernet ports feed into the router’s internal Linux software bridge (
br-lan), where firewall rules and MSS Clamping are actively applied. - Wi-Fi radios are handled by proprietary vendor chipsets (e.g., Qualcomm NSS or Broadcom FastPath/CTF) using Hardware Flow Acceleration / Cut-Through Forwarding to achieve high wireless benchmark scores.
Because Wi-Fi traffic is accelerated in hardware directly from the radio to the WAN chip, it frequently bypasses the router’s software bridge and MSS Clamping rules entirely.
On Ethernet, the router clamped the MSS for us behind the scenes. On Wi-Fi, the router failed to clamp it, leaving the Mac shouting 1460-byte segments.
2. Mesh Wireless Frame Encapsulation Overhead
Linksys Velop is an 802.11k/v/r mesh system. Mesh systems do not handle Wi-Fi frames as pure raw Ethernet packets.
When a device transmits over Wi-Fi, the access point wraps the frame in wireless mesh headers (such as an 802.11 4-Address WDS frame, a VLAN tag, or an internal CAPWAP/GRE backhaul tunnel) to route it between nodes or to the CPU. This adds 24 to 50 bytes of internal encapsulation overhead.
On Ethernet, a 1500-byte frame goes straight onto the wire with zero extra header overhead. On Wi-Fi, that 1500-byte frame plus mesh encapsulation exceeds the router’s internal buffers and drops.
3. macOS TCP Segmentation Offload (TSO) on en0 vs en1
On macOS, Ethernet (en0) uses a dedicated physical NIC with hardware TX ring buffers and IEEE 802.3x flow control (PAUSE frames), smoothly pacing bursts.
(Jude’s note: On my Mac mini, en0 is Ethernet and en1 is Wi-Fi. On my MacBook Air, en0 is Wi-Fi. Originally Gemini assumed en0 was Wi-Fi; it didn’t ask me to check before issuing terminal commands, so it originally clamped MTU to the onboard Ethernet interface before I queried its assumption).
Wi-Fi (en1) uses aggressive TCP Segmentation Offload (TSO) and 802.11 Frame Aggregation (A-MPDU). Under a 3MB upload burst, macOS dumps 64KB buffers onto the Wi-Fi card, which aggregates frames over the air. If the router’s Wi-Fi receiver experiences bufferbloat under that burst on an MTU-constrained path, whole blocks of aggregated frames get dropped, triggering an unrecoverable TCP stall.
| Attribute | Ethernet (en0) |
Wi-Fi (en1) |
|---|---|---|
| Hardware Path | Physical Switch Chip | Wireless SoC / FastPath |
| MSS Clamping Active? | Yes (clamped by router bridge) | No (bypasses software clamp) |
| Encapsulation Overhead | None (pure 802.3) | Mesh / A-MPDU headers |
| Result at MTU 1500 | Pushes succeed | Stalls / PMTUD black hole |
Act V: The Breakthrough
With the PMTUD black hole theory in hand, we chose to drop the Wi-Fi MTU to 1280—the RFC 8200 guaranteed minimum that avoids fragmentation across encapsulated tunnels without relying on path discovery:
sudo networksetup -setMTU en1 1280
We verified with ifconfig en1 | grep mtu:
mtu 1280
With Wi-Fi clamped to 1280, we generated a fresh 3MB random binary payload and pushed to GitHub:
git checkout -b wifitest
dd if=/dev/urandom of=test_payload.bin bs=1048576 count=3
git add test_payload.bin
git commit -m "Test 3MB push with Wi-Fi en1 MTU 1280"
git push origin wifitest
Output:
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 10 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 3.00 MiB | 4.66 MiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)
To github.com:heyjudeuk/ballroomwithcarole.git
* [new branch] wifitest -> wifitest
It went through cleanly in under a second.
Zero corrupt objects. Zero disconnects. Complete, repeatable success.
Lessons for the Weary Debugger
When you are debugging network anomalies, the symptoms will frequently lie to you:
inflate returned -3does not mean corrupted data. In Git and zlib, a stream severed mid-flight triggersZ_DATA_ERRORand fails the Adler-32 checksum, which Git reports as object corruption.- End-to-end crypto protects against bit rot. If you are using modern SSH or TLS, corrupt frames on the wire will manifest as cryptographic MAC failures (
Corrupted MAC on input), not application-level bad data. - Pings don’t prove Path MTU to other hosts. A clean 1500-byte ping to Cloudflare tells you nothing about the path to GitHub. Path MTU is strictly destination-specific.
- Routers treat Ethernet and Wi-Fi differently. Consumer mesh routers often apply TCP MSS Clamping to their software Ethernet bridge, while bypassing it on their hardware-accelerated Wi-Fi flow engine.
- The MTU 1280 silver bullet. If uploads stall or large Git pushes die specifically on consumer Wi-Fi mesh routers or CGNAT ISPs, clamp the interface MTU to 1280 (or tune upwards towards 1420/1452). It eliminates PMTUD black holes across the board.
Sometimes, the most bizarre Git bugs aren’t in your code, your repository, or your commits at all—they are lurking in the invisible 220 bytes between your Wi-Fi antenna and your router.