# Extending OfficeCLI with a Plugin System: A Complete Developer's Guide

> Extend OfficeCLI with our powerful plugin system. Add custom handlers exporters and readers without core modification. Discover how to build your own plugins today.

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

---

**OfficeCLI supports a three-layer plugin architecture that enables developers to add custom format handlers, dump-readers, and exporters without modifying the core binary.** The system uses language-agnostic external executables that communicate via JSON-L over standard streams, discovered at runtime through a deterministic lookup order.

OfficeCLI is a lightweight command-line tool for manipulating **Word (`.docx`), Excel (`.xlsx`), and PowerPoint (`.pptx`)** documents natively. When you need to work with additional formats—whether legacy `.doc` files, proprietary formats like `.hwpx`, or custom export pipelines—the **OfficeCLI Plugin Protocol** (v1) provides a clean extension point that keeps the core binary single-file and secure.

## Understanding the Plugin Architecture

OfficeCLI's design separates native document operations from extension logic across four conceptual layers:

| Layer | Responsibility | Plugin Integration |
|-------|---------------|-------------------|
| **L1 – Read** | High-level format-agnostic views (`view`, `text`, `html`) | Not involved—works directly on native OOXML |
| **L2 – DOM** | Structured element operations (`get`, `set`, `add`, `remove`) | Not involved—core binary manipulates native documents |
| **L3 – Raw XML** | Direct XPath access for edge cases | Not involved |
| **Plugin Layer** | New format capabilities and offline exporters | Full integration via executable spawning and JSON-L IPC |

The host never loads plugin code into memory. Instead, it launches external executables and forwards **standard streams**, making the system safe for sandboxed environments and compatible with any programming language.

### Three Plugin Kinds

Every plugin declares one or more **kinds** that determine its lifecycle and communication pattern:

- **`dump-reader`** — Converts foreign formats into native OfficeCLI targets (e.g., `.doc` → `.docx`). Short-lived, one-shot execution. Writes **JSON-L batch commands** to `stdout`.

- **`exporter`** — Renders native documents to foreign formats (e.g., `.docx` → `.pdf`). Short-lived, one-shot execution with simple CLI invocation.

- **`format-handler`** — Completely owns a foreign format (e.g., `.hwpx`). Long-lived, session-wide with full request/response JSON-L over `stdin/stdout`.

*Source: [OfficeCLI Plugin Protocol – Motivation & Kinds](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#motivation)*

## Plugin Discovery Mechanism

When OfficeCLI needs a plugin for a `(kind, ext)` pair, it searches in this deterministic order—**first match wins**:

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

Discovery results are **cached for the process lifetime**, so newly installed plugins require a fresh command invocation.

*Source: [Plugin Discovery section](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#plugin-discovery)*

## Required Manifest and `--info` Protocol

Every plugin must respond to `--info` with a single JSON object describing its capabilities. Required fields include:

| Field | Description |
|-------|-------------|
| `name` | Plugin identifier |
| `version` | SemVer string |
| `protocol` | Protocol version (currently `1`) |
| `kinds` | Array of supported kinds |
| `extensions` | Array of handled file extensions |
| `target` | For `dump-reader`: the native format produced |

Optional fields advertise runtime environment, idle-timeout budgets, and **vocabulary** (for `format-handler` plugins).

```json
{
  "name": "officecli-pdf",
  "version": "0.1.0",
  "protocol": 1,
  "kinds": ["exporter"],
  "extensions": [".pdf"],
  "runtime": "dotnet",
  "idle_timeout_seconds": { "default": 60, "verbs": { "export": 120 } },
  "supports": ["from:docx", "from:xlsx", "from:pptx"]
}

```

*Source: [Manifest examples](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#example-manifests)*

### Idle-Timeout and Heartbeat System

Long-running plugins must periodically emit heartbeats to prevent termination:

- Emit `{"heartbeat":true}` on `stderr` to reset the watchdog
- Default timeouts from manifest; override globally with `OFFICECLI_PLUGIN_IDLE_TIMEOUT_SECONDS`
- Per-verb timeouts supported (e.g., longer timeout for `export` operations)

*Source: [Idle-timeout watchdog](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#idle-timeout-watchdog--heartbeat)*

### Error Handling Conventions

Plugins report structured errors via JSON on `stdout` (`"msg_type":"error"`). Exit codes convey failure categories:

| Exit Code | Meaning |
|-----------|---------|
| `5` | Protocol mismatch |
| `6` | Idle timeout exceeded |

The host translates these into user-friendly messages with corrective suggestions.

*Source: [Exit codes & error codes](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#exit-codes)*

## Building Plugins: Complete Examples

### Dump-Reader Plugin in C#

A minimal converter that ingests `.doc` files and emits native document construction commands:

```csharp
// officecli-doc-minimal.cs
using System.Text.Json;

if (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];

// ...parse the .doc file (library omitted)...

// Emit one JSONL command per line; flush each line.
var out = Console.Out;
out.WriteLine(JsonSerializer.Serialize(new {
    command = "add",
    parent = "/body",
    type = "paragraph",
    props = new { text = "Hello from .doc" }
}));
out.Flush();
return 0;

```

*Source: [Dump-reader example](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#example-manifests)*

### Exporter Plugin in Go

A PDF exporter with concurrent heartbeat generation:

```go
// officecli-pdf-min.go
package main

import (
    "encoding/json"
    "fmt"
    "os"
    "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": 30,
                "verbs":   map[string]int{"export": 120},
            },
        })
        return
    }

    // args: export <source> --out <target>
    src := 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]
        }
    }

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

    // Delegate to actual conversion library...
    // Exit 0 on success, 3 for unsupported features, etc.
}

```

*Source: [Exporter example](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#example-manifests)*

### Using a Format-Handler Plugin

Format-handlers enable full document lifecycle management for non-native formats:

```bash

# Register custom handler via environment variable

export OFFICECLI_PLUGIN_FORMAT_HANDLER_HWPX=$HOME/.officecli/plugins/format-handler/hwpx/plugin

# Open triggers handler spawning

officecli open report.hwpx

# Operations forwarded to handler process

officecli get report.hwpx /body/p[1] --json
officecli set report.hwpx /body/p[1] --prop text="새로운 텍스트"
officecli save report.hwpx    # Forces flush to handler

officecli close report.hwpx   # Terminates handler session

```

*Source: [Format-handler flow](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#5-3-format-handler)*

## Managing Plugins with CLI Commands

OfficeCLI provides built-in plugin introspection:

```bash

# List discovered plugins with their kinds

officecli plugins list

# Validate manifest and protocol compliance

officecli plugins lint officecli-pdf

```

*Source: [Plugins command reference](https://github.com/iOfficeAI/OfficeCLI/blob/main/README.md#command-plugins)*

## Key Reference Files

| File | Purpose | Link |
|------|---------|------|
| [`plugins/plugin-protocol.md`](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md) | Complete v1 protocol specification | [GitHub](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md) |
| [`README.md`](https://github.com/iOfficeAI/OfficeCLI/blob/main/README.md) (Plugins section) | User-facing `officecli plugins` documentation | [Plugins command](https://github.com/iOfficeAI/OfficeCLI/blob/main/README.md#command-plugins) |
| [`sdk/python/README.md`](https://github.com/iOfficeAI/OfficeCLI/blob/main/sdk/python/README.md) | Python SDK with automatic plugin management | [Python SDK](https://github.com/iOfficeAI/OfficeCLI/blob/main/sdk/python/README.md) |
| `examples/` | Real-world plugin invocation samples | [Examples](https://github.com/iOfficeAI/OfficeCLI/tree/main/examples) |

## Summary

Extending OfficeCLI with custom plugins follows a straightforward but precise protocol:

- **Three plugin kinds** (`dump-reader`, `exporter`, `format-handler`) cover conversion, export, and full format ownership scenarios
- **Language-agnostic design** through standard stream IPC keeps the core secure and lightweight
- **Four-tier discovery** (environment, user directory, bundled directory, PATH) provides flexible deployment
- **Mandatory `--info` manifest** with optional heartbeat and timeout declarations enables reliable long-running operations
- **Structured error reporting** with documented exit codes ensures graceful failure handling

## Frequently Asked Questions

### What programming languages can I use for OfficeCLI plugins?

Any language that produces executable binaries and handles standard I/O. The examples in the source repository include C#, Go, and Python implementations. Since the host only spawns external processes, runtime choice has no impact on compatibility.

### How do I debug a plugin that isn't being discovered?

Run `officecli plugins list` to see discovery results. Check that your plugin responds to `--info` with valid JSON, verify the executable path matches the discovery order priority, and ensure file permissions allow execution.

### Can a single plugin handle multiple file extensions?

Yes. The `extensions` field in the manifest accepts an array. A plugin can also declare multiple `kinds` if it implements diverse functionality (e.g., both reading and exporting a format).

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

The host sends SIGTERM, then SIGKILL if shutdown doesn't complete within a grace period. Plugins should emit `{"heartbeat":true}` on `stderr` every few seconds during long operations, or request extended timeouts in their manifest's `idle_timeout_seconds` configuration.