# How the OfficeCLI Plugin System Extends Support for Additional File Formats

> Discover how the OfficeCLI plugin system supports new file formats using a sidecar architecture. Learn how independent executables integrate seamlessly via JSON manifests and stdin/stdout.

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

---

**OfficeCLI uses a sidecar plugin architecture where independent executables register via a JSON manifest and communicate through stdin/stdout to handle non-native formats like .doc, .pdf, or .hwpx.**

The OfficeCLI project's flexible plugin system allows the core binary to remain lightweight while supporting an unlimited range of document formats. By discovering and invoking external executables at runtime, OfficeCLI delegates format-specific work to specialized plugins without requiring changes to the main codebase.

## Plugin Architecture Overview

The OfficeCLI plugin protocol defines three distinct **plugin kinds**, each designed for different format integration patterns. All plugins are standalone executables that declare their capabilities through a standardized manifest returned by the `--info` command.

### The Three Plugin Kinds

| Kind | Purpose | Lifecycle | Communication Model |
|------|---------|-----------|---------------------|
| `dump-reader` | Converts foreign formats to native Office formats (.docx, .xlsx, .pptx) | Short-lived, one-shot | **JSONL** streamed to stdout; no input |
| `exporter` | Converts native files to foreign targets (PDF, EPUB, etc.) | Short-lived, one-shot | Reads native file read-only; writes foreign output |
| `format-handler` | Provides full read/write support for a non-native format | Long-lived session | Bidirectional JSON messages over stdin/stdout |

These definitions and their behavioral contracts are documented 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#L30-L44), specifically lines 30-44 for dump-readers, lines 67-78 for exporters, and lines 95-104 for format-handlers.

## Plugin Discovery and Loading

When OfficeCLI needs to process a file with a given extension, it searches for a matching plugin in a deterministic priority order. The discovery algorithm is implemented in the protocol specification at lines 35-47.

### Discovery Priority Order

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

The first match wins, and results are cached per process invocation to avoid repeated filesystem checks ([caching note](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L55-L57)).

## The Plugin Manifest Contract

Every plugin must respond to `<plugin> --info` with a JSON object defining its capabilities. This manifest enables OfficeCLI to route commands correctly and enforce resource limits.

### Required Manifest Fields

```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 } }
}

```

| Field | Description |
|-------|-------------|
| `kinds` | Supported plugin role(s): `dump-reader`, `exporter`, `format-handler` |
| `extensions` | File extensions handled, with leading dot |
| `target` | For `dump-reader`, the native format produced (`docx`/`xlsx`/`pptx`) |
| `idle_timeout_seconds` | Per-verb watchdog timeout; process killed if exceeded |
| `runtime` | Human-readable implementation tag (informational only) |

The complete manifest schema is defined in the protocol document at [lines 59-71](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L59-L71).

## Runtime Invocation Patterns

Each plugin kind uses a distinct invocation pattern and IPC contract defined in the OfficeCLI plugin protocol.

### Dump-Reader Invocation

Dump-readers are invoked as:

```bash
<plugin> dump <source>

```

They stream **one JSON command per line** to stdout, using commands like `add`, `set`, and `batch` to build a native Office document. OfficeCLI creates a blank native file, replays the command stream, and writes the result adjacent to the source file.

The JSONL schema is documented at [lines 27-33](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L27-L33).

### Exporter Invocation

Exporters are called as:

```bash
<plugin> export <source> --out <target>

```

They read the native source file read-only and write the foreign output to the specified path. The export command syntax is specified at [lines 37-43](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L37-L43).

### Format-Handler Session Protocol

Format-handlers establish a persistent session through an **open handshake**:

1. OfficeCLI sends `{"msg_type":"open", ...}` on the plugin's stdin
2. Plugin replies with capabilities and vocabulary on stdout
3. Subsequent commands (`add`, `set`, `save`, `close`) exchange bidirectionally

The handshake details are at [lines 61-68](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L61-L68).

## Example Plugin Implementations

### Minimal Dump-Reader in C#

This `officecli-doc` plugin converts `.doc` files to `.docx` by emitting native construction commands:

```csharp
// Compile as .NET console app: 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 .doc file with preferred library...

// Stream JSONL commands to stdout (flush per line)
Console.Out.WriteLine(JsonSerializer.Serialize(new {
    command = "add",
    parent = "/body",
    type = "paragraph",
    props = new { text = "Converted from .doc" }
}));
Console.Out.Flush();

return 0;

```

Key protocol references: manifest at [lines 57-66](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L55-70), dump logic at lines 68-76.

### Minimal Exporter in Go

This `officecli-pdf` plugin wraps LibreOffice for PDF generation:

```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 to satisfy idle watchdog
    go func() {
        for {
            fmt.Fprintln(os.Stderr, `{"heartbeat":true}`)
            time.Sleep(20 * time.Second)
        }
    }()

    // LibreOffice conversion (read-only source access)
    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 exit code
    }
    // Move result to target path (omitted)
}

```

Key protocol references: manifest at [lines 73-84](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L73-84), export format at lines 37-44, heartbeat at lines 75-83.

### Minimal Format-Handler in C#

This `officecli-hwpx` plugin provides full session-based support for the `.hwpx` format:

```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 + vocabulary per protocol
        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 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;
    }
    // Add, set, and other commands handled here
}

```

Key protocol references: handshake at [lines 61-71](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L61-71), command handling at lines 73-87, save/close at lines 84-90.

## Installation and Distribution

Plugins can be deployed through multiple channels:

- **Manual installation**: Place executable in any discovery location
- **Bundled distribution**: Ship with OfficeCLI releases in `<binary-dir>/plugins/`
- **Registry install**: Use `officecli plugins install <name>` to fetch from the public registry at `https://officecli.ai/plugins/registry.json`

The installation section of the protocol document covers registry conventions and version resolution.

## Choosing the Right Plugin Kind

When extending OfficeCLI to support a new file format, select the appropriate plugin kind based on your integration requirements:

- **Use `dump-reader`** when converting legacy or niche source formats to native Office formats for one-time import
- **Use `exporter`** when generating static output formats (PDF, EPUB, images) from native documents
- **Use `format-handler`** when the format requires full read/write/editing capabilities as a first-class citizen

## Summary

- OfficeCLI's core binary only natively supports `.docx`, `.xlsx`, and `.pptx`; all other formats require plugins
- Plugins are independent executables discovered via environment variables, user directories, bundled directories, or PATH
- Three plugin kinds—`dump-reader`, `exporter`, and `format-handler`—cover conversion, export, and full session-based workflows
- All plugins expose capabilities through a JSON manifest returned by `--info`
- Communication uses JSONL (dump-readers) or bidirectional JSON (format-handlers) over stdin/stdout
- The protocol specification at [`plugins/plugin-protocol.md`](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md) defines all behavioral contracts and IPC schemas

## Frequently Asked Questions

### How does OfficeCLI find plugins for a specific file extension?

OfficeCLI searches in four locations in priority order: environment variables (`OFFICECLI_PLUGIN_<KIND>_<EXT>`), the user plugins directory (`~/.officecli/plugins/`), the bundled plugins directory next to the binary, and finally PATH for executables matching naming conventions. The first match is cached for the process lifetime.

### Can plugins be written in any programming language?

Yes. Plugins are standalone executables that communicate through stdin/stdout using JSON. The `runtime` field in the manifest is informational only—OfficeCLI does not inspect or validate the implementation language. The examples in this article show C# and Go, but Python, Rust, Node.js, or any language producing a native executable work equally well.

### What happens if a plugin exceeds its idle timeout?

OfficeCLI's main process monitors plugin output and heartbeat messages. If no activity occurs within the `idle_timeout_seconds` specified for the current verb, the plugin process is terminated. Plugins should emit heartbeat objects to stderr (e.g., `{"heartbeat":true}`) during long-running operations to prevent watchdog kills.