Building an LLM Robot with My Son — EP 1. Designing the AI Coding Agent Harness
Building an LLM Robot with My Son — EP 1. Designing the AI Coding Agent Harness
There was a day when I first asked Claude Code to write robot code.
"Write Arduino code to control two DC motors with PWM using an L298N motor driver. IN1 on pin 8, IN2 on pin 9, ENA on pin 10. IN3 on pin 11, IN4 on pin 12, ENB on pin 13."
The code came back clean. But when I tested it, the motors behaved wrong. ENA was assigned to pin 10, and internally the code was touching Timer1 — which was conflicting with the Servo library I had running. The error message was vague. It took two hours to find the cause.
Claude didn't know I was also using the Servo library. It didn't consider the relationship between pin 10 and Timer1 in the context of my setup. As generic code, it wasn't bad. It just didn't know my project.
That one bug was enough to reach the conclusion: you can't just use a generic AI coding tool as-is.
What Breaks When You Use Generic AI Tools for Embedded Projects
Claude Code, Cursor, Aider — all of them write code well in general. They understand language syntax, know library APIs, catch bugs. For web services or scripts, there's usually no problem.
Embedded robot projects are different.
My project has constraints that only I know. There's a specific parts list. There's a pin map that belongs to this exact hardware configuration. L298N IN1 on pin 8, ENA on pin 10. HC-SR04 Trig on pin 4, Echo on pin 5. Claude doesn't know that. It has to be told every single time.
There are also library decisions with reasons behind them. I chose not to use the AFMotor library because it consumes Timer1 and Timer2 — and adding a servo or interrupt-based sensor later would cause conflicts. I picked NewPing for the ultrasonic sensor because it handles timing more precisely and supports non-blocking pings. Claude has no way to know any of this without being told.
The result was repeating this every new conversation:
"Our project uses L298N motor driver. IN1=8, IN2=9, ENA=10, AFMotor is banned due to Timer conflicts. Ultrasonic uses NewPing. HC-SR04 Trig=4, Echo=5. ROS2 topics follow the /robot/cmd_vel format..."
Every new session, I typed it out again. Miss any of it, get wrong code. Doing this repeatedly was exhausting — and there was no way I could hand my son this tool. He couldn't type all of that context every time.
What an Agent Harness Actually Is
I mentioned this briefly in EP 0. Let me get into specifics here.
The agent harness is everything outside the model itself. The formula: Agent = Model + Harness.
The model — Claude, GPT, whatever — isn't something I control. Weights, training data, inference behavior are Anthropic's or OpenAI's domain. But the environment the model operates in? That's mine to design. That's the harness.
In Claude Code, the harness lives in the .claude/ directory:
.claude/
├── CLAUDE.md # Full project context (pin maps, constraints, rules)
├── agents/
│ ├── hardware-agent.md # Sub-agent for Arduino/Pi code
│ └── ros2-agent.md # Sub-agent for ROS2 nodes and topics
├── skills/
│ ├── upload-code.md # Skill: upload code to device
│ └── test-sensor.md # Skill: validate sensor readings
└── settings.json # Hooks, permissions
Agents are role-specific sub-agents. hardware-agent only needs to know pin maps and library rules. ros2-agent only needs topic naming conventions and node structure. Separating roles makes each agent more accurate in its own domain.
Skills define repeatable tasks. Say "sensor test" and a defined sequence — read sensor values, check against expected range — executes automatically. No need to explain the procedure every time.
Hooks are event-driven automations. When a code file is saved, lint automatically runs. When certain conditions are met, a warning fires. These live in settings.json.
CLAUDE.md is one piece of this. It's the shared context document for the project — pin maps, library rationale, operational constraints. Part of the harness, not the whole harness.
Claude Built the Harness
The way this gets done has changed. A few years ago I would have designed every piece manually — figuring out which agents to create, how granular to make the skills, where to wire the hooks. Now I give Claude the direction and it does the design.
That's what I actually did. I described the project to Claude, had it read the relevant GitHub repos, and had it build the full harness.
Here's what I sent:
This project is an AI robot with Arduino Uno + L298N motor driver + HC-SR04
ultrasonic sensor + USB webcam. Planning to migrate to Raspberry Pi later
and add ROS2. LLM server runs locally on Mac, robot receives commands over LAN.
Using these repos as reference, build the harness — agents, skills, hooks,
and CLAUDE.md:
- https://github.com/teckel12/arduino-new-ping
- https://github.com/adafruit/Adafruit-Motor-Shield-library
(reference only — we're doing direct control, not using this library)
Claude read both repos and built the full .claude/ structure. It separated hardware-agent and ros2-agent so each stays in its domain. It read NewPing's async ping implementation and added "blocks the loop if using delay" as the reason in CLAUDE.md — without me telling it that. It read the Adafruit library source, understood how it occupies timers, and added an "AFMotor Timer conflict warning" to hardware-agent's constraints on its own.
I added the pin-specific wiring details, then corrected a few things through actual testing.
The structure — agents, skills, hooks — I didn't have to design from scratch. Claude identified what this type of project needs. I fixed what was wrong.
The CLAUDE.md that came out of this looks like:
# Robot Project — AI Coding Harness
## Hardware
### Edge (robot body)
- MCU: Arduino Uno (→ Raspberry Pi 4B planned)
- Camera: USB webcam (720p, 30fps)
- Distance sensor: HC-SR04 ultrasonic
### Pin Map (Arduino Uno)
| Component | Pin | Notes |
|---|---|---|
| L298N IN1 | 8 | Left motor direction A |
| L298N IN2 | 9 | Left motor direction B |
| L298N ENA | 10 | Left motor speed (PWM) |
| L298N IN3 | 11 | Right motor direction A |
| L298N IN4 | 12 | Right motor direction B |
| L298N ENB | 13 | Right motor speed (PWM) |
| HC-SR04 Trig | 4 | Ultrasonic transmit |
| HC-SR04 Echo | 5 | Ultrasonic receive |
## Library Choices (with reasons)
- **Ultrasonic**: NewPing
- Reason: blocking pulseIn() halts the loop. NewPing supports async pings
- **Motor driver**: direct digitalWrite/analogWrite (no AFMotor)
- Reason: AFMotor occupies Timer1, Timer2 → conflicts with Servo/interrupt sensors
- **Servo (future)**: Servo.h (standard library)
## ROS2 Structure (after Pi migration)
### Topic naming
- Motor commands: /robot/cmd_vel (geometry_msgs/Twist)
- Ultrasonic data: /robot/sensor/ultrasonic (sensor_msgs/Range)
- Camera frames: /robot/camera/frame (sensor_msgs/Image)
- LLM commands: /robot/llm/command (std_msgs/String)
## Safety Constraints
- HC-SR04 reading ≤ 20cm: ignore forward commands
- Reading ≤ 15cm: immediate stop + reverse 30cm
- Max PWM: 200/255 (battery protection)
## Coding Rules
- Timer1 forbidden (Servo library conflict)
- Serial debug output: 9600 baud throughout
- No delay() in loop() → use millis() for async
- Minimize global state, manage motor state with struct
The pin table alone isn't enough. The reasons behind the choices have to be there too. "No AFMotor" without the explanation means Claude might use it again next session. "Timer conflict" gives Claude the understanding to maintain that constraint. Same logic applies to delay() forbidden — even experienced Arduino developers know this, but without it explicitly stated, Claude would occasionally drop a delay(100) into example code. It happened once. After that, the rule went into CLAUDE.md.
This didn't start as a complete document. It started as ten lines of pin mappings. Every time code came out wrong, a new rule got added. The Timer1 incident added the library rationale section. The first ROS2 request added the topic naming section. The harness grows with the project.
What About Cursor or Aider?
You can do this with either. Cursor uses .cursor/rules, Aider uses .aider.conf.yml and system prompt files — same principle, different files.
I went with Claude Code for three reasons. I was already familiar with the Anthropic API. The CLAUDE.md approach is direct — plain markdown, no special syntax, just write what the project needs. And the MCP ecosystem integration is more developed, which matters when we eventually add server-side tools.
Tool choice is personal preference. If you're in Cursor, put the same content in .cursor/rules. The actual question isn't which tool — it's how do you systematically give AI the domain knowledge your project needs.
MCP and Tool Use — Later
Knowledge injection through CLAUDE.md is working. The next question was tooling.
With MCP (Model Context Protocol), you can give Claude external systems as tools. For example, an MCP server that uploads code directly to the Arduino would let Claude write code and deploy it to the robot without a separate step.
I started building this, then stopped. Is it necessary right now?
Basic motor control isn't finished. Code changes happen maybe a few times a day. Building an MCP server for that frequency is premature. Write code, upload from Arduino IDE, done — that's sufficient for now.
MCP gets added when upload frequency becomes a real bottleneck. Right now the harness runs on CLAUDE.md alone. That's an intentional choice. Adding unused infrastructure makes both the harness and the codebase harder to reason about.
The Real Design Challenge: Making It Work for a 12-Year-Old
This is where I spent the most time thinking.
I'm comfortable in the terminal. Claude Code, run a command, check output, iterate — that's natural for me. My son is not there yet. The terminal itself is unfamiliar. Code on screen means nothing to him. Red error text is alarming.
At first I was the bridge. He described what he wanted, I typed it into Claude Code, he watched the robot respond and gave feedback. A few sessions in, he was just watching. "You're doing everything, Dad." He was right. He'd become a spectator.
So I changed the rule. He types directly into Claude Code himself.
Broken grammar, incomplete descriptions — doesn't matter. He writes what he wants in his own words and submits it. That became the rule.
His first prompt:
"make it stop if there's something 20cm in front"
Claude Code produced code. Because CLAUDE.md had the HC-SR04 pin map, it used pins 4 and 5 correctly. Because the safety constraints section specified 20cm, it followed that logic. He uploaded it. I helped with the Arduino IDE part. He held his hand in front of the robot. It stopped at 20cm.
"It works!"
That's vibe coding. You don't need to know syntax. You don't need to read code. If you can describe what you want, you can build it. For my son, that was the right entry point — and it actually worked.
It's not perfect. When errors come up, he still gets confused. Even when Claude Code analyzes the error and suggests a fix, following that flow through the terminal is still hard for him. Right now, I step in when there's an error. As he gets more comfortable reading error messages, we'll gradually let him handle those too. Not yet.
Adding a Section for My Son in CLAUDE.md
I thought about making CLAUDE.md something he could actually reference.
If I wrote it as a technical document, I'd use it fine but he'd never read it. So I added a section specifically for him — not a spec document, but a cheat sheet for when he wants the robot to do something.
## For My Son — How to tell the robot what you want
Things the robot can do:
- Move forward, backward, stop
- Turn left or right
- Change speed (faster, slower)
- Stop if something's in the way
How to write it:
"make it [what you want]"
Examples:
"make it turn left"
"show me the sensor value on screen"
"stop if something is less than 50cm away"
Rules:
- Don't change the pin numbers (they're in the table above)
- If you use delay(), ask Dad
- If you don't know what a piece of code does, just ask
He read it and used it. "Make it turn left" — Claude Code produced code with the correct IN1/IN2/IN3/IN4 combinations from the pin map. The pin map is in CLAUDE.md, so he didn't need to remember it. He doesn't need to know why we chose the libraries. The AI holds that context. He just describes what he wants.
That's why a harness matters for a 12-year-old. The domain knowledge that would otherwise be a barrier is carried by the harness. He provides intent. He's learning to direct AI — not to memorize syntax. I think that skill, describing what you want and iterating toward a result, is more important right now than learning to write code. It's what's going to matter more going forward.
Where the Harness Stands Now
Currently around 120 lines. Started at 10. Timer1 incident added the library section. First ROS2 request added topic naming. My son asking "what does no delay mean" added a line to his section. Still getting revised.
That process is what harness engineering actually looks like. Not a complete document written upfront. It grows with the project, and new rules appear wherever Claude repeatedly makes the same mistake. New parts come in, pin maps get added. New decisions get made, reasons get recorded.
Much is still missing. When we migrate to Pi, a Pi section will appear. When the LLM server protocol is finalized, it goes in. When local LLM vision is wired up, the interface spec gets added. The current 120 lines is the Arduino-phase harness.
I showed him the file. "What's this?" "Robot documentation. But it's not written for people — it's written for the AI." He thought about that for a second. "AI documentation?"
Yeah. Exactly.
Next episode: actually using this harness to write code for the first time. Starting with HC-SR04 distance measurement, moving toward motor control. How the conversation with the AI unfolded — and what it revealed about the harness — recorded as it happened.
댓글
댓글 쓰기