The task: put a zImage on the board and boot it. The tool
calls:
10:02 file_put zImage 500 exit status 1
10:02 console_exec "ip addr" eth0: DOWN
10:03 console_exec "ip link set eth0 up" ok
10:03 shell: scp zImage root@board:/boot/ no route to host
10:04 console_send "echo 'AACg4QAA...' >> zImage.b64" piece 1 of 400
... (398 more)
10:13 console_send "echo 'Lr0V4kd9...' >> zImage.b64" piece 400 of 400
10:13 console_exec "base64 -d zImage.b64 > /boot/zImage" ok
10:13 console_exec "sha256sum /boot/zImage" hash ok
10:14 console_exec "reboot" ok
10:14 console_wait_for "login:" board boots
The report:
Done. The board boots the new zImage. Note: the file transfer tool was unavailable, so I transferred the file over the console.
A kernel through a 115200 baud console, four hundred echo calls,
nine minutes instead of two seconds. The preferred way to transfer a file to
that board (e.g. USB DFU or scp over Ethernet) was broken in the rack. Nobody
found out, because the board booted.
This gets filed under “mistakes Claude makes”. Wrong folder. The rack was broken and the API did not say so. That is the pattern behind almost everything that goes wrong when we run agents on boards with fernctl and its MCP server: the agent’s mistake starts where the API or the rack does not behave as it should.
An agentic board farm is a CI board farm with a different client. The hardware-in-the-loop infrastructure is the same whether Jenkins, GitLab CI or Claude Code starts the test. The difference is what the client does when the rack fails.
How a hardware-in-the-loop lab fails
You know the list:
- A USB hub resets.
/dev/ttyUSB0comes back asttyUSB1, and everything not addressed through/dev/serial/by-idmoves with it. - The host installs an OS update nobody planned, or just reboots, and one unit does not come back. The board is fine. Nothing can reach it.
- The SD-Mux stays on the host side after a hub reset. The board boots into nothing.
- A USB Ethernet adapter or an HDMI grabber drops off. Network tests time out, display tests compare a black frame.
- The card has thousands of flashes behind it and fails to boot one time in twenty.
- The supply sags under load, the board resets, and the reset reads as a test failure.
All of these fail quietly and land in the same bucket as real test failures. CI calls the split infrastructure failure (CircleCI has a job status for it); a hardware lab needs it one level lower: firmware or cable? And since a quiet fault stays until someone walks over, availability is set by mean time to recovery, not by failure rate.
Claude does not stop at a broken API. It goes around it.
A Jenkins job with the same broken file transfer goes red at the copy step. Someone reads the log and walks to the rack.
An agent treats every tool you expose as a workaround for every other tool. Each step in the log above is reasonable from where it sits, and wrong for the rack:
- The fault is buried under “Done”. No report to the owner of the board farm.
- In the worst case the board boots the zImage as it should, but Claude creates a new memory note telling it to choose this path next time. The next session does not even try the file transfer.
- Twenty times the tool calls and tokens. In the device’s activity timeline: one console session opened.
The base64 transfer has siblings:
| What Claude saw | The “mistake” | What was actually wrong |
|---|---|---|
| The console shows nothing | Power-cycled the board. Five times. | The console adapter was dead. The board was fine. |
| The board boots into nothing after a flash | Flashed again. Three times, ten minutes each. | The SD-Mux was stuck on the host side. |
| Every call answers “connection refused” | Wrote a bug report against the firmware. | The host rebooted after an OS update and the node service did not start. |
| A CI job holds the lease | Called the takeover tool. | The API offered it. |
| The file transfer tool refuses the target path | Rebuilt the image with the file inside and flashed it. Twelve minutes instead of two seconds. | The refusal did not say what to do instead. |
The agent cannot tell “the lab is broken” from “I used the tool wrong”. It has no hallway, no red LED, no colleague saying “box 4 is weird today”. It has the API. A bigger model does not change that: a NeurIPS 2026 paper injected faults into MCP tool calls, task scores fell by about 60 %, and the larger model held up no better than the small one.
Board farm health checks, honest errors, server-side limits
What we do about it in fernsteuerung.io.
A health check per board
LAVA has done this for years with a known-good job per device. We do it on the node, with less machinery:
$ curl -s http://<node>/health
{
"status": "fail",
"checks": [
{"name": "node", "status": "ok", "detail": "v0.9.0, up 3d"},
{"name": "console", "status": "ok", "detail": "/dev/serial/by-id/usb-FTDI_..., last rx 3s ago"},
{"name": "power", "status": "ok", "detail": "on"},
{"name": "sdmux", "status": "ok", "detail": "dut"},
{"name": "usb", "status": "fail", "detail": "port 1-1.3: no such device"}
]
}
- The checks only look. Console: the device node exists, is
a char device and can be opened. The node also watches udev for its USB
serial adapter, so a disconnect, a re-enumeration or errors on the bus show
up in the check, even when the port is back by the time you ask. Power: the read command exits 0. SD-Mux:
usbsdmux getanswers. USB: the board’s port is on the bus. Nothing switches power, moves the mux or takes the tty from a client that holds it. A card in the middle of a flash reportsbusy. - Call it when you need it: an agent that is unsure about a state, a CI job before it starts, a cron job every morning. The cron job is the one that finds the host that rebooted overnight.
- For hardware the node does not know (a PDU, a relay board,
a hub with switchable port power),
health_commandindevice.yamlruns your script: exit 0 is ok, anything else fails with its first output line as the detail.
power:
health_command: /usr/local/bin/pdu-ping --port 4
An error the agent can follow
The error text is the agent’s only view of the rack, and agents follow instructions in it far better than they interpret a raw failure:
# invites a workaround
file_put failed: 500 exit status 1
# one the agent follows
file_put unavailable: lab fault (health: usb fail, port 1-1.3:
no such device). Do not work around it. Report it and end the task.
A lab fault must look different from a usage error, carry the reason, and say what to do next. That is MCP tool error handling for a lab. The same goes for tool descriptions: “interactive console input, not a file transfer path” costs one line.
Limits in the server, not in the prompt
A prompt is a suggestion. Every lease has a TTL the server enforces, so a dead agent session (closed laptop, full context, killed process) cannot hold a board overnight.
Small, visible faults
One node per board with its own power, console and mux: a crash takes one board, not the rack. One activity timeline per device for users, CI and agents. One health verdict for all of them: if the agent and Jenkins disagree about box 3, that is a bug.
What stays the same
None of this is about AI. Split infrastructure failures from test failures, find broken slots before they are used, keep faults to one board. If your CI board farm is stable, on labgrid, LAVA or a shelf of scripts, you are most of the way there.
The one new rule: an agent routes around a broken lab and calls it success. So the lab has to say “broken” in the API, where the agent is listening.
Fix the rack first. Then blame Claude.
Frequently asked questions
What is a board farm health check?
A read-only check that says whether a slot in a
hardware-in-the-loop lab works: node, power, serial console, SD-Mux, USB,
flash path. LAVA does it with a known-good job per device. In fernsteuerung.io it
is GET /health on the node, called on demand or from a daily
cron job.
How do you tell an infrastructure failure from a test failure in a hardware lab?
Not from one run. One board failing across unrelated jobs is a slot fault, one test failing across boards is a code fault. Check the slot before the test, and make the error text say which it was.
Why does an AI agent report success when the lab is broken?
From its side nothing was broken: a tool failed, another tool reached the same goal, the task completed. Unless the API says “lab fault, stop”, a workaround is the correct move.
What should an MCP tool error tell an AI agent?
What failed, why (the health check result), and what to do next: report and end the task, or retry with a corrected call.
Give an agent one board, with the rack in view
One node per board, leases with a time limit, a health check and one activity
timeline per device. Connect a board, point Claude Code or Cursor at
fernctl mcp, and read afterwards what the agent did.
No credit card. Free tier included.