Built for coding agents
CLIs are increasingly written with — and increasingly by — coding agents. zcli is designed so that's safe and productive by construction, not by hoping the model gets Zig right.
This isn't an AI feature bolted on. It's a set of decisions made early, each aimed at one question: when a coding agent writes a command, what goes wrong, and who catches it? Zig gives an agent no garbage collector and a manual-memory model it has little training data for. A runtime CLI framework gives it a builder API to misremember. zcli's answer is to remove those failure modes at the level where they occur — the allocator, the compiler, the scaffolder, and the reference the agent reads — so the loop of write → verify → fix closes without a human decoding a type error in the middle.
Every mechanism below is real and shipping. This page is for a human deciding on a framework; the AI agents section of the docs shows the agent-facing loop in practice.
Leak-free by construction
The failure mode of AI-written Zig that is simultaneously silent, hard to debug, and specific to the language is memory: a leak or a use-after-free that compiles clean and passes the happy-path test. Every other failure has an owner — the compiler catches code that doesn't build, tests catch wrong behavior — but memory had none.
So each command's execute() receives an allocator backed by a per-command arena. A command runs once and exits; the arena is dropped wholesale when it returns. The idiom is "never call free; the arena reclaims everything at command end" — and business logic an agent writes into that body is leak-free whether or not the agent understood Zig's ownership rules.
pub fn execute(args: Args, _: Options, context: anytype) !void { const a = context.allocator; // the per-command arena const url = try std.fmt.allocPrint(a, "https://api.github.com/repos/{s}", .{args.slug}); var res = try context.http.get(a, url, .{}); const repo = try res.json(Repo, a); // no defer, no free — the arena reclaims it all when execute() returns try context.stdout().print("{s} — ★ {d}\n", .{ repo.full_name, repo.stargazers_count }); }
watch/dev loop — uses a child arena per iteration. Everything else just never frees.Errors that name the file and show the fix
A command is a contract: a meta block, an Args struct, an Options struct, and an execute function. That contract is validated at compile time — but a raw Zig type error is exactly the thing a non-Zig-fluent human (or the agent) burns a turn decoding. So the validation is written to fail with a message that names the offending command file and states the correction directly.
An agent that forgets the Options struct, mistypes a field, or wires up a default that doesn't parse doesn't get a stack of generic type mismatches — it gets a sentence it can act on. That turns the compiler into a fast, precise reviewer: the agent iterates against it directly, no human in the loop to translate.
zig build plus tests. An agent's edit either compiles into a working command or comes back with a named, actionable error — the same signal a human would want, just phrased for a machine to fix.Structure changes are mechanical edits
Because a command is a file and its path is its command path, changing the shape of a CLI is a filesystem operation — not freeform code an agent has to author and get right. The meta-CLI does those edits mechanically:
$ zcli add command users/create # → src/commands/users/create.zig $ zcli add arg users/create name --type []const u8 $ zcli add option users/create admin --type bool --default false $ zcli mv users/create accounts/create $ zcli rm command users/create
The division of labor is the point: the agent uses add/rm/mv to change structure, and writes freeform code only inside execute() bodies — where the arena has already made it safe. Renaming a command becomes a well-defined operation instead of a multi-file refactor an agent might do halfway.
Drift-proof, version-matched context
Coding agents are fluent in mainstream languages but have almost no zcli in their training data, and what little they might pick up goes stale the moment the framework changes — a renamed Context field, a plugin config shape that moved. Stale context is worse than none: it actively steers the model toward APIs that no longer exist. zcli solves this with two artifacts that can't drift.
The AGENTS.md spine
zcli init scaffolds an AGENTS.md into every project. It's deliberately thin and frozen — safe to freeze because it speaks in zcli commands, not Zig signatures, and commands are a far more stable contract than code. It carries the project's identity, the write→verify→fix loop, a short set of high-leverage invariants (never free in execute(); change structure with the scaffolder; file path = command path), and a pointer to the guide. No signature ever lives here, so it never goes out of date.
zcli guide
The volatile detail — API signatures, the primitive catalog, worked examples — lives in zcli guide, served from the pinned zcli version in your build.zig.zon. It is drift-proof by construction: the reference an agent reads is generated from the exact zcli it's compiling against, not a training-data snapshot. And its examples are the repo's canonical example CLIs, which CI compiles — so a breaking change to the framework breaks the build and forces the context up to date.
$ zcli guide arena arena — the per-command allocator context.allocator is an arena, dropped when execute() returns. Allocate freely; never free. For a long-running loop, use a child arena per iteration.
zcli tree --show-options). Drift-sensitive content lives only in the layer pinned to the code.Headless verification
An agent can't watch a terminal repaint, so "did the output come out right?" has to be answerable in code. zcli commands run in-process against a real ANSI-parsing virtual terminal, and the assertions are on meaning — text, color, attributes — not raw escape codes:
const testing = @import("zcli-testing"); test "deploy prints a green success line" { var result = try testing.runCommand(DeployCommand, .{ .args = .{ .service = "api" }, .options = .{ .env = "staging" }, }); defer result.deinit(); try std.testing.expect(result.term.containsText("Deploying")); try std.testing.expect(result.term.hasAttribute(0, 0, .bold)); }
The agent gets a truth signal for exactly the kind of output — colored, formatted, interactive — that it otherwise couldn't verify. Combined with zig build test, the write→verify→fix loop is fully closable without a human in it.
The decisions
None of this is hype; each mechanism is a recorded architecture decision, linked here for depth:
| ADR | Decision |
|---|---|
| 0001 | Arena-per-command allocator — AI-authored business logic is leak-free by construction. |
| 0004 | AI context ships via AGENTS.md, sourced from CI-compiled canonical examples — drift-resistant by construction. |
| 0008 | A thin frozen AGENTS.md spine over a version-matched zcli guide — signatures never live in the frozen layer. |
The full set of AI-authoring decisions is ADRs 0001–0008.