zhi

Plugin Development Overview

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.

Plugin Types

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.

Shared Handshake

All plugins share the same handshake defined in pkg/zhiplugin/plugin.go:

import "github.com/MrWong99/zhi/pkg/zhiplugin"

// Use zhiplugin.Handshake in your plugin.Serve config

Configuration Tree Model

All plugin types work with the shared configuration tree model (pkg/zhiplugin/config/):

Path Format

Paths 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

gRPC Layer

Proto definitions live in api/proto/zhiplugin/v1/. Generated Go stubs are placed in pkg/zhiplugin/{type}/proto/.

Each plugin type has:

Configuration values are JSON-encoded for wire transfer. Validator closures (functions) never cross the wire – they are local to the plugin process.

Plugin Binary Structure

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.

Naming Convention

Plugin binaries should follow the naming pattern zhi-<type>-<name>:

This allows zhi to discover and categorize plugins automatically from ~/.zhi/plugins/.

Testing

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)
    // ...
}

Examples

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.

Non-Go Plugin Development

Plugins communicate over gRPC, so any language with gRPC support can implement a zhi plugin. See the language-specific guides:

Scaffolding a New Plugin

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.

Built-in Provider Reference

Publishing Plugins

Once your plugin is built and tested, you can share it via OCI registries.

  1. 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
    
  2. Build binaries for your target platforms:

    make cross-compile  # if using the scaffolded Makefile
    
  3. 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.

Further Reading