# How Plugins Extend OfficeCLI to Support Additional Document Formats

> Discover how OfficeCLI plugins extend support for diverse document formats. Learn how this lightweight system enables unlimited file type compatibility through external executables and JSON manifests.

- Repository: [OfficeAI/OfficeCLI](https://github.com/iofficeai/OfficeCLI)
- Tags: how-to-guide
- Published: 2026-07-13

---

**OfficeCLI delegates all non-native document format support to side-car plugin executables that communicate via JSON manifests and stream commands through stdin/stdout, allowing the core binary to remain lightweight while supporting unlimited file types.**

The **OfficeCLI** project (`iOfficeAI/OfficeCLI`) ships with native support only for `.docx`, `.xlsx`, and `.pptx`. Every other format—whether converting legacy `.doc` files or handling modern alternatives like `.hwpx`—is processed through an external plugin system defined in [`plugins/plugin-protocol.md`](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md). These plugins are standalone executables discovered at runtime based on their declared capabilities.

## Plugin Kinds and Responsibilities

The OfficeCLI plugin protocol defines three distinct roles (kinds) that determine how an extension interacts with the core binary.

### Dump-Reader Plugins

A **dump-reader** converts foreign formats into one of the three native Office formats. When invoked as `<plugin> dump <source>`, it reads the source file and streams **JSONL** commands (one per line) to stdout, instructing OfficeCLI how to construct the equivalent native document. This kind is short-lived and one-shot, exiting after dumping the conversion commands.

### Exporter Plugins

An **exporter** translates native Office files into foreign target formats (e.g., PDF, EPUB). Invoked as `<plugin> export <source> --out <target>`, it receives read-only access to the native source and writes the final output directly. Like dump-readers, exporters are short-lived processes that terminate after completing the conversion.

### Format-Handler Plugins

A **format-handler** provides first-class, end-to-end support for a non-native format through a persistent session. When OfficeCLI opens a file like `.hwpx`, it launches the handler and maintains a bidirectional JSON connection over stdin/stdout. The handler receives commands (`add`, `set`, `save`, `close`) and manages the file format's full lifecycle, enabling editing capabilities beyond simple conversion.

## Plugin Discovery and Loading

OfficeCLI locates plugins using a fixed priority search defined in the protocol specification. When the core binary needs to handle a file extension, it searches for a matching `(kind, extension)` pair in this order:

1. **Environment variable** `OFFICECLI_PLUGIN_<KIND>_<EXT>` pointing to an absolute executable path.
2. **User directory** at `~/.officecli/plugins/<kind>/<ext>/plugin(.exe)`.
3. **Bundled directory** adjacent to the main binary at `<dir>/plugins/<kind>/<ext>/plugin(.exe)`.
4. **PATH lookup** for executables named `officecli-<kind>-<ext>` or `officecli-<ext>`.

The first match is cached for the process lifetime and used for all subsequent operations involving that extension.

## The Plugin Manifest and Runtime Contract

Every plugin must respond to the `<plugin> --info` command with a JSON manifest that declares its capabilities. OfficeCLI parses this output to determine compatibility before invoking the plugin for document operations.

A minimal manifest includes:

```json
{
  "name": "officecli-doc",
  "version": "1.0.0",
  "protocol": 1,
  "kinds": ["dump-reader"],
  "extensions": [".doc"],
  "target": "docx",
  "runtime": "dotnet",
  "idle_timeout_seconds": { "default": 60, "verbs": { "dump": 30 } }
}

```

Key fields include:

- **`kinds`**: Array containing one or more supported roles (`dump-reader`, `exporter`, `format-handler`).
- **`extensions`**: File extensions (with leading dot) the plugin handles.
- **`target`**: For dump-readers, the native output format (`docx`, `xlsx`, or `pptx`).
- **`idle_timeout_seconds`**: Watchdog timeout configuration per verb, after which OfficeCLI terminates unresponsive plugins.
- **`runtime`**: Human-readable implementation tag (e.g., `dotnet`, `go`) for debugging purposes.

## Invocation Flows by Plugin Type

Each plugin kind follows a distinct invocation pattern and IPC contract.

### Dump-Reader Invocation (JSONL Streaming)

When converting a legacy file, OfficeCLI executes `<plugin> dump <source>`. The plugin streams one JSON command per line to stdout, using operations like `add`, `set`, or `batch` to describe the document structure. OfficeCLI instantiates a blank native document, replays these commands, and saves the result.

### Exporter Invocation (One-Shot Conversion)

For export operations, OfficeCLI calls `<plugin> export <source> --out <target>`. The plugin reads the native source file read-only and writes the converted output to the specified target path. The plugin exits with code `0` on success or code `3` if the feature is unsupported.

### Format-Handler Invocation (Session-Based)

Persistent handlers begin with an open handshake. OfficeCLI sends `{"msg_type":"open",...}` on the plugin's stdin and expects a capabilities/vocabulary response on stdout. Subsequent document commands are exchanged as individual JSON messages. The session continues until OfficeCLI sends a `close` command or the idle timeout expires.

## Extending OfficeCLI to New Formats

To add support for a new document format, developers create a standalone executable implementing one of the three kinds:

- **Legacy sources requiring conversion**: Implement a `dump-reader` that emits OfficeCLI commands to reconstruct the document in `.docx`, `.xlsx`, or `.pptx` format.
- **Export targets**: Implement an `exporter` that accepts native files and produces the target format.
- **First-class formats**: Implement a `format-handler` that supports bidirectional editing through the persistent session protocol.

Once compiled, place the executable in any discovery location (such as `~/.officecli/plugins/dump-reader/doc/plugin`) and ensure it returns a valid manifest via `--info`. OfficeCLI automatically routes file operations for registered extensions to the appropriate plugin without requiring changes to the core binary.

## Implementation Examples

The following examples demonstrate minimal implementations for each plugin kind, including the required manifest response and command handling.

### Minimal Dump-Reader in C#

```csharp
// Compile as `officecli-doc`
using System.Text.Json;

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

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

// Parse source and emit JSONL commands
var stdout = Console.Out;
stdout.WriteLine(JsonSerializer.Serialize(new {
    command = "add",
    parent = "/body",
    type = "paragraph",
    props = new { text = "Converted from .doc" }
}));
stdout.Flush();

return 0;

```

### Minimal Exporter in Go

```go
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",
            "version":    "0.1.0",
            "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]
        }
    }

    // Emit heartbeat every 20s (helps the idle watchdog)
    go func() {
        for {
            fmt.Fprintln(os.Stderr, `{"heartbeat":true}`)
            time.Sleep(20 * time.Second)
        }
    }()

    // Convert using external tool (read-only source)
    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) // feature unsupported
    }
    
    // Move output to target (omitted)
}

```

### Minimal Format-Handler in C#

```csharp
// Compile as `officecli-hwpx`
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;

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)!;
    var type = (string)msg["msg_type"]!;

    if (type == "open")
    {
        // Return capabilities and vocabulary
        var reply = new {
            protocol = 1,
            msg_type = "ok",
            result = new {
                capabilities = new {
                    commands = new[] { "add","set","save","close" },
                    features = Array.Empty<string>()
                },
                vocabulary = new {
                    addable_types = new[] { "paragraph","run","table","image" },
                    settable_props = new {},
                    path_segments = new string[] {}
                }
            }
        };
        stdout.WriteLine(JsonSerializer.Serialize(reply));
    }
    else if (type == "save")
    {
        // Persist changes to .hwpx file (omitted)
        stdout.WriteLine("""{"protocol":1,"msg_type":"ok","result":null}""");
    }
    else if (type == "close")
    {
        stdout.WriteLine("""{"protocol":1,"msg_type":"ok","result":null}""");
        return 0;
    }
    // Handle additional commands (add, set, etc.) here
}

```

## Summary

- **OfficeCLI plugins** are standalone executables that extend support beyond native `.docx`, `.xlsx`, and `.pptx` formats.
- Three plugin kinds exist: **dump-reader** (conversion to native), **exporter** (conversion from native), and **format-handler** (persistent bidirectional editing).
- Plugins are discovered via environment variables, user directories, bundled directories, or PATH lookup, and must provide a JSON manifest via the `--info` flag.
- Communication uses **JSONL** for dump-readers, command-line arguments for exporters, and bidirectional JSON messages over stdin/stdout for format-handlers.
- The system allows developers to add support for any document format without modifying the core OfficeCLI binary.

## Frequently Asked Questions

### What is the difference between a dump-reader and a format-handler plugin?

A **dump-reader** is a short-lived process that converts a foreign format into a native Office format (like `.docx`) and exits immediately after streaming conversion commands. A **format-handler** maintains a persistent session, enabling read/write operations and editing capabilities for the foreign format through continuous bidirectional communication with the OfficeCLI core.

### How does OfficeCLI discover plugins on my system?

OfficeCLI searches for plugins in a specific priority order: first checking the `OFFICECLI_PLUGIN_<KIND>_<EXT>` environment variable, then the user directory at `~/.officecli/plugins/<kind>/<ext>/`, followed by the bundled directory next to the binary, and finally performing a PATH lookup for executables named `officecli-<kind>-<ext>`. The first match found is cached and used for the duration of the process.

### Can I write an OfficeCLI plugin in any programming language?

Yes. Plugins communicate through standard streams (stdin/stdout) using JSON or JSONL, making them language-agnostic. The protocol only requires that your executable handle the `--info` flag for manifest requests and implement the appropriate IPC contract for its declared kind, whether written in C#, Go, Python, Rust, or any other language capable of producing standalone executables.

### What happens if a plugin becomes unresponsive during a document operation?

OfficeCLI monitors plugins using the `idle_timeout_seconds` specified in the manifest. If a plugin fails to produce output or send heartbeats (via stderr logging) within the configured timeout for the current verb, the core binary terminates the plugin process and returns an error to the user, preventing hung operations from blocking the CLI indefinitely.