Zig Programming skill for Codex
Zig for the compiler you have, leak-free. Codex checks zig version first and writes for that release only, reading the installed standard library when an API is in doubt instead of guessing. Every allocation gets a visible allocator and a matching defer or errdefer, build.zig uses the createModule and root_module shape, and work is proven with zig build test and the leak-checking testing allocator.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- zig-programming Installs to ~/.agents/skills/zig-programming/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install zig-programming The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project zig-programming Installs to .agents/skills/zig-programming/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/zig-programming.zipunzip -o zig-programming.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) zig-programming PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $zig-programming, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes, builds and debugs Zig code that matches the installed compiler - explicit allocators (DebugAllocator, ArenaAllocator, FixedBufferAllocator, testing.allocator), error unions with try, errdefer and defer, comptime generics, build.zig with createModule and root_module, build.zig.zon dependencies, zig test, and C interop through @cImport, linkLibC and zig cc. Use when starting or fixing a Zig project, upgrading build.zig across Zig releases, chasing leaks reported by the testing allocator, or calling C libraries from Zig.
Use it when
- Starting or extending a Zig project or library
- Upgrading build.zig or std usage across Zig releases
- Tests report leaks or errdefer and defer cleanup looks wrong
- Calling C code from Zig or cross-compiling with zig cc
Use something else when
- The code is C and you are hunting memory corruption: use c-memory-safety
- You need embedded Rust instead of Zig for firmware: use embedded-rust
How to check the result
zig fmt --check, zig build and zig build test pass on the pinned compiler version with the testing allocator reporting no leaks.
The mistake it prevents
Copying build.zig or ArrayList code from a tutorial written for a different Zig release; the APIs changed in 0.14 and again in 0.15.
Zig is pre-1.0 and the standard library and build API change between minor releases.
Good Zig work starts by pinning down the exact compiler version, then writes code where
every allocation has a visible allocator and a matching defer/errdefer, every error
is handled or propagated with try, and zig build test passes with the testing
allocator reporting no leaks.
Before writing code
zig version. Checkbuild.zig.zonfor.minimum_zig_versionand CI for the version it installs. Write for that version only.- Read
AGENTS.md,build.zig,build.zig.zon. - When unsure whether an API exists in this version, read the installed stdlib source
instead of guessing:
zig envprintslib_dir; grep under<lib_dir>/std/. This works offline and is the source of truth. zig buildfetches.dependencieson first run; that needs network access. Ask before running it in a sandbox without network.
Version drift to expect
| Area | Older code (0.11-0.13) | Current style (0.14 / 0.15) |
|---|---|---|
| Artifacts | b.addExecutable(.{ .root_source_file = ..., .target, .optimize }) | b.addExecutable(.{ .name, .root_module = b.createModule(.{ .root_source_file = b.path(...), .target, .optimize }) }) |
| Paths | .{ .path = "src/main.zig" } | b.path("src/main.zig") |
| General allocator | std.heap.GeneralPurposeAllocator(.{}){} | std.heap.DebugAllocator(.{}) (debug) or std.heap.smp_allocator (release) |
| ArrayList | managed, stores the allocator | 0.15: std.ArrayList(T) is unmanaged; pass the allocator to each call |
| Stdout | std.io.getStdOut().writer() | 0.15: std.fs.File.stdout().writer(&buf) then .interface, and flush() |
.zon name | string "myapp" | enum literal .myapp plus a .fingerprint |
usingnamespace, async | present | removed |
When upgrading a project across versions, change the compiler, fix compile errors one by one with the release notes open, and do not mix idioms from two versions.
Allocators
| Situation | Allocator |
|---|---|
| Tests | std.testing.allocator (fails the test on leaks) |
| Debug builds of an app | std.heap.DebugAllocator(.{}), check deinit() result |
| Release, multithreaded app | std.heap.smp_allocator, or std.heap.c_allocator when linking libc |
| Request/frame/batch lifetime | std.heap.ArenaAllocator wrapping a parent; free everything at once |
| Known small upper bound, no heap | std.heap.FixedBufferAllocator over a stack buffer |
Functions that allocate take allocator: std.mem.Allocator as a parameter and document
who frees the result. Libraries never pick a global allocator.
const std = @import("std");
pub fn main() !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer std.debug.assert(debug_allocator.deinit() == .ok);
const gpa = debug_allocator.allocator();
var arena = std.heap.ArenaAllocator.init(gpa);
defer arena.deinit();
const names = try loadNames(arena.allocator(), "names.txt");
for (names) |name| std.debug.print("{s}\n", .{name});
}
Errors, defer and errdefer
const ParseError = error{ EmptyInput, TooLong };
/// Caller owns the returned slice and must free it with `allocator`.
pub fn upperCopy(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
if (input.len == 0) return ParseError.EmptyInput;
if (input.len > 4096) return ParseError.TooLong;
const out = try allocator.alloc(u8, input.len);
errdefer allocator.free(out); // runs only if a later step fails
for (input, out) |c, *o| o.* = std.ascii.toUpper(c);
try validate(out);
return out;
}
deferruns at scope exit always;errdeferonly when the scope returns an error. Puterrdeferimmediately after the allocation it protects.- Use explicit error sets for public APIs so callers can
switchon them; inferred!Tis fine internally. catch |err| switch (err) { ... }to handle specific errors;catch unreachableonly when it is impossible and you would accept a crash in safe builds.- Optionals:
?T, unwrap withorelse,if (opt) |v|, or.?when null is a bug.
Collections in 0.15 style
var list: std.ArrayList(u32) = .empty;
defer list.deinit(gpa);
try list.append(gpa, 42);
try list.appendSlice(gpa, &.{ 1, 2, 3 });
const owned = try list.toOwnedSlice(gpa); // list is now empty; caller frees `owned`
defer gpa.free(owned);
On 0.14 the same code compiles with std.ArrayListUnmanaged(u32). Hash maps: use
std.StringHashMapUnmanaged(V) / std.AutoHashMapUnmanaged(K, V) with the allocator
passed per call; remember map keys are not copied, so the key memory must outlive the map.
comptime generics
fn RingBuffer(comptime T: type, comptime capacity: usize) type {
return struct {
items: [capacity]T = undefined,
head: usize = 0,
len: usize = 0,
const Self = @This();
pub fn push(self: *Self, value: T) error{Full}!void {
if (self.len == capacity) return error.Full;
self.items[(self.head + self.len) % capacity] = value;
self.len += 1;
}
};
}
Use comptime parameters for types and sizes; use anytype sparingly (it hides the
contract) and check it with @hasDecl / @TypeOf where it matters.
build.zig (0.14/0.15 shape)
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true, // only if you call C
});
mod.linkSystemLibrary("sqlite3", .{});
const exe = b.addExecutable(.{ .name = "app", .root_module = mod });
b.installArtifact(exe);
const run = b.addRunArtifact(exe);
if (b.args) |args| run.addArgs(args);
b.step("run", "Run the app").dependOn(&run.step);
const tests = b.addTest(.{ .root_module = mod });
b.step("test", "Run unit tests").dependOn(&b.addRunArtifact(tests).step);
}
Dependencies: zig fetch --save <tarball-url> writes the URL and hash into
build.zig.zon; then const dep = b.dependency("name", .{ .target = target, .optimize = optimize });
and mod.addImport("name", dep.module("name"));. Pin release tarballs or commit
tarballs, not branch archives.
C interop
const c = @cImport({
@cInclude("sqlite3.h");
});
pub fn openDb(path: [:0]const u8) !*c.sqlite3 {
var db: ?*c.sqlite3 = null;
if (c.sqlite3_open(path.ptr, &db) != c.SQLITE_OK) return error.OpenFailed;
return db.?;
}
- C strings need a sentinel: use
[:0]const u8andallocator.dupeZto create one. - C pointers arrive as
[*c]T; convert to?*Tor slices at the boundary and keep the rest of the code in Zig types. zig cc/zig c++are drop-in cross-compiling C compilers (zig cc -target aarch64-linux-musl);addCSourceFilescompiles C insidebuild.zig.@cImportis slated to move into the build system (translate-c steps); if the installed version deprecates it, follow the release notes rather than this snippet.
Verification
-
zig fmt --check . -
zig buildandzig build testpass; the testing allocator reports no leaks. - Run the binary in
-Doptimize=Debug(safety checks on) for any change touching indexing or pointer math;ReleaseSafekeeps checks in release. - Cross-compile once for each shipped target (
zig build -Dtarget=x86_64-windows). - Report the Zig version the code was verified with.
Gotchas
undefinedmemory is not zeroed; reading it is a bug that Debug mode only sometimes catches (it fills with 0xaa).- Slices of a
std.ArrayList(list.items) are invalidated by any append. - String literals are
*const [N:0]u8; comparing strings needsstd.mem.eql(u8, a, b), not==. - Integer overflow panics in Debug/ReleaseSafe and is UB in ReleaseFast; use
+%orstd.math.addwhen wrapping or checked arithmetic is intended. - Unused locals and parameters are compile errors; discard with
_ = x;only when the value is truly not needed.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Zig Programming"
short_description: "Zig for the compiler you have, leak-free"
default_prompt: "$zig-programming Our build.zig stopped compiling after upgrading Zig, fix it" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.