Restish Plugin Quickstart
May 1, 2026 · View on GitHub
This guide is the shortest path from "I want to extend Restish" to a working plugin binary.
If you want design rationale, read the records in
docs/design/. If you want the smallest practical path,
start here.
Choose A Plugin Type
Use the smallest plugin shape that fits your job:
- Hook plugin: one request in, one reply out. Best for auth, request/response middleware, custom spec loaders, and output formatters.
- Command plugin: a long-lived top-level command such as
restish mcp serve ...orrestish bulk ...that can ask the host to make HTTP requests. - TLS signer plugin: advanced mTLS use cases where the private key must stay outside the Restish process.
Start with a hook plugin unless you know you need a custom command lifecycle.
The Wire Protocol
All messages between Restish and plugins — including manifest responses, command declarations, hook inputs/outputs, and command-plugin runtime messages — are plain CBOR data items written directly to stdin/stdout. CBOR is self-delimiting, so no length prefix or other framing is needed. Any language with a CBOR library can implement a plugin.
The startup flags (--rsh-plugin-manifest, --rsh-plugin-commands) use the
same format as runtime messages: write one CBOR map to stdout and exit. Command
discovery responses include protocol_version; Go plugins get that field from
plugin.WriteCommands.
The Public Helper Package
Plugin authors in Go should build against the public
plugin package.
The helpers you will use most often are:
plugin.WriteMessageandplugin.ReadMessagefor CBOR messages (one-shot);plugin.NewDecoder+(*Decoder).ReadMessagefor streaming reads (command and TLS-signer plugins that receive multiple messages on the same stdin)plugin.WriteManifestandplugin.WriteCommandsfor startup responsesplugin.HandleStartupFlagsfor--rsh-plugin-manifestand--rsh-plugin-commandsplugin.Runfor simple command pluginsplugin.CommandClientfor command plugins that delegate HTTP and terminal output back to Restish
Smallest Formatter Plugin
Formatter plugins are a good first plugin because they only need to read formatter messages from stdin and write final bytes to stdout.
package main
import (
"fmt"
"os"
"github.com/rest-sh/restish/v2/plugin"
)
func main() {
manifest := plugin.Manifest{
Name: "hello-format",
Version: "0.1.0",
Description: "Example formatter plugin",
RestishAPIVersion: 2,
Hooks: []string{"formatter"},
FormatterNames: []string{"hello"},
}
if plugin.HandleStartupFlags(os.Stdout, manifest, nil) {
return
}
dec := plugin.NewDecoder(os.Stdin)
var req plugin.FormatterRequest
if err := dec.ReadMessage(&req); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for {
switch req.Event {
case "start":
if req.Response.Body != nil {
fmt.Fprintf(os.Stdout, "hello: %#v\n", req.Response.Body)
}
case "item":
fmt.Fprintf(os.Stdout, "hello: %#v\n", req.Response.Body)
case "end":
return
}
if err := dec.ReadMessage(&req); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
}
Build it as restish-hello-format, install it into the Restish plugin
directory, then run:
restish get https://httpbin.org/json -o hello
The same formatter message type is used for ordinary responses, pagination,
and event streams. The sequence is always:
event: "start"- zero or more
event: "item" event: "end"
For a normal non-streaming response, Restish usually includes the full body on
the start message. The CSV plugin in cmd/restish-csv/ is the reference
implementation for a stateful formatter.
Smallest Command Plugin
Command plugins contribute top-level commands and can delegate authenticated HTTP back to the host instead of building their own client stack.
package main
import "github.com/rest-sh/restish/v2/plugin"
func main() {
manifest := plugin.Manifest{
Name: "hello-cmd",
Version: "0.1.0",
Description: "Example command plugin",
RestishAPIVersion: 2,
Hooks: []string{"command"},
}
commands := []plugin.CommandDecl{
{Name: "hello", Short: "Print a greeting"},
}
plugin.Run(manifest, commands, func(command string, args []string, c *plugin.CommandClient) error {
return c.WriteStdout([]byte("hello from plugin\n"))
})
}
That binary will show up as:
restish hello
To make authenticated delegated requests from a command plugin:
resp, err := c.Do(&plugin.HTTPRequestMsg{
Method: "GET",
URI: "myapi/users",
})
Restish handles auth, TLS, retries, cache settings, and response normalization before the reply comes back to your plugin.
Prefer CommandClient helpers over hand-written protocol messages for host
capabilities that wait for a reply:
apis, err := c.ListAPIs()
profiles, err := c.ListProfiles("myapi")
cfg, err := c.ConfigRead("myapi", "default", "hello-cmd")
answer, err := c.Prompt("Label", false)
ok, err := c.Confirm("Continue?")
err = c.Response(200, nil, map[string]any{"configured": cfg.PluginConfig != nil})
The context-aware variants (ListAPIsContext, ConfigReadContext,
PromptContext, and friends) let plugins bound waits when the host is busy or
the user does not answer.
Local Development Loop
- Build the plugin binary with a
restish-prefix. - Install it with
restish plugin install ./restish-name. - Check startup output directly:
./restish-name --rsh-plugin-manifest
./restish-name --rsh-plugin-commands
- Use
restish plugin listto confirm discovery. - Use
restish plugin debug <name> ...when you need to inspect CBOR traffic. Expected traces start with startup discovery (manifest, andcommandsfor command plugins), then show hook messages such as auth, request middleware, response middleware, formatter, loader, or TLS signer sign/shutdown frames depending on the plugin's manifest.
Good Reference Implementations
These are the best examples in the repo:
cmd/restish-csv/main.gofor a small formatter hook plugincmd/restish-mcp/main.gofor a real command plugininternal/cli/testdata/testplugin/main.gofor tiny command, hook, and TLS signer test fixturesdocs/design/019-hook-plugins.mdfor hook payload shapesdocs/design/020-command-plugins.mdfor command-plugin protocol details
Plugin Ideas That Should Stay Out Of Core For Now
Some useful features fit better as plugins or wrappers than as built-in Restish behavior:
- Page/count pagination strategies for APIs that do not expose standard links. A response-middleware plugin can inspect the first response and ask Restish to follow calculated page URLs while the host still owns auth, retries, TLS, and output.
- Swagger/OpenAPI 2.0 loading. A loader plugin can convert Swagger 2.0 into OpenAPI 3.x and return that document to Restish, keeping generated command creation on the canonical OpenAPI path.
- Rate-limit experiments or light load-test workflows. A command plugin can own pacing, concurrency, and reporting while delegating each request to the host.
Use these patterns when the behavior is valuable but provider-specific, still experimental, or too workflow-shaped for a generic request flag.
Common Pitfalls
- Plugin executables must be named
restish-<name>. - All messages — startup responses and runtime messages alike — are plain CBOR
data items. Use
plugin.WriteMessagefor all writes. For reads, useplugin.ReadMessagefor one-shot hook plugin reads; for command and TLS-signer plugins that loop over messages, create aplugin.NewDecoderonce and callReadMessageon it throughout. - Formatter plugins write final bytes to stdout directly; they do not send a CBOR reply envelope.
- Formatter plugins receive a sequence of
formattermessages withstart/item/endevents, not a single one-shot request. - If you start a subprocess or long-lived goroutine inside a plugin, make sure it exits cleanly when stdin closes.