# Protocol Used by Linux cowork-vm-service: Unix Socket & JSON Framing Guide

> Discover the Linux cowork-vm-service protocol. Learn how it uses Unix sockets and JSON framing for efficient inter-process communication on Linux systems. Understand this key aspect of the claude-desktop-debian repository.

- Repository: [Aaddrick/claude-desktop-debian](https://github.com/aaddrick/claude-desktop-debian)
- Tags: deep-dive
- Published: 2026-04-19

---

**The Linux `cowork-vm-service` daemon communicates via a Unix-domain socket using a length-prefixed JSON protocol, mirroring the Windows named-pipe implementation but adapting to Linux filesystem conventions.**

The `cowork-vm-service` is a core component of the Claude Desktop Linux port (`aaddrick/claude-desktop-debian`), enabling the Electron frontend to spawn and manage isolated VMs. Understanding the protocol used by Linux `cowork-vm-service` is essential for debugging connection issues or building alternative clients.

## Transport Layer: Unix-Domain Socket Location

Unlike the Windows version that uses named pipes, the Linux implementation binds to a filesystem socket.

### Socket Path Configuration

The daemon creates its listening socket at:

```bash
"$XDG_RUNTIME_DIR/cowork-vm-service.sock"

```

If the `XDG_RUNTIME_DIR` environment variable is unset, the service falls back to:

```bash
"/tmp/cowork-vm-service.sock"

```

*Source: lines 20-22 of [`scripts/cowork-vm-service.js`](https://github.com/aaddrick/claude-desktop-debian/blob/main/scripts/cowork-vm-service.js)*

## Message Framing Protocol

To handle JSON message boundaries over the stream-oriented socket, the protocol implements explicit length-prefix framing.

### 4-Byte Big-Endian Length Prefix

Every message is prefixed with a **4-byte unsigned integer** in big-endian format, indicating the length of the subsequent UTF-8 JSON payload.

The `writeMessage()` function implements this encoding:

```javascript
function writeMessage(sock, msg) {
  const json = JSON.stringify(msg);
  const buf = Buffer.from(json, 'utf8');
  const len = Buffer.alloc(4);
  len.writeUInt32BE(buf.length, 0);
  sock.write(Buffer.concat([len, buf]));
}

```

*Source: lines 107-112 of [`scripts/cowork-vm-service.js`](https://github.com/aaddrick/claude-desktop-debian/blob/main/scripts/cowork-vm-service.js)*

## JSON Message Specifications

The protocol supports three distinct message types: requests, responses, and asynchronous events.

### Request Format

Clients send method invocations with a unique identifier for correlation:

```json
{
  "method": "methodName",
  "params": { },
  "id": 1
}

```

The `id` field is echoed back in the response to allow the client to match asynchronous replies.

### Response Format

The daemon replies with either a success or error payload:

**Success:**

```json
{
  "success": true,
  "result": { },
  "id": 1
}

```

**Error:**

```json
{
  "success": false,
  "error": "Error message",
  "id": 1
}

```

### Event Notifications

Asynchronous events broadcast state changes without requiring a request:

```json
{
  "type": "stdout",
  "data": "..."
}

```

Supported event types include `stdout`, `stderr`, `exit`, `error`, `networkStatus`, and `apiReachability`.

*The complete protocol definition is documented in the comment block at the top of the daemon source file.*

## Server Lifecycle and Connection Management

The daemon implements robust lifecycle management to handle crashes and restarts.

### Startup Behavior

On initialization, the service:
1. Removes any stale socket file to prevent "address already in use" errors
2. Creates a `net.createServer` instance
3. Begins listening on the configured socket path

### Message Buffering

The server maintains a per-connection buffer to handle partial reads. The `parseMessage()` function extracts complete messages from the buffer only when the 4-byte length header and full payload are available.

### Error Handling

Malformed messages trigger error logging followed by a buffer reset, preventing corrupted data from desynchronizing the connection. The daemon continues processing subsequent valid messages.

## Implementation Examples

### Node.js Client Example

This complete client demonstrates connecting to the daemon and sending a ping request:

```javascript
const net = require('net');
const fs = require('fs');
const path = require('path');

// Build the same length‑prefixed writer the daemon uses
function writeMessage(sock, msg) {
  const json = JSON.stringify(msg);
  const buf = Buffer.from(json, 'utf8');
  const len = Buffer.alloc(4);
  len.writeUInt32BE(buf.length, 0);
  sock.write(Buffer.concat([len, buf]));
}

// Parser – identical to daemon's parseMessage
function parseMessage(buffer) {
  if (buffer.length < 4) return null;
  const len = buffer.readUInt32BE(0);
  if (buffer.length < 4 + len) return null;
  const json = buffer.subarray(4, 4 + len).toString('utf8');
  const remaining = Buffer.from(buffer.subarray(4 + len));
  return { message: JSON.parse(json), remaining };
}

// Connect to the daemon socket
const socketPath = (process.env.XDG_RUNTIME_DIR || '/tmp') + '/cowork-vm-service.sock';
const client = net.createConnection(socketPath, () => {
  console.log('connected');
  writeMessage(client, { method: 'ping', params: {}, id: 1 });
});

let buf = Buffer.alloc(0);
client.on('data', (data) => {
  buf = Buffer.concat([buf, data]);
  let parsed;
  while ((parsed = parseMessage(buf))) {
    buf = parsed.remaining;
    console.log('reply:', parsed.message);
    // → { success:true, result:{…}, id:1 }
  }
});
client.on('error', console.error);

```

### Bash Client with socat

For shell scripting, use `socat` to handle the socket connection and `xxd` for binary length encoding:

```bash
#!/usr/bin/env bash
SOCK="${XDG_RUNTIME_DIR:-/tmp}/cowork-vm-service.sock"

# Build a JSON request and prepend length (big‑endian)

REQ='{"method":"ping","params":{},"id":42}'
LEN=$(printf '%04x' $(printf '%s' "$REQ" | wc -c) | xxd -r -p)

# Send request and read raw reply

{
  printf "$LEN"
  printf '%s' "$REQ"
} | socat - UNIX-CONNECT:"$SOCK" | {
  # Read 4‑byte length

  read -r -n4 LEN_RAW
  LEN=$((0x$(echo -n "$LEN_RAW" | xxd -p)))
  # Read the JSON payload

  read -r -n"$LEN" JSON
  echo "Response: $JSON"
}

```

Both examples follow the same **4‑byte length prefix + JSON** framing that the daemon expects.

## Key Source Files

| File | Role | Link |
|------|------|------|
| [`scripts/cowork-vm-service.js`](https://github.com/aaddrick/claude-desktop-debian/blob/main/scripts/cowork-vm-service.js) | Main daemon implementation – defines transport, framing, request handling, and backend selection. | [scripts/cowork‑vm‑service.js](https://github.com/aaddrick/claude-desktop-debian/blob/main/scripts/cowork-vm-service.js) |
| [`docs/learnings/cowork-vm-daemon.md`](https://github.com/aaddrick/claude-desktop-debian/blob/main/docs/learnings/cowork-vm-daemon.md) | Narrative description of the daemon’s design, debugging steps, and socket lifecycle. | [docs/learnings/cowork‑vm‑daemon.md](https://github.com/aaddrick/claude-desktop-debian/blob/main/docs/learnings/cowork-vm-daemon.md) |
| [`docs/cowork-linux-handover.md`](https://github.com/aaddrick/claude-desktop-debian/blob/main/docs/cowork-linux-handover.md) | High‑level hand‑over doc that lists the service, transport, and status matrix. | [docs/cowork‑linux‑handover.md](https://github.com/aaddrick/claude-desktop-debian/blob/main/docs/cowork-linux-handover.md) |
| `tests/cowork-path-translation.bats` | Test suite exercising guest‑path translation – indirectly validates that the daemon can be started and the socket created. | [tests/cowork‑path‑translation.bats](https://github.com/aaddrick/claude-desktop-debian/blob/main/tests/cowork-path-translation.bats) |

These files together give a complete picture of the **protocol**, its **implementation**, and **how clients should communicate** with the Linux `cowork‑vm‑service`.

## Summary

- The **Linux `cowork-vm-service`** uses a **Unix-domain socket** located at `$XDG_RUNTIME_DIR/cowork-vm-service.sock` (falling back to `/tmp`).
- Communication follows a **length-prefixed JSON protocol**: each message is prefixed with a 4-byte big-endian integer indicating the UTF-8 JSON payload length.
- The protocol supports **three message types**: synchronous requests/responses (with numeric IDs for correlation) and asynchronous events (stdout, stderr, exit, etc.).
- The `writeMessage()` and `parseMessage()` functions in [`scripts/cowork-vm-service.js`](https://github.com/aaddrick/claude-desktop-debian/blob/main/scripts/cowork-vm-service.js) implement the framing logic at lines 107-112 and corresponding read logic.
- Clients can interact with the daemon using Node.js `net` modules or shell tools like `socat` by respecting the 4-byte length prefix.

## Frequently Asked Questions

### What transport protocol does Linux cowork-vm-service use?

The daemon uses a **Unix-domain socket** (AF_UNIX) rather than TCP/IP or named pipes. This provides secure, filesystem-based inter-process communication between the Claude Desktop Electron frontend and the VM management daemon, with the socket file created at `$XDG_RUNTIME_DIR/cowork-vm-service.sock` or `/tmp` as a fallback.

### How is message framing implemented in the protocol?

Messages use **4-byte big-endian length-prefix framing**. Each JSON payload is preceded by a 32-bit unsigned integer (in network byte order) indicating the byte length of the following UTF-8 data. This allows the `parseMessage()` function to correctly reconstruct boundaries from the stream-oriented socket, even when multiple messages arrive in a single read or are split across packets.

### What is the difference between the Linux and Windows implementations?

Both implementations share the **identical JSON message schema and length-prefix framing**, ensuring protocol compatibility. The only difference is the **transport layer**: Windows uses named pipes (`\\.\pipe\...`), while Linux uses **Unix-domain sockets** created on the filesystem. The `writeMessage()` and request/response handling logic remains consistent across platforms.

### Where is the socket file created by default?

By default, the socket is created at `"$XDG_RUNTIME_DIR/cowork-vm-service.sock"`, respecting the XDG Base Directory specification for runtime files. If the `XDG_RUNTIME_DIR` environment variable is not set—which commonly occurs in minimal environments or SSH sessions—the daemon falls back to creating the socket at `/tmp/cowork-vm-service.sock` to ensure operational continuity.