Hub
since 0.5.0
This page is about using a hub from your machines. A hub is optional, and self-hosted: you run wdx-hub yourself, for your own machines or your organisation’s, and log in to it with --url. To run one, see Running a hub.
What it is
Section titled “What it is”A hub is a server, wdx-hub, that keeps accounts. Logging in makes this machine one of your account’s nodes: wdx makes a key on it, the hub’s certificate authority gives it a certificate, and the daemon keeps a connection to the hub from then on, so the hub knows which of your nodes are online. wdx hub node list shows them all, and wdx hub node remove cuts one off at once.
The daemon never waits on the hub: with no hub, or with the hub down, everything else works exactly as before.
How the nodes and the hub work together inside: The hub, in Architecture.
Try it
Section titled “Try it”$ wdx hub login --url https://hub.example.comopening https://hub.example.com/nodes/approve?login=nl-1a2b3c4d in your browser; if none opens, open it yourselfwaiting for the login to be approved, until 3:42PM; Ctrl-C cancels itlogged in to https://hub.example.com as alice; this machine is node desk
$ wdx hub statusconnected to https://hub.example.com as alice, node deskhub version: 0.5.0remote access off, no code set up hint: run wdx hub code setup to allow remote access
$ wdx hub node listNAME ONLINE LAST SEEN VERSION OS IDdesk (this) yes 2026-11-08 15:33:10 0.5.0 linux/amd64 node-1a2b3c4dlaptop no 2026-11-07 19:02:44 0.5.0 darwin/arm64 node-5e6f7a8bOver SSH, or anywhere no browser can open, log in with a code typed on the hub’s page from any device:
$ wdx hub login --url https://hub.example.com --no-browseropen https://hub.example.com/device and type the code WDX-4F7K-9QPAHow it behaves
Section titled “How it behaves”- Logging in opens the hub’s page in the browser, where you log in, pass your second factor and approve the machine, renaming it there if your account has a node of that name. A browser whose second factor passed more than 10 minutes ago logs in again first; it stays logged in until that login passes, which replaces its session. The browser comes back to wdx on
127.0.0.1only, so a link sent elsewhere is of no use to anyone. Without a browser to open (xdg-openin a graphical session on Linux,openon macOS; see Dependencies), or with--no-browser, the page and a code are printed instead. The link and the code go to stderr, with--jsontoo, so that a script or an AI can hand them to you. The node’s name is the hostname unless--namegives one. It waits until you approve or deny, at most 10 minutes; Ctrl-C cancels it, as does starting another login. - The hub’s address is the one
--urlnames, and--urlis required unreleased: there is no default hub, sowdx hub loginwithout it is ausageerror. With--jsonthe result’shub.urlsays which hub it was. Once logged in, every other command goes to the hub this machine logged in to.--urlishttps://, or plainhttp://tolocalhostand127.0.0.1only. TLS is always verified, and there is no flag to skip it. - The connection is
connected,connecting, orofflinewhile the hub cannot be reached, which the daemon tries again and again, waiting longer each time up to 1 minute.revoked(another node removed this one) anddisabled(the hub’s admin disabled your account) are for good: runwdx hub logout.wdx hub statussays which, with what to do; not being logged in is a state, not an error. - Removing a node cuts it off at once: the hub closes its connection and renews no certificate for it. It also ends that node’s sessions on your other nodes: at once, or, for a node not connected to the hub then, when it next connects. They keep its key pinned, so that the hub cannot make them forget it; keys are forgotten at
wdx hub logoutonly. You are asked first;--yesanswers, and--jsonneeds it. This machine cannot remove itself:wdx hub logoutdoes. - Logging out asks the hub to revoke this node, then deletes the key, the certificate and the login from
~/.wdx/hub/, whether the hub heard or not: without its key, the node is of no use. Your other nodes end their sessions with it, as when it is removed. When the hub could not be told, remove the node from another one. Projects, agents and docs stay; the doc changes waiting for the hub are dropped, and the next login sends every synced project’s docs anew, to whichever hub it is. - Nothing secret is logged: not the node’s key, nor the login’s codes, nor a doc’s text, by wdx or by the hub.
Docs on the hub
Section titled “Docs on the hub”wdx project sync --name app --on sends every doc of the project to the hub, with all its revisions, then each change as it happens: a new doc, a revision, a change of its status, labels, folder or name, a removal. The hub keeps each doc’s revisions encrypted under a key of its own. Plans are never sent, and nothing comes back down yet.
$ wdx project sync --name app --onsync turned onsync on, 12 changes waiting, never pushed, repository github.com/acme/app
$ wdx project sync --name appsync on, 0 changes waiting, last pushed 2 minutes ago, repository github.com/acme/app- It needs a git remote: the hub files a project’s docs under its repository, which is how the same project is matched across your machines. A project without one is refused with
no_remote. - Logged out or offline, changes wait, in order, and go up once the node is connected.
wdx project syncsays how many wait, and hintsnot logged inwhen the node is not. - A doc the hub refuses is listed with the hub’s code, and its later changes wait while other docs go on.
doc_name_takenmeans another of your nodes made a doc of that name in the repository: rename yours withwdx doc rename, which sends it again.not_foundmeans the hub keeps a doc of that id for another account: nothing this node does changes that, so the doc is not sent again.revision_conflictmeans the hub has that revision of the doc with another text, which another node wrote. - A change the hub’s API does not allow, such as one a later wdx sends to an older hub, has the whole push refused with
usage: the changes are then sent one at a time, so that only that change’s doc waits, listed withusage. A push refused for the node rather than its docs, such asnode_version_unsupported, marks no doc: the changes wait and go up once the hub takes them. --offstops sending and drops what waits; what reached the hub stays there. So does what a removed project sent.
Remote access
Section titled “Remote access”Your other nodes will open sessions into this one through the hub, end to end encrypted, so that the hub passes bytes it cannot read. A session needs a code from this node’s own authenticator, typed at that moment: neither a hacked hub nor a hacked account opens the machine. This node allows none until you set up its code and turn hub.remote_access on.
$ wdx hub code setupscan this with your authenticator app, to add the entry wdx: alice@desk:
(a QR code)
or type the secret in it: JBSWY3DPEHPK3PXP4QZA6TRG5MLYWXKE
code from the app: 492133set up the authenticator code of this node, as the entry wdx: alice@deskrecovery key: 7K2M-QX9D-4RTV-HN8P-C3WBwarning: write this down: it is the only way to set up a new authenticator here
$ wdx setting set --name hub.remote_access --value true- Each node has an entry of its own, set up while at it: only this node checks its code, and a secret shared by several would have to travel between them.
- Interactive only:
wdx hub code setupandwdx hub code recoverhave no--jsonand need a terminal, so that the secret is never printed where an AI’s transcript would keep it. The QR code is drawn for a dark terminal; an app that cannot scan it takes the secret as text. You type a code back to prove the entry works, three tries at most. - The recovery key is shown once, and only its hash is kept.
wdx hub code recoverasks for it, hidden as you type it, and sets up a new entry with a new key; the old one is spent. A wrong key makes the next try wait, from 1 second doubling to 5 minutes. - Replacing the entry, with
wdx hub code setup --replace, asks first: the old entry stops working, and sessions open stay. - The files are
code.secret,code.stepandrecovery.hashin~/.wdx/hub/, which only you may read, andwdx hub logoutdeletes them with the rest.code.stepkeeps a code from being used twice, also across a restart. hub.remote_access,falseby default, says whether sessions may come in; turning it off closes every session into the node at once, and refuses one opening then withremote_access_off; a node the hub cuts off refuses one withsession_closed.hub.session_idle_timeout,1hby default, closes one left unused.wdx hub statusshows both, with what is missing.- The node’s audit chain records what matters about access to it, each row’s hash chained to the one before: the code set up, replaced or recovered, each wrong code and recovery key, remote access turned on or off, each session opened and closed with why, and a node that showed another key than the one pinned for it.
wdx hub audit verifywalks it and fails withaudit_broken, naming the first row that does not match, when one was changed, removed or inserted.
Sessions
Section titled “Sessions”On another node of your account, open a session into this one with the code your authenticator shows for it. Commands then run on it with --node (below).
$ wdx hub session open --node deskcode for desk: 492133opened a session into node desk, sess-4k2m9qx7d3rtvhn8; it closes after 1h0m0s unused
$ wdx hub session listID DIRECTION NODE OPENED LAST USED IDLE UNTILsess-4k2m9qx7d3rtvhn8 outgoing desk 2026-11-08 15:40:02 2026-11-08 15:40:02 2026-11-08 16:40:02
$ wdx hub session close --allclosed 1 sessions outgoing with node desk, sess-4k2m9qx7d3rtvhn8- End to end: the two nodes speak TLS 1.3 between their own keys inside a stream the hub passes along and cannot read. The code proves itself through CPace, bound to that TLS channel, so a hub in the middle has one guess at it per try. At the first session each node pins the other’s key, and refuses any other from then on with
node_key_mismatch: it never takes the hub’s word for another node’s key.wdx hub node getsays whether a node’s key is known, and shows its fingerprint to compare by eye with the one that node shows. - A code opens one session, and is never logged. Without
--codeit is asked for;--jsonneeds it. With a session into that node open already, nothing is asked:unchanged, once a stream to the host found it open there too; one the host closed is opened anew, with the code. Closing a session the host closed already returns it, closed. - Wrong codes slow down at the host, per node and in total: after one, the next is refused unread for 1 second, doubling up to 5 minutes. A node’s own wait goes on doubling after its 10th wrong code in a row, up to 1 hour, until a right code from that node: another node’s right code ends only the total. A client that sends no confirmation within 10 seconds of the host’s answer counts as a wrong code. Remote access is never turned off by wrong codes alone.
- Wrong codes are shown:
wdx hub statuscounts those since the last session opened into the node, with the time of the latest, underwrong_codesand as a warning line; TIDE’s status line showswrong codes: 3beside the sessions. Wrong codes you did not type mean someone is guessing at the code: replace it withwdx hub code setup --replace, and remove any node you do not know fromwdx hub node list. - A session ends when closed, on either node; when the same node opens another into the host, so that one node has one session into another, which its every TIDE and command shares; when unused for the host’s
hub.session_idle_timeout, where anything passing through it counts as use, a TIDE only following its project or an attach as much as a command, and both nodes’wdx hub session listshow the same times; when the host’s daemon restarts; whenhub.remote_accessgoes off there; atwdx hub logout, which also forgets the keys pinned; and when the other node is removed from your account, or logs out, which the hub tells your other nodes. The daemon logs the reason of a session ended so asnode_removed. When the hub cuts a node off, removed from the account or the account disabled, that node closes every session both ways at once, ascut_off, and the hub cuts every stream between its nodes, so nothing open goes on carrying. The other node finds out at its next use, assession_closed. - Not into this machine: a node named that is this one, by name or by id, is a
usageerror, before any code is asked. - Refused with
remote_access_offorcode_not_set_upon the host,code_wrong,node_offlinewhen the node is not connected,node_not_found,too_many_streams, ornode_version_unsupportedwhen the two speak no version in common.
Commands on another node
Section titled “Commands on another node”--node <name> runs a command on another node of your account, through a session into it: its output, --json and exit code are that node’s, as if it ran there. Without a session open, the code is asked for first.
$ wdx doc get --node desk --project-name app --name SPEC --out spec.mdon node deskcode for desk: 492133revision 4 of SPEC written to /home/alice/spec.md
$ wdx version --node deskon node deskwdx 0.5.0 (1a2b3c4), API 1daemon 0.5.1 (5e6f7a8), API 1- Where it is: on the commands of
agent,terminal,project,doc,plan,worktreeandmessage,terminal attachandagent attachamong them (below), ontide(below), and onversionandusage, which shows the limits the node’s AI coding agents last told. Not on the commands that manage this machine (daemon,setting,update,observe,deps,hub). - The code is asked for when no session into the node is open, on a terminal, then the command goes on. With
--jsonor--no-input, or without a terminal, it fails withsession_requiredinstead: open one first withwdx hub session open --node desk --code <code>. A session that ends between the command’s requests fails it at the next withsession_closedand the reason it ended,logged_outwhen this node logged out meanwhile; one that ends during a request cuts its stream, which fails it withconnection_lost, as a network that goes away does (below). - Reaching the node through the hub takes at most 30 seconds: a hub that does not answer by then fails the command with
hub_unreachable, and Ctrl-C ends the wait at once. Once the command reaches the node, it runs to its end there. - A network that goes away under a command, a sleep or a wifi drop, is noticed within about 30 seconds: a stream quiet for 15 seconds is checked with the hub, and closed when the hub does not answer within 15 more. A stream cut once the node was reached, before or while its answer is read, fails the command with
connection_lost(exit 3): the session may still be open, and the command may have run there, so check first whether its change was made before running it again. - In text,
on node deskcomes first on stderr, so that you see where it ran. With--json, stdout holds only the node’s answer. - Paths the node’s daemon receives (
project open --local,project create --path) must be absolute, and are that node’s: a relative one is ausageerror. So is one typed at a prompt, asproject openasks for:~is not expanded there, as this machine’s home is not the node’s. Files wdx reads or writes itself (--pathofdoc create,doc update,plan createandplan update,--outofdoc getandplan get) stay this machine’s, so--out spec.mdwrites here. - The project cannot come from the working directory, which is this machine’s: pass
--project-idor--project-name, or--allwhere a list takes it.agent create --parent-idneeds neither, as the parent gives the project;--parent-namedoes, as an agent’s name is unique only within its project. - Not this machine:
--nodenaming this one, by name or by id, is ausageerror before any code is asked: run the command without--node. - Not for agents: where
WDX_AGENT_IDis set, as it is for an agent wdx runs,--nodefails withremote_not_for_agents, before anything is asked. It is a guard against an agent reaching another machine by accident, not a boundary: the agent runs as you, and anything running as you can do what you can. - The host’s rules: it serves only what a person at another node should reach. The hub’s routes, the settings, stopping or reloading the daemon and TIDE’s state are refused with
route_not_allowed, as an operation the node does not have yet is: update it. - Versions: the two nodes need not run the same release, only an API version both speak:
node_version_unsupportedsays which to update. A node newer than the host is served while it still speaks the host’s version, and a request refused for its version never runs on the host. A flag the host’s release does not know yet, from a newer node, isnode_version_unsupportednaming the host, not a usage error.wdx version --nodeshows the node’s release and API version, and no warning is given that its daemon runs another release. - Completions on a command line with
--nodeask that node, through a session open into it, and wait up to 5 seconds for it, as the stream goes through the hub, where one on this machine waits half a second; with no session, or no answer by then, they offer nothing, and never ask for the code.--nodeitself completes with your account’s nodes.
Each command’s errors with --node are listed in its reference, under With –node.
Attaching on another node
Section titled “Attaching on another node”wdx terminal attach --node and wdx agent attach --node show a terminal or an agent of the other node full-window, as attaching here does: what you type goes to its shell or its AI CLI, and its output comes back, through the session.
$ wdx terminal attach --node desk --project-name app --name serveron node desk(the terminal, full-window, until C-\ d)detached from terminal server in app- Everything else is as attaching here: the keys, the status line, scrolling through history, several clients on one terminal, the last to attach or resize setting its size (Terminals).
- The other node’s daemon relays it: a terminal’s own program, its holder, is reached only through its socket on that machine, so the daemon there passes the bytes between the session and the socket. The holder logs that the client is remote, and from which node. Neither daemon logs what passes, only how many bytes.
- Typing keeps the session open: the bytes either way count as use, so a session you are typing in does not close for
hub.session_idle_timeout. - Attaching here still goes straight to the holder’s socket, without the daemon: a session outlives a daemon restart, as before.
- A terminal started by an older wdx cannot be attached to from another node:
holder_version_unsupported. Restart its shell there, by closing and opening its project, or removing the terminal and creating it again. A terminal’s holder serves attaches from wdx of older and newer releases; one from a wdx older than the holder still serves isholder_version_unsupportedtoo, withdetails.updateset toclient: update wdx on the node you attach from. - A session that ends while attached ends the attach with
connection_lost; run it again, which asks for the code. So does a network gone, within about 30 seconds.
TIDE on another node
Section titled “TIDE on another node”wdx tide --node opens TIDE on the other node, through the session: its projects, agents and terminals, live, with every terminal attached through its daemon as above.
$ wdx tide --node deskon node deskcode for desk: 492133(TIDE, its status line cyan and starting with desk, until C-q q)TIDE closed- The code is asked in the terminal, before the window opens, when no session is open;
remote_not_for_agentswhereWDX_AGENT_IDis set, as for every command with--node. - It shows that node only, and says so: a cyan status line that starts with the node’s name, and
wdx tide · deskas the window’s title. - It keeps its view in memory: it never reads or writes the node’s TIDE state, which the node refuses anyway, so the node’s own TIDE stays as it was left.
- No settings tab, as no setting is reached through a session; and a new project takes an absolute path on the node, typed, as this machine’s directories are the wrong ones. The node’s paths are shown whole, never with
~for this machine’s home. - When the session ends, a message replaces the window and says why: type the code to open another and go on where you were, or
C-qto quit. The node unreachable, or the stream of the project shown breaking, showsreconnecting to desk…first, and the message after 30 seconds without reaching it. - One TIDE per machine, local or not:
tide_runningrefuses a second. - The node’s own TIDE shows the session: its status line turns cyan and starts with
remote: laptopwhile a session from the laptop is open.C-q H, or a click onremote:, opens the Sessions dialog, which closes a session, every one into this node, or removes a node from the account, each after asking (TIDE’s layout).
More in TIDE’s layout.
Commands
Section titled “Commands”| Command | What it does |
|---|---|
wdx hub login | Make this machine a node of your account on a hub |
wdx hub logout | Leave the hub: this machine is no longer a node of the account |
wdx hub status | Show this machine's login to a hub and its connection |
wdx hub node list | List the nodes of your account, and which are online |
wdx hub node get | Show a node of your account, with its key's fingerprint |
wdx hub node remove | Remove another node from your account, cutting it off at once |
wdx project sync | Show, start or stop pushing a project's docs to the hub |
wdx hub code setup | Set up an authenticator entry for this node, and get its recovery key |
wdx hub code recover | Set up a new authenticator entry with the recovery key, when the old is lost |
wdx hub audit verify | Check that no row of the node's audit chain was changed, removed or inserted |
wdx hub session open | Open a session into another node of your account, with its code |
wdx hub session list | List the sessions open into this node and from it |
wdx hub session close | Close a session, or every one, either way |
wdx terminal attach | Show a terminal full-window and type into it, as tmux does |
wdx agent attach | Show an agent full-window and talk to it, as tmux does |
wdx tide | Work in the terminal IDE: a project, an agent and terminals in one window |