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 throughssh,docker exec -i,kubectl exec -i --orwsl.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_percentis whole-machine CPU, from 0 to 100. - Optional fields are always present, as
nullwhen empty. The tables below mark them “ornull”. - “Clean up” is the list of servers
everyportsuggests 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
snapshotwhenever anything changes (it scans every 2 s by default) - an
alertwhen a server crosses a threshold - a
resultfor 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:
| Field | Meaning |
|---|---|
memory_total, memory_used | The machine’s physical memory and how much is in use |
memory_other_apps | Memory used by everything that isn’t a listed server |
cpu_percent | Whole-machine CPU, 0 to 100 |
servers is sorted by port. Each server is a listening port and the process tree behind it:
| Field | Meaning |
|---|---|
port | The listening TCP port |
pid | The process that owns the socket |
root | The 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_name | Name of the process that owns the socket |
addresses | Bound addresses, such as 127.0.0.1 and ::1 |
cwd, cwd_exists | The server’s folder (or null), and whether it still exists. It’s false once a worktree is deleted. |
command, launch_dir | The root’s command line and the folder it started in, or null. restart runs command in launch_dir. |
started_at | When the root process started, or null |
project | name (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. |
workspace | The 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. |
agent | The Claude Code or Codex session that started the server, or null. kind is claude_code or codex. resume_command resumes it in directory. |
processes | The whole tree, depth first and root first. depth is 0 for the root. |
memory, cpu_percent | Sums 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. |
connections | Open connections to the port |
history | Samples covering up to the last 10 minutes, oldest first. Don’t assume a fixed spacing. |
last_active | Last time the server had connections or used CPU |
protected | A process in its tree is on the protected list, such as postgres. Clean up never suggests it, and stop and restart need confirm_protected. |
status | running, attention (over the memory threshold or leaking) or idle (idle for over an hour, or its folder is gone) |
clean_up | Why 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.
| Field | Meaning |
|---|---|
port | The listening TCP port |
addresses | Bound addresses, such as 0.0.0.0 and :: |
owner | The user the process runs as, such as root, or null when the OS doesn’t say |
process_name | Name 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.
| Field | Meaning |
|---|---|
min_port, max_port | Ports to report |
interval_ms | Time between scans |
alert_memory | Memory above which a server needs attention and raises an alert |
leak_growth | Growth over the history window that counts as leaking |
idle_after_secs | Idle time after which Clean up suggests a server |
long_running_after_secs | Uptime after which Clean up suggests a server |
protected | Process names that protect a server: Clean up never suggests it, and stop and restart need confirm_protected |
auto_kill | What 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_previews | Look 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
typeyou don’t know - expect an error
resultfrom an oldereveryportfor 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.comeveryport 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://eyJ0b2tlbiI6Ii4uLiIsInVybCI6Imh0dHA6Ly8xMjcuMC4wLjE6Nzc2NyJ9It’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/eventsdata: {"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
| Status | When |
|---|---|
200 | /events stream, or /call ran the request. A failed request is still 200, with error set. |
400 | The body isn’t a valid request (the body is a result with the error), or the HTTP request is malformed |
401 | The token is missing or wrong |
404, 405 | Unknown 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:5173Responses 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.