Skip to main content
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

/plugin list
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:
/plugin 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

  1. Executable — The plugin must be an executable file (any language)
  2. Location — Placed in ~/.chatcli/plugins/
  3. Command name — The file name becomes the command. E.g., file kind = command @kind
  4. 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"
}
  1. Schema (--schema) — Optional. Describes the accepted parameters:
{
  "parameters": [
    {
      "name": "cluster-name",
      "type": "string",
      "required": true,
      "description": "Kubernetes cluster name"
    }
  ]
}
  1. 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:
InterfaceMethodPurpose
ReadOnlyAwareIsReadOnly(args []string) boolEnables the capability security gate (auto-allow) and participation in parallel batches
ConcurrencySafeAwareIsConcurrencySafe(args []string) boolSignals that multiple calls can run in parallel
DescriberWithInputDescribeCall(args []string) stringDynamic spinner label (“Reading: main.go” instead of “RUNNING: @read”)
JSONSchemaAwareJSONSchema() stringdraft-2020-12 schema validated before dispatch — invalid input becomes ToolResult{IsError:true, ErrorCode:"InvalidArgs"}
TruncationAwareMaxResultChars() intPer-tool output cap (versus the 30 000 global default)
PrompterPrompt(opts PromptOpts) (string, error)Contextual system-prompt slice
StreamingInputAwareUpdateStreamingInput(field, value string)Partial-input events as the LLM streams args
StructuredExecutorExecuteStructured(...) (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:
1

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!)
2

Sign a plugin

chatcli plugin sign --key ~/.chatcli/signing-keys/my-org.key my-plugin
# Creates: my-plugin.sig
3

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