How the OfficeCLI Plugin System Works: A Complete Guide to Extending Office CLI Functionality

The OfficeCLI plugin system is a language-agnostic extension framework that uses JSON-L communication over stdin/stdout to add support for non-native formats through three plugin kinds: dump-reader, exporter, and format-handler.

Office CLI provides native support for .docx, .xlsx, and .pptx files out of the box. For every other format—from legacy .doc files to proprietary formats—the tool relies on an external plugin system that discovers, launches, and communicates with independent executables. This article explains how the OfficeCLI plugin system works and how you can build your own extensions.

Understanding the Three Plugin Kinds

The OfficeCLI plugin architecture defines three distinct plugin types, each with different lifecycles and IPC patterns. The official specification lives in [plugins/plugin-protocol.md](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L25), starting at line 25.

Kind Purpose Lifecycle IPC Pattern
dump-reader Parses foreign source files (e.g., .doc) and streams OfficeCLI commands to build native documents Short-lived, one-shot Streams JSONL to stdout; no request/response
exporter Renders native OfficeCLI documents into foreign targets (e.g., PDF) Short-lived Plain CLI invocation; diagnostics on stderr
format-handler Owns non-native formats for entire sessions (e.g., .hwpx) Long-lived, session-duration Request/response JSON messages via stdin/stdout

Choosing the right kind depends on your workflow: dump-reader for import conversions, exporter for export rendering, and format-handler for full read/write session support.

How OfficeCLI Discovers Plugins

Plugin discovery follows a strict priority order implemented in [src/officecli/Core/Plugins/PluginRegistry.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/PluginRegistry.cs#L16) (line 16). When OfficeCLI needs a plugin for a specific (kind, ext) pair, it searches:

  1. Environment variable: OFFICECLI_PLUGIN_<KIND>_<EXT> (absolute path to executable)
  2. User directory: ~/.officecli/plugins/<kind>/<ext>/plugin(.exe)
  3. Bundled directory: <officecli-exe-dir>/plugins/<kind>/<ext>/plugin(.exe)
  4. PATH lookup: Executable named officecli-<kind>-<ext> or officecli-<ext> in $PATH

The first match wins. This hierarchy lets users override bundled plugins with custom versions or environment-specific configurations.

The Plugin Manifest: Required Metadata

Every OfficeCLI plugin must respond to <plugin> --info with a JSON manifest describing its capabilities. The schema is enforced in [src/officecli/Core/Plugins/PluginManifest.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/PluginManifest.cs#L10) (line 10).

Required manifest fields include:

  • name, version, protocol (must be 1 for current spec)
  • kinds (array of declared plugin kinds)
  • extensions (handled file extensions with leading dot)
  • Kind-specific fields: target for dump-reader, vocabulary for format-handler
  • runtime (diagnostic tag)
  • idle_timeout_seconds (watchdog budget for heartbeat monitoring)

This self-describing approach lets OfficeCLI validate compatibility before attempting invocation.

Plugin Invocation Patterns

Each plugin kind uses a distinct invocation pattern, implemented in dedicated invoker classes.

Dump-Reader Invocation

Launched as: <plugin> dump <source> [--media-dir <dir>]

Emits one JSONL command per line to stdout. Example command structure:

{ "command":"add", "parent":"/body", "type":"paragraph", "props":{ "text":"..." } }

The invoker code resides in [DumpReaderInvoker.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/DumpReaderInvoker.cs#L10) (line 10).

Exporter Invocation

Launched as: <plugin> export <source> --out <target> [--options <json>]

Reads the native file read-only, writes the foreign file, and exits. Diagnostics go to stderr. See [ExporterInvoker.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/ExporterInvoker.cs#L7) (line 7).

Format-Handler Invocation

Launched as: <plugin> open <file>

Engages in a request/response JSON envelope protocol (see §5.3 of the protocol specification). The first request is an open handshake returning capabilities and vocabulary snapshot. Long-lived session management is handled by [FormatHandlerSession.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/FormatHandlerSession.cs) and [FormatHandlerProxy.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/FormatHandlerProxy.cs).

Managing Plugins with the CLI

OfficeCLI includes a plugins subcommand for plugin management, implemented in [CommandBuilder.Plugins.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/CommandBuilder.Plugins.cs#L17) (line 17).


# List all discoverable plugins

officecli plugins list

# Display detailed manifest for a specific plugin

officecli plugins info officecli-doc

# Validate a dump-reader plugin against the protocol

officecli plugins lint officecli-doc

These commands help troubleshoot discovery issues and verify plugin compliance before deployment.

Building a Minimal Dump-Reader Plugin (C#)

Here's a complete minimal dump-reader implementation, adapted from lines 84-100 of the protocol specification:

using System;
using System.Text.Json;

// Build as: dotnet new console -n officecli-doc-minimal

class Program
{
    static int Main(string[] args)
    {
        // Manifest request
        if (args.Length > 0 && args[0] == "--info")
        {
            var manifest = 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 }
            };
            Console.WriteLine(JsonSerializer.Serialize(manifest));
            return 0;
        }

        // Dump invocation: args[0] == "dump", args[1] == source path
        string sourcePath = args[1];

        // Parse your .doc file here...
        
        // Emit JSONL commands to build the native document
        Console.WriteLine(JsonSerializer.Serialize(new
        {
            command = "add",
            parent = "/body",
            type = "paragraph",
            props = new { text = "Hello from .doc" }
        }));
        
        Console.Out.Flush();  // Critical: flush each line immediately
        return 0;
    }
}

Install to ~/.officecli/plugins/dump-reader/.doc/officecli-doc-minimal and run officecli plugins list to verify discovery.

Building a Minimal Exporter Plugin (Go)

This Go exporter example, from lines 21-48 of the protocol specification, converts to PDF using LibreOffice:

package main

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

func main() {
	// Manifest request
	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
	}

	// Parse: 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]
		}
	}

	// Heartbeat to prevent host timeout
	go func() {
		for {
			fmt.Fprintln(os.Stderr, `{"heartbeat":true}`)
			time.Sleep(20 * time.Second)
		}
	}()

	// Perform conversion
	cmd := exec.Command("soffice", "--headless", "--convert-to", 
		"pdf", "--outdir", "/tmp", source)
	if err := cmd.Run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(3) // Exit code 3 = unsupported feature
	}
	// Move /tmp output to target...
}

Building a Minimal Format-Handler Plugin (C#)

For session-based formats, implement the request/response protocol from lines 76-118 of the specification:

using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;

// args: open <file>
string filePath = args[1];

using var stdin = new StreamReader(
    Console.OpenStandardInput(), 
    new UTF8Encoding(false));
using 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)!;
    var msgType = (string)msg["msg_type"]!;
    
    switch (msgType)
    {
        case "open":
            // Handshake: return capabilities and vocabulary
            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 { /* format-specific schema */ }
                }
            }));
            break;
            
        case "save":
            // Persist changes
            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 0;
            
        // Handle get, set, and other commands...
    }
}

Using Your Plugin from OfficeCLI

Once installed in a discovery location, plugins activate automatically:


# Verify installation

officecli plugins list

# Dump-reader automatically handles .doc files

officecli dump legacy.doc

# Exporter renders to target format

officecli view report.docx pdf --out report.pdf

# Format-handler provides transparent session support

officecli edit document.hwpx

Key Source Files for Plugin Development

File Purpose Link
plugins/plugin-protocol.md Complete protocol specification view
PluginRegistry.cs Discovery order and caching view
PluginManifest.cs Manifest parsing and validation view
DumpReaderInvoker.cs Dump-reader execution view
ExporterInvoker.cs Exporter execution view
FormatHandlerSession.cs Long-lived session management view
FormatHandlerProxy.cs Command translation layer view
CommandBuilder.Plugins.cs CLI plugin commands view

Summary

  • The OfficeCLI plugin system extends native format support through three kinds: dump-reader (import), exporter (export), and format-handler (full session).
  • Discovery follows a 4-tier priority: environment variables, user directory, bundled directory, then PATH.
  • Every plugin must provide a manifest via --info with protocol version 1, declared kinds, and handled extensions.
  • IPC is language-agnostic: JSON-L over stdin/stdout with specific patterns per kind—streaming for dump-reader, plain CLI for exporter, request/response for format-handler.
  • The protocol requires line-flushed output, heartbeat messages for long-lived plugins, and idle timeout compliance.
  • CLI integration via officecli plugins list|info|lint streamlines development and debugging.

Frequently Asked Questions

What programming languages can I use for OfficeCLI plugins?

Any language that produces an executable and can read/write JSON to stdin/stdout. The protocol is intentionally language-agnostic—examples in the official repository include C#, Go, Python, and Rust implementations. The only requirements are proper JSON serialization, line-terminated messages, and respect for the heartbeat/timeout rules in §5.6 of the protocol.

Why does my format-handler plugin get killed after 30 seconds?

The idle_timeout_seconds field in your manifest controls this. According to [PluginManifest.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/PluginManifest.cs), you must either increase the default timeout or emit heartbeat messages to stderr: {"heartbeat":true}. The protocol allows per-verb timeout overrides in the verbs sub-object for long-running operations like export.

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

Run officecli plugins list to see what OfficeCLI detects. If missing, check: (1) file permissions are executable, (2) directory structure matches ~/.officecli/plugins/<kind>/<ext>/plugin, (3) the --info output returns valid JSON with protocol: 1, and (4) your environment variable names use uppercase with underscores: OFFICECLI_PLUGIN_DUMP_READER_DOC. Use officecli plugins lint <name> to validate protocol compliance.

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 →