Skip to content

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.

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.

Terminal window
$ wdx hub login --url https://hub.example.com
opening https://hub.example.com/nodes/approve?login=nl-1a2b3c4d in your browser; if none opens, open it yourself
waiting for the login to be approved, until 3:42PM; Ctrl-C cancels it
logged in to https://hub.example.com as alice; this machine is node desk
$ wdx hub status
connected to https://hub.example.com as alice, node desk
hub version: 0.5.0
remote access off, no code set up
hint: run wdx hub code setup to allow remote access
$ wdx hub node list
NAME ONLINE LAST SEEN VERSION OS ID
desk (this) yes 2026-11-08 15:33:10 0.5.0 linux/amd64 node-1a2b3c4d
laptop no 2026-11-07 19:02:44 0.5.0 darwin/arm64 node-5e6f7a8b

Over SSH, or anywhere no browser can open, log in with a code typed on the hub’s page from any device:

Terminal window
$ wdx hub login --url https://hub.example.com --no-browser
open https://hub.example.com/device and type the code WDX-4F7K-9QPA
  • 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.1 only, so a link sent elsewhere is of no use to anyone. Without a browser to open (xdg-open in a graphical session on Linux, open on macOS; see Dependencies), or with --no-browser, the page and a code are printed instead. The link and the code go to stderr, with --json too, so that a script or an AI can hand them to you. The node’s name is the hostname unless --name gives 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 --url names, and --url is required unreleased: there is no default hub, so wdx hub login without it is a usage error. With --json the result’s hub.url says which hub it was. Once logged in, every other command goes to the hub this machine logged in to. --url is https://, or plain http:// to localhost and 127.0.0.1 only. TLS is always verified, and there is no flag to skip it.
  • The connection is connected, connecting, or offline while 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) and disabled (the hub’s admin disabled your account) are for good: run wdx hub logout. wdx hub status says 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 logout only. You are asked first; --yes answers, and --json needs it. This machine cannot remove itself: wdx hub logout does.
  • 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.

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.

Terminal window
$ wdx project sync --name app --on
sync turned on
sync on, 12 changes waiting, never pushed, repository github.com/acme/app
$ wdx project sync --name app
sync 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 sync says how many wait, and hints not logged in when 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_taken means another of your nodes made a doc of that name in the repository: rename yours with wdx doc rename, which sends it again. not_found means 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_conflict means 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 with usage. A push refused for the node rather than its docs, such as node_version_unsupported, marks no doc: the changes wait and go up once the hub takes them.
  • --off stops sending and drops what waits; what reached the hub stays there. So does what a removed project sent.

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.

Terminal window
$ wdx hub code setup
scan 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: 492133
set up the authenticator code of this node, as the entry wdx: alice@desk
recovery key: 7K2M-QX9D-4RTV-HN8P-C3WB
warning: 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 setup and wdx hub code recover have no --json and 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 recover asks 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.step and recovery.hash in ~/.wdx/hub/, which only you may read, and wdx hub logout deletes them with the rest. code.step keeps a code from being used twice, also across a restart.
  • hub.remote_access, false by default, says whether sessions may come in; turning it off closes every session into the node at once, and refuses one opening then with remote_access_off; a node the hub cuts off refuses one with session_closed. hub.session_idle_timeout, 1h by default, closes one left unused. wdx hub status shows 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 verify walks it and fails with audit_broken, naming the first row that does not match, when one was changed, removed or inserted.

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).

Terminal window
$ wdx hub session open --node desk
code for desk: 492133
opened a session into node desk, sess-4k2m9qx7d3rtvhn8; it closes after 1h0m0s unused
$ wdx hub session list
ID DIRECTION NODE OPENED LAST USED IDLE UNTIL
sess-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 --all
closed 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 get says 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 --code it is asked for; --json needs 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 status counts those since the last session opened into the node, with the time of the latest, under wrong_codes and as a warning line; TIDE’s status line shows wrong codes: 3 beside the sessions. Wrong codes you did not type mean someone is guessing at the code: replace it with wdx hub code setup --replace, and remove any node you do not know from wdx 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 list show the same times; when the host’s daemon restarts; when hub.remote_access goes off there; at wdx 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 as node_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, as cut_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, as session_closed.
  • Not into this machine: a node named that is this one, by name or by id, is a usage error, before any code is asked.
  • Refused with remote_access_off or code_not_set_up on the host, code_wrong, node_offline when the node is not connected, node_not_found, too_many_streams, or node_version_unsupported when the two speak no version in common.

--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.

Terminal window
$ wdx doc get --node desk --project-name app --name SPEC --out spec.md
on node desk
code for desk: 492133
revision 4 of SPEC written to /home/alice/spec.md
$ wdx version --node desk
on node desk
wdx 0.5.0 (1a2b3c4), API 1
daemon 0.5.1 (5e6f7a8), API 1
  • Where it is: on the commands of agent, terminal, project, doc, plan, worktree and message, terminal attach and agent attach among them (below), on tide (below), and on version and usage, 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 --json or --no-input, or without a terminal, it fails with session_required instead: open one first with wdx hub session open --node desk --code <code>. A session that ends between the command’s requests fails it at the next with session_closed and the reason it ended, logged_out when this node logged out meanwhile; one that ends during a request cuts its stream, which fails it with connection_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 desk comes 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 a usage error. So is one typed at a prompt, as project open asks for: ~ is not expanded there, as this machine’s home is not the node’s. Files wdx reads or writes itself (--path of doc create, doc update, plan create and plan update, --out of doc get and plan get) stay this machine’s, so --out spec.md writes here.
  • The project cannot come from the working directory, which is this machine’s: pass --project-id or --project-name, or --all where a list takes it. agent create --parent-id needs neither, as the parent gives the project; --parent-name does, as an agent’s name is unique only within its project.
  • Not this machine: --node naming this one, by name or by id, is a usage error before any code is asked: run the command without --node.
  • Not for agents: where WDX_AGENT_ID is set, as it is for an agent wdx runs, --node fails with remote_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_unsupported says 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, is node_version_unsupported naming the host, not a usage error. wdx version --node shows the node’s release and API version, and no warning is given that its daemon runs another release.
  • Completions on a command line with --node ask 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. --node itself completes with your account’s nodes.

Each command’s errors with --node are listed in its reference, under With –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.

Terminal window
$ wdx terminal attach --node desk --project-name app --name server
on 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 is holder_version_unsupported too, with details.update set to client: 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.

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.

Terminal window
$ wdx tide --node desk
on node desk
code 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_agents where WDX_AGENT_ID is 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 · desk as 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-q to quit. The node unreachable, or the stream of the project shown breaking, shows reconnecting to desk… first, and the message after 30 seconds without reaching it.
  • One TIDE per machine, local or not: tide_running refuses a second.
  • The node’s own TIDE shows the session: its status line turns cyan and starts with remote: laptop while a session from the laptop is open. C-q H, or a click on remote:, 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.

CommandWhat it does
wdx hub loginMake this machine a node of your account on a hub
wdx hub logoutLeave the hub: this machine is no longer a node of the account
wdx hub statusShow this machine's login to a hub and its connection
wdx hub node listList the nodes of your account, and which are online
wdx hub node getShow a node of your account, with its key's fingerprint
wdx hub node removeRemove another node from your account, cutting it off at once
wdx project syncShow, start or stop pushing a project's docs to the hub
wdx hub code setupSet up an authenticator entry for this node, and get its recovery key
wdx hub code recoverSet up a new authenticator entry with the recovery key, when the old is lost
wdx hub audit verifyCheck that no row of the node's audit chain was changed, removed or inserted
wdx hub session openOpen a session into another node of your account, with its code
wdx hub session listList the sessions open into this node and from it
wdx hub session closeClose a session, or every one, either way
wdx terminal attachShow a terminal full-window and type into it, as tmux does
wdx agent attachShow an agent full-window and talk to it, as tmux does
wdx tideWork in the terminal IDE: a project, an agent and terminals in one window