ChatCLI supports a plugin system to extend its functionality. A plugin is an executable that follows a specific contract, allowing ChatCLI to discover, execute, and interact with it securely.
This lets you create custom commands (such as @kind, @deploy) that can orchestrate tools, interact with APIs, or perform any logic you can program.
For Users: Managing Plugins
List installed plugins
Shows all available plugin commands, including [builtin] plugins (such as @coder) and [remote] plugins (from connected servers).
Install a plugin
Install directly from a Git repository:
/plugin install https://github.com/usuario/meu-plugin-chatcli.git
ChatCLI will clone, compile (if itâs Go), and install the executable in ~/.chatcli/plugins/.
Security: Installing a plugin involves downloading and executing third-party code. Only install plugins from sources you trust.
View plugin details
/plugin show <plugin-name>
Uninstall a plugin
/plugin uninstall <plugin-name>
Reload plugins
ChatCLI automatically monitors ~/.chatcli/plugins/ and reloads when it detects changes (creation, removal, modification). A 500ms debounce prevents multiple reloads.
To force a manual reload:
Develop plugins iteratively: edit the code, recompile, send it to the plugins directory â ChatCLI will detect the change automatically.
For Developers: Creating a Plugin
The plugin contract
- Executable â The plugin must be an executable file (any language)
- Location â Placed in
~/.chatcli/plugins/
- Command name â The file name becomes the command. E.g., file
kind = command @kind
- Metadata (
--metadata) â Required. The executable must respond to this flag with JSON:
{
"name": "@meu-comando",
"description": "A brief description of what the plugin does.",
"usage": "@meu-comando <subcommand> [--flag value]",
"version": "1.0.0"
}
- Schema (
--schema) â Optional. Describes the accepted parameters:
{
"parameters": [
{
"name": "cluster-name",
"type": "string",
"required": true,
"description": "Kubernetes cluster name"
}
]
}
- Communication (stdout vs stderr):
- stdout â Only the final result (returned to ChatCLI/AI)
- stderr â Progress logs, status, and warnings (displayed in real time to the user)
Example: âHello Worldâ Plugin in Go
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"time"
)
type Metadata struct {
Name string `json:"name"`
Description string `json:"description"`
Usage string `json:"usage"`
Version string `json:"version"`
}
// logf sends progress messages to the user (via stderr).
func logf(format string, v ...interface{}) {
fmt.Fprintf(os.Stderr, format, v...)
}
func main() {
metadataFlag := flag.Bool("metadata", false, "Exibe os metadados do plugin")
schemaFlag := flag.Bool("schema", false, "Exibe o schema de parĂąmetros")
flag.Parse()
if *metadataFlag {
meta := Metadata{
Name: "@hello",
Description: "Plugin de exemplo que demonstra o fluxo stdout/stderr.",
Usage: "@hello [seu-nome]",
Version: "1.0.0",
}
jsonMeta, _ := json.Marshal(meta)
fmt.Println(string(jsonMeta))
return
}
if *schemaFlag {
schema := map[string]interface{}{
"parameters": []map[string]interface{}{
{
"name": "nome",
"type": "string",
"required": false,
"description": "Nome da pessoa a ser cumprimentada",
"default": "Mundo",
},
},
}
jsonSchema, _ := json.Marshal(schema)
fmt.Println(string(jsonSchema))
return
}
// LĂłgica principal
logf("Plugin 'hello' iniciado!\n")
time.Sleep(2 * time.Second)
logf(" - Realizando uma tarefa demorada...\n")
time.Sleep(2 * time.Second)
name := "Mundo"
if len(flag.Args()) > 0 {
name = flag.Args()[0]
}
logf("Tarefa concluĂda!\n")
// Resultado final vai para stdout
fmt.Printf("OlĂĄ, %s! A hora agora Ă© %s.", name, time.Now().Format(time.RFC1123))
}
Compilation and installation
# 1. Compile
go build -o hello ./hello/main.go
# 2. Make it executable
chmod +x hello
# 3. Create the plugins directory (if it doesn't exist)
mkdir -p ~/.chatcli/plugins/
# 4. Move the executable
mv hello ~/.chatcli/plugins/
# 5. Use in ChatCLI
/agent Hello, my name is John
Capability Interfaces (opt-in)
Plugins can expose capabilities through optional Go interfaces. Legacy plugins that donât implement them keep working unchanged â every interface is fail-closed (conservative default). When implemented, the plugin participates in orchestrator optimizations:
| Interface | Method | Purpose |
|---|
ReadOnlyAware | IsReadOnly(args []string) bool | Enables the capability security gate (auto-allow) and participation in parallel batches |
ConcurrencySafeAware | IsConcurrencySafe(args []string) bool | Signals that multiple calls can run in parallel |
DescriberWithInput | DescribeCall(args []string) string | Dynamic spinner label (âReading: main.goâ instead of âRUNNING: @readâ) |
JSONSchemaAware | JSONSchema() string | draft-2020-12 schema validated before dispatch â invalid input becomes ToolResult{IsError:true, ErrorCode:"InvalidArgs"} |
TruncationAware | MaxResultChars() int | Per-tool output cap (versus the 30 000 global default) |
Prompter | Prompt(opts PromptOpts) (string, error) | Contextual system-prompt slice |
StreamingInputAware | UpdateStreamingInput(field, value string) | Partial-input events as the LLM streams args |
StructuredExecutor | ExecuteStructured(...) (StructuredResult, error) | Typed return in place of the legacy (string, error) |
Example â a plugin that wants auto-allow + parallelization + custom label:
func (p *MyReadOnlyPlugin) IsReadOnly(_ []string) bool { return true }
func (p *MyReadOnlyPlugin) IsConcurrencySafe(_ []string) bool { return true }
func (p *MyReadOnlyPlugin) DescribeCall(args []string) string {
return i18n.T("plugins.myplugin.describe", extractTarget(args))
}
func (p *MyReadOnlyPlugin) JSONSchema() string {
return `{"type":"object","properties":{"file":{"type":"string"}},"required":["file"]}`
}
The four atomic plugins (@read, @search, @tree, @todo) â see Atomic Tools â use every relevant capability as a reference implementation.
Signature Verification
Starting with this version, plugins require Ed25519 digital signatures by default. This ensures that only plugins from trusted sources are loaded and executed.
How It Works
Each plugin must have a corresponding .sig file in the same directory:
~/.chatcli/plugins/
my-plugin # plugin executable
my-plugin.sig # Ed25519 signature of the executable
ChatCLI verifies the signature against registered public keys before loading the plugin. If verification fails, the plugin is rejected.
Managing Trusted Keys
Ed25519 public keys are stored in the ~/.chatcli/trusted-keys/ directory:
Generate a key pair
# Generate private and public keys
chatcli plugin keygen --name my-org
# Creates: ~/.chatcli/trusted-keys/my-org.pub
# Creates: ~/.chatcli/signing-keys/my-org.key (private - protect it!)
Sign a plugin
chatcli plugin sign --key ~/.chatcli/signing-keys/my-org.key my-plugin
# Creates: my-plugin.sig
Distribute the public key
Share the .pub file with users who should trust your plugins. They should place it in ~/.chatcli/trusted-keys/.
File Permissions
The plugins directory uses 0o700 permissions (owner-only read, write, and execute). ChatCLI checks permissions on startup and warns if they are more permissive.
Development Mode
For local development, you can disable signature verification:
export CHATCLI_ALLOW_UNSIGNED_PLUGINS=true
chatcli
Never enable CHATCLI_ALLOW_UNSIGNED_PLUGINS in production. Unsigned plugins can execute arbitrary code with the ChatCLI process permissions.
Remote Plugins
When connecting to a server via chatcli connect, server plugins are discovered automatically:
- They appear in
/plugin list with the [remote] tag
- They are executed on the server (not downloaded locally by default)
- Local and remote plugins coexist without conflict