How to Create Custom OfficeCLI Plugins: A Complete Developer Guide

OfficeCLI plugins are standalone executables that communicate via JSONL streams over stdout, discovered through environment variables or specific directory paths, and activated by implementing the --info flag to return a valid manifest describing capabilities and supported extensions.

OfficeCLI extends its core document processing capabilities for .docx, .xlsx, and .pptx files through a well-defined plugin protocol. Creating custom OfficeCLI plugins allows developers to add support for legacy formats, custom exporters, or specialized format handlers by implementing a simple JSONL-based IPC contract defined in the plugins/plugin-protocol.md file of the iOfficeAI/OfficeCLI repository.

Understanding the OfficeCLI Plugin Architecture

The plugin system in OfficeCLI follows a strict separation between the main binary and external executables. According to the source code in the plugins/plugin-protocol.md specification, the main binary parses commands, discovers plugins, and manages their lifecycle through JSONL batch streaming and idle timeout watchdogs.

Plugin Discovery Mechanism

The main binary searches for plugins in a specific hierarchy defined in the protocol specification. Discovery occurs in the following order:

  1. Environment variables
  2. User-local directory at ~/.officecli/plugins/<kind>/<ext>/plugin
  3. Bundled plugins/ directory within the installation
  4. System PATH using the naming convention officecli-<kind>-<ext>

Each plugin must reside in a directory structure matching its kind (such as dump-reader, exporter, or format-handler) and the file extension it handles.

Plugin Kinds and Lifecycle

OfficeCLI supports three distinct plugin kinds, each with a fixed lifecycle and I/O contract:

  • dump-reader: Imports legacy formats by streaming add and set commands as JSONL lines
  • exporter: Renders native documents to target formats like PDF or EPUB by writing output files
  • format-handler: Maintains long-lived stdin/stdout sessions for interactive document manipulation

Each kind expects specific sub-commands (dump, export, or open) and uses a strict message envelope requiring protocol and msg_type fields in every JSON object.

Steps to Create a Custom OfficeCLI Plugin

Choose Your Plugin Kind

Determine which interaction pattern fits your use case. Most custom development starts with either a dump-reader to import legacy formats or an exporter to render documents to new formats. The format-handler kind is reserved for scenarios requiring persistent document sessions.

Implement the --info Manifest

Every plugin must respond to the --info flag by emitting a single JSON object to stdout. This manifest declares the plugin name, version, protocol version, supported kinds, extensions, runtime, and idle timeout budget. The main binary validates this manifest against protocol version 1 during discovery.

Required manifest fields include:

  • name: Unique identifier for the plugin
  • version: Semantic version string
  • protocol: Integer protocol version (currently 1)
  • kinds: Array of supported kinds (e.g., ["dump-reader"])
  • extensions: Array of file extensions (e.g., [".doc"])
  • idle_timeout_seconds: Object with default timeout values

Handle the IPC Protocol

Plugins communicate via JSONL (JSON Lines) over stdout, with each line representing a single message. The protocol enforces UTF-8 encoding without BOM. For dump-reader plugins, the main binary sends a dump command with the source file path, and the plugin streams batch items back. For exporter plugins, the main binary sends an export command with source and target paths.

Manage the Idle Timeout Watchdog

The main binary terminates plugins that exceed their declared idle_timeout_seconds. Plugins performing long operations must emit heartbeat messages to stderr to remain alive. The heartbeat format is {"heartbeat":true}.

Code Examples for Custom OfficeCLI Plugins

Dump-Reader Example in C#

This minimal dump-reader converts legacy .doc files by emitting document structure commands:

using System.Text.Json;

if (args.Length > 0 && args[0] == "--info")
{
    Console.WriteLine(JsonSerializer.Serialize(new {
        name = "officecli-doc-minimal",
        version = "0.0.1",
        protocol = 1,
        kinds = new[] { "dump-reader" },
        extensions = new[] { ".doc" },
        target = "docx",
        runtime = "dotnet",
        idle_timeout_seconds = new { @default = 30 }
    }));
    return 0;
}

// args: dump <source-file>
string sourcePath = args[1];

// Emit a paragraph to the document body
var stdout = Console.Out;
stdout.WriteLine(JsonSerializer.Serialize(new {
    command = "add",
    parent = "/body",
    type = "paragraph",
    props = new { text = "Hello from .doc" }
}));
stdout.Flush();
return 0;

Exporter Example in Go

This exporter converts .docx to PDF using LibreOffice while maintaining heartbeat messages to prevent timeout:

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "os/exec"
    "time"
)

func main() {
    if len(os.Args) > 1 && os.Args[1] == "--info" {
        json.NewEncoder(os.Stdout).Encode(map[string]any{
            "name":       "officecli-pdf-min",
            "version":    "0.0.1",
            "protocol":   1,
            "kinds":      []string{"exporter"},
            "extensions": []string{".pdf"},
            "runtime":    "go",
            "idle_timeout_seconds": map[string]any{
                "default": 60,
                "verbs":   map[string]int{"export": 120},
            },
        })
        return
    }

    // args: export <source> --out <target>
    source := os.Args[2]
    var target string
    for i, a := range os.Args {
        if a == "--out" && i+1 < len(os.Args) {
            target = os.Args[i+1]
        }
    }

    // Send heartbeat every 20 seconds to stderr
    go func() {
        for {
            fmt.Fprintln(os.Stderr, `{"heartbeat":true}`)
            time.Sleep(20 * time.Second)
        }
    }()

    cmd := exec.Command("soffice", "--headless", "--convert-to", "pdf", "--outdir", "/tmp/officecli-pdf", source)
    if err := cmd.Run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(3) // Exit code 3 indicates unsupported feature
    }
    // Move the produced PDF to the requested target location...
}

Format-Handler Example in C#

This sketch demonstrates a long-lived session handling open, save, and close commands:

// args: open <file>
var stdin  = new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false));
var stdout = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false))
{
    NewLine = "\n",
    AutoFlush = true,
};

while (true)
{
    var line = stdin.ReadLine();
    if (line == null) break;
    var msg = JsonNode.Parse(line)!;
    switch ((string)msg["msg_type"]!)
    {
        case "open":
            // Load file and reply with capabilities
            stdout.WriteLine(JsonSerializer.Serialize(new {
                protocol = 1,
                msg_type = "ok",
                result = new {
                    capabilities = new {
                        commands = new[] { "get", "set", "save" },
                        features = Array.Empty<string>()
                    },
                    vocabulary = new {}
                }
            }));
            break;

        case "save":
            // Flush pending writes
            File.WriteAllBytes(filePath, currentBytes);
            stdout.WriteLine("""{"protocol":1,"msg_type":"ok","result":null}""");
            break;

        case "close":
            stdout.WriteLine("""{"protocol":1,"msg_type":"ok","result":null}""");
            return;
    }
}

Installing Your Custom OfficeCLI Plugin

Manual Installation

Place your compiled binary in the user plugins directory following the required structure:


# Make the binary executable

chmod +x officecli-doc-minimal

# Create the directory structure for kind and extension

mkdir -p ~/.officecli/plugins/dump-reader/.doc

# Move the binary to the plugin location

mv officecli-doc-minimal ~/.officecli/plugins/dump-reader/.doc/plugin

# Verify discovery

officecli plugins list

Registry Installation

For distribution, publish your plugin to the public registry at https://officecli.ai/plugins/registry.json. Users can then install via the built-in installer:

officecli plugins install officecli-doc-minimal

The installer validates the SHA-256 hash, downloads the binary, and places it in the appropriate ~/.officecli/plugins subdirectory.

Summary

  • OfficeCLI plugins are standalone executables that extend the CLI's support for document formats through a JSONL-based protocol.
  • Three plugin kinds exist: dump-reader for imports, exporter for rendering, and format-handler for interactive sessions.
  • Discovery follows a hierarchy from environment variables to ~/.officecli/plugins/<kind>/<ext>/plugin to PATH entries named officecli-<kind>-<ext>.
  • Required implementation includes the --info flag for manifest output and proper JSONL message envelopes with protocol and msg_type fields.
  • Heartbeat messages on stderr prevent termination by the idle watchdog during long operations.
  • Installation can be manual or via officecli plugins install from the registry.

Frequently Asked Questions

What programming languages can I use to create OfficeCLI plugins?

You can use any programming language that can write to stdout and read from stdin. The protocol only requires that your executable outputs valid JSON when called with --info and streams JSONL lines during operation. The repository includes examples in C# and Go, but Python, Rust, or Node.js work equally well as long as they handle the UTF-8 JSONL contract correctly.

How does the plugin discovery mechanism work?

The main binary searches for plugins in a specific order: first checking environment variables, then the user-local ~/.officecli/plugins directory, then the bundled plugins/ directory, and finally the system PATH. For PATH discovery, the binary looks for executables matching the pattern officecli-<kind>-<ext>. The discovery mechanism is documented in plugins/plugin-protocol.md under the plugin discovery section.

Why does my plugin need to send heartbeat messages?

The main binary implements an idle watchdog that terminates plugins exceeding their declared idle_timeout_seconds budget. If your plugin performs long-running operations like converting large documents, it must emit {"heartbeat":true} JSON objects to stderr at regular intervals to signal it is still active. This prevents the watchdog from killing the process during legitimate busy periods.

How do I debug a plugin that OfficeCLI doesn't recognize?

First, verify your plugin executable responds correctly to --info by running it manually and checking the JSON output matches the protocol version 1 schema. Ensure the binary is placed in the correct directory structure (~/.officecli/plugins/<kind>/<ext>/plugin) or follows the PATH naming convention (officecli-<kind>-<ext>). Run officecli plugins list to see discovered plugins, and check that file permissions allow execution.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →