Extensions
An extension is a Lua entrypoint that teaches zi one habit.
Use one when you need a command, a tool, a prompt rule, a lifecycle hook, a provider claim, or a small piece of UI.
Good extensions are boring to inspect: clear name, clear inputs, visible effects.
What extensions can do
- add model-visible tools
- add slash commands
- customize prompts and provider context
- observe session, message, model, tool, and job events
- publish messages, status, progress, reports, prompts, and surfaces
- ask side-channel model questions
- register provider/model claims
- store per-session extension state
- attach notes and labels to durable session entries
- spawn child zi runs
The host API is in API. Handler context is in Context.
zi loads extensions from the extensions/ directory in any resource root. See Resource discovery for user/project locations, settings paths, packages, and root precedence.
Extension files
Supported shapes:
extensions/<id>.lua- A single-file extension. It can require modules from shared root-level
lua/folders.
extensions/<id>/init.lua- A bundled extension. Private modules live under
extensions/<id>/lua/and should use a namespaced module id, for examplerequire("code_review.render").
Module lookup is deterministic:
1. the active bundled extension's private lua/ 2. shared lua/ folders from runtime roots 3. Lua's default paths
Within one root, discovery is lexical. Most duplicate registrations are first claimant wins. Slash commands keep duplicate names callable through resolved names.
After roots
Runtime extension roots may contain an after/ directory:
<root>/
└─ after/
├─ 10-local/
│ └─ extensions/
│ └─ tweak.lua
└─ 20-team/
└─ extensions/
└─ policy.luaEach directory directly under after/ is treated as another runtime root, loaded after the parent root in lexical order. Use this for late customization, not for hidden policy.
Loading
zi installs a global zi table. An extension may register at top level or return a function that receives zi.
return function(zi)
zi.tool({
name = "greet",
description = "Generate a greeting.",
parameters = {
type = "object",
properties = {
name = { type = "string", description = "Name to greet" },
},
},
execute = function(params, ctx)
local name = params.name or "world"
return { content = { { type = "text", text = "hello, " .. name } } }
end,
})
endLoad/register cannot yield. Keep it cheap. Register capabilities there. Do real work in tools, commands, events, jobs, or context helpers.
Lifecycle
An extension has setup work and session work.
Setup registers tools, commands, providers, and event handlers.
Session work runs later with a live ctx.
Rules:
- keep setup cheap and deterministic
- do not start long work while loading
- use tools and commands for visible actions
- use events for policy and reaction
- recreate live UI, prompts, jobs, and provider handles after session changes
- keep ephemeral private state in Lua locals
- keep durable session facts in
ctx.sessionartifacts
Common lifecycle events:
session_startsession_shutdownsession_before_switchsession_before_fork