Writing an AGENTS.md for C
The one language where the instructions file should be blunt, specific, and mostly about memory — because the compiler will happily accept everything it forbids.
AGENTS.md is a Markdown file in your repository root that coding agents read before they start. Claude Code reads CLAUDE.md; most other tools read AGENTS.md. Write one and symlink:
ln -s AGENTS.md CLAUDE.mdC inverts the usual advice about this file. Everywhere else the guidance is "keep it short, the tooling covers most of it." Here the tooling covers a great deal only if it is switched on, and the consequences of a miss are unbounded rather than annoying. So the C file is longer than the Go one, and almost all of the extra length is memory rules.
Turn the tooling on first, then write the file#
There is no point writing "check every allocation" if your debug build is not sanitized. Get this working before anything else:
SAN := -fsanitize=address,undefined -fno-omit-frame-pointer
WARN := -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-compare \
-Wformat=2 -Wformat-security -Wnull-dereference -Wstrict-prototypes
debug: CFLAGS = -g -O1 $(WARN) -D_FORTIFY_SOURCE=3 -fstack-protector-strong $(SAN)
debug: LDFLAGS = -fsanitize=address,undefinedThat build finds unchecked allocations, buffer overflows, use-after-free, leaks, signed overflow and bad shifts, with a precise stack trace. It is the difference between a bug you find at your desk and a bug you find in production six months later. Full detail in the C failure modes.
Then the file states the rules the sanitizers cannot check statically, and the ones that stop the bug being written at all.
The template#
C17, clang. The sanitized build is the default. Never disable sanitizers to
make something pass.
## Commands
- Build: `make debug` (-fsanitize=address,undefined + full warnings)
- Test: `make test` <- must pass, sanitizer-clean, under 5 seconds
- All: `make check` (test + clang-tidy + cppcheck)
- Fuzz: `make fuzz`
- Release build is `make release`. Do not develop against it.
## Memory — non-negotiable
- Check EVERY allocation before use. malloc, calloc, realloc, strdup.
- `calloc(n, size)`, never `malloc(n * size)` — the multiply can overflow
and give you a tiny buffer.
- realloc into a temporary, check it, then reassign. Never `p = realloc(p,…)`.
- One cleanup path per function: `goto cleanup`, one free per allocation,
pointers set to NULL after freeing.
- Never return a pointer to a local.
- Ownership is documented in the header: who allocates, who frees.
## Strings
- `snprintf` only. Never sprintf, strcpy, strcat, gets, or strncpy.
(strncpy does not guarantee a null terminator. snprintf always does.)
- Always pass `sizeof dest`, never a hardcoded length.
- Arrays decay to pointers at function boundaries — never `sizeof` a
parameter to get a length. Pass the length explicitly.
## Integers
- `size_t` for sizes, counts and indices. Never `int`.
- Signed overflow is undefined and the compiler may delete your check.
Use `__builtin_add_overflow` / `__builtin_mul_overflow`.
- Shift unsigned values: `1u << 31`, not `1 << 31`.
## Errors and resources
- Check the return value of read, write, close, fclose. They fail.
- Our own APIs return negative errno on failure. Mark them
__attribute__((warn_unused_result)).
## Untrusted input
Any new function that parses bytes from outside the process gets a fuzz
target in the SAME commit. Fifteen lines, in fuzz/.
## Dependencies
Do not add any. This builds with a C compiler and nothing else. If you think
we need a library, say so and stop — do not vendor one.
## Landmines
- src/proto/ is wire format. Changing struct layout or padding breaks the
daemon on the other side of the socket.
- src/alloc.c is a custom pool allocator. ASan does not see inside it.
Changes there need extra scrutiny, not less.Two lines in there deserve highlighting.
"Do not add any." In C, dependency management is manual, so the default answer to "should we add a library" should be no — and saying it once removes a recurring argument. Most languages cannot afford this rule. C can.
The custom-allocator landmine. This is the class of thing nobody writes down and everybody needs: a place where your safety tooling is blind. If you have a pool allocator, a memory-mapped region, or anything ASan cannot instrument, that fact belongs in this file in capital letters.
What to leave out#
Things the toolchain already handles, which you do not need to write:
- Formatting —
clang-formatwith a committed.clang-format. - Most style —
clang-tidywith a.clang-tidyconfig. - "Do not use uninitialised variables" —
-Walland MSan. - "Watch for off-by-one" — that is what ASan is for. Prose does not help.
The distinction worth holding: state the rules that change what gets written; let the tooling catch what gets written wrong anyway.
Common questions#
Is a longer file justified here when every other page says keep it short?#
The principle is the same — every line must earn its context cost. In C more lines earn it, because the substitutions are specific and the consequence of getting one wrong is a vulnerability rather than a wrong answer. Even so, if yours is over 120 lines, the excess is probably style advice that belongs in .clang-tidy.
Should I tell it which C standard?#
Yes, in the first line, and make sure the Makefile agrees. Generated C otherwise drifts between C89-era idioms (declarations at the top of a block, /* */ comments) and modern ones depending on what the surrounding code looks like.
What about C++?#
The memory sections mostly transfer, but the advice inverts on the tooling: in C++ you want the file to push the model towards RAII, smart pointers and standard containers, which removes most of the manual rules above. Different file, same principle — state what changes what gets written.
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.