agentleFS
mazreanmazrean/agent-skills/skills/writing-zig-cli-tools/SKILL.md

writing-zig-cli-tools

Skill2 starsChanged 8 months agoMITView on GitHub

Writes CLI tools in Zig using modern 0.15+ syntax and best practices. Use when creating command-line applications in Zig, parsing CLI arguments, setting up Zig project structure with build.zig, or working with Zig's I/O and error handling for CLI programs.

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Always flush stdout/stderr: before program exit

    line 237
  2. Very specific:

    Use b.addLibrary(): instead of removed addStaticLibrary()

    line 241

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Writing Zig CLI Tools

Build high-performance CLI tools in Zig using modern 0.15+ idioms. Covers project setup, argument parsing, I/O, error handling, and cross-compilation.

Use this skill when creating CLI applications in Zig, parsing command-line arguments, setting up build.zig projects, or writing robust terminal I/O code.

Supporting files: SYNTAX-GUIDE.md for 0.15+ syntax changes, BUILD-SYSTEM.md for build.zig patterns, ARGUMENT-PARSING.md for CLI argument handling.

Quick Start

Project Structure

my-cli/
├── build.zig
├── build.zig.zon
└── src/
    └── main.zig

Minimal build.zig

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "my-cli",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| run_cmd.addArgs(args);

    const run_step = b.step("run", "Run the application");
    run_step.dependOn(&run_cmd.step);

    const unit_tests = b.addTest(.{
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });
    const run_unit_tests = b.addRunArtifact(unit_tests);
    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&run_unit_tests.step);
}

Minimal build.zig.zon

.{
    .name = .{ "my-cli" },
    .version = "0.1.0",
    .fingerprint = .auto,
    .minimum_zig_version = "0.15.0",
    .paths = .{ "build.zig", "build.zig.zon", "src" },
    .dependencies = .{},
}

Minimal main.zig (0.15+ I/O)

const std = @import("std");

pub fn main() !void {
    var stdout_buf: [4096]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buf);
    const stdout = &stdout_writer.interface;

    var stderr_buf: [4096]u8 = undefined;
    var stderr_writer = std.fs.File.stderr().writer(&stderr_buf);
    const stderr = &stderr_writer.interface;

    // Parse arguments
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var args = try std.process.argsWithAllocator(allocator);
    defer args.deinit();

    _ = args.next(); // skip program name

    const filename = args.next() orelse {
        try stderr.print("Usage: my-cli <filename>\n", .{});
        try stderr.flush();
        std.process.exit(1);
    };

    try stdout.print("Processing: {s}\n", .{filename});
    try stdout.flush();
}

Critical: 0.15+ I/O Pattern

Zig 0.15+ uses buffered I/O by default. Always flush before exit:

var buf: [4096]u8 = undefined;
var writer = std.fs.File.stdout().writer(&buf);
const stdout = &writer.interface;

try stdout.print("output\n", .{});
try stdout.flush(); // REQUIRED - output won't appear without this

For unbuffered I/O (debugging, progress output):

var writer = std.fs.File.stderr().writer(&.{});
const stderr = &writer.interface;
try stderr.print("debug: immediate output\n", .{});
// No flush needed with empty buffer

Argument Parsing with zig-clap

For complex CLI tools, use zig-clap:

zig fetch --save git+https://github.com/Hejsil/zig-clap

Add to build.zig:

const clap = b.dependency("clap", .{});
exe.root_module.addImport("clap", clap.module("clap"));

Usage:

const clap = @import("clap");
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const params = comptime clap.parseParamsComptime(
        \\-h, --help             Display help and exit.
        \\-o, --output <str>     Output file path.
        \\-v, --verbose          Enable verbose output.
        \\<str>...
        \\
    );

    var diag = clap.Diagnostic{};
    var res = clap.parse(clap.Help, &params, clap.parsers.default, .{
        .diagnostic = &diag,
        .allocator = gpa.allocator(),
    }) catch |err| {
        diag.report(err);
        return err;
    };
    defer res.deinit();

    if (res.args.help != 0) {
        return clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{});
    }

    for (res.positionals) |pos| {
        std.debug.print("positional: {s}\n", .{pos});
    }
}

Error Handling Patterns

pub fn main() !void {
    run() catch |err| {
        var stderr_buf: [4096]u8 = undefined;
        var stderr_writer = std.fs.File.stderr().writer(&stderr_buf);
        const stderr = &stderr_writer.interface;
        try stderr.print("Error: {}\n", .{err});
        try stderr.flush();
        std.process.exit(1);
    };
}

fn run() !void {
    // Application logic here
    // Errors propagate naturally with `try`
}

File I/O

fn readFile(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
    const file = try std.fs.cwd().openFile(path, .{});
    defer file.close();
    return try file.readToEndAlloc(allocator, std.math.maxInt(usize));
}

fn writeFile(path: []const u8, data: []const u8) !void {
    const file = try std.fs.cwd().createFile(path, .{});
    defer file.close();
    try file.writeAll(data);
}

Cross-Compilation

Build for multiple targets:

zig build -Dtarget=x86_64-linux
zig build -Dtarget=aarch64-macos
zig build -Dtarget=x86_64-windows

Key 0.15+ Gotchas

  1. Always flush stdout/stderr before program exit
  2. ArrayList is unmanaged - pass allocator to each method: try list.append(allocator, val)
  3. Type tags are lowercase - .int, .@"struct", .pointer
  4. Use b.createModule() in build.zig, not root_source_file directly
  5. Use b.addLibrary() instead of removed addStaticLibrary()
  6. Calling convention is .c (lowercase), not .C

See SYNTAX-GUIDE.md for complete 0.15+ migration details.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.