← Back to list

Claude Code: Creating a C++ Linter for Embedded Development

A configurable C++ linter based on simplified JSF AV C++ Coding Standards (Lockheed Martin, 2005), adapted for embedded AI and edge…

David Such in Level Up Coding · 2026-04-02 19:53 · 146 claps · 10.0 min read paywalled
#embedded-systems #embedded-ai #cplusplus #linter #python
Open on Medium ↗
Wiki topics: LLM · Large Language Models 💻 · Programming

Claude Code: Creating a C++ Linter for Embedded Development

A configurable C++ linter based on simplified JSF AV C++ Coding Standards (Lockheed Martin, 2005), adapted for embedded AI and edge deployment

I know! I’m late to the Claude Code party but now I’m here, I’m all in. If you write C++ for microcontrollers, or edge inference, you already know that the rules are different from desktop software. No heap allocation after startup. No exceptions. No recursion on a 4 KB stack. And these constraints are not optional if you want your firmware to survive.

The problem is that general-purpose linters do not enforce the rules you need. Clang-tidy is powerful, but configuring it to catch you just used int instead of int32_t, requires writing custom checks in C++ against the AST. That is a significant investment for what should be a simple rule. I wanted something I could tweak for each project.

Image generated with Midjourney.

Image generated with Midjourney.

This article walks through building a lightweight, configurable C++ linter in Python, driven by a YAML rules file you can edit in thirty seconds. The rules are based on a simplified version of the JSF AV C++ Coding Standards, the Lockheed Martin standard originally written for the Joint Strike Fighter avionics software. And the whole thing integrates directly into Claude Code so it enforces these constraints while writing code.

[embed]What the F-35 can Teach us about Writing Safer Embedded C++ Performance can be optimized later. Reliability must be designed in from the start.levelup.gitconnected.com

Why JSF AV?

The JSF AV standard (Document 2RDU00001 Rev C, December 2005) contains over 220 rules for safety-critical C++. It predates C++11 and some of its rules are now handled by the language itself, but its core philosophy is still applicable for modern embedded development:

  • Deterministic memory usage (no heap after init)
  • Bounded execution (no recursion, limited function complexity)
  • Type safety (fixed-width integers, no implicit narrowing)
  • Explicit control flow (no goto, no continue, mandatory braces)
  • No reliance on implementation-defined behaviour

If you strip out the rules that modern compilers enforce automatically, the rules that only matter in a DO-178B certification context, and the rules about documentation formatting, you are left with roughly 35 to 40 rules that are directly useful for any embedded C++ project. Those are the ones we will implement.

What is a Linter?

A linter is a tool that reads your source code and flags problems without compiling or running it. The name comes from lint, a Unix utility written by Stephen Johnson at Bell Labs in 1978 to catch dodgy constructs in C code that the compiler would accept. The compiler's job is to produce machine code. The linter's job is to tell you that your machine code will do something you did not intend.

[embed]Embedded AI: Launch Updates and Early Access Sign up to receive launch updates for Embedded AI: Intelligence at the Edge, published by No Starch Press. You will be…embedded-ai.kit.com

Modern linters check for a broad spectrum of issues: style violations, type safety problems, unreachable code, undefined behaviour, naming convention breaches, and patterns that are technically legal but historically associated with bugs. Some operate on raw text with pattern matching. Others build a full abstract syntax tree and reason about control flow, data flow, and type relationships. The tradeoff is always the same: deeper analysis catches more problems but takes longer to run and is harder to configure.

A linter cannot catch every class of embedded bug. It will not find a race condition between your ISR and your main loop. But it can catch the kinds of mistakes that account for a big share of embedded failures: uninitialized variables, implicit type narrowing, heap allocation in code paths that run after startup, missing braces that cause an else to bind to the wrong if, and magic numbers that nobody can trace back to a datasheet.

The alternative to a linter is discipline. Every developer memorises the coding standard, and every code review checks for violations manually. Hmmmmm, I think there is a better way.

Architecture in Two Files

The linter has two files that matter (Table 1):

Table 1. Linter file descriptions

Table 1. Linter file descriptions

The idea is that you never need to touch the Python code in order to change a rule’s behaviour. Want to allow printf in debug builds? Flip enabled: false in the YAML. Want to tighten the line length limit from 100 to 80 for book formatting? Change one number. Want to add an entirely new pattern-based rule? Add a YAML block. No Python required. Simples.

The Rules File

The YAML file is organised by category. Here is a representative sample showing the structure:

meta:
  name: "Embedded AI Linter"
  version: "1.0.0"
  file_extensions: [".cpp", ".h", ".hpp", ".c"]

complexity:
  max_function_length:
    enabled: true
    severity: warning
    jsf_ref: "AV Rule 1"
    params:
      max_lines: 100
    description: "Function bodies shall not exceed the configured line limit."

  max_function_args:
    enabled: true
    severity: warning
    jsf_ref: "AV Rule 110"
    params:
      max_args: 6
    description: "Functions shall not have more than the configured number of parameters."

prohibited_features:
  no_goto:
    enabled: true
    severity: error
    jsf_ref: "AV Rule 189"
    pattern: "\\bgoto\\b"
    description: "The goto statement shall not be used."

  no_exceptions:
    enabled: true
    severity: error
    jsf_ref: "AV Rule 208"
    patterns:
      - "\\bthrow\\b"
      - "\\bcatch\\b"
      - "\\btry\\b"
    description: "C++ exceptions (throw, catch, try) shall not be used."

Every rule has a jsf_ref field that traces back to the original standard. This is useful if someone asks "why can't I use goto here?" and you can point them at AV Rule 189 and the rationale in the original document. I know this is a bad example, but you get the idea.

Severity has three levels: error for things that will break your code (heap allocation, exceptions), warning for things that are poor practice but might be acceptable with justification (magic numbers, raw int), and info for stylistic preferences (tabs, C-style comments).

The Pattern Checker: Most Rules for Free

The secret sauce that keeps the Python code manageable is that most embedded C++ rules can be expressed as “this token or pattern should not appear in code.” No goto. No malloc. No bare int. No errno. No C-style casts. These are all regex matches.

The PatternChecker class handles all of them:

class PatternChecker(RuleChecker):
    """Checks for forbidden patterns via regex."""

    def __init__(self, rule_id, config):
        super().__init__(rule_id, config)
        raw = config.get("patterns", [])
        if not raw and "pattern" in config:
            raw = [config["pattern"]]
        self.patterns = [re.compile(p) for p in raw]

    def check(self, filepath, lines, stripped):
        violations = []
        for i, line in enumerate(stripped):
            safe = strip_string_literals(line)
            for pat in self.patterns:
                for m in pat.finditer(safe):
                    if not is_in_comment(lines[i], m.start()):
                        violations.append(self._violation(
                            filepath, i + 1, m.start() + 1,
                            self.description,
                        ))
        return violations

The stripped parameter is the source with comments replaced by whitespace (preserving line numbers). The strip_string_literals call further removes string content so that "goto" inside a string literal does not trigger the no_goto rule. These two preprocessing steps eliminate most false positives without needing a full parser.

Adding a new pattern-based rule is done in YAML:

no_volatile_misuse:
  enabled: true
  severity: warning
  jsf_ref: "AV Rule 205"
  pattern: "\\bvolatile\\b"
  description: "The volatile keyword requires manual review."

The engine sees the pattern field, routes it to PatternChecker, and job done.

Rules That Need Custom Logic

Some rules cannot be expressed as a single regex. Function length requires brace counting. Function argument count requires parsing parameter lists. C-style cast detection requires matching the (type)expr pattern without false-positiving on if (condition). These all get their own checker classes.

For example, here is the function length checker:

class MaxFunctionLengthChecker(RuleChecker):
    def check(self, filepath, lines, stripped):
        limit = self.params.get("max_lines", 100)
        violations = []
        brace_depth = 0
        func_start = None
        func_name = ""
        for i, line in enumerate(stripped):
            safe = strip_string_literals(line)
            if (brace_depth == 0
                    and re.search(r"\)\s*\{?\s*$", safe.strip())
                    and not re.match(
                        r"^\s*(if|else|while|for|switch|catch)\b", safe)):
                name_match = re.search(
                    r"(\w+)\s*\([^)]*\)\s*(?:const)?\s*\{?\s*$", safe)
                if name_match:
                    func_name = name_match.group(1)
            for ch in safe:
                if ch == "{":
                    if brace_depth == 0:
                        func_start = i
                    brace_depth += 1
                elif ch == "}":
                    brace_depth -= 1
                    if brace_depth == 0 and func_start is not None:
                        length = i - func_start + 1
                        if length > limit:
                            violations.append(self._violation(
                                filepath, func_start + 1, 1,
                                f"Function '{func_name}' is {length} "
                                f"lines (limit: {limit}).",
                            ))
                        func_start = None
        return violations

It is a simple brace-depth counter. It will not correctly handle every edge case, but for the kind of C++ you write for embedded targets, it works reliably. The tradeoff is deliberate: a regex-based linter you can read and modify in an afternoon versus a clang-tidy plugin that takes a week to write and requires linking against LLVM.

Each custom checker is registered in a dictionary that maps YAML rule keys to Python classes:

CHECKER_MAP = {
    "max_function_length": MaxFunctionLengthChecker,
    "max_function_args":   MaxFunctionArgsChecker,
    "max_line_length":     MaxLineLengthChecker,
    "no_goto":             PatternChecker,
    "no_exceptions":       PatternChecker,
    "no_c_style_casts":    NoCStyleCastChecker,
    "braces_required":     BracesRequiredChecker,
    "switch_default_required": SwitchDefaultChecker,
    # ... and so on
}

If a YAML rule key appears in this map and is enabled, its checker runs. If a key has a pattern or patterns field and no explicit map entry, it routes to PatternChecker. This makes the system easy to extend.

Preprocessing: Avoiding False Positives

The two biggest sources of false positives in regex-based linting are string literals and comments. The word goto inside "use goto for error handling" is not a violation.

The linter handles this with dedicated preprocessing functions. Comment stripping walks through each line tracking // and /* */ block state, replacing comment characters with spaces which preserves line and column positions. String literal stripping does the same for content between quote characters, respecting escape sequences.

Both functions preserve the original line count, so violation line numbers are reported accurately for the source code.

def strip_string_literals(line):
    """Remove string and character literal content to avoid false positives."""
    result = []
    in_string = False
    in_char = False
    escape = False
    for ch in line:
        if escape:
            escape = False
            result.append("_" if (in_string or in_char) else ch)
            continue
        if ch == "\\":
            escape = True
            result.append("_" if (in_string or in_char) else ch)
            continue
        if ch == '"' and not in_char:
            in_string = not in_string
            result.append('"')
            continue
        if ch == "'" and not in_string:
            in_char = not in_char
            result.append("'")
            continue
        result.append("_" if (in_string or in_char) else ch)
    return "".join(result)

This is not a full lexer. It will not handle raw string literals (R"(...)") or some edge cases with nested quotes in macros.

Inline Suppressions

Every linter needs an escape hatch. Sometimes you genuinely need a magic number in a hardware register definition, or a volatile qualifier on a DMA buffer pointer. The linter supports inline suppression comments:

volatile uint32_t* dma_reg = DMA_BASE_ADDR; // NOLINT(volatile_only_for_hardware)

The suppression marker is configurable in YAML:

suppressions:
  inline_marker: "NOLINT"
  excluded_paths:
    - "third_party/"
    - "vendor/"
    - "test/"

Path-based exclusions keep third-party code and test harnesses out of the lint results.

Try it Out

Given this deliberately terrible embedded code:

#include <stdlib.h>

#define BUFFER_SIZE 256

union SensorPacket {
    int raw;
    float calibrated;
};

int process(int a, int b, int c, int d, int e, int f, int g, int h)
{
    int* p = (int*)malloc(sizeof(int) * 256);
    if (!p) goto cleanup;

    try {
        *p = a + b;
    } catch (...) {
        abort();
    }

    if (a > 0)
        *p = 42;

cleanup:
    free(p);
    return *p;
}

Run the command python jsf_lint.py examples/bad_embedded.cpp and the linter produces:

examples/bad_embedded.cpp:8:1: [WARNING] define_only_for_guards (AV Rule 29, 30, 31): #define 'BUFFER_SIZE' should be replaced with constexpr or inline function.
examples/bad_embedded.cpp:10:1: [ERROR] no_unions (AV Rule 153): Unions shall not be used.
examples/bad_embedded.cpp:11:5: [WARNING] no_raw_int_types (AV Rule 209): Basic types int, short, long shall not be used directly. Use fixed-width types: int8_t, int16_t, int32_t, uint8_t, etc.
...
examples/bad_embedded.cpp:17:14: [ERROR] no_c_style_casts (AV Rule 185): C-style cast detected. Use static_cast, reinterpret_cast, or const_cast.
examples/bad_embedded.cpp:17:20: [ERROR] no_malloc_free (AV Rule 206): C-style memory allocation (malloc/free/calloc/realloc) shall not be used.
...
Checked 1 file(s): 10 error(s), 17 warning(s), 0 info(s)

If you run the linter on the sensor_filter.h file in the examples folder of the repo you should get a report with zero violations.

Integrating with Claude Code

This is where the setup pays compound interest. Claude Code reads a CLAUDE.md file from your project root at the start of every session. If that file tells Claude what coding standards to follow, it will apply them when writing new code and when reviewing existing code.

Note — I used Claude Code to help develop this linter, so there is a CLAUDE.md file in the root directory for this purpose. The directory claude-code/ has the distributable CLAUDE.md and /lint custom command intended to be copied into your C++ projects.

With this in place, Claude Code will generate compliant code by default. When it does not, you can run the linter and feed the output back. It forms a feedback loop: the CLAUDE.md sets expectations, the linter verifies them, and deviations are caught before they reach your repository. Feedback is a beautiful thing.

Create a custom Claude Code /lint command by copying lint.md into .claude/commands/. The text for this file is available in the repo. Then you can type /lint src/chapter_03/ inside Claude Code and get a structured lint report with suggested fixes.

Limitations and When to Reach for the Big Guns

This linter is regex-based. It does not build an abstract syntax tree (AST). That means it cannot detect:

  • Recursion (requires call graph analysis)
  • Cross-translation-unit violations (one file at a time)
  • Template instantiation depth issues
  • Semantic type narrowing or implicit conversion chains
  • Unreachable code after complex control flow

For production safety-critical work under DO-178C or IEC 61508, pair this tool with clang-tidy (which has MISRA and CERT checkers), Polyspace, or a MISRA-specific tool. This linter occupies a different niche: fast, configurable feedback during development, particularly when writing book examples or prototyping embedded AI systems where you want the discipline without the certification overhead.

Getting Started

The entire linter is two files plus a CLAUDE.md. All the files can be found in the Reefwing Software GitHub repository. Drop them into your project root:

your-project/
    CLAUDE.md
    jsf_lint.py
    linter_rules.yaml
    .claude/commands/lint.md
    src/
        ...

Install the one dependency:

pip install pyyaml

Run it:

python jsf_lint.py src/

Edit linter_rules.yaml to match your project's constraints. Disable no_stdio if you need printf. Tighten max_function_length to 50 for a teaching codebase. Add 255 and 1024 to the magic number whitelist if you work with byte-oriented protocols.

The entire point is that the rules are yours to change. The JSF standard provides a solid foundation for you to build your code on, but you will have your own deployment constraints. I would be interested in learning about other linting options that people use, please leave a comment if you have a favourite.

The JSF AV C++ Coding Standards document (2RDU00001 Rev C) is approved for public release by Lockheed Martin under Distribution Statement A. The linter implementation described in this article is available under the MIT License.

My first book, Embedded AI, is coming later this year from No Starch Press, covering 25 hands-on hardware projects deploying machine learning on microcontrollers. Sign up for launch updates and bonus material! To support my writing here, please show your appreciation by following me, or subscribe to get an email whenever I publish a new article.

[embed]Embedded System Design Principles This is a subject that deserves its own book, and indeed, many have been written, but our goal here is to give you a…levelup.gitconnected.com


메타데이터
post_id
b2db36d85819
slug
claude-code-creating-a-c-linter-for-embedded-development-b2db36d85819
url
https://levelup.gitconnected.com/claude-code-creating-a-c-linter-for-embedded-development-b2db36d85819
canonical_url
https://levelup.gitconnected.com/claude-code-creating-a-c-linter-for-embedded-development-b2db36d85819
author_url
https://medium.com/@reefwing
status
ok
fetched_at
2026-06-09 15:37:30