<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>zcli blog</title>
    <link>https://zcli.sh/blog/</link>
    <description>zcli release notes and deep-dives.</description>
    <item>
      <title>Introducing zcli: fast, slim and typesafe command-line tools in Zig</title>
      <link>https://zcli.sh/blog/introducing-zcli/</link>
      <guid>https://zcli.sh/blog/introducing-zcli/</guid>
      <pubDate>Tue, 07 Jul 2026 00:00:00 +0000</pubDate>
      <description>&lt;p&gt;I’ve always loved building command-line tools. I have often joined developer experience teams because I love creating great developer tooling. I love crafting a really great user experience for customers, but I think my &lt;em&gt;favorite&lt;/em&gt; customers are other developers. Developer tooling has always been a passion of mine, and I’ve always looked for ways to improve the developer experience. With most of the companies I’ve worked for, I have found that there are common utilities that are spread across different repos, random collections of scripts scattered between codebases, or even scripts that live directly on developer machines, which get shared organically, copied from one user’s machine to another. One of my passions has been to collect those tools, dig in to figure out what problem the scripts are trying to &lt;em&gt;solve&lt;/em&gt;, and build a CLI shaped around the very best solutions, tailored to the products and processes of the company.&lt;/p&gt;&lt;p&gt;Along the way, I’ve tried a lot of different CLI frameworks. I built a company-wide CLI in Node using Pastel and Ink. We had a lot of frontend engineers, and the idea that they could possibly contribute, since Ink uses React, was appealing. I really loved the simplicity that came from building a CLI from a folder structure: to me, it was an elegant mapping that made sense. Create a folder, add a JS file with a function as the default export, and that function returns a React component. To me, it was incredibly elegant. It was also very heavy.&lt;/p&gt;&lt;p&gt;I’ve always held a strong belief that we’ve accepted too much bloat for the simplicity that a high-level language like JS affords us. I get it. JavaScript and TypeScript are two of my most loved languages, and I’ve probably spent more time with them than any other. It hurts, though, when I see that we’ve accepted that 40MB is the &lt;em&gt;floor&lt;/em&gt; on the size of a CLI built with JS. It makes sense, we’re packaging an entire &lt;em&gt;runtime&lt;/em&gt;, one that is built to do all &lt;em&gt;sorts&lt;/em&gt; of things, and we package it up to run a set of scripts. Most have dependencies, and in the JS ecosystem that means &lt;em&gt;transitive&lt;/em&gt; dependencies. Honestly, most CLIs don’t need this.&lt;/p&gt;&lt;p&gt;It also hurts &lt;em&gt;speed&lt;/em&gt;. Having to load an entire JS runtime, then have that runtime parse JS files, takes &lt;em&gt;time&lt;/em&gt;. Yes, we’re only talking hundreds of milliseconds, but I’ve learned that perceived time in a CLI &lt;em&gt;matters&lt;/em&gt;. There’s a thin line between a CLI not feeling responsive, and it doesn’t take long to cross that line.&lt;/p&gt;&lt;p&gt;It feels like we’ve accepted this. A &lt;em&gt;lot&lt;/em&gt; of AI tooling, especially CLIs, are being written in JS. Again, I understand when there are so many fantastic libraries and frameworks already built, but they come at a cost: bloated binaries, slower load times, and, for long-lived tasks, high memory usage.&lt;/p&gt;&lt;p&gt;I have had a dream, for quite a while, of building a CLI framework to solve a lot of these issues. I’ve really enjoyed watching Zig grow, and have felt for a long time that it sits right at the intersection of simplicity and performance. After a lot of iterating on design and UX, &lt;code&gt;zcli&lt;/code&gt; was born.&lt;/p&gt;&lt;p&gt;There were two core design principles I decided on when architecting &lt;code&gt;zcli&lt;/code&gt;:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Wherever possible, have only one way to do things, and&lt;/strong&gt;&lt;br&gt;&lt;strong&gt;Everything that &lt;em&gt;can&lt;/em&gt; be done at compile time &lt;em&gt;should&lt;/em&gt; be.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Zig was a perfect language for this. Users can define a command tree, as complex as they want, and the router for it is automatically built, at comptime. This means a router doesn’t need to be built and loaded when the CLI starts up. Arg and option parsing is determined at comptime. When an option is passed, it goes to machinery that was optimized at comptime exactly &lt;em&gt;for that option&lt;/em&gt;. Using structs to define the inputs (args and options), we could specify &lt;em&gt;exactly&lt;/em&gt; what we want from the user, layering on top of Zig’s powerful type system.&lt;/p&gt;&lt;p&gt;Following the principle of a single way to do things led to choices that some may not agree with. That’s fine, I completely understand, there were certain features I had to actively decide &lt;em&gt;against&lt;/em&gt; in order to keep the zcli patterns simple. For example, &lt;code&gt;Args&lt;/code&gt; and &lt;code&gt;Options&lt;/code&gt; must be defined if you define the &lt;code&gt;execute&lt;/code&gt; function. If you don’t it’s not a runtime error, it’s a &lt;em&gt;compile time&lt;/em&gt; error (core principle 2). To some, that may seem a bit heavy-handed, not being able to just import and use a struct from another file. (side note - you can still do that, you just have to specify &lt;code&gt;pub const Args = YourExternalType&lt;/code&gt; so it’s defined in your file). The benefits, to me, outweigh the slight cons. Having a structure that is followed consistently means less guessing. Errors are also much easier to design and can cleanly inform the user when something is wrong.&lt;/p&gt;&lt;p&gt;I’m pretty happy with the result. I’m sure it’s going to have some growing pains: being a batteries-included framework means there’s a &lt;em&gt;lot&lt;/em&gt; of surface area, and with a lot of surface area, there will be issues. I feel confident in the structure, and building the &lt;code&gt;zcli&lt;/code&gt; CLI on &lt;em&gt;top&lt;/em&gt; of this, as the first use case of the framework itself, has made me more confident.&lt;/p&gt;&lt;p&gt;So, what does this look like in practice? Here’s an example file:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;zig&quot;&gt;&lt;span class=&quot;keyword_modifier&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constant variable_builtin type variable&quot;&gt;meta&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;constant variable_builtin type variable_member variable&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;quot;Deploy your application&amp;quot;&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;keyword_modifier&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constant variable_builtin type variable&quot;&gt;Args&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword_type&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;constant variable_builtin type variable_member variable&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;type_builtin&quot;&gt;u8&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;keyword_modifier&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constant variable_builtin type variable&quot;&gt;Options&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword_type&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;constant variable_builtin type variable_member variable&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;type_builtin&quot;&gt;u8&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;quot;development&amp;quot;&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;keyword_modifier&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;keyword_function&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;constant variable_builtin type variable function&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constant variable_builtin variable_parameter variable type&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;constant variable_builtin type variable&quot;&gt;Args&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant variable_builtin variable_parameter variable type&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;constant variable_builtin type variable&quot;&gt;Options&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant variable_builtin variable_parameter variable type&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type_builtin&quot;&gt;anytype&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;type_builtin&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;keyword_exception&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;constant variable_builtin type variable&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type constant variable_builtin function_call variable_member variable&quot;&gt;stdout&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type constant variable_builtin function_call variable_member variable&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;Deploying {s} to {s}&lt;/span&gt;&lt;span class=&quot;string string_escape&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;constant variable_builtin type variable&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;constant variable_builtin type variable_member variable&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant variable_builtin type variable&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;constant variable_builtin type variable_member variable&quot;&gt;env&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Enabling LLM generation&lt;/h2&gt;&lt;p&gt;One of my recent goals was to make it easier for LLM generation of CLIs. I understand people have &lt;em&gt;very strong opinions&lt;/em&gt; on this, and while I respect that, I want to make it easy for those who have a need for a CLI to be able to generate one cleanly. The benefit to following “one way to do things” &lt;em&gt;also&lt;/em&gt; leads to LLMs being able to more easily reason about and iterate on CLI code. Overall, zcli makes it surprisingly easy to get a small, portable CLI that does what you need, without disk space and memory bloat, and with about as snappy performance as possible.&lt;/p&gt;&lt;p&gt;zcli is MIT licensed, targets stable Zig 0.16.0, and the &lt;a href=&quot;https://zcli.sh/why/#comparison&quot;&gt;comparison to zig-clap and zli&lt;/a&gt; is on the site if you’re weighing options. Issues and PRs welcome on &lt;a href=&quot;https://github.com/ryanhair/zcli&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
