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

> Explore the OfficeCLI plugin system, a language-agnostic framework for extending OfficeCLI functionality. Learn how dump-reader, exporter, and format-handler plugins work with JSON-L communication.

- Repository: [OfficeAI/OfficeCLI](https://github.com/iofficeai/OfficeCLI)
- Tags: deep-dive
- Published: 2026-08-08

---

**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)](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)](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)](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:

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

```

The invoker code resides in [[`DumpReaderInvoker.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/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/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/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/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/CommandBuilder.Plugins.cs)](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/CommandBuilder.Plugins.cs#L17) (line 17).

```bash

# 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](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L84):

```csharp
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](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L21), converts to PDF using LibreOffice:

```go
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](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L76):

```csharp
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:

```bash

# 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`](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md) | Complete protocol specification | [view](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md) |
| [`PluginRegistry.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/PluginRegistry.cs) | Discovery order and caching | [view](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/PluginRegistry.cs) |
| [`PluginManifest.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/PluginManifest.cs) | Manifest parsing and validation | [view](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/PluginManifest.cs) |
| [`DumpReaderInvoker.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/DumpReaderInvoker.cs) | Dump-reader execution | [view](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/DumpReaderInvoker.cs) |
| [`ExporterInvoker.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/ExporterInvoker.cs) | Exporter execution | [view](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/ExporterInvoker.cs) |
| [`FormatHandlerSession.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/FormatHandlerSession.cs) | Long-lived session management | [view](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/FormatHandlerSession.cs) |
| [`FormatHandlerProxy.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/FormatHandlerProxy.cs) | Command translation layer | [view](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/FormatHandlerProxy.cs) |
| [`CommandBuilder.Plugins.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/CommandBuilder.Plugins.cs) | CLI plugin commands | [view](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/CommandBuilder.Plugins.cs) |

## 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/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.