ReferencesDesktop PluginsRuntime & JSON-RPC

Runtime & JSON-RPC Protocol

Plugins run as sidecar processes that communicate with the desktop app host via JSON-RPC 2.0 over stdin/stdout. The host spawns your entrypoint, sends requests as newline-delimited JSON on stdin, and reads responses from stdout.

Protocol Basics

Every message follows the JSON-RPC 2.0 format:

Request (host → plugin):

{
  "jsonrpc": "2.0",
  "id": "request-123",
  "method": "plugin/health",
  "params": {}
}

Response (plugin → host):

{
  "jsonrpc": "2.0",
  "id": "request-123",
  "result": {
    "ok": true,
    "plugin_id": "your-org.your-plugin",
    "version": "0.1.0"
  }
}

Error response:

{
  "jsonrpc": "2.0",
  "id": "request-123",
  "error": {
    "message": "Something went wrong"
  }
}

Messages are delimited by newlines, one JSON object per line. Your process must read from stdin line-by-line and write responses to stdout with a trailing newline.

Required Methods

Every plugin must implement these two methods:

plugin/health

Health check called by the host to verify your plugin is running.

Request params: {}

Expected response:

{
  "ok": true,
  "plugin_id": "your-org.your-plugin",
  "version": "0.1.0"
}

plugin/shutdown

Graceful shutdown request. Your plugin should clean up resources and exit.

Request params: {}

Expected response:

{
  "ok": true
}

After responding, your process should exit with code 0.

Calling the Host

Your plugin can call host methods by sending a JSON-RPC request on stdout. The host will respond on stdin. This is the reverse direction: your plugin acts as the client.

def host_call(method, params=None):
    request_id = f"host-{os.getpid()}-{method}"
    send({
        "jsonrpc": "2.0",
        "id": request_id,
        "method": method,
        "params": params or {},
    })
    # Read responses until you find the matching ID
    while True:
        message = recv()
        if str(message.get("id")) == request_id:
            if "error" in message:
                raise RuntimeError(message["error"]["message"])
            return message.get("result", {})

Available Host Methods

MethodDescription
host/pingVerify bidirectional communication. Returns {"pong": true}.
host/request_approvalRequest user approval for a permission. Params: {"permission": "workspace.read"}. Returns {"approved": true/false}.

Agent provider plugins have additional host methods available for session management, CLI adapter integration, and approval decisions. See the Codex plugin example for details.

Notifications (Fire-and-Forget)

Your plugin can send notifications to the host as JSON-RPC messages without an id field. These are one-way and don’t expect a response.

{
  "jsonrpc": "2.0",
  "method": "codex/event",
  "params": {
    "kind": "notification",
    "method": "item/completed",
    "params": { "item": { "type": "agentMessage", "text": "Done!" } }
  }
}

The desktop app listens for these notifications to update the UI in real time (e.g., streaming agent responses).

Message Flow Diagram

Host (Desktop App)                    Plugin Process
       │                                     │
       │──── plugin/health ─────────────────►│
       │◄─── {ok: true} ───────────────────│
       │                                     │
       │──── your/custom/method ───────────►│
       │                                     │
       │     (plugin calls host)             │
       │◄─── host/request_approval ─────────│
       │──── {approved: true} ─────────────►│
       │                                     │
       │◄─── result ───────────────────────│
       │                                     │
       │──── plugin/shutdown ──────────────►│
       │◄─── {ok: true} ───────────────────│
       │                              exit(0)│

Threading Considerations

If your plugin handles long-running operations, you’ll need to handle concurrency. The host may send new requests while you’re processing a previous one.

For Python plugins, use threading to separate the stdin reader from request processing:

import threading
import queue
 
class HostBridge:
    def __init__(self):
        self._requests = queue.Queue()
        self._pending = {}
        self._reader = threading.Thread(target=self._read_loop, daemon=True)
        self._reader.start()
 
    def _read_loop(self):
        while True:
            line = sys.stdin.readline()
            if not line:
                return
            message = json.loads(line)
            if "method" in message:
                # Incoming request from host
                self._requests.put(message)
            else:
                # Response to our outgoing host call
                pending = self._pending.pop(str(message.get("id")), None)
                if pending:
                    pending.put(message)
 
    def next_request(self):
        return self._requests.get()

This pattern lets your plugin make host calls (e.g., host/request_approval) in the middle of processing a request without deadlocking.

Stderr

Anything your plugin writes to stderr is captured by the host as diagnostic output. Use stderr for logging. It won’t interfere with the JSON-RPC protocol on stdout.

import sys
 
def log(message):
    print(message, file=sys.stderr)
⚠️

Never write non-JSON content to stdout. The host expects one valid JSON object per line. Debug output should go to stderr.