Skip to Content
Protocol

The Everyport protocol

everyport reports the servers on a machine as a stream of JSON events and takes JSON requests to act on them. Anything that can run a command or open a URL can use it: an editor extension, a status bar, a dashboard, or a workspace app.

There are two transports, with the same JSON:

  • everyport stdio: events on stdout, requests on stdin. The desktop app runs it as a sidecar, and runs it on remote machines through ssh, docker exec -i, kubectl exec -i -- or wsl.
  • everyport serve: HTTP on loopback, with events as server-sent events.

The types are defined in crates/everyport/src/protocol.rs, and TypeScript types are generated from it into @everyport/protocol.

Conventions

  • Timestamps are Unix milliseconds.
  • Memory is bytes.
  • CPU values are floats, in percent of one core, so a busy server on an 8-core machine can report up to 800.0. system.cpu_percent is whole-machine CPU, from 0 to 100.
  • Optional fields are always present, as null when empty. The tables below mark them “or null”.
  • “Clean up” is the list of servers everyport suggests stopping: those whose worktree was deleted, or that are idle, long-running or leaking.

Framing

On stdio, every message is one line of JSON ending in \n: one event per line on stdout, and one request per line on stdin. Blank lines on stdin are ignored.

Over HTTP, events are server-sent events with one event per data: line, and a request is the body of a POST. See everyport serve.

Handshake

Every connection starts with hello, then a snapshot of the current state. After that, everyport sends:

  • a snapshot whenever anything changes (it scans every 2 s by default)
  • an alert when a server crosses a threshold
  • a result for each request

A client needs no reply to hello. After a request, its result comes first, then any snapshot it caused. everyport stdio exits with status 0 when stdin closes, after answering every request it already read.

In this transcript, → is a line from everyport and ← a line to it.

→ {"type":"hello","protocol":1,"everyport_version":"0.1.0","host":{"hostname":"devbox","os":"linux","arch":"x86_64","cores":8}} → {"type":"snapshot","taken_at":1790195040000,"system":{...},"servers":[...]} ← {"id":1,"method":"refresh"} → {"type":"result","id":1,"error":null} → {"type":"snapshot","taken_at":1790195041000,"system":{...},"servers":[...]}

Events

Every event has a type.

hello

The first event on every connection.

{ "type": "hello", "protocol": 1, "everyport_version": "0.1.0", "host": { "hostname": "devbox", "os": "linux", "arch": "x86_64", "cores": 8 } }

os is macos, linux or windows. arch is the Rust target arch, such as aarch64 or x86_64.

snapshot

The full state. It’s sent after hello, after every scan where something other than taken_at changed, and after every refresh. Replace your state with it; there are no diffs.

{ "type": "snapshot", "taken_at": 1790195040000, "system": { "memory_total": 17179869184, "memory_used": 12777527706, "memory_other_apps": 7516192768, "cpu_percent": 18.0 }, "servers": [ { "port": 3000, "pid": 48213, "root": { "pid": 48198, "started_at": 1790183520000 }, "process_name": "node", "addresses": ["127.0.0.1", "::1"], "cwd": "/Users/dev/conductor/workspaces/everyport/providence", "cwd_exists": true, "command": "npm run dev", "launch_dir": "/Users/dev/conductor/workspaces/everyport/providence", "started_at": 1790183520000, "project": { "name": "everyport", "root": "/Users/dev/conductor/workspaces/everyport/providence", "framework": "Next.js", "branch": "menubar-port-monitor", "worktree": "providence", "github": "greenfield-inc/everyport", "vercel": { "project_id": "prj_everyport", "preview_url": "https://everyport-git-menubar-port-monitor.vercel.app" } }, "workspace": { "kind": "conductor", "name": "providence", "open_url": null }, "agent": { "kind": "claude_code", "id": "68c8fda6-2f4e-4c1a-9a7b-1d2e3f4a5b6c", "title": "Tray count badge", "started_at": 1790183040000, "transcript_path": "/Users/dev/.claude/projects/providence/68c8fda6.jsonl", "directory": "/Users/dev/conductor/workspaces/everyport/providence", "resume_command": "claude --resume 68c8fda6-2f4e-4c1a-9a7b-1d2e3f4a5b6c" }, "processes": [ { "proc": { "pid": 48198, "started_at": 1790183520000 }, "name": "npm run dev", "depth": 0, "memory": 60817408, "cpu_percent": 0.1 }, { "proc": { "pid": 48213, "started_at": 1790183520900 }, "name": "next-server", "depth": 1, "memory": 1095761920, "cpu_percent": 9.8 } ], "memory": 1328545792, "cpu_percent": 12.0, "connections": 4, "history": [ { "at": 1790194440000, "memory": 1267015352, "cpu_percent": 3.0 }, { "at": 1790194470000, "memory": 1305507759, "cpu_percent": 4.0 } ], "last_active": 1790195000000, "protected": false, "status": "running", "clean_up": null } ], "other_ports": [ { "port": 5432, "addresses": ["0.0.0.0"], "owner": "root", "process_name": "docker-proxy" } ] }

system:

FieldMeaning
memory_total, memory_usedThe machine’s physical memory and how much is in use
memory_other_appsMemory used by everything that isn’t a listed server
cpu_percentWhole-machine CPU, 0 to 100

servers is sorted by port. Each server is a listening port and the process tree behind it:

FieldMeaning
portThe listening TCP port
pidThe process that owns the socket
rootThe topmost process of the server’s tree, such as npm run dev. When one command runs several servers, such as concurrently starting an API and Vite, each server’s tree starts just below where their trees meet, so stop and restart cover that server only. Pass it to stop and restart.
process_nameName of the process that owns the socket
addressesBound addresses, such as 127.0.0.1 and ::1
cwd, cwd_existsThe server’s folder (or null), and whether it still exists. It’s false once a worktree is deleted.
command, launch_dirThe root’s command line and the folder it started in, or null. restart runs command in launch_dir.
started_atWhen the root process started, or null
projectname (from package.json, the repo folder or the folder) is always set. For / and folders under a package manager or the OS, such as /opt/homebrew/var/postgresql@15, it’s the process name. root, framework, branch, worktree, github (owner/repo) and vercel are null when unknown.
workspaceThe Conductor workspace, Pane worktree or git worktree the server runs in, or null. kind is conductor, pane or git_worktree. open_url opens it in its app, such as pane://open?pane=<id>&panel=<id>, or is null.
agentThe Claude Code or Codex session that started the server, or null. kind is claude_code or codex. resume_command resumes it in directory.
processesThe whole tree, depth first and root first. depth is 0 for the root.
memory, cpu_percentSums over processes. A process that several servers hold, such as one listening on two ports, counts only on the lowest port, so the servers’ sum counts each process once.
connectionsOpen connections to the port
historySamples covering up to the last 10 minutes, oldest first. Don’t assume a fixed spacing.
last_activeLast time the server had connections or used CPU
protectedA process in its tree is on the protected list, such as postgres. Clean up never suggests it, and stop and restart need confirm_protected.
statusrunning, attention (over the memory threshold or leaking) or idle (idle for over an hour, or its folder is gone)
clean_upWhy Clean up suggests stopping it, or null

clean_up is one of:

{ "kind": "worktree_deleted" } { "kind": "idle", "seconds": 18000 } { "kind": "long_running", "seconds": 259200 } { "kind": "leaking", "bytes": 1191182336 }

other_ports lists the ports that processes of other users or the system hold, such as a Docker-published port or a system database. everyport can’t inspect those processes, so these ports have no tree and no actions. The list is sorted by port and covers the configured port range. A port in servers never appears here.

FieldMeaning
portThe listening TCP port
addressesBound addresses, such as 0.0.0.0 and ::
ownerThe user the process runs as, such as root, or null when the OS doesn’t say
process_nameName of the process that owns the socket, or null when the OS doesn’t say. Linux doesn’t tell a normal user which process holds another user’s socket.

On macOS and Linux, when everyport runs as root, every port is a server and other_ports is empty. On macOS, everyport reads other users’ ports from nettop at most every 10 seconds, so a new one can take that long to appear. macOS lists other users’ sockets only to its own tools.

alert

A server crossed a threshold. everyport sends it once, and again only after the server recovers and crosses it again. Clients decide whether and how to notify.

{ "type": "alert", "port": 6006, "kind": "leaking", "memory": 3017089024 }

kind is over_threshold (memory above alert_memory), leaking (grew by leak_growth over the history window), or clean_up (Clean up started to suggest stopping it and auto_kill is ask).

result

The answer to the request with the same id. error is null on success, or a message for the user.

{ "type": "result", "id": 7, "error": null } { "type": "result", "id": 8, "error": "pid 48198 has exited or now belongs to another process" }

A line that isn’t a valid request still gets a result with an error. Its id is the request’s id when one can be read, and 0 otherwise, so avoid using 0 as an id.

Requests

A request has a numeric id that you choose, a method, and params for the methods that take them. Every field of params is required, except in configure, which takes only the fields to change. Requests run one at a time, in order.

Use ids from 1 up to 2^53, so JavaScript clients keep them exact. everyport only echoes them back, so they need to be unique only among your requests in flight.

refresh

Scan now and send a fresh snapshot, even if nothing changed.

{ "id": 1, "method": "refresh" }

stop

Stop a server’s process tree, deepest processes first. everyport asks each process to quit, and kills whatever is left after 3 s. With force: true, it kills at once. On macOS and Linux, asking is SIGTERM and killing is SIGKILL. On Windows, asking sends Ctrl+C to the server’s console when only the server and the shells that launched it are on that console. Otherwise it closes the process’s windows, or terminates a process that has none. Killing terminates it. port is the server’s port, and root must be the server’s root from the snapshot. everyport checks every process’s start time first, so it never signals a process whose pid was reused.

A server is protected when any process in its tree is on the protected list, such as postgres. everyport refuses to stop it unless confirm_protected is true, and the error result names the protected process, such as postgres :5432 is protected; send confirm_protected to stop it anyway. confirm_protected defaults to false. Clients ask the user before sending true.

{ "id": 2, "method": "stop", "params": { "port": 3000, "root": { "pid": 48198, "started_at": 1790183520000 }, "force": false, "confirm_protected": false } }

The result arrives as soon as the processes are asked to quit. The server leaves the snapshot once its tree is gone.

restart

Stop the server, then run its command again in its launch_dir, detached from everyport. The result arrives as soon as the old tree is asked to quit. Once that tree is gone and port is free, everyport starts the command, and the new server shows up in a later snapshot.

A protected server needs confirm_protected: true, as for stop. An error result covers what everyport can check up front: the server is protected, the process changed, or its command or folder can’t be read. A failure after that gets no second result. The server just doesn’t come back on port in the snapshots over the next 10 s or so. The new server’s output is in everyport/port-<port>.log in the system temp folder ($TMPDIR or %TEMP%), and a failure to start it is one line on everyport’s stderr.

{ "id": 3, "method": "restart", "params": { "port": 3000, "root": { "pid": 48198, "started_at": 1790183520000 }, "confirm_protected": false } }

configure

Change scanner settings. Fields you leave out keep their current value; everyport starts from config.toml. On stdio, they apply to that connection’s scanner. Over HTTP, one scanner serves every client, so they apply to all of them.

{ "id": 4, "method": "configure", "params": { "min_port": 3000, "max_port": 65535, "interval_ms": 2000, "alert_memory": 2147483648, "leak_growth": 524288000, "idle_after_secs": 14400, "long_running_after_secs": 259200, "protected": ["postgres", "redis-server", "mongod", "mysqld", "mysql"], "auto_kill": "off", "vercel_previews": false } }

These are the defaults.

FieldMeaning
min_port, max_portPorts to report
interval_msTime between scans
alert_memoryMemory above which a server needs attention and raises an alert
leak_growthGrowth over the history window that counts as leaking
idle_after_secsIdle time after which Clean up suggests a server
long_running_after_secsUptime after which Clean up suggests a server
protectedProcess names that protect a server: Clean up never suggests it, and stop and restart need confirm_protected
auto_killWhat happens when a server starts to qualify for Clean up while everyport watches: off lists it, ask also sends a clean_up alert, act stops it. Servers that already qualify when everyport starts or when auto_kill changes are only listed, and so are leaking servers and servers whose process tree runs a protected process.
vercel_previewsLook up each branch’s Vercel preview through the GitHub CLI (gh), which goes online

Every everyport command starts from config.toml in the Everyport config folder (see everyport serve for where it is), which the desktop app writes from Settings. It holds these same fields, and any it leaves out take their defaults. everyport never auto-kills on its own: auto_kill starts off whatever the file says, and only a client that sends it in configure turns it on. The desktop app does that for its own computer only.

Versioning

hello.protocol is 1. It goes up only when a change would break existing clients, such as removing or renaming a field or changing its meaning. New fields, event types and methods are added without a bump, so:

  • ignore fields you don’t know
  • ignore events whose type you don’t know
  • expect an error result from an older everyport for a method it doesn’t know

If hello.protocol is higher than the version you support, ask the user to update the client, and show hello.everyport_version in the message.

Other machines

On another machine, run the same everyport stdio through a command prefix, such as ssh devbox everyport stdio or docker exec -i box everyport stdio. The protocol is the same.

To open a server from a machine whose prefix can’t forward ports, such as docker exec -i, run everyport connect <port> through the prefix. It connects to localhost:<port> on that machine and pipes the connection to its stdin and stdout, so a client can relay one TCP connection per everyport connect. It exits when the server closes the connection, and fails with status 1 when nothing answers on the port. everyport connect --check <port> only connects and exits, so a client can check the port before it listens locally. ssh and kubectl forward ports themselves, so they don’t need it.

everyport serve

everyport serve # listens on 127.0.0.1:7767 everyport serve --listen 127.0.0.1:8080 everyport serve --url https://devbox.tail1234.ts.net everyport serve --allow-origin https://dash.example.com

everyport serve listens only on loopback, and refuses any other address. Put a tunnel or proxy you trust in front of it, such as tailscale serve --bg http://127.0.0.1:7767.

Token and connection code

Every request needs Authorization: Bearer <token>. The token is created on first run and kept in serve-token in the Everyport config folder (~/Library/Application Support/everyport on macOS, %APPDATA%\everyport on Windows, ~/.config/everyport on Linux), readable only by your user. To make a new token, delete the file and restart everyport serve.

On start, everyport serve prints a connection code:

everyport://eyJ0b2tlbiI6Ii4uLiIsInVybCI6Imh0dHA6Ly8xMjcuMC4wLjE6Nzc2NyJ9

It’s everyport:// followed by the unpadded base64url encoding of a JSON object with the URL and the token:

{ "url": "http://127.0.0.1:7767", "token": "..." }

url is the listen address unless you pass --url, which you should when clients reach the server through a tunnel. The code contains the token, so share it only with people who may control your servers.

GET /events

A server-sent event stream. Each event is one data: line with the event’s JSON, followed by a blank line. It starts with hello and a snapshot, like stdio. A : ping comment arrives every 15 s so proxies keep the stream open. There are no id: or event: fields and no resume: after a reconnect, you get hello and a full snapshot again.

curl -N -H "Authorization: Bearer $TOKEN" http://127.0.0.1:7767/events
data: {"type":"hello","protocol":1,"everyport_version":"0.1.0","host":{...}} data: {"type":"snapshot","taken_at":1790195040000,"system":{...},"servers":[...]} : ping

Results come only in the /call response.

POST /call

Runs one request and returns its result event as the response body. The body is read as JSON whatever its Content-Type.

curl -H "Authorization: Bearer $TOKEN" -d '{"id":1,"method":"refresh"}' http://127.0.0.1:7767/call
{"type":"result","id":1,"error":null}

Snapshot changes the request causes arrive on /events.

Status codes

StatusWhen
200/events stream, or /call ran the request. A failed request is still 200, with error set.
400The body isn’t a valid request (the body is a result with the error), or the HTTP request is malformed
401The token is missing or wrong
404, 405Unknown path, or the wrong method for it

Browsers

By default, responses carry no CORS headers, so a web page can’t read them, even with a leaked connection code. To use the API from a page, allow its origin with --allow-origin, once per origin:

everyport serve --allow-origin https://dash.example.com --allow-origin http://localhost:5173

Responses to a request from an allowed origin carry Access-Control-Allow-Origin with that origin, and OPTIONS preflight requests from it are answered without a token. Every other request still needs the token. EventSource can’t send headers, so read /events with fetch and a stream reader.