The context
Every command’s execute takes a concrete context: *Context, imported from the generated registry. It’s your handle to I/O, memory, the environment, the active theme, and plugin data — a real type, not anytype, so the compiler checks your usage and your editor autocompletes it.
const Context = @import("command_registry").Context;
pub fn execute(args: Args, options: Options, context: *Context) !void {
const allocator = context.allocator; // arena, freed after this command
const out = context.stdout();
if (args.name.len == 0) return context.fail("name is required", .{});
try out.print("{s}\n", .{args.name});
}
Memory: the arena
context.allocator is an arena scoped to the command: its allocations stay valid through invocation completion, failure rendering, and plugin teardown, then are reclaimed together. An owned invocation result retains its copied failure details until deinit(). The idiom is never call free — no ownership tracking, no leaks, no double-frees in command code. (Non-memory resources — files, processes — still want their defer close.)
This is a deliberate design choice (ADR-0001), and it’s a large part of why AI-written commands are safe by construction: an agent can’t leak what the arena reclaims.
I/O
context.io— thestd.Iofor anything that needs it (spawning processes, timers, the HTTP client)context.stdout()/context.stderr()— buffered writers; the framework flushes them on normal exitcontext.stdin()— buffered reader
Failing well
return context.fail("no note: {s}", .{name}) records an explanation and returns error.CommandFailed. During finalization, the invocation selects the status (default 1) and renderer. The default renderer prints the explanation without a Zig trace; an application renderer can use JSON or another format.
Use return context.failWith(error.NotFound, "no note: {s}", .{name}) to keep a domain error available for application status rules. An ordinary Zig error may also be mapped or described by policy. Unexpected errors propagate to main with their available trace only when unreported and without a message or identity. See Error handling for the full contract.
context.exit(code) flushes buffered output and exits immediately, bypassing normal cleanup. Prefer returning a failure and configuring application policy.
The environment
context.environ is the process environment as a map — no ambient getenv reaching around your back. Option fields can bind env vars declaratively with meta.options.<field>.env; environ is for everything else:
const home = context.environ.get("HOME") orelse ".";
For the specific question of where this app’s files belong on this platform, don’t read HOME yourself — context.paths() resolves config, cache, and data directories from that same environ, and errors rather than guessing when the environment names no home. See Application paths.
App and command metadata
context.app_name,context.app_version— version read frombuild.zig.zonat build timecontext.command_config.<plugin_id>— typed settings for the routed command, resolved from pluginCommandConfigdefaults andmeta.pluginsoverridescontext.command_path— the resolved path of the running command (e.g.{"users", "create"})context.getAvailableCommandInfo(),context.getGlobalOptions()— the registry’s command and global-option metadata, useful for meta-commands and error hooks
Pre-wired builders
The context hands you each terminal package already wired to the command’s streams, allocator, theme, and detected capabilities:
| Builder | Returns | Guide |
|---|---|---|
context.prompts() | seven primary prompt types, searchable choices, and the immediate edit operation | Prompts |
context.table() | a static table printer with terminal width handling and complete redirected output | Static tables |
context.progress() | spinners, progress bars, multi-bars | Progress |
context.ui(.{}) | a hybrid CLI/TUI App (static + live region) | The CLI/TUI hybrid |
context.uiFullScreen(.{}) | a full-screen App (alt-screen, event loop) | Full-screen mode |
context.markdown() | a markdown-to-ANSI formatter on your palette | — |
context.paths() | platform-standard config, cache, and data directories | Application paths |
context.theme carries the app’s theme plus the terminal capabilities detected once per run — every builder above consumes it, so NO_COLOR, piped output, and 16-color terminals are handled uniformly.
Plugin data
Anything a plugin stores in its ContextData is namespaced under context.plugins.<plugin_id> and fully typed. Say you wrote a timing plugin that tracks a --time flag:
if (context.plugins.timing.started_ns != 0) {
// the --time flag was set; your timing plugin is tracking this run
}
The secrets plugin is the same shape: try context.plugins.zcli_secrets.get("token"). See Plugins for the full model.