The C stack we would set up today
C has the best free bug-finding tooling of any language, and almost nobody turns it on. Here is the list.
The single most important thing to understand about C tooling in 2026: the free tools are extremely good, and most projects use none of them. Everything below is either bundled with your compiler or a one-line install.
Sanitizers — start here#
clang -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer main.c -o app
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 ./appAddressSanitizer finds use-after-free, buffer overflow, double free and leaks. UndefinedBehaviorSanitizer finds signed overflow, bad shifts, misaligned access and null dereference. Both give you a precise stack trace at the point of the bug rather than a crash somewhere else later.
The cost is roughly 2x runtime. That is irrelevant during development and it is the highest return on any configuration change available in this language. ThreadSanitizer (-fsanitize=thread, separately) covers data races.
Make the sanitized build your default make debug. See the C failure modes for exactly which bugs each one catches.
Compiler warnings#
WARN = -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-compare \
-Wformat=2 -Wformat-security -Wnull-dereference -Wstrict-prototypes \
-Wwrite-strings -Wcast-qual
HARDEN = -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fstack-clash-protection-Wconversion is noisy on existing code and finds real integer bugs on new code. Turn it on for new files if the volume is too high to fix at once.
Static analysis#
gcc -fanalyzer -c src/*.c # GCC's built-in, very good on malloc paths
clang-tidy src/*.c -- # broad, configurable
cppcheck --enable=all src/ # different blind spots to the other twoThese are complementary, not redundant — run more than one. -fanalyzer in particular is excellent at the unchecked-malloc class of bug that generated C produces most.
Fuzzing#
If your code parses bytes from outside — a file format, a protocol, a config — fuzz it. Fifteen lines, and it finds inputs nobody would think to write a test for.
int LLVMFuzzerTestOneInput(const uint8_t *d, size_t n) { parse(d, n); return 0; }clang -g -O1 -fsanitize=fuzzer,address,undefined fuzz.c src/parser.c -o fuzz
./fuzz -max_total_time=300 corpus/Five minutes of this on a generated parser is worth an hour of reading it.
Build system#
Make for anything small. It is universal, everyone can read it, and a 30-line Makefile is more maintainable than a 30-line anything else.
CMake once you need cross-platform builds, multiple targets or dependency management — not because it is pleasant, but because it is what the ecosystem uses.
Meson if you get to choose freely. Faster, much more readable, and it works well.
Add bear to generate compile_commands.json so clangd and clang-tidy understand your project:
bear -- makeEditor#
clangd gives you real completion, go-to-definition and inline diagnostics in any editor that speaks LSP. This is free and it is most of what an IDE was for.
CLion's case is the debugger and the memory view. When generated C is subtly wrong, watching the actual bytes beats reading the code.
Testing#
Unity or Criterion for unit tests; both are small and easy to vendor. The important thing is not which — it is that there is a make test an agent can run in under five seconds.
Learning#
C is a language where the books genuinely beat the tutorials, because the things that matter — memory model, undefined behaviour, the standard's actual guarantees — need more space than a web page gives them.
For filling a specific gap quickly rather than working through a whole book.
Where to run it#
For a build box, a CI runner or anything compute-heavy, dedicated hardware at Hetzner prices is hard to argue with.
What to skip#
- Rolling your own string library. Use
snprintfand bounded functions. Every hand-rolled string API has the same bug in it. -Ofastor-ffast-mathunless you know precisely which guarantees you are giving up.- Ignoring warnings.
-Werrorin CI, not locally — locally it interrupts flow; in CI it holds the line. - A custom allocator, early. Profile first. It is almost never the problem.
Common questions#
Do sanitizers slow things down too much to use?#
About 2x for ASan, which is irrelevant during development and in CI. You do not ship a sanitized binary; you develop with one. The trade is finding a heap overflow at your desk instead of in production.
ASan or Valgrind?#
ASan is much faster and gives better reports, so it is the default. Valgrind still finds things ASan does not — uninitialised memory reads in particular, where MemorySanitizer is the ASan-family answer but is harder to set up. Running Valgrind occasionally is worthwhile.
Should I use C at all for new projects?#
If you have a choice and no constraint forcing C, memory safety by construction is a strong argument for Rust or Zig — and it gets stronger as the volume of generated code rises. Most C exists because of a constraint, though, and for that code the tooling on this page is what makes it survivable.
Get the C agent pack
A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for C. One email, then occasional updates when the tooling shifts. No course pitch.
AGENTS.md now — no email needed.
Disclosure: some links on this page are affiliate links. If you buy something through one, we earn a commission at no extra cost to you. We only list tools we would tell a friend to use, and we say so when we have not used something ourselves. This is how the site stays free and ad-light.