zhi uses hashicorp/go-plugin for its plugin system. Plugins run as separate processes and communicate with the host over gRPC via stdio. This provides process isolation, language independence (in theory), and clean crash boundaries.
zhi has four plugin types:
| Type | Purpose | Communication |
|---|---|---|
| Config | Manage configuration values (List, Get, Set, Validate) | Host -> Plugin |
| Transform | Mutate configuration before display or after save | Host -> Plugin |
| Store | Persist and retrieve configuration trees | Host -> Plugin |
| UI | Provide interactive frontends | Bidirectional |
Any of the above types can be implemented as a meta-plugin – a plugin that internally launches and composes child plugins using the Meta-Plugin SDK. From the host’s perspective a meta-plugin is indistinguishable from a regular plugin.
All plugins share the same handshake defined in pkg/zhiplugin/plugin.go:
ZHI_PLUGINzhiplugin-v11import "github.com/MrWong99/zhi/pkg/zhiplugin"
// Use zhiplugin.Handshake in your plugin.Serve config
All plugin types work with the shared configuration tree model (pkg/zhiplugin/config/):
Tree – a flat key-value store with slash-delimited pathsTreeReader – read-only interface (Get, List)Value – holds Val any, optional Metadata, and local ValidatorsValidationResult – carries Severity (Info, Warning, Blocking) and MessagePaths use / as the hierarchy separator. Each segment must match:
[a-z][a-z0-9._-]*[a-z0-9]
Examples: database/host, app/tls/cert.pem, pokedex/trainer.name
Proto definitions live in api/proto/zhiplugin/v1/. Generated Go stubs are placed in pkg/zhiplugin/{type}/proto/.
Each plugin type has:
grpc_client.go – host-side gRPC clientgrpc_server.go – plugin-side gRPC serverConfiguration values are JSON-encoded for wire transfer. Validator closures (functions) never cross the wire – they are local to the plugin process.
A minimal plugin binary needs a main function that calls plugin.Serve:
package main
import (
"github.com/hashicorp/go-plugin"
"github.com/MrWong99/zhi/pkg/zhiplugin"
"github.com/MrWong99/zhi/pkg/zhiplugin/config"
)
func main() {
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: zhiplugin.Handshake,
Plugins: map[string]plugin.Plugin{
"config": &config.GRPCPlugin{Impl: &myPlugin{}},
},
GRPCServer: plugin.DefaultGRPCServer,
})
}
Replace "config" and config.GRPCPlugin with the appropriate type for your plugin.
Plugin binaries should follow the naming pattern zhi-<type>-<name>:
zhi-config-pokedexzhi-transform-evolvezhi-store-vaultzhi-ui-httpapiThis allows zhi to discover and categorize plugins automatically from ~/.zhi/plugins/.
Use goplugin.TestPluginGRPCConn() for in-process gRPC testing without starting a subprocess:
func TestMyPlugin(t *testing.T) {
client, server := goplugin.TestPluginGRPCConn(t, true, map[string]goplugin.Plugin{
"config": &config.GRPCPlugin{Impl: &myPlugin{}},
})
defer client.Close()
defer server.Stop()
raw, err := client.Dispense("config")
require.NoError(t, err)
p := raw.(config.Plugin)
// Test your plugin through the gRPC interface
paths, err := p.List(context.Background())
require.NoError(t, err)
// ...
}
The examples/ directory contains working reference implementations:
| Example | Type | Description |
|---|---|---|
| zhi-config-pokedex | Config | Typed values, metadata, single and cross-value validation |
| zhi-transform-pokedex | Transform | Tree mutation, value mapping |
| zhi-store-json | Store | File-based persistence |
| zhi-store-memory | Store | Minimal in-memory store |
| zhi-store-vault | Store | HashiCorp Vault KV v2 backend |
| zhi-store-mirror | Store (meta) | Meta-plugin: mirrors writes to memory + JSON file |
| zhi-ui-httpapi | UI | HTTP/JSON API with SSE streaming |
| zhi-ui-mcp-sse | UI | MCP server over HTTP for LLM clients |
| zhi-ui-webui | UI | Browser-based Web UI |
| zhi-config-javabean | Config | Java bean with Bean Validation, GraalVM native-image |
All Go examples are published as OCI artifacts to ghcr.io/mrwong99/zhi/ on every release and can be installed with zhi plugin install oci://ghcr.io/mrwong99/zhi/<plugin-name>:<tag>. See the Sharing and Registries guide for the full list.
Plugins communicate over gRPC, so any language with gRPC support can implement a zhi plugin. See the language-specific guides:
The fastest way to start a new plugin project is the scaffolding command:
zhi plugin new --name my-config --type config --author myorg
This generates a complete, standalone Go project with implementation stubs, tests, a Makefile, CI/CD workflows, and a sample workspace. See the Scaffolding Guide for the full reference.
Once your plugin is built and tested, you can share it via OCI registries.
Scaffold a project (includes zhi-plugin.yaml and CI/CD workflow):
zhi plugin new --name my-config --type config --registry ghcr.io/myorg
Or create just the manifest for an existing project:
zhi plugin init --name my-config --type config --version 1.0.0
Build binaries for your target platforms:
make cross-compile # if using the scaffolded Makefile
Publish to a registry:
zhi plugin publish --registry ghcr.io/myorg --sign
See the Sharing and Registries guide for the full publishing workflow, including signing, marketplace registration, and version management.