How to Make AI Code Your Way Part 1

Teach Claude Code Your Project-Specific Skills

By Andrey Pozdnyakov and Justin Noel

This post is first in a series that tackles a familiar frustration: AI writes code, but not your code. Learn how to define project-specific skills and route tasks across models so outputs match your standards with far less cleanup.

At ICS we build a lot of Qt and QML applications for embedded Linux. Over the last year we have also been building with coding agents — Claude Code in particular — sitting in our IDEs and the usual tooling. The promise is simple: describe a change, get a patch, review it, push it to PR and move on.

The reality is messier. Out of the box, Claude writes reasonable and compiling Qt and C++ code, but it does not write with exactly the patterns and styles we prefer for our projects. This post is about closing that gap with project-specific skills, using one of our internal projects as the running example: a Qt 6 application with a QML user interface, strict interface-first architecture, and an in-house QML test harness.

Our project looks like this: Qt 6, QML user interfaces running on embedded Linux devices, C++ with strict interface-first separation to support SOLID design, Google Test for C++ unit tests, and a very extended QtTest for QML tests. No model is trained on that combination, especially our in-house QML testing harness that we use to attain 100% code coverage of QML.

Frontier models like Claude Sonnet and Claude Opus are very good at pattern mimicking. Given a handful of existing classes, Claude can usually infer our conventions (interface naming, who owns a QObject instance, how we prefer to register types for QML) and stay inside them.

But "usually" hides a lot of variance, which gets worse as the session gets longer. Our first attempt at fixing this was the obvious one: we appended architectural advice to CLAUDE.md, or pushed it into an @-linked ARCHITECTURE.md. That worked a little better. Claude would read the file, and for short prompts we got back code that roughly respected the constraints.

It degraded fast as the session got long. By the time Claude was running a multi-step plan, rules that lived hundreds of lines up in the context were drifting out of sight. The agent file is always present, but "present in context" and "salient to the next token" are not the same thing.

We decided the best fix was to stop treating architectural advice as static project preamble and start treating it as on-demand guidance. That's what skills are for.

What Is a Skill?

In Claude Code, a skill is a folder containing a SKILL.md file with YAML frontmatter and Markdown instructions. The frontmatter has two main fields: a name and a description. Claude reads the name and description of every available skill, and when it decides a prompt or a plan step matches, it loads the full SKILL.md contents into the context window right next to the active turn.

That proximity is a real advantage. The instructions end up close to the prompt being answered rather than buried at the top of a long transcript.

The minimal shape:

skills/
  writing-qml-and-tests/
    SKILL.md
---
name: writing-qml-and-tests
description: Use when the user asks to create a new QML component,
  write a tst_*.qml test, or add a QML mock of a C++ singleton.
---

# Writing Testable QML & QML Tests
...

Short, opinionated, full of "this is how we do it." That is the shape we kept reaching for.

Our Architecture: Aggressive DI and SOLID in Qt

Our project enforces a few non-negotiable patterns. Every non-trivial C++ class is defined as an interface (IFoo), with QObject living on the interface, not the concrete class. Concrete classes take their dependencies through their constructor, avoid reaching for global singletons, and never know about each other directly. Each interface has a corresponding mock, enabling isolated unit tests for every class.

This is a textbook application of dependency inversion and single responsibility. In practice, it's a stream of small, repeatable decisions — what to inherit from, how constructors are shaped, what the initializer list calls, where Q_OBJECT lives, and how QML registration is wired.

An LLM that does not know exactly what our conventions are will get some of these small decisions wrong. Not in a way that fails to compile, but in a way that quietly violates the architecture. So we wrote two project-specific skills for the things we add to this codebase the most often:

  • creating-qt-classes — covers how we structure interfaces, concrete classes, mocks and QML type registration.
  • writing-qml-and-tests — covers testable QML structure, UnitTestCase helpers and mock singleton conventions.

Inside creating-qt-classes

The body of the skill is where the teeth are. A condensed excerpt:

# Creating Qt Classes

## Overview

All C++ classes in this project inherit from QObject through their
interface. QObject lives on the **interface**, not the concrete
class. This enables signals/slots at the interface level and avoids
diamond inheritance.

## Interface Pattern

```cpp
#pragma once
#include <QObject>
#include <QtQml/qqmlregistration.h>

class IFoo : public QObject
{
    Q_OBJECT
public:
    explicit IFoo(QObject* parent = nullptr) : QObject(parent) {}
    virtual ~IFoo() = default;
    virtual void doSomething() = 0;
};
```

## Concrete Class Pattern

```cpp
// Header — inherits ONLY from interface, never directly from QObject
class Foo : public IFoo
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name NOTIFY nameChanged)
public:
    explicit Foo(IDependency& dep, QObject* parent = nullptr);
    void doSomething() override;
};

// Source — initializer list calls the INTERFACE, not QObject
Foo::Foo(IDependency& dep, QObject* parent)
    : IFoo(parent)     // NOT QObject(parent)
    , m_dependency(dep)
{}
```

## Google Mock Pattern

```cpp
class MockFoo : public IFoo
{
public:
    MOCK_METHOD(void, doSomething, (), (override));
};
```

Every constructor takes its dependency through a reference, every concrete class inherits the interface and only the interface, every mock is a drop-in for the same interface. That is the architecture in 40 lines, sitting one tool call away from any prompt that needs it.

Inside writing-qml-and-tests

The QML skill is shaped the same way: short, opinionated and full of project conventions.

Here's a small slice:

## ObjectName Convention

Every interactive or testable element gets an `objectName`
following the pattern `FileName_itemId`:

```qml
CalcButton {
    id: button1
    objectName: "Calculator_button1"
    text: "1"
}
```

## Async Operations — Always tryVerify/tryCompare

Never use `wait()` followed by `verify()` or `compare()`. Use
the try-variants which run the Qt event loop while checking:

```qml
// WRONG
wait(100)
verify(drawer.opened)

// CORRECT
tryVerify(function() { return drawer.opened })
tryCompare(errorMessage, "text", "Invalid credentials")
```

These are exactly the conventions we used to repeat in code review three times a week. Now they live in a file that Claude reads on its own.

Making Claude Reach for the Skill

Three invocation scenarios mattered to us:

  • Explicit slash-command invocation. A user typing /writing-qml-and-tests.
  • Trigger conditions in a direct prompt. A user saying "add a new QML component and a test for it" should pull in the QML skill without the user knowing it exists.
  • Trigger conditions inside an agent plan. When Claude is running a multi-step plan and a later step says "implement the new IFoo interface and a mock," that step alone should pull in the skill.

Explicit invocation was rock solid. Prompt-time invocation was mostly reliable on Claude Code. Plan-step invocation was where we saw the biggest drop-off. Once Claude was five or six steps deep into a plan, it sometimes forged ahead writing a class without loading the skill first.

We made two tweaks to our skills, which closed most of that gap:

Change 1: Explicit TRIGGER and DO NOT TRIGGER clauses

We rewrote every skill description to include both positive and negative triggers. Here is the actual frontmatter from creating-qt-classes:

---
name: creating-qt-classes
description: Create new C++ classes, interfaces, or mocks in a Qt 6
  QML project. TRIGGER when: user asks to add a new C++ class, create
  an interface (I<Name>.h), write a mock (Mock<Name>.h), add a new
  service, expose a C++ type to QML, or add a Google Test target for
  a new class. DO NOT TRIGGER for: modifying existing classes,
  QML-only work, or build/CMake issues.
---

The DO NOT TRIGGER clause does real work. Without it, creating-qt-classes would sometimes get pulled in for a plain CMake edit or a QML-only change, crowding out the skill that actually fit.

And the matching frontmatter for writing-qml-and-tests:

---
name: writing-qml-and-tests
description: This skill should be used when the user asks to create a
  new QML components or writing QML tests. Covers testable QML
  structure, UnitTestCase-based testing with async patterns
  (tryVerify/tryCompare), and mock singleton conventions. TRIGGER
  when: Adding a new QML component or writing a QML test file
  (tst_*.qml), QML mock for a C++ singleton, fix a failing QML test,
  add objectName properties for testability, use findChildByName or
  other UnitTestCase helpers, creating or update qmldir, or register
  a test in CMakeLists. DO NOT TRIGGER for: Writing C++ classes.
---

This description format — positive and negative triggers in plain prose — works for any agent harness that routes by description, not just Claude Code. Claude Code does add its own first-class frontmatter fields on top: allowed-tools (whitelist which tools the skill may call), context (files to inject), model (override the default), and hooks (shell commands to run on skill events). While those are Claude Code-specific, the trigger-clause pattern is universal.

 

Change 2: A one-line mention of each skill in CLAUDE.md

CLAUDE.md is always in context. A short table at the top that names each skill and when to use it gives the model a cheap, reliable reminder:

SkillWhen to invoke
creating-qt-classesNew C++ class, interface, or mock
writing-qml-and-testsNew QML component or QML test file

Notice that we kept the entry short. The full trigger conditions live in each skill's frontmatter; CLAUDE.md only needs to remind Claude that the skill exists. The combination of clearer descriptions and a CLAUDE.md quick-reference pushed spontaneous skill invocation from "hit or miss" to "usually works" when executing plans on Claude Code.

Every Project Should Have Its Own Skills

After using project-specific skills on a number of projects, we landed on an opinion. Every nontrivial project should have within its repository a set of skills scoped to that project, not just to the language or framework, but to that team's architecture, style and test conventions.

Some frameworks have skills for their mainstream patterns (think "write a React component"). That is necessary but not quite sufficient. What makes a codebase internally consistent is the thousand small decisions that are not in any framework doc: which layer owns what, which types are exposed to which consumers, what a test is allowed to include.

If those decisions live only in formal SDD documents, or worse in your engineers' tribal knowledge, an LLM is going to get them wrong. The engineers will have longer sessions with the LLM prompting it to perform rework, or the engineers will give up and fix it by hand. But if they live in skills, the LLM gets the details right most of the time without those engineers having to repeat themselves in prompting or in PR review.

This is the same argument people used to make for style guides, lint configs, and editor snippets, reframed for agents. The goal is the same: less time spent reminding a tool to do things your way, more time spent on the parts of the job that actually need judgment. Skills are also cheap to write incrementally — every time you catch yourself re-explaining a project convention in a code review, that convention is a candidate for a paragraph in a skill. You can even ask the LLM to add it for you!

What Made the Difference

Two short SKILL.md files, a few well-chosen TRIGGER and DO NOT TRIGGER clauses, and a small table at the top of CLAUDE.md were enough to take Claude Code from "writes correct Qt that does not look like ours" to "writes Qt that looks like ours, most of the time, on its own."

Though effort was modest, the impact wasn't: fewer reminders mid-plan, fewer architectural drifts in PRs, and a pattern we now apply at the start of every project.

If your codebase has strong conventions, this is the move. Pull architectural guidance out of CLAUDE.md, turn it into one or two focused skills, reference them up top, and let Claude do the remembering for you.

In part 2 of our series How to Make AI Code Your Way we take the same SKILL.md approach to a different setup: running Qwen Code CLI against a locally hosted Qwen3-Coder. We'll show why identical skills behaved differently, and how a simple hook closed the gap.