You ask Claude to "add a Subscription service that calls Stripe" inside your C++ codebase, and you get back something that compiles cleanly and is still wrong:
- A function returning
Subscription*whose ownership convention is "trust me, youdeleteit." - Three
new/deletepairs in the happy path because that's what the model saw on Stack Overflow circa 2009. - A
using namespace std;at the top of a header, leaking into every consumer. - A
std::mutexlocked withm.lock()/m.unlock()by hand — guaranteed to leak on the next thrown exception. - A C-style cast
(MyType*)ptrthat would bestatic_castif the model knew you were past 2003. - A class with seven uninitialized member variables, default-constructed into UB.
The model isn't lazy. It's been trained on 25 years of C++ code, the median of which is C++98 with <vector>. A CLAUDE.md at the root of your project drags it forward to where you actually live — C++17 with selective C++20.
Here are 12 rules I drop into every Modern C++ project. Each one closes a class of bug AI assistants generate by default.
Rule 1 — No Raw new / delete in Application Code
Why: Every new paired with a hand-written delete is a leak waiting on the next thrown exception. AI tools default to manual allocation because that's what the average training-set C++ does. The fix is structural: ownership lives in a smart pointer, period.
Bad:
class Service {
public:
Service() : client_(new HttpClient(/*...*/)) {}
~Service() { delete client_; } // and now it's not copy-safe
private:
HttpClient* client_;
};
Good:
class Service {
public:
Service() : client_(std::make_unique<HttpClient>(/*...*/)) {}
// destructor, copy/move semantics: defaulted correctly by the compiler
private:
std::unique_ptr<HttpClient> client_;
};
Rule for CLAUDE.md:
No raw new / delete / malloc / free in application code.
Use std::make_unique<T>(args...) and std::make_shared<T>(args...).
Owning raw pointers (T* that the caller must delete) are forbidden across API boundaries.
Return std::unique_ptr<T>, return by value, or hand out a non-owning view.
Rule 2 — RAII Everything, No Init() / Cleanup() Pairs
Why: Resources that aren't memory — file handles, sockets, mutexes, GPU buffers, OS handles — are leaked exactly as easily as memory. AI assistants love Init() / Cleanup() patterns because they translate cleanly from imperative pseudocode. The C++ idiom is the opposite: the destructor runs on every exit path, including thrown exceptions.
Bad:
File f;
f.Open("config.toml");
auto data = parse(f.Read()); // throws → f.Close() never called
f.Close();
Good:
class File {
public:
explicit File(const std::string& path)
: fd_(::open(path.c_str(), O_RDONLY)) {
if (fd_ < 0) throw std::system_error(errno, std::generic_category(), path);
}
~File() { if (fd_ >= 0) ::close(fd_); }
File(const File&) = delete;
File& operator=(const File&) = delete;
File(File&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {}
File& operator=(File&& other) noexcept {
if (this != &other) { if (fd_ >= 0) ::close(fd_); fd_ = std::exchange(other.fd_, -1); }
return *this;
}
// ...
private:
int fd_ = -1;
};
Rule for CLAUDE.md:
Every resource (file, socket, mutex, GL handle, GPU buffer, OS handle)
is wrapped in an RAII type whose destructor releases it.
No Init() / Cleanup() pairs that callers must remember to invoke.
Wrappers delete copy and explicitly = default the move pair.
Rule 3 — std::mutex Is Always Held by lock_guard / scoped_lock
Why: Manual mutex.lock() / mutex.unlock() is broken the moment any code between them throws — and "any code" in C++ includes most allocations. AI-generated concurrency code is full of this pattern. RAII fixes it for free.
Bad:
void Cache::insert(Key k, Value v) {
mu_.lock();
map_[k] = std::move(v); // bad_alloc here → mutex stays locked
mu_.unlock();
}
Good:
void Cache::insert(Key k, Value v) {
std::scoped_lock lock(mu_); // released on every exit path
map_[k] = std::move(v);
}
Rule for CLAUDE.md:
std::mutex is held via std::lock_guard / std::scoped_lock — never by hand.
Hold locks for the minimum span. Never call user code (callbacks, virtual methods)
while a lock is held — ordering inversions are silent and brutal.
For multi-mutex critical sections, use std::scoped_lock(m1, m2, ...).
Rule 4 — Move Semantics: noexcept Move, = delete Copy When Owning
Why: A move constructor that isn't noexcept is silently downgraded to a copy by the standard library. std::vector<T>::push_back will copy your "movable" type on every reallocation if T's move isn't noexcept. AI assistants forget noexcept constantly because the language doesn't force them.
Bad:
class Buffer {
public:
Buffer(Buffer&& other) : data_(other.data_), size_(other.size_) { // not noexcept!
other.data_ = nullptr; other.size_ = 0;
}
// copy constructor implicitly defined → deep copy happens silently
char* data_;
std::size_t size_;
};
Good:
class Buffer {
public:
Buffer() = default;
Buffer(const Buffer&) = delete; // no implicit deep copy
Buffer& operator=(const Buffer&) = delete;
Buffer(Buffer&&) noexcept = default; // explicit, fast move
Buffer& operator=(Buffer&&) noexcept = default;
~Buffer() = default; // unique_ptr handles it
private:
std::unique_ptr<char[]> data_;
std::size_t size_ = 0;
};
Rule for CLAUDE.md:
Move constructor and move-assignment are noexcept — the standard library only uses
moves when they are. A non-noexcept move silently becomes a copy.
Types that own resources delete the copy pair unless deep copy is genuinely cheap.
Default the move pair (= default), don't hand-roll if a member-by-member move works.
Rule 5 — const Correctness Is Not Optional
Why: Every method that doesn't mutate state should say so. AI-generated classes treat const as decoration — a method that "looks read-only" but isn't marked const infects every caller, since const references can't call non-const methods.
Bad:
class Polygon {
public:
double area() { // missing const
return /* ... */;
}
};
void render(const Polygon& p) {
auto a = p.area(); // compile error — method isn't const
}
Good:
class Polygon {
public:
double area() const { // marked const
return /* ... */;
}
};
Rule for CLAUDE.md:
Every method that doesn't mutate observable state is marked const.
Every parameter the function won't modify is const T& (or T for trivially copyable).
Member fields that genuinely don't change after construction are const,
or are exposed only via const accessors.
Rule 6 — Sink Parameters: Take by Value, std::move In
Why: The constructor pattern most AI assistants generate — Foo(const std::string& name) : name_(name) {} — costs a copy from every rvalue caller. Take by value and std::move into the member: the compiler picks the right move/copy at the call site, and you have one overload.
Bad:
class User {
public:
User(const std::string& name, const std::string& email)
: name_(name), email_(email) {} // always copies
private:
std::string name_;
std::string email_;
};
User u(get_name(), get_email()); // two copies of temporaries
Good:
class User {
public:
User(std::string name, std::string email)
: name_(std::move(name)), email_(std::move(email)) {}
private:
std::string name_;
std::string email_;
};
User u(get_name(), get_email()); // moves from temporaries
User u2(name_var, email_var); // copies once, moves into member
Rule for CLAUDE.md:
Sink parameters (functions that store the value) take by value and std::move.
Read-only parameters take const T& (or T for trivially copyable / string_view / span).
Don't write four overloads (const&, &&, etc) — by-value-and-move handles all callers.
Rule 7 — std::string_view and std::span<T> for Read-Only Views
Why: Functions that take const std::string& reject const char* literals at the call site without a std::string allocation. std::string_view accepts both for free. The same logic applies to arrays vs std::vector<T> vs std::array<T, N> — std::span<T> accepts all of them.
Bad:
bool starts_with(const std::string& s, const std::string& prefix) { /* ... */ }
starts_with("/api/v1/users", "/api/"); // two allocations
Good:
bool starts_with(std::string_view s, std::string_view prefix) {
return s.size() >= prefix.size() &&
s.substr(0, prefix.size()) == prefix;
}
starts_with("/api/v1/users", "/api/"); // zero allocations
double sum(std::span<const double> values) {
return std::accumulate(values.begin(), values.end(), 0.0);
}
std::vector<double> v = {/* ... */};
double arr[] = {1.0, 2.0, 3.0};
sum(v); // works
sum(arr); // works
Rule for CLAUDE.md:
Read-only string parameters take std::string_view, not const std::string&.
Read-only array parameters take std::span<const T> (C++20), not const std::vector<T>&.
Never store a string_view or span past the lifetime of its source — they're non-owning.
Rule 8 — No using namespace std; in Headers, Ever
Why: A using namespace std; in a header pollutes every translation unit that includes that header — directly or transitively. Name lookup ambiguities surface in code that doesn't even mention std, and the diagnostic points somewhere unrelated.
Bad:
// renderer.hpp
#include <vector>
#include <string>
using namespace std; // poisons every consumer
class Renderer { /* ... */ };
Good:
// renderer.hpp — fully qualify in headers
#include <string>
#include <vector>
class Renderer {
public:
void draw(std::span<const Mesh> meshes);
};
// renderer.cpp — narrow `using` in implementation files is fine
using std::vector, std::string;
Rule for CLAUDE.md:
No `using namespace std;` at file scope, ever — especially not in headers.
Narrow using-declarations (`using std::vector;`) inside .cpp files are acceptable.
In headers, always fully qualify (std::vector<T>, std::string_view).
Rule 9 — Concepts and if constexpr Replace SFINAE
Why: SFINAE-based templates (std::enable_if, void_t tricks) produce diagnostics that scroll for pages and are unreadable. C++20 concepts produce errors that read like English.
Bad:
template <typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
T half(T x) { return x / 2; }
// Error message on a non-integral T: "no matching function for call to half<...>"
// followed by 40 lines of substitution failure
Good (C++20):
template <std::integral T>
T half(T x) { return x / 2; }
// Error on a non-integral T:
// "constraints not satisfied for `half<std::string>`:
// the constraint `std::integral<std::string>` was not satisfied"
Rule for CLAUDE.md:
Every template parameter with any requirement is constrained — std::integral,
std::ranges::range, or a custom concept defined in the header.
Replace std::enable_if SFINAE with concepts as you touch the code.
Use if constexpr (C++17) for compile-time branching, not tag dispatch.
Rule 10 — Every Member Is Initialized at Declaration
Why: An uninitialized scalar member is undefined behavior on first read. AI-generated classes leave int count; and bool ready; to "be set in the constructor body" — and then they're not, on the path the model didn't think about.
Bad:
class Counter {
public:
Counter() {} // count_ uninitialized → UB
void tick() { ++count_; } // reading uninitialized int
private:
int count_;
bool active_;
};
Good:
class Counter {
public:
Counter() = default;
void tick() { ++count_; }
private:
int count_ = 0; // default member initializers
bool active_ = false;
std::string name_{};
std::vector<int> events_{};
};
Rule for CLAUDE.md:
Every member variable has a default initializer at its declaration.
Constructor bodies should be empty or near-empty — initialize in the
member-initializer list, not by assignment in the body.
Uninitialized scalar reads are UB and silent. Brace-init defaults are free.
Rule 11 — C-Style Casts Are Banned; Use the Verbose Forms
Why: A C-style cast (T)x silently does whatever it takes — static_cast, const_cast, reinterpret_cast, or all three at once. The verbose forms are searchable, intent-revealing, and rejected by the compiler when they're wrong.
Bad:
void* raw = some_api();
MyType* p = (MyType*)raw; // is this a static_cast or reinterpret_cast?
const auto& s = (std::string&)other; // const_cast hidden in plain sight
Good:
auto* p = static_cast<MyType*>(raw); // safe pointer conversion
const auto* derived = dynamic_cast<const Derived*>(b); // checked downcast
auto* bytes = reinterpret_cast<std::byte*>(buffer); // intent-flagged
auto& mutable_s = const_cast<std::string&>(other); // const_cast is loud — use sparingly
Rule for CLAUDE.md:
C-style casts (T)x are forbidden.
Use static_cast for safe conversions, dynamic_cast for checked downcasts,
const_cast only at well-documented boundaries, reinterpret_cast for
intentional bit-level reinterpretation. Each one is searchable in review.
Rule 12 — CMake: target_*, Out-of-Source, No include_directories
Why: include_directories and link_libraries are directory-scoped and global — they leak into every target defined in or below that CMakeLists.txt. AI-generated CMake is full of them because they look like "the simple version." The target_* family is per-target, scoped (PRIVATE / PUBLIC / INTERFACE), and exported correctly by install.
Bad:
# CMakeLists.txt — global, leaks everywhere
include_directories(${CMAKE_SOURCE_DIR}/include)
link_libraries(fmt::fmt)
add_library(engine src/engine.cpp)
add_library(physics src/physics.cpp) # silently links fmt::fmt too
Good:
add_library(engine src/engine.cpp)
target_include_directories(engine
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(engine PUBLIC fmt::fmt)
target_compile_features(engine PUBLIC cxx_std_20)
target_compile_options(engine PRIVATE -Wall -Wextra -Wpedantic -Werror)
Rule for CLAUDE.md:
Use target_include_directories, target_link_libraries, target_compile_options —
never include_directories / link_libraries (directory-scoped, leak everywhere).
PRIVATE for impl, PUBLIC for API, INTERFACE for header-only deps.
Out-of-source builds only (build/). cmake_minimum_required(VERSION 3.21) at minimum.
Sanitizers wired via option(ENABLE_ASAN "..." OFF) — Debug builds default ON in dev.
Why This Is Worth Doing Once
Every rule above traces to a real production bug from an AI-generated PR. A delete that ran twice on a hot path because move semantics weren't thought through. A std::mutex that stayed locked for the rest of the process when an allocation threw inside a critical section. An uninitialized bool active_ whose value depended on whatever was on the stack — green in debug, red in release. A (MyType*)voidptr that reinterpret_cast-ed into a different type because the layouts coincidentally matched on x86_64 and didn't on ARM.
You can keep catching these in review forever. Or you can write a CLAUDE.md, drop it at the repo root, and stop seeing 80% of them.
The 12 rules above are a starting point — the full pack has 50+ production-tested rules covering Modern C++, Rust, Go, TypeScript, React, Vue, Django, FastAPI, Postgres, Kubernetes, Docker, and more.
Free C++ gist with all 3 rules → https://gist.github.com/oliviacraft/1f74c314f1f2f7b47f2bddf236977dcb
Full CLAUDE.md Rules Pack → https://oliviacraftlat.gumroad.com/l/skdgt
Top comments (0)