AI-Native Updated 2026-09 8 min read View as Markdown

Setting up a coding agent for a C project

In C the review is not optional, so the setup is about making the machine do as much of it as possible.

Everything in this setup exists to answer one question: when the agent writes something wrong, what tells us, and how fast?

In Python a wrong program raises. In Go it usually fails to compile. In C it runs, produces the right answer for six months, and then corrupts the heap. So the setup here is more elaborate than elsewhere, and it is worth every minute.

1. Make the sanitized build the default#

Makefile
CC     ?= clang
SRC    := $(wildcard src/*.c)
WARN   := -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-compare \
          -Wformat=2 -Wformat-security -Wnull-dereference -Wstrict-prototypes
HARDEN := -D_FORTIFY_SOURCE=3 -fstack-protector-strong
SAN    := -fsanitize=address,undefined -fno-omit-frame-pointer

.PHONY: debug release test check clean

debug:   CFLAGS  = -g -O1 $(WARN) $(HARDEN) $(SAN)
debug:   LDFLAGS = -fsanitize=address,undefined
debug:   app

release: CFLAGS  = -O2 -DNDEBUG $(WARN) $(HARDEN)
release: app

app: $(SRC)
	$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)

test: debug
	ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 \
	UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
	./app --test

check: test
	clang-tidy $(SRC) -- $(WARN)
	cppcheck --enable=all --error-exitcode=1 --inline-suppr src/

make debug is what everybody — human and agent — runs. The 2x slowdown is irrelevant; the precise stack trace at the moment of the bug is everything. See the C failure modes for exactly which bugs each sanitizer catches.

2. Give the tools a compilation database#

shell
bear -- make debug        # writes compile_commands.json

Without it, clangd and clang-tidy guess at your include paths and produce noise. With it, the agent's editor integration actually resolves your headers, which materially improves what it writes.

Commit it, or generate it in make install.

3. A test binary that runs in under five seconds#

The loop matters as much here as anywhere. Something the agent can run after every edit:

test/main.c
#include "unity.h"

void test_parse_rejects_truncated_input(void) {
    packet_t p;
    TEST_ASSERT_EQUAL(PARSE_TRUNCATED, parse(&p, (uint8_t[]){0x01}, 1));
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_parse_rejects_truncated_input);
    return UNITY_END();
}

Unity or Criterion — the choice matters much less than the existence of make test.

4. A fuzz target for anything parsing bytes#

Fifteen lines, and it is the highest-value test you will write for a parser.

fuzz/fuzz_parse.c
#include <stddef.h>
#include <stdint.h>
int LLVMFuzzerTestOneInput(const uint8_t *d, size_t n) { parse_packet(d, n); return 0; }
makefile
fuzz:
	clang -g -O1 -fsanitize=fuzzer,address,undefined \
	  fuzz/fuzz_parse.c src/parser.c -o fuzz_parse
	./fuzz_parse -max_total_time=120 corpus/

Ask the agent to add a fuzz target in the same commit as any new parser. Two minutes of fuzzing finds inputs no reviewer would think to try.

5. Permissions#

.claude/settings.json
{
  "permissions": {
    "allow": [
      "Bash(make debug)", "Bash(make test)", "Bash(make check)",
      "Bash(clang-tidy:*)", "Bash(cppcheck:*)",
      "Bash(git status)", "Bash(git diff:*)"
    ],
    "ask": ["Bash(make fuzz)", "Bash(git push:*)"],
    "deny": ["Bash(rm -rf:*)", "Bash(curl:*)", "Read(./.env)"]
  }
}

Note that ./app is not on the allowlist. In C, running the program the agent just wrote is a meaningfully different act from running it in a memory-safe language — and if you are going to auto-allow it, do it in a container.

6. AGENTS.md#

AGENTS.md
C17, clang. Sanitizers are on in the debug build. Debug is the default.

## Commands
- Build:  `make debug`
- Test:   `make test`   <- must pass, sanitizers clean
- All:    `make check`  (test + clang-tidy + cppcheck)
- Fuzz:   `make fuzz`

## Non-negotiable
- Check every allocation before use.
- calloc(n, size), never malloc(n * size).
- snprintf only. Never sprintf, strcpy, strcat, gets.
- size_t for sizes and indices, never int.
- One cleanup path per function: `goto cleanup`, one free per allocation.
- Never return a pointer to a local.
- Signed overflow is UB: use __builtin_add_overflow and friends.
- Any new parser of external input gets a fuzz target in the same commit.

## Landmines
- src/proto/ is wire-format code. Changing struct layout breaks the daemon.
- Do not add dependencies. This builds with a C compiler and nothing else.

That last line is worth more in C than in any other language: dependency management is manual, so the default answer to "should we add a library" is no, and saying so once saves a recurring argument.

The full file, with the reasoning behind each memory rule, is in writing an AGENTS.md for C.

What this costs#

Agent sessions are billed, and the bill is driven by context size more than by how much you ask for. The levers — prompt caching, pruning unused MCP servers, starting a fresh session when the task changes — are in what tokens actually cost.

The checklist#

[ ] make debug is sanitized and is what everyone runs
[ ] -Wall -Wextra -Wconversion clean on new files
[ ] compile_commands.json generated
[ ] make test runs in under 5 seconds
[ ] a fuzz target for every parser of untrusted input
[ ] running the built binary is confirmed, or containerised
[ ] AGENTS.md states the memory rules explicitly

Common questions#

Is it reasonable to write C with an agent at all?#

With this setup, yes. Models know C idioms well and generate competent code; the risk is that the residual errors are memory-safety bugs rather than wrong answers. Sanitizers plus static analysis plus fuzzing shift that risk enough to make the trade reasonable. Without them it is not.

Should the agent be allowed to run the binary it just built?#

Under sanitizers, in a container or a VM, yes. Directly on your machine with no isolation, think about it — a buffer overflow in freshly written code does whatever it does, and "it was only a test run" is how bad afternoons start.

Why not just use Rust?#

If you have the choice, that is a reasonable answer, and it gets more reasonable as generated code volume rises. Most C exists because of a constraint — an existing codebase, a platform, a kernel — and for that code, this setup is what makes the work 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.

Unsubscribe in one click. We never sell the list. Or just take the 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.