C coding
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.From its SKILL.md
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.
One thing to look at
- 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.
SKILL.md
44.3 KB, ~10.7k tokens by cl100k_base, 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)
What ships with it: 185 files
347.4 KB alongside SKILL.md
rules/
- anti-casting-malloc-return.md1.6 KB
- anti-comparing-floats-equality.md1.9 KB
- anti-deeply-nested-code.md1.7 KB
- anti-global-mutable-state.md1.4 KB
- anti-goto-spaghetti.md2.1 KB
- anti-huge-functions.md2.2 KB
- anti-ignoring-compiler-warnings.md1.8 KB
- anti-ignoring-syscall-return-value.md1.8 KB
- anti-macro-abuse.md1.7 KB
- anti-magic-numbers.md1.4 KB
- anti-mixing-signed-unsigned-compare.md1.6 KB
- anti-not-checking-snprintf-truncation.md1.7 KB
- anti-return-stack-address.md1.4 KB
- anti-sizeof-array-parameter.md1.7 KB
- anti-unchecked-malloc.md1.1 KB
- anti-unsafe-string-functions.md1.3 KB
- api-avoid-global-state.md1.9 KB
- api-callback-with-userdata.md2.0 KB
- api-consistent-prefix-naming.md1.5 KB
- api-const-correct-signatures.md1.6 KB
- api-error-propagation-design.md2.2 KB
- api-header-c-linkage-guard.md1.8 KB
- api-init-cleanup-pair.md2.0 KB
- api-minimal-public-surface.md1.9 KB
- api-opaque-struct-encapsulation.md2.1 KB
- api-out-param-convention.md1.9 KB
- api-printf-style-format-attribute.md2.1 KB
- api-return-owned-vs-borrowed-doc.md1.8 KB
- api-single-responsibility-function.md2.0 KB
- api-stable-abi-layout.md2.4 KB
- conc-atomic-for-flags-counters.md1.9 KB
- conc-atomic-memory-order.md2.5 KB
- conc-avoid-data-races.md2.0 KB
- conc-avoid-deadlock-lock-ordering.md2.2 KB
- conc-condvar-wait-predicate.md1.8 KB
- conc-mutex-protect-shared-state.md1.7 KB
- conc-once-init-pthread-once.md2.0 KB
- conc-thread-create-join-discipline.md1.9 KB
- conc-thread-local-storage.md2.1 KB
- conc-volatile-not-for-sync.md2.4 KB
145 more files not listed here. See all 185 in the repository.