Extension design rules
Good zi extensions are small, inspectable, and kind to the user.
They do one useful thing, say what they are doing, and leave the user in control. They avoid hidden policy, surprising network calls, and clever abstractions that only the author can debug.
This page is guidance for humans and for zi when generating extensions.
Prefer documented host APIs over host details. Do not depend on TUI components, terminal input streams, mailbox payloads, provider runtime handles, transcript row objects, or render caches.
Keep load/register cheap and deterministic. Register capabilities at load time; do real work later, when a tool, command, or event is running with a live context.
Choose the smallest surface
Use tools for model-visible capabilities.
Use commands for direct user actions.
Use events for lifecycle policy, prompt/context rewriting, message watching, and tool/provider interception.
Use `ctx.ui` to publish UI intent.
Use `ctx.state` for durable per-extension session state.
Use `ctx.ai.complete` for side-channel model calls that should not mutate the transcript.
Use `zi.register_provider` for provider/model visibility, not for request rewriting. Use `before_provider_request` for request rewriting.
Capability guide
- Need: add an action the model can call
- Use tools.
- Need: add a slash command for the user
- Use commands.
- Need: change the system prompt
- Use `before_agent_start`.
- Need: rewrite submitted user input
- Use `input`.
- Need: rewrite messages before a provider call
- Use `context`.
- Need: rewrite the provider request
- Use `before_provider_request`.
- Need: show feedback, status, progress, a report, or a prompt
- Use context ui api.
- Need: remember a per-session choice
- Use context state api.
- Need: inspect the transcript, attach notes, label important entries, or query labeled entries
- Use context session api.
- Need: ask a side-channel model question
- Use `ctx.ai.complete`.
- Need: expose a model/provider
- Use providers.
- Need: run a bounded OS command from extension code
- Use `zi.system`.
- Need: delegate work to a child zi run
- Use spawn helper.
Canonical patterns
A model-visible tool has one job: accept structured parameters, perform the action, and return content. If the behavior cannot be described in one sentence, split it.
return function(zi)
zi.register_tool({
name = "project_status",
description = "Summarize project status.",
parameters = { type = "object", properties = {} },
execute = function(params, ctx)
return { content = { { type = "text", text = "No status provider configured." } } }
end,
})
endA slash command is for direct user intent. It should be safe to run because a person asked for it.
return function(zi)
zi.register_command({
name = "note",
description = "Save a session note.",
handler = function(args, ctx)
local ok = ctx.session.append_note({ kind = "manual", body = tostring(args or "") })
if ctx.ui then ctx.ui.message(ok and "note saved" or "note failed") end
end,
})
endA semantic message observer can attach durable session metadata without touching raw jsonl or UI rows. Prefer this kind of visible memory over private state when the information belongs to the session.
zi.on("message", function(event, ctx)
local message = event.message or {}
if message.role == "user" and message.text and message.text:match("decision") then
ctx.session.label(message.entry_id, "decision")
ctx.session.append_note({
kind = "observation",
body = "possible decision",
source_entry_id = message.entry_id,
})
end
end)An event is for policy or reaction. Keep event behavior easy to explain when someone reads the session later.
zi.on("message", function(event, ctx)
local message = event.message or {}
if message.role == "assistant" and ctx.ui then
ctx.ui.message("assistant replied")
end
end)Kindness rules
Ask before destructive actions.
Show paths before changing files outside the current project.
Name models and providers when an extension changes them.
Prefer local files, local state, and project-relative paths when possible.
Do not hide network calls inside unrelated commands.
Return readable errors. A person should know what failed and what they can try next.
Keep generated extensions short enough to review.
Extension examples
hello.lua- Minimal tool registration.
commands.lua- Slash command with a host-owned report.
dynamic_tools.lua- Dynamic tool registration from an event or command.
prompt_customizer.lua- System prompt customization with
before_agent_start.
status_line.lua- Turn lifecycle status publication.
message_watch.lua- Semantic message observer.
model_completion.lua- Model catalog inspection and
ctx.ai.complete.
session_lifecycle.lua- Session lifecycle observation and cancellable pre-hooks.
session_notes.lua- Session note storage and retrieval.
auto_label.lua- Durable message labels, label queries, and entry lookup.
git_status.lua- Yieldable
zi.systemcommand execution with host-owned report output.
message.lua- Short feedback through
ctx.ui.message.
question.lua,questionnaire.lua,timed_confirm.lua- Host-owned prompts.
custom_header.lua,widget_placement.lua,hidden_thinking_label.lua,titlebar.lua- Pi-mono parity examples rewritten onto host-owned semantic UI primitives.
input_transform.lua,permission_gate.lua- Input and permission-style interception patterns.
summarize.lua,handoff.lua,qna.lua- Workflow-shaped commands/tools built from the same primitives.