Imported from agoodway/GoodSkills (
skills/zig-cli/SKILL.md). Install upstream withnpx skills add agoodway/GoodSkills --skill zig-cli. Copyright stays with the author.
Zig CLI
Build and maintain production-ready Zig CLI applications. Single static binary (~1MB), zero external dependencies, cross-compiles to macOS, Linux, and Windows from any platform.
Subcommands
| Subcommand | Purpose |
|---|---|
bootstrap |
Scaffold a new Zig CLI project from scratch |
openapi-update |
Regenerate the API client from an OpenAPI spec |
add-command <name> |
Add a new subcommand to an existing Zig CLI |
dist |
Cross-compile release binaries for all platforms |
/zig-cli help
Display a list of all available subcommands. Output the following exactly:
/zig-cli subcommands:
bootstrap — Scaffold a new Zig CLI project
openapi-update — Regenerate API client from OpenAPI spec
add-command <name> — Add a new subcommand to the CLI
dist — Cross-compile for all 6 platforms
help — Show this help message
If /zig-cli is invoked without a subcommand, show the help output above.
Dispatch
- Parse the subcommand and args from the user's invocation. Examples:
/zig-cli bootstrap→ subcommandbootstrap/zig-cli openapi-update→ subcommandopenapi-update/zig-cli add-command verify→ subcommandadd-command, argverify/zig-cli dist→ subcommanddist/zig-cli help→ show help
- If the subcommand is unknown, list available subcommands and stop.
- Follow the matching workflow below.
/zig-cli bootstrap
Scaffold a production-ready Zig CLI application with zero external dependencies. Produces a single static binary (~1MB) that cross-compiles to macOS, Linux, and Windows.
Prerequisites
Verify before starting:
zig versionreturns 0.15.x or later- User has a directory for the project
Gather Input
Ask the user for:
- App name — binary name (lowercase, hyphen-separated, e.g.
my-tool) - Description — one-line description for root help
- Subcommands — initial commands to scaffold (e.g.
sync,deploy,status) - Config needed? — whether the app needs per-environment config (
~/.{app-name}.json) - API client? — whether to include an HTTP client for a REST API (if yes, ask for base URL and whether they have an OpenAPI spec)
Generate Project
Read references/templates.md for all file templates.
Create files in this order:
1. Initialize project
mkdir -p <app-name>/src/commands
cd <app-name>
zig init
rm src/root.zig # No library consumers
2. Create files from templates
Apply placeholder substitution ({{APP_NAME}}, {{DESCRIPTION}}) to each template:
| File | Template | Notes |
|---|---|---|
build.zig |
build.zig template | Binary named {{APP_NAME}}, no deps |
build.zig.zon |
build.zig.zon template | Zero dependencies |
src/main.zig |
main.zig template | Command routing + arg helpers |
src/help.zig |
help.zig template | All help text, agent-readable |
src/commands/<subcmd>.zig |
subcommand template (one per command) | |
justfile |
justfile template | build/test/dist/release recipes |
install.sh |
install.sh template | macOS/Linux installer |
install.ps1 |
install.ps1 template | Windows PowerShell installer |
.gitignore |
.gitignore template | |
CLAUDE.md |
CLAUDE.md template |
If config is needed, also create:
| File | Template |
|---|---|
src/config.zig |
config.zig template |
src/commands/configure.zig |
configure.zig template |
If API client is needed, also create:
| File | Template |
|---|---|
src/generated.zig |
generated.zig template |
src/table.zig |
table.zig template |
2b. OpenAPI client generation (if user has a spec)
If the user has an OpenAPI JSON spec, use openapi2zig to seed the types and client:
which openapi2zig || curl -fsSL https://christianhelle.com/openapi2zig/install | INSTALL_DIR="$HOME/.local/bin" bash
~/.local/bin/openapi2zig generate -i <spec-path> -o src/generated.zig
CRITICAL: The generated output will NOT compile on Zig 0.15.2. You MUST manually fix it:
- Function names — dotted names like
Namespace.Controller.actionare invalid Zig. Replace with valid identifiers likelistItems,getItem. - Nested types flattened — fields like
hero: ?[]const u8should behero: ?Herowith proper struct types. - Old API calls — Replace
std.http.Client.init(allocator)withstd.http.Client{ .allocator = allocator }. Replacestd.ArrayList(u8).init(allocator)withvar list: std.ArrayList(u8) = .{};. - No auth headers — Add Bearer token auth via
extra_headersinfetch(). - No response handling — Functions return
!void. Change to returnRawResponsewith status + body. - No base URL — Functions use relative paths. Add a
Clientstruct that holdsbase_urland prepends it.
See the generated.zig template in references/templates.md for the corrected pattern.
Add a generate recipe to the justfile:
generate:
~/.local/bin/openapi2zig generate -i <spec-path> -o src/generated.zig
@echo "IMPORTANT: Generated code needs manual fixes for Zig 0.15.2 — see CLAUDE.md"
3. Fix fingerprint
The first zig build will fail with an invalid fingerprint error and suggest the correct value. Run zig build 2>&1, then update build.zig.zon with the suggested fingerprint and build again.
4. Verify
zig build
zig build test
zig build run -- --help
Output
After generation, print a summary of created files and next steps.
/zig-cli openapi-update
Regenerate the API client (src/generated.zig) from an OpenAPI spec.
Workflow
-
Find the OpenAPI spec — look for common locations:
- Check if the user specified a path in args
- Look for
openapi.jsonoropenapi.yamlin the project root - Look for a sibling
../app/openapi.json(common for companion API projects) - If not found, ask the user for the spec path
-
Install openapi2zig if needed:
which openapi2zig || curl -fsSL https://christianhelle.com/openapi2zig/install | INSTALL_DIR="$HOME/.local/bin" bash -
Backup existing generated.zig (if it exists):
cp src/generated.zig src/generated.zig.bak -
Generate from spec:
~/.local/bin/openapi2zig generate -i <spec-path> -o src/generated.zig -
Fix the generated output for Zig 0.15.2 — the raw output will NOT compile. Apply these fixes:
- Function names — replace dotted identifiers (e.g.,
AppWeb.Api.V1.Controller.index) with valid Zig names (e.g.,listItems). Use the backup file as reference for naming conventions if it exists. - Nested types — ensure
$reffields use proper struct types, not[]const u8. - HTTP client pattern — replace generated client code with the
Clientstruct pattern from references/templates.md (base URL, Bearer auth,fetch(),RawResponse). - ArrayList API —
var list: std.ArrayList(u8) = .{};notstd.ArrayList(u8).init(allocator). - HTTP Client init —
std.http.Client{ .allocator = allocator }notstd.http.Client.init(allocator). - JSON —
std.json.fmt()notstd.json.stringify().
- Function names — replace dotted identifiers (e.g.,
-
Verify:
zig build zig build test -
Clean up:
- If build succeeds, remove the backup:
rm src/generated.zig.bak - If build fails, report errors and keep the backup for reference
- If build succeeds, remove the backup:
-
Summary — report what changed: new endpoints, removed endpoints, type changes.
/zig-cli add-command <name>
Add a new subcommand to an existing Zig CLI project.
Workflow
-
Validate project — verify
src/main.zig,src/help.zig, andsrc/commands/exist. -
Create the command file —
src/commands/<name>.zig:const std = @import("std"); const main_mod = @import("../main.zig"); const File = std.fs.File; pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void { _ = args; try main_mod.writeOut(allocator, "TODO: implement <name>\n", .{}); } -
Update src/main.zig — add three things:
- Import:
const <name> = @import("commands/<name>.zig"); - Command routing in
main(): add anifbranch for the command name - Help dispatch in
dispatchHelp(): route to the help constant
- Import:
-
Update src/help.zig — add two things:
- A new
pub const <name>_helpconstant with full usage, flags, behavior, exit codes, and examples - Add the command to the
root_helpcommands listing
- A new
-
Verify:
zig build && zig build test -
Remind the user — when adding a command, you MUST update THREE files:
src/main.zig— routingsrc/help.zig— help textsrc/commands/<name>.zig— implementation
/zig-cli dist
Cross-compile release binaries for all 6 supported platforms.
Workflow
-
Check for justfile — if
justfileexists with adistrecipe, runjust dist. -
Manual fallback — if no justfile:
mkdir -p dist zig build -Dtarget=aarch64-macos -Doptimize=ReleaseSafe && cp zig-out/bin/<app> dist/<app>-darwin-arm64 zig build -Dtarget=x86_64-macos -Doptimize=ReleaseSafe && cp zig-out/bin/<app> dist/<app>-darwin-amd64 zig build -Dtarget=x86_64-linux -Doptimize=ReleaseSafe && cp zig-out/bin/<app> dist/<app>-linux-amd64 zig build -Dtarget=aarch64-linux -Doptimize=ReleaseSafe && cp zig-out/bin/<app> dist/<app>-linux-arm64 zig build -Dtarget=x86_64-windows -Doptimize=ReleaseSafe && cp zig-out/bin/<app>.exe dist/<app>-windows-amd64.exe zig build -Dtarget=aarch64-windows -Doptimize=ReleaseSafe && cp zig-out/bin/<app>.exe dist/<app>-windows-arm64.exe -
Generate checksums:
cd dist && shasum -a 256 <app>-* > checksums.txt -
Report — list binaries with sizes.
Zig 0.15.2+ Critical Patterns
These are non-obvious and will cause compilation failures if done wrong:
- stdout/stderr —
std.fs.File.stdout(), notstd.io.getStdOut()(removed in 0.15) - ArrayList — Unmanaged:
var list: std.ArrayList(u8) = .{};then pass allocator to each method - Formatted output —
std.fmt.allocPrint(allocator, fmt, args)thenFile.stdout().writeAll(msg) - JSON serialization —
std.json.fmt(value, .{.whitespace = .indent_2})withstd.fmt.allocPrint("{f}", .{...}) - JSON parsing —
std.json.parseFromSlice(T, allocator, content, .{.ignore_unknown_fields = true, .allocate = .alloc_always}) - HTTP client —
std.http.Client.fetch()withstd.Io.Writer.Allocating. No.open()method. - Table rows in loops — heap-allocate:
const row = try allocator.alloc([]const u8, n); - main signature —
pub fn main() !void - Allocator —
std.heap.page_allocatorfor CLI processes - Config path (cross-platform) — use
std.process.getEnvVarOwned(allocator, "HOME")notstd.posix.getenv("HOME").std.posix.getenvis not available on Windows.
Help System Design
Every command must have comprehensive help text in src/help.zig designed to be read by AI coding agents:
- Usage line with exact syntax
- All flags with types and descriptions
- All positional arguments
- Behavior notes (defaults, side effects)
- Exit codes
- Examples
$ARGUMENTS