# Writing an AGENTS.md for C

> Source: https://learn-c.net/ai/agents-md/
> Part of Learn C, free to read.

`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:

```bash
ln -s AGENTS.md CLAUDE.md
```

C 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:

```makefile
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,undefined
```

That 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](/review/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

```markdown AGENTS.md
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.

:::warn Do not write "be careful with memory"
Vague instructions are worse than none — they consume context and change nothing. Every line in the memory section above is a specific, checkable substitution: this function instead of that one, this type instead of that type. That is what a model can act on.
:::

## What to leave out

Things the toolchain already handles, which you do not need to write:

- Formatting — `clang-format` with a committed `.clang-format`.
- Most style — `clang-tidy` with a `.clang-tidy` config.
- "Do not use uninitialised variables" — `-Wall` and 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.
