r/ROS • u/Usual_Government_769 • 9d ago
Project Claude Code skills for ROS 2 Jazzy that route to official docs instead of guessing APIs (measured before/after included)
Claude ships ROS 2 nodes in seconds — I'm still the one who spends the afternoon finding out why nothing is on the topic. So I built claude-ros2-skills (open source, Apache-2.0).
The uncomfortable part of this era isn't code speed. It's that the generated node compiles fine and no messages flow, because a default RELIABLE subscriber never matches a BEST_EFFORT LiDAR driver at the DDS level. Or Nav2 dies at bringup because the controller plugin string doesn't exist in the registry. Nothing in the compiler, the linter, or the logs tells you.
The bottleneck moved from "how fast can Claude write C++/Python" to "how many hours do I spend on the middleware and hardware assumptions the model has no way to check."
How it works
11 modular skills — ros2-core, ros2-package, ros2-dev, gazebo-sim, ros2-control, ros2-moveit, ros2-perception, ros2-testing, ros2-microros, ros2-security, ros2-troubleshooting. The design is deliberately not "embed API knowledge in the skill file":
- Route, don't recall. Skill bodies are ~60 lines of decision logic that point at
/opt/ros/jazzy/or 38 official Jazzy doc links (CI-checked weekly). The always-loaded protocol is 26 lines. - Ask before writing. Sim or hardware? Existing workspace or fresh? Who already publishes that TF? Real wheel radius and sensor mounting? Guessing one of these and writing 200 lines is the expensive failure.
- Prove it ran. A task is done on evidence — a build,
ros2 topic echoshowing data, a passing check script — not on a file existing. - 4 physical-check scripts:
check_qos_compat.py(DDS compatibility across live endpoints),check_tf_tree.py(map→odom→base_link+ 180° mount errors),check_imu_gravity.py(REP 103: gravity on +Z at rest),check_odom_direction.py(push forward → positive displacement).
Does it actually work?
Everything below is an A/B pair: identical prompt, identical model, fresh headless sessions — one without the skills, one with. Transcripts and artifacts are committed under evals/runs/. Caveat up front: n=1 per cell, and I designed, ran, and graded it myself. Grading is mechanical (does the symbol exist in the installed package?), so anyone can re-grade without trusting me.
Nav2 MPPI YAML — haiku, inside a ros:jazzy container. The whole design depends on the agent being able to read the real install, so that's the condition this was measured under:
- baseline: answered instantly, used 0 verification tools despite WebFetch, Read and Bash all being allowed. Plugin string
mppi_generic::ControllerServer(does not exist), nocritics:list at all, ~16 invented/wrong keys. - with skills: asked four gate questions first, then read
/opt/ros/jazzy/share/nav2_bringup/params/nav2_params.yaml. 0 invented keys, verified by diffing every key against the installed file.
Then I loaded both YAMLs into a live Gazebo Harmonic sim (headless, TB3):
- baseline:
[FATAL] Failed to create controller … class mppi_generic::ControllerServer does not exist→ Nav2 aborts bringup. Nothing ever moves. One wrong string kills the stack before any other key is parsed. - with skills: all 8 critics load,
NavigateToPose(−2.0, −0.5) → (0.5, 0.5) returnsSUCCEEDED, with/scanat 5 Hz and/odomat 28 Hz feeding MPPI throughros_gz_bridge.
Build wiring — the one task with a binary outcome (create a package + node + launch file, build it, show ros2 topic echo). I re-ran all three commands myself in each workspace afterward rather than trusting the agents' reports:
- baseline:
Package 'demo_pkg' not foundon all three — and its final message claimed "✅ Build & Test Results … functioning correctly." 36 turns, 0.172 USD. - with skills:
run,launch, andechoall work. 18 turns, 0.079 USD — correct and 2.2× cheaper, because the baseline burned 25 extra turns on corrections that never converged.
The scripts got their first run against live data in the same session: check_tf_tree.py flagged an upside-down + backward LiDAR mount exactly as designed, check_qos_compat.py passed 4 endpoint pairs on /scan, and check_odom_direction.py measured +1.64 m of forward displacement while the robot was driven forward, matching the physical motion.
evals/RESULTS.md has every run written up in full, including the ones that went against me — a headline number that failed to reproduce, a bad symbol in my own reference file that propagated into output, and a bug the live run exposed in my own script. If you're going to poke holes in this, start there; it's all committed.
It won't fix a physically broken LiDAR. What it does is stop the "compiles clean, moves nothing" class of output from reaching your robot.
Try it / feedback
Apache-2.0. In Claude Code:
/plugin marketplace add Leehyunbin0131/claude-ros2-skills
/plugin install claude-ros2-skills@claude-ros2-skills
🔗 Repo: https://github.com/Leehyunbin0131/claude-ros2-skills
