TL;DR
- 75% of Zephyr firmware development time goes to Kconfig/devicetree configuration and build error interpretation, not writing code — Claude Code can intervene in that 75% directly from the terminal.
- Kconfig hallucination (AI inventing nonexistent symbols) is structurally preventable by combining a shell script that greps
build/zephyr/.configand Kconfig source files with Claude Code's skill and hook system.- Feeding all three files (
.dts,.dtsi, binding YAML) as context via the@syntax preventscompatiblestring hallucination in devicetree overlays.
AI coding tools still feel distant for embedded firmware developers. "Can AI even understand devicetree?" "What if it invents Kconfig symbols?" — reasonable doubts. I had them too.
I deployed Claude Code on production Zephyr/NCS firmware projects at my day job. Kconfig hallucination broke builds. I built safeguards using Claude Code's skill and hook system to fix it. This post covers the methodology I developed. I can't share proprietary code, but I can explain where things break when you hand devicetree and Kconfig to AI, and how to structurally prevent it.
Why I Left the GUI IDE for the Terminal
A typical day for a Zephyr firmware developer breaks down like this:
- Run
west build→ interpret CMake/Kconfig/devicetree errors - Edit
prj.conf→ dig throughmenuconfigto find the rightCONFIG_*symbols - Write devicetree overlays → cross-reference
.dts,.dtsi, and binding YAML simultaneously - Flash and check serial logs → track down bugs
Actual C code writing accounts for roughly 25% of the day. The remaining 75% is spent fighting configuration files and error logs.
GUI IDE AI features focus on code autocompletion — predicting function signatures, suggesting next lines. They help with the 25%. Claude Code runs in the terminal, so it can execute west build directly, read the entire build log, interpret errors, and suggest next actions. It greps .config files and traces Kconfig dependencies back to source. It can intervene in the other 75% — and that's why a terminal-based AI agent has an edge in embedded.
Dedicated embedded AI tools are emerging. Embedder (YC S25) generates driver code from uploaded PDF datasheets and is preparing serial console and GDB integration. If you work within supported chipsets (STM32, ESP32) using standard workflows, these packaged tools deliver real productivity gains.
I chose Claude Code instead. The reason comes down to what software engineering calls the double-edged sword of opinionated design. Packaged tools are productive within the Golden Path their creators designed, but friction increases the moment you step outside it. When the tool's assumed build pipeline doesn't match your project structure, you end up contorting your workflow to fit the tool. When the tool's baked-in "best practices" conflict with your hardware's nonstandard constraints, AI suggestions derail rather than help. Embedded development — where hardware configuration, SDK structure, and build pipelines vary project to project — makes this especially pronounced.
It's a tradeoff between convenience and control. I deal with Zephyr/NCS west workspace structures and per-project build configurations, so I chose to tune a general-purpose tool to fit my pipeline directly.
Devicetree: Teaching Hardware to AI
Zephyr's devicetree system has three layers. SoC .dtsi files define base hardware. Board .dts files declare pin mappings. Binding YAML files specify valid properties for each node. Developers must cross-reference all three layers when writing overlays.
To get accurate overlays from AI, you need to feed all three files as context
No Context Means Hallucination
Tell Claude Code "add SPI flash to this board" without context and you'll get a plausible but wrong overlay. The compatible string won't match any actual binding, or it'll reference a nonexistent node label.
For accurate results, feed all three files explicitly:
"Read @boards/arm/nrf52840dk_nrf52840.dts and
@zephyr/dts/arm/nordic/nrf52840.dtsi, then reference
@zephyr/dts/bindings/spi/spi-device.yaml to write
an overlay adding W25Q128 flash to SPI1."
With the @ syntax pointing to specific files, Claude Code correctly references existing SPI node labels from the board and doesn't miss required properties (reg, spi-max-frequency) defined in the binding YAML.
Three Patterns Where AI Gets Devicetree Wrong
-
compatiblestring hallucination — invents a compatible that doesn't exist in any binding. Feeding the binding YAML as context prevents this. - Node address collision —
@0must matchreg = <0>, but AI assigns addresses that duplicate existing nodes. Feeding the board.dtslets it check existing assignments. - Ignoring overlay detection rules — Zephyr's build system auto-detects overlays in this order when
DTC_OVERLAY_FILEis unset:socs/<SOC>.overlay→boards/<BOARD>.overlay→<BOARD>.overlay→app.overlay. If AI creates an overlay with an arbitrary filename, the build system ignores it.
The third problem is solved by adding one line to CLAUDE.md:
## Devicetree
- Overlay files must be named `app.overlay` or `boards/<BOARD>.overlay`
AI's role in devicetree work isn't "writing overlays for you." It's reducing the overhead of cross-referencing three files — checking available nodes in .dtsi, pulling required properties from binding YAML, and combining settings that don't conflict with existing .dts. The question is whether you do this manually by switching between three files, or hand all three to AI and get it in one pass.
Kconfig: Building a Skill to Prevent AI Hallucination
Zephyr's Kconfig system has thousands of symbols in a tree structure. Enabling CONFIG_BT auto-activates NET_BUF. Choosing CONFIG_BT_HCI under BT_STACK_SELECTION triggers another dependency chain. Symbols forced on by select, conditionally enabled by depends on, and suggested by imply are intertwined. The Zephyr project itself acknowledges excessive select usage and is migrating to depends on — that's how complex the dependency structure is.
Ask AI to "create a minimal prj.conf for BLE Central scan only" and you get a plausible result. But it might invent nonexistent symbols or miss required dependencies.
When I hit this problem at work, I solved it by making incremental requests to Claude Code. I didn't start with "build me a Kconfig hallucination prevention skill." I asked questions one at a time, and automation emerged naturally.
The Conversation Flow
"Where does the final Kconfig output go after west build?"
build/zephyr/.config contains the fully resolved symbol list — thousands of CONFIG_*=y/n lines. You can also check via menuconfig/guiconfig, but this file is the ground truth the build system actually uses.
"Find the .config file in this project's build output."
Claude Code locates build/zephyr/.config and shows its contents. A follow-up question:
"Does this project have a separate kernel source? Is there a separate kernel .config like in Linux?"
Unlike the Linux kernel, Zephyr builds the app and RTOS kernel into a single binary. build/zephyr/.config is the entire system's configuration. There's no separate kernel .config.
"Find all the Kconfig source files in the west workspace."
zephyr/Kconfig, zephyr/subsys/bluetooth/Kconfig, Kconfig files in each driver directory — organized into a tree. These source files contain each symbol's depends on, select, and help text. If AI references these when modifying prj.conf, it can't fabricate nonexistent symbols.
Here's where a problem arises. The .config file is thousands of lines. Kconfig source files are scattered across the entire west workspace. Reading everything every time wastes tokens.
"I want to reference these files when modifying .conf, but minimize token usage. How can I query only the relevant symbols?"
Claude Code proposes a shell script + skill combination. A script that greps for relevant symbols, called by a skill.
"Build that skill."
A Kconfig reference skill appears under .claude/skills/. When a .conf modification is requested:
- Grep the built
.configfor the current state of relevant symbols - Extract the
depends on,select,helptext from Kconfig source - Use this as context when modifying
.conf
Instead of reading thousands of lines, it extracts only the needed symbols and their dependencies.
"I want this skill to trigger only when modifying .conf files. Check what needs to change in .claude/."
Claude Code's hook system handles this. Set PostToolUse with matcher: "Write|Edit" in .claude/settings.json, extract the file path from stdin JSON, and conditionally trigger only for .conf files:
FILE_PATH=$(jq -r '.tool_input.file_path' < /dev/stdin)
if [[ ! "$FILE_PATH" =~ \.conf$ ]]; then
exit 0 # ignore non-.conf files
fi
## Run Kconfig reference logic
CLAUDE.md instructions are advisory — AI can ignore them. Hooks are deterministic. They execute without exception. Making Kconfig source reference automatic on every .conf edit structurally prevents AI from inventing nonexistent symbols.
What This Flow Reveals
Six conversations produced a hallucination prevention skill. Asking "build me a Kconfig hallucination prevention skill" upfront wouldn't have worked — you can't design the solution without knowing .config exists.
The pattern here is discovering AI's limitation, then using the same AI to build a tool that fills the gap. Don't try to turn Claude Code into an embedded engineer. Instead, as the engineer, identify AI's weak spots and co-build compensating tools. That approach works.
Build Error Debugging — Where AI Delivers the Highest ROI
When west build fails, the terminal floods with errors from CMake, Kconfig, devicetree, the C compiler, and the linker all mixed together. Even experienced engineers spend time just figuring out whether an error is a devicetree problem or a Kconfig problem.
Feed the entire build log to Claude Code and it classifies errors by category, then traces root causes. Embedded build errors fall into three categories, each with different AI utility.
Devicetree Binding Mismatch
When a compatible string doesn't match any binding YAML, the build system throws an error. The error message is clear enough to solve without AI, but Claude Code finds the correct binding YAML across hundreds of directories under zephyr/dts/bindings/ and proposes a fix in one step. Manually searching takes time.
Kconfig Dependency Failure — "Silent Failure"
This one is trickier. Set CONFIG_X=y in prj.conf, but if CONFIG_X has depends on CONFIG_Y and CONFIG_Y=n, the Kconfig system silently ignores CONFIG_X. The build succeeds, but the intended feature doesn't work.
Have Claude Code compare prj.conf against build/zephyr/.config and it finds symbols present in prj.conf but missing from .config. Tracing the unmet dependency requires Kconfig source — and the Kconfig reference skill from earlier connects here too.
Linker Errors
Embedded linker errors are typically RAM/Flash overflow or duplicate symbol definitions. Claude Code reads the linker script (.ld) and build.map to identify which object files conflict. For memory overflow, it extracts each section's size from build.map and suggests reduction priorities.
Build error debugging is where Claude Code's ROI is highest. Error messages are text, root-cause tracing requires cross-referencing multiple files, and resolution patterns are relatively well-defined.
Runtime Debugging: Log Analysis and GDB
After a successful build and flash, a different kind of debugging begins.
Serial Log Pattern Analysis
Capture printk or Zephyr LOG_MODULE output over serial and feed it to Claude Code. It identifies timestamp intervals, repeating error code patterns, and state changes preceding specific events. Faster than scrolling through hundreds of lines manually.
Automating the copy-paste step is possible too. serial-mcp-server is a Rust-based MCP server that exposes UART communication as Claude Code tools — list_ports, open, read, write, close. It supports STM32, ESP32, and USB-serial converters like CH340 and FTDI. With MCP configured, you can say "open the serial port and read the log" mid-conversation.
HardFault Analysis
When a HardFault occurs during J-Link + GDB debugging, feed the call stack and register dump to Claude Code. It interprets Cortex-M CFSR (Configurable Fault Status Register) bits and traces the faulting function to build a cause hypothesis.
Stack overflows, null pointer dereferences, and unaligned memory access — common patterns — are caught accurately. Nondeterministic bugs like timing issues between DMA completion interrupts and the main loop are harder, since logs alone can't reproduce them. AI help has limits there.
Limitations and Workarounds
Areas where Claude Code struggles in embedded:
Binary protocol parsing — for byte-packed data like BLE custom profiles or proprietary sensor protocols, AI makes frequent errors in bit shifting and endianness handling. Packed struct field offset calculations vary by compiler and target architecture, and AI overlooks these differences.
Timing-critical interrupt logic — when ISR execution time is constrained to microseconds, AI generates functionally correct code but doesn't optimize for execution time. volatile access ordering, cache line alignment, and compiler barrier insertion remain the engineer's domain.
Hardware register maps — don't expect AI to know your SoC's register map accurately. It gets the general structure right, but hallucinates on specific bit reset values and reserved bit handling.
Mitigation Strategies
Keep hardware specs in CLAUDE.md, but keep it short. Claude Code's official best practices call this "Progressive Disclosure" — don't dump all information, tell AI how to find it. Instead of pasting the entire register map, write "refer to nrf52840.svd for nRF52840 register details." Long CLAUDE.md files cause AI to ignore instructions.
## Hardware
- SoC: [nRF52840](https://www.nordicsemi.com/Products/nRF52840) (Cortex-M4F, 256KB RAM, 1MB Flash)
- Board: nRF52840-DK (PCA10056)
- NCS SDK: v2.9.0
- Register reference: nrf52840.svd
Always provide verification. Have AI write code, then run west build and check the result — it catches its own mistakes. According to Claude Code's official docs, "run tests after every change — this alone increases output quality 2–3x." Unit testing is often impractical in embedded, but west build pass/fail serves as a first-order verification.
Claude Code Configuration Strategy for Embedded Projects
Effective Claude Code configuration for embedded requires separating the roles of CLAUDE.md, skills, and hooks.
Component
Purpose
Execution
CLAUDE.md
Per-session context (board, SDK, build commands)
Auto-loaded (advisory)
Skills
Domain knowledge, workflows (Kconfig reference, etc.)
On-demand
Hooks
Non-negotiable rules (.conf validation, etc.)
Auto-executed (deterministic)
CLAUDE.md: Minimal Context Only
CLAUDE.md loads every session. Exclude what AI can infer from code; include only what it can't. Board pin maps, SoC specs, NCS SDK version, and build commands are typical entries.
## Build
- west build -b nrf52840dk/nrf52840 -- -DOVERLAY_CONFIG=overlay-debug.conf
- west flash --runner jlink
Conventions
- Overlay filenames: app.overlay or boards/<BOARD>.overlay
- Kconfig: prj.conf (shared), boards/<BOARD>.conf (board-specific)
- After .conf edits, verify against build/zephyr/.config
Skills: Isolate Domain Knowledge
Knowledge needed only in specific workflows — like the Kconfig reference skill — belongs in .claude/skills/. Putting everything in CLAUDE.md wastes the context window, and as it grows longer, AI starts ignoring instructions.
Hooks: Advisory vs. Deterministic
Writing "always reference .config when editing .conf" in CLAUDE.md doesn't guarantee compliance — AI can skip it. If a rule must execute without exception, implement it as a hook. Hooks run shell scripts before or after Claude Code's tool usage, independent of AI judgment.
Build-Debug Cycle
When these components combine, the embedded build-debug cycle looks like this:
Three feedback loops converge on a single edit point — Claude Code intervenes at the red and navy nodes
Hooks auto-validate Kconfig on .conf edits. AI classifies and traces build errors. AI analyzes serial log patterns. The 75% outside of code writing — that's where AI intervenes.
Wrapping Up
Applying AI to embedded development isn't about how well AI writes code. It's about reducing friction in the other 75% — build configuration, error interpretation, log analysis.
Claude Code can intervene in that 75%. But embedded's domain specifics (Kconfig hallucination, inaccurate hardware register maps) mean you can't use it out of the box. You need skills and hooks as compensating mechanisms. After building and deploying these at my day job, the loop of using AI to compensate for AI's own limitations works.
I wrote this post methodology-first because I can't share proprietary code. When I start a personal side project, I plan to apply the same approach and share devicetree overlay sessions, the Kconfig skill in action, and build error debugging logs — with actual code.


Top comments (0)