agentleFS
pmarreckpmarreck/llm_skills/scaffold-zig-project/SKILL.md

scaffold-zig-project

Skill2 starsChanged 4 months agoMITView on GitHub

Use when starting a new Zig project from scratch — "new project", "scaffold", "init", or greenfield Zig repo.

The file

We split this skill into the rules it gives an agent

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

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    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:

    Do not scaffold an optional-Nix switch, direct backend or non-Nix fallback.

    line 18
  2. Distinctive:

    Ask/determine these before generating:

    line 47
  3. Distinctive:

    Use the templates below. After generating, remind the user:

    line 90
  4. Very specific:

    When the project has Zig dependencies, add this to the let block and wire it into buildPhase:

    line 192

≈ “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

Scaffold Zig Project

Overview

Generate the complete boilerplate for a new Zig project following Peter's hexagonal architecture pattern: pure Zig core (no I/O), C FFI boundary, C CLI consumer. Includes a Nix flake prepared for Mechatron Prime CI, build.zig, build.zig.zon, PLAN.md, and .gitignore.

Require nix build from the start. ./build is a thin flake-output selector and publisher; zig build runs inside derivations. Follow cross-platform-project-build-arch for output layout and test preparation. Do not scaffold an optional-Nix switch, direct backend or non-Nix fallback.

<important> The user already has `setup_zig_repo` in their shell which handles: jj/git init, AGENTS.md/CLAUDE.md links, jj cheatsheet, Zig guide, commit hooks, and some .gitignore entries. This skill covers everything that alias does NOT: flake.nix, build.zig, build.zig.zon, directory structure, PLAN.md, and remaining .gitignore entries. </important>

<when_to_use>

  • User says "new project", "start a new Zig project", "scaffold", "greenfield"
  • User wants to create a Zig library, tool, or codec from scratch
  • User is setting up build infrastructure for a new Zig codebase </when_to_use>

Workflow

digraph scaffold {
  "User wants new Zig project" -> "Gather requirements";
  "Gather requirements" -> "Has Zig deps?";
  "Has Zig deps?" -> "Choose dep strategy" [label="yes"];
  "Has Zig deps?" -> "No dep machinery needed" [label="no"];
  "Choose dep strategy" -> "Generate files";
  "No dep machinery needed" -> "Generate files";
  "Generate files" -> "Remind user to run setup_zig_repo";
  "Remind user to run setup_zig_repo" -> "Create GitHub repo + push";
}

Step 1: Gather Requirements

Ask/determine these before generating:

QuestionDefaultAffects
Project name (underscored)requiredeverywhere
CLI name (hyphenated)project-namebuild.zig, flake.nix
One-line descriptionrequiredflake.nix, PLAN.md
Has Zig dependencies?noflake.nix dep strategy
Needs macOS system headers?noDarwin nativeBuildInputs
Optimize modeReleaseFastbuild.zig, flake.nix
Include C FFI + C CLI?yes (hexagonal)build.zig directory structure

Step 2: Generate Directory Structure

For hexagonal (C FFI) projects:

project_name/
  src/
    lib.zig           # Core library (pure Zig, no I/O, exports C FFI)
  include/
    project_name.h    # C header for the FFI
  cli/
    main.c            # C CLI that calls through FFI
  flake.nix
  build.zig
  build.zig.zon
  PLAN.md

For pure-Zig projects (no C FFI):

project_name/
  src/
    main.zig          # Entry point
    lib.zig           # Core library (if also a library)
  flake.nix
  build.zig
  build.zig.zon
  PLAN.md

Step 3: Generate Files

Use the templates below. After generating, remind the user:

Run setup_zig_repo in the project directory to set up jj/git, AGENTS.md, CLAUDE.md, hooks, and base .gitignore entries.

Then append the additional .gitignore entries (see template below) that setup_zig_repo doesn't cover.

Step 4: GitHub + CI

gh repo create pmarreck/PROJECT_NAME --public --source=. --push
<remember> Invoke `$mechatron-ci` after the flake outputs are real and locally verified. That skill owns `.mechatron-prime/targets`, webhook provisioning, the dynamic README badge, and live verification; do not reproduce those details here. </remember>

Templates

flake.nix (no dependencies)

<template> ```nix { description = "{{DESCRIPTION}}";

inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-utils.url = "github:numtide/flake-utils"; };

outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; pname = "{{PNAME}}"; version = "0.1.0"; zigPkg = pkgs.zig; in { packages.default = pkgs.stdenv.mkDerivation { inherit pname version; src = ./.; nativeBuildInputs = [ zigPkg ]; dontConfigure = true; dontFixup = true; buildPhase = '' export HOME=$TMPDIR ${pkgs.lib.optionalString pkgs.stdenv.isDarwin "unset NIX_CFLAGS_COMPILE NIX_LDFLAGS"} zig build -Doptimize=ReleaseFast --prefix $out ''; dontInstall = true; };

    checks.${system} = {
      build = self.packages.${system}.default;
      test = pkgs.stdenv.mkDerivation {
        pname = "${pname}-test";
        inherit version;
        src = ./.;
        nativeBuildInputs = [ zigPkg ];
        dontConfigure = true;
        dontFixup = true;
        buildPhase = ''
          export HOME=$TMPDIR
          ${pkgs.lib.optionalString pkgs.stdenv.isDarwin "unset NIX_CFLAGS_COMPILE NIX_LDFLAGS"}
          # FLEET FLOOR — tests run ReleaseSafe (fleet finding 2026-07-01,
          # adopted fleet-wide 2026-07-29; new projects are BORN with it).
          # ReleaseFast compiles OUT the runtime safety checks (integer
          # overflow, bounds, illegal cast), so a green ReleaseFast suite
          # cannot observe UB — it passes *because* the check that would
          # have failed it is gone. This is not theoretical: it hid three
          # real crashers in rarz (every multi-file archive was silently
          # CORRUPT while reporting VALID) and a u32 underflow in tiffz
          # that produced the correct answer by accident for months.
          #
          # Set HERE on the command line, not as a per-module `.optimize`
          # in build.zig: Zig honours per-module optimize, so pinning only
          # the test module leaves imported library code at ReleaseFast.
          #
          # Shipped artifact and benchmarks stay ReleaseFast.
          timeout 600 zig build test -Doptimize=ReleaseSafe || { echo "Tests failed"; exit 1; }
        '';
        installPhase = ''
          mkdir -p $out
          echo "tests passed" > $out/result
        '';
      };
    };

    devShells.default = pkgs.mkShell {
      packages = [ zigPkg pkgs.hyperfine ];
    };
  });

}

</template>

### flake.nix addition: zigDeps (fixed-output derivation)

When the project has Zig dependencies, add this to the `let` block and wire it into buildPhase:

<template>
```nix
        # Set to pkgs.lib.fakeHash, run `nix build`, use printed hash
        zigDepsHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
        zigDeps = pkgs.stdenv.mkDerivation {
          pname = "${pname}-zig-deps";
          inherit version;
          src = ./.;
          nativeBuildInputs = [ zigPkg pkgs.git pkgs.cacert ];
          outputHashMode = "recursive";
          outputHashAlgo = "sha256";
          outputHash = zigDepsHash;
          dontFixup = true;
          dontPatchShebangs = true;
          buildPhase = ''
            export HOME=$TMPDIR
            export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache
            mkdir -p $ZIG_GLOBAL_CACHE_DIR
            export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
            zig build --fetch=all
          '';
          installPhase = ''
            mkdir -p $out
            cp -r $TMPDIR/zig-cache/p $out/p
          '';
        };

And in the package/test buildPhase, before zig build:

            export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache
            mkdir -p $ZIG_GLOBAL_CACHE_DIR
            cp -r ${zigDeps}/* $ZIG_GLOBAL_CACHE_DIR/
            chmod -R u+w $ZIG_GLOBAL_CACHE_DIR
</template>

build.zig (hexagonal: Zig core + C FFI + C CLI)

<template> ```zig const std = @import("std");

pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.option( std.builtin.OptimizeMode, "optimize", "Optimization mode (default: ReleaseFast)", ) orelse .ReleaseFast;

// -- Core library module (pure Zig, no I/O) --
const core_mod = b.addModule("{{PROJECT_NAME}}", .{
    .root_source_file = b.path("src/lib.zig"),
    .target = target,
    .optimize = optimize,
});
_ = core_mod; // exposed for downstream Zig consumers

// -- Static library with C ABI (FFI boundary) --
const lib = b.addLibrary(.{
    .name = "{{PROJECT_NAME}}",
    .linkage = .static,
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/lib.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    }),
});
b.installArtifact(lib);

// -- C CLI executable (dogfoods the FFI) --
const cli_mod = b.createModule(.{
    .target = target,
    .optimize = optimize,
    .link_libc = true,
});
cli_mod.addCSourceFile(.{
    .file = b.path("cli/main.c"),
    .flags = &.{ "-std=c11", "-Wall", "-Wextra" },
});
cli_mod.addIncludePath(b.path("include"));
// Zig 0.16: linkLibrary lives on the MODULE, not on Build.Step.Compile.
// The 0.12-0.15 form `cli.linkLibrary(lib)` fails with
//   "no field or member function named 'linkLibrary' in 'Build.Step.Compile'"
// See ZIG_RECENT_API_CHANGES.md §3 (Build System) — it has documented this
// since before this template was written.
cli_mod.linkLibrary(lib);
const cli = b.addExecutable(.{
    .name = "{{CLI_NAME}}",
    .root_module = cli_mod,
});
b.installArtifact(cli);

// -- Run step --
const run_cmd = b.addRunArtifact(cli);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
b.step("run", "Run the CLI").dependOn(&run_cmd.step);

// -- Unit tests --
const run_tests = b.addRunArtifact(b.addTest(.{
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/lib.zig"),
        .target = target,
        .optimize = optimize,
    }),
}));
b.step("test", "Run unit tests").dependOn(&run_tests.step);

}

</template>

### build.zig (pure Zig, no C FFI)

<template>
```zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.option(
        std.builtin.OptimizeMode,
        "optimize",
        "Optimization mode (default: ReleaseFast)",
    ) orelse .ReleaseFast;

    const exe = b.addExecutable(.{
        .name = "{{CLI_NAME}}",
        .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);
    b.step("run", "Run the executable").dependOn(&run_cmd.step);

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

build.zig.zon

<template> ```zig .{ .name = .{{PROJECT_NAME}}, .version = "0.1.0", .minimum_zig_version = "0.15.2", .dependencies = .{}, .paths = .{ "build.zig", "build.zig.zon", "src", "include", "cli", }, .fingerprint = 0x0000000000000000, } ``` </template>

Note: .fingerprint is auto-generated by zig build on first run.

.gitignore additions (beyond setup_zig_repo)

Append these after running setup_zig_repo:

<template> ```gitignore

Zig build artifacts

zig-out/ zig-cache/ .zig-cache/

Nix build result symlinks

result result-*

macOS

.DS_Store

LLM inbox (inter-project messages)

inbox/

Tooling indexes

.codescan/ .serena/

</template>

### PLAN.md

<template>
```markdown
# {{Project Title}} -- Plan

## In Progress
- [ ] Initial scaffolding and CI setup

## Future Enhancements
- [ ] (add items as they arise)

## Completed
</template>

Starter src/lib.zig (hexagonal)

<template> ```zig const std = @.import("std");

// ── Public Zig API ──────────────────────────────────────────────────

// TODO: Add core logic here. This module must have NO I/O.

// ── C FFI exports ───────────────────────────────────────────────────

export fn {{project_name}}_version() [*:0]const u8 { return "0.1.0"; }

// ── Tests ───────────────────────────────────────────────────────────

test "version returns valid string" { const v = {{project_name}}_version(); try std.testing.expect(v[0] == '0'); }

</template>

### Starter include/project_name.h

<template>
```c
#ifndef {{PROJECT_NAME_UPPER}}_H
#define {{PROJECT_NAME_UPPER}}_H

const char* {{project_name}}_version(void);

#endif
</template>

Starter cli/main.c

<template> ```c #include <stdio.h> #include "{{project_name}}.h"

int main(int argc, char* argv[]) { (void)argc; (void)argv; printf("{{project_name}} version %s\n", {{project_name}}_version()); return 0; }

</template>

## Common Mistakes

<important>
- **READ `ZIG_RECENT_API_CHANGES.md` BEFORE using these templates.** It is the
  canonical reference and it is *ahead of this skill*. On 2026-07-31 this
  template still carried the 0.15 `cli.linkLibrary(lib)` form even though the
  doc had documented the 0.16 module-level form all along — the scaffold failed
  to build, purely because the template was used without consulting the
  reference. If a template here disagrees with that doc, **the doc wins**, and
  fix the template.
- **Tests must be built ReleaseSafe, shipped artifacts ReleaseFast.** A green
  ReleaseFast test suite cannot see UB, because the checks that would fail it
  are compiled out. New projects start with the floor already in the flake.
- **Forgetting `export HOME=$TMPDIR`** in flake.nix buildPhase -- Zig needs a writable HOME
- **Missing `dontConfigure = true`** -- Nix tries to run `./configure` otherwise
- **Using `main` branch** -- Peter's projects always use `yolo`
- **Omitting CI targets** -- Always include intentional `checks` in `flake.nix`,
  then use `$mechatron-ci` to select real attributes in
  `.mechatron-prime/targets`
- **Importing Zig core directly from a Zig CLI** -- The CLI MUST call through the C FFI to dogfood the boundary, even when both are Zig
</important>

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.