C coding
Project-scoped OpenCode TUI plugin for grouping and managing MCP servers, custom agent skills, and pinned vendor skill registries.
npx -y skills add nguyenthdat/opencode-manager --skill c-codingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 13 days oldThe repository was created 13 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Comprehensive idiomatic C guidance: 185 prioritized rules across 15 categories, covering C99/C11/C17/C23 (plain C, not C++). Use aggressively when writing, reviewing, refactoring, debugging, or security-auditing any `.c`/`.h` file — manual memory management, pointer arithmetic, buffer sizing, error-code conventions, undefined behavior, and concurrency are exactly the areas where C code silently goes wrong. Preserve the project's declared C standard version and existing conventions; apply C11/C17/C23 features (`_Generic`, `static_assert`, designated initializers, `<stdint.h>`, `nullptr`, `constexpr`, `#embed`) only when the project's declared standard supports them.
SKILL.md
44.3 KB, as published. Nobody here has run it
C Best Practices
Comprehensive guide for writing high-quality, memory-safe, portable C code. Contains 185 rules across 15 categories, prioritized by impact. C has no ownership system, no borrow checker, no destructors, and no bounds checking — nearly every correctness guarantee that other languages give you for free must be maintained by convention and discipline in C. Project constraints override generic defaults: preserve the declared C standard version, target platform assumptions, and existing error-handling conventions unless the user explicitly requests a modernization or migration.
When to Apply
Reference these guidelines when:
- Writing new C functions, structs, or modules
- Implementing manual memory management (allocation, ownership, cleanup)
- Designing public C library APIs and headers
- Reviewing code for buffer overflows, use-after-free, or undefined behavior
- Handling errors via return codes,
errno, or the goto-cleanup pattern - Writing multi-threaded C code (pthreads, atomics)
- Optimizing hot paths or reducing allocation overhead
- Refactoring legacy C code toward a modern standard
- Setting up compiler warnings, sanitizers, and static analysis in CI
Modern C: C11/C17/C23 Features Worth Using
C has evolved substantially since C89/C99. For an existing codebase, preserve its declared standard version (-std=c99, -std=c11, -std=c17, -std=c23) unless a modernization is explicitly in scope. For new code, default to -std=c17 (widely supported, stable) or -std=c23 where the toolchain is confirmed to support it, and apply these features where the project's standard allows:
/* C11 */
_Static_assert(sizeof(int) == 4, "this code assumes 32-bit int"); /* static_assert since C23 is a keyword */
_Generic((x), int: handle_int, double: handle_double)(x); /* type-generic dispatch */
_Thread_local int counter; /* thread-local storage */
_Alignas(64) struct cache_line_data data; /* explicit alignment */
#include <stdatomic.h> /* atomic types and operations */
/* C99, foundational and universally supported today */
struct point p = { .x = 1, .y = 2 }; /* designated initializers */
#include <stdint.h> /* int32_t, uint64_t, etc. — fixed-width types */
#include <stdbool.h> /* bool, true, false */
int arr[n]; /* variable-length arrays: use with caution, see mem-stack-vs-heap */
/* C23 */
bool ok = true; /* bool/true/false/nullptr are now keywords, no #include needed */
nullptr_t np = nullptr; /* type-safe null pointer constant, distinct from integer 0 */
constexpr int max = 100; /* true compile-time constant, stronger than #define or const */
#embed "data.bin" /* embed binary file contents directly as an initializer list */
[[nodiscard]] int must_check(void); /* standard attribute, replaces compiler-specific warn_unused_result */
Annex K's _s-suffixed "bounds-checking interfaces" (strcpy_s, memcpy_s, ...) are part of the C11/C17 standard but remain optional for implementations to provide, and adoption is inconsistent: glibc has never implemented them, while Microsoft's CRT provides its own similar-but-not-identical _s functions. Do not rely on Annex K being available; prefer the well-supported bounded alternatives this skill recommends (snprintf, strlcpy where available, explicit length-checked helpers) instead.
For the authoritative, complete feature list per standard, consult the ISO C standard drafts (N1570 for C11, N2310 for C23) or your compiler's C conformance documentation. Everything below applies across standard versions; prefer the modern forms above where the project's declared standard supports them.
Rule Categories by Priority
| Priority | Category | Impact | Prefix | Rules |
|---|---|---|---|---|
| 1 | Memory Management & Safety | CRITICAL | mem- | 16 |
| 2 | Pointers & Arrays | CRITICAL | ptr- | 14 |
| 3 | Error Handling | CRITICAL | err- | 12 |
| 4 | Undefined Behavior Avoidance | CRITICAL | ub- | 14 |
| 5 | Concurrency | CRITICAL | conc- | 10 |
| 6 | API/Interface Design | HIGH | api- | 14 |
| 7 | String Handling | HIGH | str- | 12 |
| 8 | Naming Conventions | MEDIUM | name- | 12 |
| 9 | Type Safety | MEDIUM | type- | 12 |
| 10 | Testing | MEDIUM | test- | 12 |
| 11 | Documentation | MEDIUM | doc- | 10 |
| 12 | Performance Patterns | MEDIUM | perf- | 11 |
| 13 | Project Structure | LOW | proj- | 10 |
| 14 | Linting & Static Analysis | LOW | lint- | 10 |
| 15 | Anti-patterns | REFERENCE | anti- | 16 |
Quick Reference
1. Memory Management & Safety (CRITICAL)
mem-arena-allocator- Use arena/pool allocation for batches of allocations that share a lifetimemem-avoid-buffer-overflow- Never write or read past the bounds of an allocated buffermem-calloc-over-malloc-memset- Usecalloc()when you need zeroed memory, notmalloc()+memset()mem-check-malloc-failure- Always check the return value ofmalloc/calloc/reallocforNULLmem-flexible-array-member- Use a C99 flexible array member for variable-length trailing data instead of a fixed oversized buffer or two allocationsmem-free-list-pool- Use a free-list pool allocator for objects that are frequently created and destroyed in a fixed sizemem-free-null-pointer- Set pointers toNULLimmediately afterfree()to prevent accidental reusemem-init-before-use- Initialize every variable before it is read; never rely on indeterminate valuesmem-no-double-free- Never free the same pointer twicemem-no-use-after-free- Never dereference or use memory after it has been freedmem-realloc-temp-pointer- Assignrealloc()'s result to a temporary pointer, never overwrite the original in placemem-single-owner-free- Establish one clear owner responsible for freeing each allocationmem-sizeof-pointer-pitfall- Usesizeof(*ptr)instead ofsizeof(type)to keep allocation size in sync with the variable's actual typemem-stack-vs-heap- Prefer stack allocation for small, short-lived, bounded-size data over heap allocationmem-struct-padding-awareness- Be aware of compiler-inserted struct padding when reasoning aboutsizeof, serialization, or ABImem-valgrind-asan-verify- Verify allocator discipline continuously with Valgrind and/or AddressSanitizer, not just code review
2. Pointers & Arrays (CRITICAL)
ptr-array-decay-awareness- Know that arrays decay to pointers at function boundaries, and always pass the length alongsideptr-array-vs-pointer-param- Write array-style function parameters in the way that best documents intent, understanding both are pointersptr-bounds-before-index- Validate an index against the buffer's bounds before using it to index or offset a pointerptr-const-correct-params- Mark pointer parametersconstwhenever the function does not modify the pointeeptr-explicit-void-cast- Cast avoid *explicitly when assigning to or from an incompatible pointer type, and never obscure a type change with an implicit castptr-function-pointer-typedef- Typedef function pointer types instead of spelling out raw function-pointer syntax at every use siteptr-multidim-indexing-bounds- When flattening multi-dimensional data into a 1D buffer, centralize the index math and bound-check every dimensionptr-no-arithmetic-past-bounds- Only form pointers within an array (or one past its end); never compute or dereference a pointer beyond that rangeptr-no-dangling-return- Never return a pointer to a local (stack) variable from a functionptr-no-uninitialized-pointer- Always initialize pointer variables, even toNULL, at declarationptr-null-check-before-deref- Check a pointer againstNULLbefore dereferencing it whenever it can plausibly beNULLptr-pointer-to-pointer-clarity- Use pointer-to-pointer parameters only to let a function modify the caller's pointer itself, and name/document them clearlyptr-restrict-keyword-usage- Userestricton pointer parameters only when you can guarantee the pointed-to objects never overlapptr-type-punning-memcpy- Usememcpy(or aunion) for type punning, never cast a pointer to an unrelated type and dereference it
3. Error Handling (CRITICAL)
err-assert-vs-runtime-check- Useassert()for programmer errors and invariants you control; use runtime error handling for anything derived from external inputerr-check-return-values- Check the return value of every function that can fail, including "boring" ones likeclose,write, andfcloseerr-consistent-return-codes- Pick one return-code convention per module/library and apply it consistentlyerr-document-error-contract- Document, in the header, exactly which error codes a function can return and what each one meanserr-errno-usage- Readerrnoonly immediately after a call that failed, and never assume it was reset to zero on successerr-error-enum-not-magic-int- Represent error codes as a namedenum, not bare integer literalserr-fail-fast-invariant- Abort immediately when an internal invariant is violated, rather than continuing with corrupted stateerr-goto-cleanup-single-exit- Usegototo jump forward to a single cleanup section when a function acquires multiple resourceserr-negative-errno-convention- When adopting the negative-errno return convention, return-errno_valueon failure and never mix it with-1/errnoin the same APIerr-out-param-for-result- Return the status code from the function and hand back the actual result through an output parametererr-partial-init-rollback- When a multi-step initialization fails partway through, roll back exactly the steps that already succeedederr-perror-strerror- Report system-call failures withstrerror/perror(or thread-safestrerror_r), not a bare error number
4. Undefined Behavior Avoidance (CRITICAL)
ub-cast-away-const- Never cast awayconstand then write through the resulting pointerub-format-string-mismatch- Everyprintf/scanf-family format specifier must exactly match the type of its corresponding argumentub-indeterminate-padding-bits- Never rely on the contents of struct padding bytes, and zero them explicitly before comparing, hashing, or transmitting a structub-integer-division-by-zero- Check the divisor before performing integer division or modulo; division by zero is undefined behavior for integersub-invalid-function-pointer-cast- Never call a function through a function pointer cast to an incompatible function typeub-modifying-string-literal- Never write through a pointer to a string literal; string literals may be stored in read-only memoryub-null-pointer-arithmetic- Never perform pointer arithmetic on aNULLpointer, includingNULL + 0ub-out-of-bounds-access- Accessing an array or buffer outside its allocated bounds is undefined behavior, regardless of whether it "seems to work"ub-restrict-correctness- Never mark a pointer parameterrestrictif a caller can supply overlapping/aliased memory for itub-sequence-point-violation- Never modify a variable more than once, or read and modify it in an unsequenced way, between sequence pointsub-shift-by-invalid-amount- Never shift a value by a negative amount or by an amount greater than or equal to its type's bit widthub-signed-integer-overflow- Never let a signed integer computation overflow; use unsigned types, wider types, or overflow-checked arithmetic insteadub-strict-aliasing-rule- Never access an object through a pointer of an incompatible type; the compiler is allowed to assume this never happensub-uninitialized-variable-read- Reading an automatic (stack) variable before it has been assigned a value is undefined behavior
5. Concurrency (CRITICAL)
conc-atomic-for-flags-counters- Use C11_Atomic(or<stdatomic.h>) for simple shared flags and counters instead of a mutexconc-atomic-memory-order- Choose the weakest memory order that is still correct for each atomic operation, and default tomemory_order_seq_cstwhen unsureconc-avoid-data-races- Treat any variable touched by more than one thread as requiring explicit synchronization, with no implicit exceptionsconc-avoid-deadlock-lock-ordering- When a thread must hold more than one lock at a time, always acquire them in the same global order everywhereconc-condvar-wait-predicate- Always wait on a condition variable inside a loop that re-checks the actual predicate, never a bareifconc-mutex-protect-shared-state- Guard every piece of mutable state shared across threads with a mutex (or another synchronization primitive), no exceptionsconc-once-init-pthread-once- Usepthread_once(or astaticlocal with C11's guaranteed thread-safe initialization) for one-time, thread-safe lazy initializationconc-thread-create-join-discipline- Join or explicitly detach every thread you create; never let a joinable thread outlive your interest in its result silentlyconc-thread-local-storage- Use_Thread_local(C11) for per-thread state instead of hand-rolled indexing or unsynchronized globalsconc-volatile-not-for-sync- Do not usevolatilefor thread synchronization; it prevents compiler caching but provides no atomicity or memory ordering
6. API/Interface Design (HIGH)
api-avoid-global-state- Prefer passing explicit state through function parameters (often a context/handle struct) over mutable global variablesapi-callback-with-userdata- Give every callback-accepting API avoid *user_data(orctx) parameter, threaded through unchanged to the callbackapi-consistent-prefix-naming- Prefix every public symbol in a library with a short, consistent module nameapi-const-correct-signatures- Applyconstthroughout public function signatures so the API itself documents what can and cannot be mutatedapi-error-propagation-design- Design a library's API around one propagation mechanism (return codes) and make every fallible function follow it, including "unlikely to fail" onesapi-header-c-linkage-guard- Wrap public C headers inextern "C"guards so they remain usable from C++ callers without name mangling issuesapi-init-cleanup-pair- Every_create/_init/_openfunction must have a matching_destroy/_deinit/_close, and both must be documented togetherapi-minimal-public-surface- Expose the smallest possible set of public functions and types; make everything elsestaticor move it to a private headerapi-opaque-struct-encapsulation- Hide a struct's fields from consumers by exposing only a forward-declared (opaque) pointer type in the public headerapi-out-param-convention- Order output parameters consistently (after inputs), name them with anout_/_outconvention, and never write to them on failureapi-printf-style-format-attribute- Annotate everyprintf-style variadic public function with__attribute__((format(printf, ...)))(or the MSVC equivalent) so the compiler checks format strings at call sitesapi-return-owned-vs-borrowed-doc- Document, for every function returning a pointer, whether the caller owns it (must free) or is only borrowing it (must not free, may not outlive the source)api-single-responsibility-function- Give each public function exactly one responsibility, and split functions that both compute and have side effects into separate calls where practicalapi-stable-abi-layout- For a shared library with a versioned ABI, avoid changing struct layout or function signatures in ways that break binary compatibility
7. String Handling (HIGH)
str-avoid-gets- Never usegets(); it was removed from the C standard entirely because it cannot be used safelystr-avoid-scanf-unbounded- Always specify a field width with%s/%[...]inscanf-family calls; an unbounded%sis as unsafe asgets()str-avoid-sprintf-use-snprintf- Usesnprintfinstead ofsprintf, and always check its return value against the destination buffer sizestr-avoid-strcpy-strcat- Avoidstrcpy/strcat; use a bounded alternative that takes the destination buffer's sizestr-buffer-size-discipline- Always pass a buffer's size alongside its pointer, computed withsizeofat the buffer's declaration site, never as a separately-tracked magic numberstr-compare-with-strncmp- Usestrncmp/memcmpwith an explicit, known length when comparing strings whose length you already control, instead of unboundedstrcmpstr-null-termination-invariant- Maintain the C string invariant everywhere: every byte buffer treated as a string must have a'\0'within its bounds before anystr*function touches itstr-safe-string-copy-pattern- Standardize on one bounded, always-null-terminating copy helper and use it everywhere instead of ad hocstrcpy/strncpycallsstr-string-building-dynamic- Build large or unbounded strings with a growable buffer that tracks length and capacity, not repeated fixed-sizestrcat/snprintfinto a static bufferstr-strlen-cost-awareness- Rememberstrlen()is O(n); cache the length instead of recomputing it repeatedly in a loopstr-strncpy-null-termination-footgun-strncpydoes not guarantee null-termination and pads the remainder with zeros; handle both surprises explicitly or avoid itstr-utf8-byte-vs-char- Never assume onecharequals one displayed character; treat UTF-8 text as a byte sequence and use a proper library for character-level operations
8. Naming Conventions (MEDIUM)
name-avoid-abbreviation-ambiguity- Avoid cryptic or ambiguous abbreviations in identifiers; spell out names unless the abbreviation is truly universal in contextname-avoid-reserved-identifiers- Never name your own identifiers with a leading underscore, or a leading underscore followed by a capital letter or another underscore — those are reserved to the C implementationname-boolean-is-has-prefix- Name boolean-returning functions and variables with anis_/has_/can_/should_prefix so their meaning is unambiguous at every call sitename-consistent-module-prefix- Apply the same short module prefix to every public function, type, and constant belonging to that module, without exceptionname-enum-constant-prefix- Prefix every enumerator with the enum's own name so its origin and intent are clear wherever it's used, since C enum constants share the global namespacename-header-guard-naming- Name include guards after the full relative header path inALL_CAPS_WITH_UNDERSCORES, so guard names never collide across a projectname-macro-all-caps- Name object-like and function-like macros inALL_CAPS_WITH_UNDERSCORESto visually distinguish them from ordinary functions and variablesname-pointer-variable-suffix- Adopt a lightweight, optional naming signal for pointer variables (e.g. ap/ptrprefix or suffix) only when it measurably improves clarity, and apply it consistently if you doname-snake-case-functions- Uselower_snake_casefor function and variable names, matching the convention used by the C standard library and most C codebasesname-static-file-scope-prefix- Adopt a lightweight naming signal (or at minimum, consistent use ofstatic) so internal-linkage helpers are visually distinguishable from the module's public APIname-struct-typedef-convention- Pick one consistent convention for naming structs and their typedefs, and apply it project-wide: eithertypedef struct foo foo;or a distinguishing suffix, never both styles mixedname-verb-noun-function-names- Name functions asverb_noun(ormodule_verb_noun) so the name alone communicates the action performed
9. Type Safety (MEDIUM)
type-avoid-implicit-int- Always write an explicit return type and explicit parameter types; never rely on old, now-removed "implicit int" defaultstype-avoid-implicit-narrowing- Make narrowing conversions (wide type to narrow type, e.g.longtoint) explicit, and check the value's range before converting when data loss would be a bugtype-avoid-plain-char-arithmetic- Cast tounsigned charbefore passing acharto functions liketoupper/isdigit, or before using it as an array index; plainchar's signedness is implementation-definedtype-bool-stdbool- Useboolfrom<stdbool.h>(C99) for boolean values, not a bareintwith implied0/1meaningtype-const-correctness- Applyconstto every variable, parameter, and pointee that is not intentionally mutated, throughout the codebase, not just at public API boundariestype-enum-for-closed-sets- Represent a fixed, closed set of named states or options with anenum, not a bareintwith implied meaningstype-fixed-width-stdint- Use<stdint.h>fixed-width types (int32_t,uint64_t, ...) whenever a value's exact size matters, instead ofint/long/shorttype-generic-macro- Use C11_Genericto write type-safe, type-dispatching macros instead of unsafe function-like macros or void-pointer-based generic functionstype-size-t-for-sizes- Usesize_tfor sizes, counts, and indices, matching whatsizeof,strlen, and the allocation functions already returntype-static-assert-invariants- Usestatic_assert(C11, standard keyword in C23) to verify type-layout and configuration invariants at compile time instead of discovering violations at runtimetype-struct-designated-init- Use C99 designated initializers to initialize structs by field name, rather than positional initializationtype-volatile-for-hardware-mmio- Usevolatilefor memory-mapped hardware registers and signal-handler-shared variables, and understand that it is not a concurrency primitive
10. Testing (MEDIUM)
test-arrange-act-assert-c- Structure every C test in three clear phases — arrange (set up inputs), act (call the function under test), assert (check the result) — with a blank line between themtest-assert-based-harness- For small projects, a minimalassert-based test harness is an acceptable, honest alternative to a full framework, as long as it reports aggregate resultstest-boundary-value-testing- Write explicit tests for boundary values — zero, one, the maximum, the minimum, empty, and off-by-one neighbors — not just "typical" inputstest-ci-matrix-compilers- Run the test suite in CI across multiple compilers (GCC and Clang, at minimum) and at least two C standard versionstest-coverage-gcov- Measure test coverage withgcov/llvm-covand use it to find untested code paths, not as a target to gametest-descriptive-test-names- Name each test function after the specific behavior it verifies, in the formtest_<unit>_<condition>_<expected_result>test-fuzz-entry-point- Expose a dedicated fuzz-testing entry point (LLVMFuzzerTestOneInput) for parsers and anything that handles untrusted inputtest-integration-test-separate-binary- Build integration tests as a separate test binary/executable from unit tests, linking against the library rather than duplicating its sourcetest-mock-via-function-pointers- Inject dependencies (I/O, time, randomness) through function pointers or a small interface struct so tests can substitute fakestest-sanitizers-in-test-ci- Run the full test suite under AddressSanitizer and UndefinedBehaviorSanitizer on every CI build, not just occasionally by handtest-static-functions-via-include- Teststatic(internal-linkage) helper functions either by#include-ing the.cfile directly into a test-only translation unit, or by exposing them through a test-only internal headertest-unit-test-framework- Use an established C unit-testing framework (Unity, Check, or CMocka) instead of ad hocprintf-based assertions
11. Documentation (MEDIUM)
doc-changelog-versioning- Maintain a changelog documenting every public-API-visible change, tagged against the library's version number, especially breaking changesdoc-comment-why-not-what- Write comments that explain why the code does something non-obvious, not comments that just restate what the code already saysdoc-document-error-conditions- Enumerate every specific error condition a function can produce in its documentation, not just "may fail"doc-document-ownership-lifetime- Document, in the comment for every function that returns or accepts a pointer, exactly who owns the memory and how long it remains validdoc-doxygen-function-comments- Document every public function with a Doxygen-style comment covering its purpose, parameters, return value, and error conditionsdoc-example-usage-in-header- Include a short, realistic usage example in the header comment for any non-trivial public API, especially ones with a specific required call orderdoc-header-comment-convention- Start every source and header file with a brief comment stating its purpose, and keep it current as the file's role changesdoc-module-level-overview-comment- Give every module (a header plus its.cfile(s)) a top-level overview comment describing its responsibilities, key types, and how it fits into the larger systemdoc-thread-safety-notes- State explicitly, for every public function and type, whether it is safe to call/access concurrently from multiple threadsdoc-todo-fixme-convention- Mark known-incomplete or known-broken code with a consistent, greppableTODO/FIXMEtag that includes an owner or issue reference
12. Performance Patterns (MEDIUM)
perf-avoid-alloc-in-hot-loop- Hoist allocation out of hot loops; allocate once before the loop and reuse the buffer, or use a pool/arenaperf-avoid-false-sharing- Pad or align per-thread data so independently-updated fields don't share the same CPU cache lineperf-branch-prediction-hints- Use__builtin_expect(or C++20/C23-style[[likely]]/[[unlikely]]attributes where available) to hint rare error branches to the compiler, only after profiling shows it mattersperf-cache-friendly-struct-layout- Order struct fields and lay out arrays so the data actually accessed together in hot code lives close together in memoryperf-const-for-optimizer- Mark valuesconst(and pointers-to-dataconst-qualified) wherever true, giving the optimizer more freedom to cache, reorder, and avoid redundant reloadsperf-inline-small-functions- Mark small, frequently-called functionsstatic inline(typically in a header) to let the compiler eliminate call overhead, and let the compiler override you when it disagreesperf-loop-invariant-hoisting- Move computation that doesn't change between loop iterations outside the loop, even though optimizing compilers often do this automaticallyperf-minimize-copies-pass-by-pointer- Pass large structs byconstpointer rather than by value, to avoid copying their full contents on every callperf-profile-before-optimize- Profile with real workloads before optimizing anything; intuition about where time is spent in C code is frequently wrongperf-restrict-optimizer-hint- Addrestrictto pointer parameters in hot numeric loops once you've verified no aliasing, to let the compiler vectorize more aggressivelyperf-struct-of-arrays- For hot loops that process one field across many objects, prefer a struct-of-arrays (SoA) layout over an array-of-structs (AoS) layout
13. Project Structure (LOW)
proj-avoid-circular-includes- Never let two headers#includeeach other directly or indirectly; break the cycle with forward declarations or by extracting shared types into a third headerproj-build-system-cmake-makefile- Use a real build system (CMake or a well-structured Makefile) with explicit warning/sanitizer flags, rather than ad hoc compile-and-run commandsproj-consistent-directory-layout- Adopt a conventional, predictable directory layout (src/,include/,tests/,docs/) so contributors and tooling can find things without askingproj-header-source-split- Separate a module's public declarations (.h) from its implementation (.c), and keep only what consumers genuinely need in the headerproj-include-what-you-use-#includeexactly the headers a file directly uses symbols from — never rely on a symbol being transitively available through another headerproj-internal-header-naming- Name and locate internal-only headers so they are obviously not part of the public API — e.g. aninternal/subdirectory or an_internal.hsuffixproj-one-module-per-file- Keep each.c/.hpair focused on a single, cohesive responsibility; split a file once it accumulates more than one clear reason to changeproj-public-vs-private-headers-dir- Physically separate a library's public headers (installed, part of the API) from its private/internal headers (never installed) using distinct directoriesproj-single-header-library-tradeoffs- Use the single-header-library pattern (STB_IMPLEMENTATION-style) deliberately, understanding its build-time and compile-time trade-offs, rather than as a default distribution formatproj-versioned-public-header- Expose a library's version number programmatically through its public header, not just in documentation or a build script
14. Linting & Static Analysis (LOW)
lint-address-sanitizer- Build and run tests with AddressSanitizer (-fsanitize=address) to detect buffer overflows, use-after-free, and double-free at the exact point they occurlint-clang-tidy-checks- Runclang-tidywith a curated check set in CI to catch bug patterns and style issues beyond what compiler warnings coverlint-cppcheck-static-analysis- Runcppcheckin CI as a fast, low-false-positive complement to clang-tidy and compiler warningslint-enable-wall-wextra-wpedantic- Compile every C project with-Wall -Wextra -Wpedanticat minimum, as a non-negotiable baselinelint-memory-sanitizer- Use MemorySanitizer (-fsanitize=memory, Clang-only) to detect reads of uninitialized memory that other tools misslint-scan-build-clang-analyzer- Run Clang's path-sensitive static analyzer (scan-build) periodically to find deep, cross-function bugs that pattern-based linters misslint-static-analysis-in-ci- Run static analysis (clang-tidy, cppcheck, scan-build) as a required, blocking CI job, not as an optional local-only toollint-thread-sanitizer- Build and run multi-threaded test suites with ThreadSanitizer (-fsanitize=thread) to detect data races directlylint-undefined-behavior-sanitizer- Build and run tests with UndefinedBehaviorSanitizer (-fsanitize=undefined) to catch signed overflow, misaligned access, invalid casts, and other UB at runtimelint-werror-in-ci- Build with-Werrorin CI (though not necessarily in every local dev build) so warnings cannot silently accumulate
15. Anti-patterns (REFERENCE)
anti-casting-malloc-return- Don't cast the return value ofmalloc/calloc/reallocin C; it's unnecessary and can mask a missing#include <stdlib.h>anti-comparing-floats-equality- Don't compare floating-point values with==/!=; compare against an epsilon-bounded difference (or, for exact cases, use integer/fixed-point representations)anti-deeply-nested-code- Don't nest conditionals and loops more than 2-3 levels deep; use early returns/guard clauses to flatten control flowanti-global-mutable-state- Don't rely on mutable global/static variables for state that should be explicit and scopedanti-goto-spaghetti- Don't usegotofor arbitrary jumps, backward loops, or jumping into the middle of a block; reserve it for the forward-only cleanup patternanti-huge-functions- Don't let a function grow to hundreds of lines covering multiple responsibilities; split it along natural sub-task boundariesanti-ignoring-compiler-warnings- Don't ship code with unaddressed compiler warnings; treat every warning as a bug report until proven otherwiseanti-ignoring-syscall-return-value- Don't ignore the return value of system calls likewrite,read,close, andfork; each can fail or partially completeanti-macro-abuse- Don't use function-like macros where astatic inlinefunction would be equally efficient and type-safeanti-magic-numbers- Don't use unexplained numeric literals in code; name them as constants or enum valuesanti-mixing-signed-unsigned-compare- Don't compare a signed and an unsigned integer directly; the signed value is implicitly converted to unsigned, which can silently invert the comparison's intentanti-not-checking-snprintf-truncation- Don't ignoresnprintf's return value; a return>= buffer sizemeans the output was silently truncatedanti-return-stack-address- Don't return a pointer or reference to a local (automatic-storage) variable from a functionanti-sizeof-array-parameter- Don't callsizeofon a pointer parameter expecting the original array's size; arrays decay to pointers at function boundariesanti-unchecked-malloc- Don't callmalloc/calloc/reallocwithout checking the result forNULLanti-unsafe-string-functions- Don't usegets, unboundedstrcpy/strcat/sprintf, or unboundedscanf("%s", ...); use their bounded counterparts
Recommended Build Configuration
Makefile
CC = cc
STD = -std=c17
WARN = -Wall -Wextra -Wpedantic -Werror -Wshadow -Wconversion -Wformat=2
SAN = -fsanitize=address,undefined
CFLAGS = $(STD) $(WARN) -g -O1 $(SAN)
LDFLAGS = $(SAN)
SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:.c=.o)
app: $(OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS)
%.o: %.c
$(CC) $(CFLAGS) -Iinclude -c $< -o $@
test: app
./app
clean:
rm -f $(OBJS) app
.PHONY: test clean
CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(mylib C)
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)
add_compile_options(-Wall -Wextra -Wpedantic -Werror -Wshadow -Wconversion)
option(ENABLE_SANITIZERS "Build with ASan/UBSan" ON)
if(ENABLE_SANITIZERS)
add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer)
add_link_options(-fsanitize=address,undefined)
endif()
add_library(mylib src/widget.c src/connection.c)
target_include_directories(mylib PUBLIC include PRIVATE src)
enable_testing()
add_executable(mylib_tests tests/unit/test_widget.c)
target_link_libraries(mylib_tests PRIVATE mylib)
add_test(NAME mylib_tests COMMAND mylib_tests)
How to Use
This skill provides rule identifiers for quick reference. When generating or reviewing C code:
- Check relevant category based on task type
- Apply rules with matching prefix
- Prioritize CRITICAL > HIGH > MEDIUM > LOW
- Read rule files in
rules/for detailed examples
Rule Application by Task
| Task | Primary Categories |
|---|---|
| New function | err-, ptr-, name- |
| Memory allocation/ownership | mem-, api- |
| New struct/public API | api-, type-, doc- |
| String/buffer handling | str-, mem-, ub- |
| Error handling | err-, api- |
| Multi-threaded code | conc-, mem- |
| Undefined-behavior audit | ub-, ptr-, lint- |
| Performance tuning | perf-, mem-, ptr- |
| Code review | anti-, lint-, ub- |
| CI/build setup | lint-, test-, proj- |
Related Skills
- design-patterns - choosing and implementing GoF and idiomatic design patterns; apply its C-adaptable patterns (opaque handles, function-pointer-based strategy/visitor, object pools) alongside this skill's
api-andmem-rules. - security-review - security-audit checklists (memory-safety, injection, unsafe-function findings) for reviewing/auditing C code; use together with this skill's
mem-,ptr-,ub-, andstr-categories when doing a security-focused pass.
Sources
This skill synthesizes best practices from:
- CERT C Coding Standard
- MISRA C:2012 (with Amendments)
- C Programming: A Modern Approach, 2nd Edition, by K. N. King
- Effective C: An Introduction to Professional C Programming, by Robert C. Seacord
- ISO/IEC 9899 (the C standard): C99, C11, C17, and C23 drafts
- POSIX.1-2017 (IEEE Std 1003.1) for system-call and threading conventions
- Linux kernel coding style
- Production codebases: SQLite, Redis, curl, PostgreSQL, the Linux kernel
- Clang/GCC diagnostics documentation; AddressSanitizer/UBSan/ThreadSanitizer documentation
- Community conventions (2024-2026)