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 — the std.Io for anything that needs it (spawning processes, timers, the HTTP client)
  • context.stdout() / context.stderr() — buffered writers; the framework flushes them on normal exit
  • context.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 from build.zig.zon at build time
  • context.command_config.<plugin_id> — typed settings for the routed command, resolved from plugin CommandConfig defaults and meta.plugins overrides
  • context.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:

BuilderReturnsGuide
context.prompts()seven primary prompt types, searchable choices, and the immediate edit operationPrompts
context.table()a static table printer with terminal width handling and complete redirected outputStatic tables
context.progress()spinners, progress bars, multi-barsProgress
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 directoriesApplication 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.