CLI Reference
Complete command-line reference for the noBGP agent. This covers all commands, options, and configuration methods.
Overview
The nobgp command-line tool provides:
- User authentication — log in from your personal machine to manage your infrastructure
- Remote operations — execute commands, open shells, and manage services on your nodes
- File management — upload, download, list, and delete files on the shared filesystem
- Network and node management — list, create, and register infrastructure
- Agent runtime — registration, configuration, service control, and upgrades
Command Structure
nobgp [command] [options]
Root privileges
The agent's own state belongs to root: the profile configuration and the identity
files beside it are mode 0600, the local API socket is owned by the configured
user, and the system service and installed binary are root's as well. A command
that has to read or write any of it says so and takes the privileges itself —
you are never asked to retype it with sudo, and it never prints a partial view
in place of the real one.
On Linux and macOS such a command prints Root privileges required, re-executing with sudo...
and re-runs itself under sudo. Where sudo has no way to ask for a password —
no interactive terminal, no SUDO_ASKPASS, and no cached or NOPASSWD
credential — it fails immediately with that explanation instead of hanging on a
prompt nobody can answer.
On Windows nothing self-elevates: a console cannot raise its own privileges, and re-launching through UAC would move the output and the exit code into a new window, away from the shell you typed in. The command refuses instead, naming the invocation to repeat from an Administrator terminal (right-click Windows Terminal or PowerShell → Run as administrator). The Windows installer is the one exception — it accepts that trade and requests UAC elevation for you.
Either way the refusal also names the unprivileged alternative: the status tool
on the node's local MCP server answers read-only questions
about the node without root, which is the path for scripts and unattended
callers.
| Needs root | Never needs root |
|---|---|
agent, config, list, mcp install, peers, register, remove, resolve, service (every subcommand, including logs), show, status, uninstall, upgrade | events, exec, file (all subcommands), login, logout, mcp status, mcp uninstall, network (all subcommands), notify, proxy (all subcommands), shell, version |
list joined the first column in agent 0.4.158: the profiles are the files in
the config directory, which is root's, so an unprivileged run could not read it at
all — see nobgp list.
The second column is deliberate, not an oversight. login and logout — and the
router-facing commands exec, shell, file, network and proxy —
authenticate with your credential from your own keyring, so elevating would
consult root's keyring and report the wrong account. notify and events reach
the agent through its local API socket, which exists precisely so an unprivileged
consumer can use it (see Local API socket keys); a
sudo re-exec there would break the scripts the event bus
dispatches. mcp status and mcp uninstall touch only your own account's Claude
configuration.
Commands
nobgp agent
Run the noBGP agent as a standalone process. If not yet registered, the agent will auto-register non-interactively (requires a registration key in config or environment).
Usage:
nobgp agent [profile] [options]
Examples:
# Run with default profile from /etc/nobgp/default.yml
sudo nobgp agent
# Run a specific profile
sudo nobgp agent production
# Run with debug output
sudo nobgp agent --debug
# Run with custom log level
sudo nobgp agent --log-level debug
Global Options (available on all commands):
| Option | Short | Type | Description |
|---|---|---|---|
--router | -r | string | Router FQDN, optionally host:port (default: router.nobgp.com). The agent connects over wss:// (TLS); legacy wss:///ws:// URLs are still accepted and normalized to the bare FQDN |
--insecure | boolean | Allow a plaintext (ws://) control channel — for local or self-hosted routers without TLS only (default: false) | |
--transport | string | Control-channel transport: auto, quic, or wss (default: auto). auto negotiates the fastest available transport and falls back to WebSocket automatically | |
--transport-family | string | Which IP address family this node's own outbound connections lead with: auto, v4, or v6 (default: auto). auto races the two families, which is what every node has always done; v4 or v6 tries that family first and on its own, and only falls back to the other if it fails — so a node told to avoid a family actually stops putting it on the wire. A preference, never a force: both values fall back, and an unrecognised value is read as auto. See Address family. Agent 0.4.113+ | |
--quic-router | string | QUIC control endpoint host:port override for self-hosted routers (default: unset — the endpoint is learned from the router, falling back to the router's own domain) | |
--debug | -d | boolean | Enable debug mode (shortcut for --log-level debug) |
--log-level | -l | string | Log level: error, warning, info, debug, trace (default: info) |
--compress | -z | boolean | Enable compression (default: true) |
--encrypt | -e | boolean | Require encryption (default: true) |
--idle-ttl | duration | Recycle a peer session after this much idle time with no data traffic (default: 1h; 0 = defer to peer). Negotiated end-to-end as the shorter of the two peers' values; clamped to a 1m floor | |
--interface | -i | string | Network interface to use (default: auto) |
--ping | duration | Ping interval for health checks (default: 58s) | |
--mount | -m | string | Filesystem mount point (default: platform-dependent) |
--user | string | Account that command sessions, dispatched bus commands and file operations run as whenever the caller does not ask to be elevated — independent of --allow-root. Registration sets it to the installing account, so a default install does not run sessions as root. Leaving it unset on a node whose agent runs as root does not fall back to root: unelevated calls are refused there until an account is named or the caller passes root (unelevated never means root). --allow-root=false additionally pins work to this account. An account that does not exist on the machine — or one that resolves to uid 0, --user root included — is refused at the point of setting rather than failing at every unelevated session later. Also settable as NOBGP_USER, which outranks the config file. On Windows the account is the node's second identity from agent 0.4.44, covering file operations first and execution as well from agent 0.4.46 (0.4.44 and 0.4.45 refused an unelevated execution call rather than running it as LocalSystem) | |
--overlay-cidr | string | Overlay TUN CIDR: a /20 within 100.64.0.0/10 (auto-picked if unset) | |
--overlay-ipv6 | boolean | Take an IPv6 overlay slice beside the IPv4 /20, so peer names resolve to both families (default: true). false is the off switch for the IPv6 datapath on this node; a host that cannot take an IPv6 slice runs on IPv4 anyway and says why in nobgp status. Agent 0.4.132+ | |
--mtu | int | Overlay TUN MTU in bytes (default: 8000; clamped to [1280, 8000]; lower for a constrained path) | |
--fs-cache-ttl | duration | How long the shared drive may serve a cached directory listing before re-asking noBGP (default: 30s; clamped to [0, 10m]). A staleness window, not a performance dial. 0 re-asks every time and still serves unchanged file contents from the local cache. Requires a unit — 30 is thirty nanoseconds. Read by the nfs, fuse and winfsp backends (agent 0.4.67; nfs alone in 0.4.66), where it also sets the timeouts the kernel caches with — those are mount options, so a change reaches them on the next mount. Not persisted by nobgp config: set fs-cache-ttl in the profile file or NOBGP_FS_CACHE_TTL to make it durable. Agent 0.4.66+ | |
--allow-tools | strings | Capability domains this node serves: fs, command, logs (default: all three; logs is the domain since agent 0.4.153). Covers MCP tools, event-bus sources and published terminal services alike. Domains, not tool names — a value that is not one is refused before the write since agent 0.4.153. See Capability keys | |
--allow-roots | strings | Filesystem roots the fs tools and the fs event source may touch (default: the whole filesystem). The agent's own configuration directory is refused whatever this is set to — see Capability keys | |
--allow-root | boolean | Permit execution and file access as root / Administrator (default: true). false refuses anything that would run as uid 0 instead of downgrading it — an elevated request, or a terminal service published to run as root — and drops the rest to --user. On a node with no usable --user it is an off switch rather than a narrowing, since unelevated work is refused there too. --allow-admin is its old name and still works — see The veto on root was renamed. Agent 0.4.153+ for the new name | |
--local-mcp | string | The local MCP server on this node: on (default — it runs whenever a controller of the node turns it on at the router) or off (it never runs, whatever the router says). The node owner's off switch, which the router's grant cannot override. A value that is neither is refused before the write, and a bad value already in the file reads as off. Agent 0.4.153+ | |
--peer-loopback | boolean | Whether an overlay peer may reach a port bound only on this node's loopback — 127.0.0.1, [::1], and the loopback interface's own link-local (default: true, the behaviour of every earlier release). false keeps a loopback-only service off the network. See Peer reach into loopback. Agent 0.4.153+ | |
--windows-firewall | string | Windows only: whether the agent keeps its inbound allow rule in Windows Firewall — allow-overlay (default, the behaviour of every earlier release) or respect-windows. See Windows Firewall rule. Agent 0.4.124+ | |
--peer-key-pinning | string | What this node does when the encryption key offered for a peer is not the one it pinned the first time the two talked: warn (default — report it and carry on), enforce (refuse that session) or off (remember nothing, check nothing). See Peer key pinning and nobgp peers. Agent 0.4.98+ | |
--lan-names | string | Which host names on this machine's own LAN it will resolve for the rest of the network: comma-separated patterns with * and ? wildcards (default *, any name). !pattern excludes and wins whatever the order, and off, none or an empty value means this node answers for no LAN name at all. Names this machine's own applications look up are never filtered. See Which LAN names this node serves. Agent 0.4.128+ | |
--mdns-report-services | boolean | Whether this node tells the network which services its own host offers — file sharing, screen sharing, remote login, RDP (default: true). false is the owner's veto: the node looks for nothing and reports an empty list, which also clears a list it reported earlier. See mDNS keys. Agent 0.4.128+ | |
--mdns-announce | boolean | macOS and Linux: register each peer as <name>-nobgp.local with this host's own mDNS responder, so applications on this machine — Finder among them — see peers and the services they report (default: false). The registrations are local-only: nothing is announced on the LAN. See mDNS keys. Agent 0.4.128+ |
Exit Codes:
| Code | Meaning |
|---|---|
0 | Clean shutdown |
1 | General error |
10 | The profile's configuration file changed on disk, so the agent exited for a reload. On Linux and macOS the agent normally re-executes itself in place instead, so this code is mainly seen on Windows and when a profile is removed. The process manager treats it as a prompt reload, with no crash backoff |
11 | The router rejected this node's credentials (node deleted, key revoked, network removed). Permanent until an operator acts, so the process manager retries every 15 minutes instead of using the crash backoff. Re-register the node, or uninstall the agent |
nobgp register
Register the agent with a noBGP network. Opens your browser for OAuth login by default. For Docker/automation, pass a registration key with --key for non-interactive registration.
Usage:
nobgp register [profile] [options]
Examples:
# Register via OAuth browser login (recommended)
sudo nobgp register
# Register with a specific name and network
sudo nobgp register --name "my-server" --network "production"
# Register a named profile
sudo nobgp register production --name "prod-server-1"
# Register with a key
sudo nobgp register --key "<YOUR_REGISTRATION_KEY>" --name "my-server"
Options:
| Option | Type | Description |
|---|---|---|
--key | string | Registration key for the network. Also NOBGP_KEY |
--name | string | Node name (defaults to hostname) |
--network | string | Network name (for OAuth login flow) |
If you are already signed in with nobgp login, that sign-in is used and
no browser opens. sudo nobgp register carries your saved sign-in through the elevation, so
the machine enrolls with nothing to approve on a page: you pick one of your networks in the
terminal, or name it with --network. From agent
0.4.158 the access token is refreshed first — as you, because the refresh token is in your
own keyring and root's is empty — and where noBGP refuses it anyway (it was revoked, or the
session is no longer known) the terminal says Your saved sign-in was refused (…). Continue in the browser. and carries on with the browser way below. Before that release the command
stopped there with register failed (401): invalid token, on exactly the machine where
sudo nobgp register — which has no saved sign-in of its own and therefore asks in the browser
— worked. With no terminal to ask at, the refusal names the remedy instead: run nobgp login
again, or register with a key. ⚠ This is the Linux and macOS path: on Windows nothing carries a
sign-in into the Administrator terminal, so registering there always asks in the browser (or
takes a key).
⚠ On noBGP's routers the browser way needs agent 0.4.153 or newer.
An older agent cannot tie the page a person opens to the terminal that started the
registration, so noBGP refuses that way and nothing is registered. Upgrade the machine's
agent (sudo nobgp upgrade -f, or reinstall with the install script),
or register with a key (--key, NOBGP_KEY), which is unaffected and needs no browser.
From router 0.4.185 the refusal comes before anybody signs in: the terminal falls
back to the sign-in link, and the page there reads Sign-in refused and asks for the
upgrade. Before that, the page took the sign-in, the network choice and the approval, and
only then had nothing to hand the machine — the terminal sat waiting out the rest of its
15 minutes with nothing on either screen saying why. A router of your own decides this for
itself; see nobgp login for the same rule on signing in.
On a machine with no browser, scan the sign-in link. From agent 0.4.131 the browser flow prints the authentication URL as a QR code below the link, so you can sign in from your phone. The sign-in completes there: this command polls noBGP for the result rather than waiting for the browser to come back to the machine. Notes:
- The code is printed only when output is a terminal. In a log or a pipe it is left out.
- From agent 0.4.154 a terminal always gets a code, in one of two drawings picked from the window size. Where the whole screen fits with it, each module is two cells of background colour, one module row per line — no glyph at all, so neither the console font nor the terminal's line spacing can break it. Where it does not fit, the code is drawn in half blocks instead: one cell per module, two module rows to a line, about 41×21 including the quiet zone, so a default 80×24 window still gets a scannable code. ⚠ Agent 0.4.153 drew only the taller form and left the code out of any window too small for it, which for a registration link of the length noBGP sends was a default window on macOS, Linux and Windows. There is no
(Make the terminal at least <columns>×<rows> to show a QR code.)line any more, and nothing for you to resize by hand. - A window too short for the tall drawing is asked to grow, from agent 0.4.158, with the terminal's own resize request — and the drawing is then chosen by the size the window has, never by the terminal's name. Terminal.app grows (measured: 80×24 to 80×40) and gets the tall code; a terminal that keeps its size — tmux, a full-screen window, anything that ignores the request — gets the half blocks as before. The tall code needs about 40 rows for a registration link at 80 columns. ⚠ This exists because the half blocks are glyphs: a terminal that draws them from the font leaves a hairline across every line of the code whatever the colours, and in a default 80×24 Terminal.app window that was enough to stop a desktop scanner reading the code off a screenshot. Where the half blocks are still drawn, each mixed cell is now the lower half block over the top module's colour, which puts the larger part of that gap in the right colour.
- Both colours are painted from agent 0.4.154 — dark modules and light modules alike, the quiet zone included — so the symbol is the same on a light terminal and a dark one. Earlier agents left the light field as the terminal's own background, which on a dark terminal produced an inverted code: phone cameras read it (they try the inverse), desktop scanners such as OpenCV and ZXing with its defaults do not. From agent 0.4.158 the palette is black and white on every terminal — the 256-colour indexes a colour scheme leaves alone — where agents 0.4.154 to 0.4.157 drew the dark modules in noBGP orange. Black on white is the highest contrast a scanner can get, and it is what made a Terminal.app screenshot decodable where the orange drawing was not.
- Where no colour may be written —
NO_COLOR,TERM=dumb, or a classic Windows console that will not turn on escape-sequence processing — the code is drawn with the block glyphs▀,▄and█in the terminal's own colour. It is still printed, where agent 0.4.153 printed none at all; on a dark terminal it is inverted, so scan it with a phone camera. ⚠ The Linux text console (TERM=linux) was on that list through agent 0.4.157, because the kernel rendered one orange as two different shades; from 0.4.158 it gets the painted black-and-white code like any other terminal. Those glyphs have to be in the console font: an Ubuntu Server console without them shows diamonds, and the link above the code is the way through there. - The code is printed last, just above the
Registration key:prompt or the waiting line, so its top edge and the cursor are on screen together. nobgp loginprints the same code, and its sign-in completes on the phone the same way.
The terminal prints a code to check against the browser, from agent 0.4.153
with a router that sends one. Under the link it reads Check that the browser shows the code <code>. If it shows another code, do not approve. — and … deny the sign-in. under nobgp login, where the browser's step is a sign-in
rather than an approval of this machine. Through router 0.4.181 the browser showed
the same code before you continued, so two registrations started at once could not be
confused for each other, and a page you did not open from this terminal read as one to
refuse. Nothing is withheld
where the router sends no code: the line is simply absent and the flow is what it was.
⚠ From router 0.4.182 the approval page noBGP's router serves itself shows no code,
and names the machine instead — see nobgp login for what it carries.
Reading the machine's own details is what identifies it; a code that you typed, or that
the page was reached by, told you nothing when it was shown back. The terminal still
prints its Check that the browser shows the code … line where an older flow sends one,
so on such a router take the card, not the line, as the thing to check.
From agent 0.4.154 the code can be one you type, where the router hands the terminal
a short-code page. The screen then reads Open this URL in a browser and enter the code, or scan the QR code below:, with the page's address on one line and Code: <code> on the
next. Entering that code on the page is what picks out this machine, so there is nothing to
compare and the Check that the browser shows the code … line is not printed. The QR code
carries the address and the code together, so scanning skips the typing, and a browser the
agent opens for you goes straight to the page with the code already filled in. A router that
sends no such page — a self-hosted router with no app, or one that predates this — keeps the
sign-in link and the comparison code above, unchanged. Notes, all of them router 0.4.181 unless
one says otherwise:
- The code is four characters, from the alphabet noBGP prints every code in:
A–Zand2–7, with no0,1,8or9. Typing it is forgiving — lower case, spaces and dashes are ignored, a typed0is read asOand a1asI— so what you read off a terminal in a server room goes in as you would write it. - Each command has its own page. A
sudo nobgp registercode is entered athttps://app.nobgp.com/register, anobgp logincode athttps://app.nobgp.com/activate. A code entered on the other one is not found, exactly as a code that never existed is: enter it on the page the terminal printed. - You have five minutes to enter it, and one entry. Entering it spends it, and the rest of
the registration — picking the network, approving the machine — runs on the session it found,
within the 15 minutes below. Where nobody enters it in time the terminal stops waiting and
says so rather than sitting out the remaining ten:
the code expired before anybody entered it; run the command again. - You enter the code signed in, and that account is the one that finishes. The page signs you in first if you are not already. A sign-in then completed by a different account is refused — The code for this sign-in was entered by another account. — so a code passed on to somebody else cannot put their token on your terminal, nor yours on theirs.
- The terminal names the account that entered the code, from agent 0.4.156 against router
0.4.183 or newer, and it prints as soon as the code is entered rather than at the end:
A browser signed in as <account> entered the code. Approve there. If that is not your account, press Ctrl-C and run the command again.The account that enters the code is the one that finishes, so this is the moment to notice the wrong one — a code read out in a room, or a browser this command opened on a shared machine that is already signed in as somebody else and claims the code first. Ctrl-C cancels the registration at the router, so the approval that browser was about to give is refused. Notes:- The
Registration key:prompt is reprinted under the line, so pasting a key is still the other way through. - The account is printed as plain text with control characters and direction overrides removed,
so one address cannot be dressed up as another — the same treatment the
Approved byline gets. - Where the router does not report it, nothing is printed and the wait is what it was.
- The
- Wrong codes are limited. Five from one account in a minute (and a larger number from one
network address) and the page answers
Too many wrong codes. Wait a minute and try again.A wrong code always reads the same — unknown or expired code — whether it was mistyped, expired, already used, or meant for the other page.
The terminal names who approved it, from agent 0.4.154 with a router that says.
When the browser answers, the line reads Approved by <account> into "<network>". for a
registration and Signed in as <account>. for nobgp login — so an
approval given by somebody else, or a sign-in a stranger completed with your code, names
that account on your screen before the machine acts on it. Where the router does not say,
it reads Approved in the browser. or Authentication successful! as before. The account
is printed as plain text with control characters and direction overrides removed, so an
address cannot be dressed up as another one.
The page shows a code only for a machine that prints one, from router 0.4.180. Registering from an agent below 0.4.153 leaves both screens without one, so there is nothing to compare and the page asks you to approve the machine it names. Before, such a registration put a code on the page that the terminal never printed — and the page's own if the terminal does not show the same code, do not approve warning then told you to refuse your own machine. ⚠ On noBGP's routers such a registration no longer reaches the page at all — an agent below 0.4.153 is refused and told to upgrade, as the note under Options above says. This is what a router that still accepts one does.
The terminal claims a browser only when one opened, from agent 0.4.136. Where the agent saw the browser open, the first way through reads Continue in the browser that opened, or open this URL, or scan the QR code below:; on a machine with no browser to open — a headless server, a box with no xdg-open — it reads Open this URL in a browser, or scan the QR code below: instead. Where the router sends a code to type, both forms gain and enter the code, and where no QR code is drawn — output that is not a terminal — the clause about it is left off. The link is printed either way, so nothing is withheld. Notes:
- It waits up to two seconds to find out. A browser that takes longer than that to appear gets the shorter wording even though a window does open.
- Earlier agents said a window had just opened whatever happened, which on a headless machine was an instruction to look at something that was never there.
- On Windows the link reaches the browser whole, also from agent 0.4.136. It is handed to the operating system's own URL handler instead of to the command interpreter, which read the
&in a sign-in link as the end of the command — sonobgp loginin particular opened a truncated URL and the wrong page. Where a browser opened for you but showed something unexpected on an earlier agent, this is why.
From agent 0.4.132 the browser finishes the registration, and the terminal stops asking. The command tells noBGP what machine is about to enrol — its hostname, platform and agent version — and prints the page that machine is waiting on. Approve it there and the page says which network to join and, if you renamed it, what to call the node; the terminal takes that answer and enrols. What you approved wins over what the command would have asked for, because nobody may be watching the terminal at all.
- The page names the machine before you sign in. It shows the hostname and platform the machine reported about itself, so you can tell from a phone that it is the box in front of you before handing over an account. The agent version, and where the machine is connecting from — its public address, and the rough location that address resolves to — appear only once you have signed in. Where the address noBGP observes is not a public one, there is nothing to show there.
- The name it suggests is the name this machine would have chosen, from agent 0.4.145:
--nameif you passed one, thennode-namein the configuration file, thenNOBGP_NAME, then the hostname. ⚠ Agents 0.4.132 to 0.4.144 suggested the hostname whatever those said — and because what the page answers wins,curl … | NOBGP_NAME=web-1 shon those releases enrolled the machine under its hostname instead. Rename the node, or on such an agent register with a key, where the name you set is used as it always was. - Pick the organization, then the network. From router 0.4.163 the page groups your networks under the organization that owns each, and the machine joins the one you picked. Network names are unique within an organization and not across the organizations you belong to, so two of them may each have a
default; picking on the page is what says which. When the account has exactly one network there is nothing to choose and the page only asks you to approve.- Your personal organization is labelled by its owner — your profile name, or the email address you sign in with — from router 0.4.165, the same derived label the rest of noBGP uses. Earlier routers showed the one organization every account has as Unnamed organization.
- A name another node already holds is shown before you approve, from router 0.4.164. The page checks the node name it suggests — the one you typed, else the one the machine would have chosen for itself — against the network you picked, and refuses the approval with a free name to use instead when it is taken. Where the name belongs to a node that is offline, you can approve anyway and take that node over: the machine comes back as that node, keeping its node ID, labels, role grants and storage area, which is what you want when reinstalling the same box and not what you want for a different one. Where the name belongs to a node that is answering, or noBGP cannot rule out another claim on it, there is no such option — use the name it offers, or pick your own. Before, the approval was taken either way and noBGP settled it on its own: the machine enrolled under a suffixed name, or took the offline node over, with nothing said on the page.
- The registration belongs to the first account that opens it. Someone else signing in with the same link is told the registration belongs to another user rather than being shown the machine. Approving is one-shot as well: a second answer is refused, because the machine may already be enrolling with the first.
- Approving finishes the registration whether or not the page had to sign you in, from router 0.4.165. The approval itself gives the machine what it needs to enrol, so a page opened in a browser already signed in to noBGP completes exactly as one opened from a fresh sign-in. Before, only a sign-in handed the machine anything: a reader who was already signed in approved the page, the machine went on waiting, and the page waited out its 15 minutes for a node that could never register, with nothing on either screen saying why.
- The page follows the machine to the end, from agent 0.4.133 with router 0.4.163. The machine names the registration it is completing as it enrols, so the page shows the node and you can leave the terminal alone. On agent 0.4.132 it named nothing, and the page waited out the full 15 minutes for a node that was already online.
- The network prompt is gone when the page answered it.
--networkand--nameare still honoured, and the terminal still asks when the page did not choose — an older router, or one whose handoff could not be reached. Nothing about this is required: a router that does not know the handshake falls back to the sign-in page every earlier release used, and the command works as before. - You can paste a registration key instead of using the browser. From agent 0.4.135 the terminal opens with
Register this machine in one of two ways:and names both as a numbered choice — 1. in a browser, with the link (and the code to type, where the router sends one); 2.With a registration key: paste it at the prompt below (it is not shown)., pasted at aRegistration key:prompt on the last line of the screen. On a terminal the QR code is drawn between the two ways and the prompt, so the whole screen holds the code and the cursor together. Paste one, press Enter, and it takes the unattended key path immediately, which is the way through on a machine whose browser and phone are both inconvenient. Earlier agents printed the browser instructions and added the key as a trailing... or paste a registration key here:line, which read as a footnote to the browser rather than the second way through. The key offer appears only when the terminal can actually be typed into.- Leaving the wait ends the browser registration, from router 0.4.165 with an agent that reports it. The machine says it has stopped waiting, so the page reports the registration as cancelled rather than going on offering an approval nothing will act on, and an approval given after that is refused — the browser path cannot hand the machine a second identity while the key path is enrolling it. Pasting a key does that from agent 0.4.134; from agent 0.4.135 so do Ctrl-C and the wait running out. A registration that has already produced its node is past cancelling, so a key pasted at that point changes nothing. On an earlier router, or an agent that does not report it, the page goes on waiting until the registration expires.
- The browser way ending does not always end the registration. Where the code expired, or the registration was cancelled from elsewhere, the terminal prints
The browser way ended: <reason>and reprints theRegistration key:prompt with the rest of its wait left, because you may be on your way to fetch a key. ⚠ A Deny in the browser is the end, from agent 0.4.155 with router 0.4.182, which records that a person refused the machine rather than only that the session ended: the prompt closes and the command stops with the router's reason —registration denied in the browser by <account>; nothing was registered, or the same line without the account where the page that denied it had not asked who you were. Somebody said no to this machine, so the terminal does not go on offering the other way in. A router that does not say which of the two it was leaves the prompt open, as every earlier agent did.
- The key you paste is not shown, from agent 0.4.133: it is a credential, and the terminal may be shared, recorded or on a projector. Editing still works — Backspace deletes, Enter submits — and Ctrl-C leaves the terminal as it found it. Where the terminal will not turn echo off, the key is still accepted and is visible as you paste it.
- A blank line does not cancel anything. Pressing Enter, or closing standard input, leaves the browser as the way through rather than abandoning a registration that may be seconds from completing.
- The wait is up to 15 minutes, since it now includes a person reading a page and approving a machine. Without a handoff it is the five minutes it has always been. From router 0.4.181, where the terminal printed a code to type, it also ends after the code's own five minutes if nobody entered it — there is nothing left to approve with, so the terminal says to run the command again rather than waiting out the rest.
After successful registration the agent installs and starts the system service itself — there is no prompt. Re-registering a machine whose service has gone missing repairs it, so an uninstall/reinstall cycle converges instead of leaving a registered node with nothing running.
On a Mac, registering asks for the one grant the shared drive needs, from agent 0.4.144. Where this command's output goes to a terminal on macOS, the agent checks whether it can read its own mount and, where it is refused, opens System Settings on Privacy & Security → Full Disk Access. The terminal names the row to switch on — "nobgp" (/usr/local/bin/nobgp), the daemon's own binary — and the grant applies to the running daemon at once: nothing to restart, nothing to remount. Notes:
- It never fails the registration. The node is enrolled and running whatever you do with the pane. Skipping the grant leaves the Mac where every Mac was before this release: a working node whose file tools cannot read its own drive.
- Nothing is printed when there is nothing to grant — an agent that already reads its mount, a profile with
fs: off, and every platform other than macOS say nothing at all. - Where the drive is not mounted yet, the terminal says the window will open within a minute or two, and the agent opens it then if it turns out to be refused. It waits up to two minutes and opens the pane at most once in that window, however many times you register.
- It opens on the screen of the person who ran the command, in that user's own session, and only when they are the one logged in at the Mac.
sudo nobgp registerover SSH therefore opens no window on somebody else's screen — the terminal prints where the setting is instead. - A registration with a key asks too, from agent 0.4.145. The offer reads nothing from you — it prints text and asks the agent to open the pane — so what decides is where this command's output goes, not what is on its standard input:
install.shwithNOBGP_KEY, run at your own terminal on the Mac, makes the same offer. An enrolment whose output is not a terminal — an MDM, a script logging to a file, any other headless install — arms nothing. ⚠ Agent 0.4.144 gated the offer on standard input instead, and the install script hands registration/dev/nullthere, so the one install this exists for asked nothing at all: on a Mac enrolled by that release, runsudo nobgp registerat the machine, or grant it by hand. - Where the agent cannot be reached — it is still starting, or the service did not come up — the terminal says the grant may be needed and that
nobgp statusreports the mount as denied when it is. The check is the daemon's to make: a terminal that holds its own consent would read the mount the daemon cannot. - It runs on an already-registered node too.
sudo nobgp registeron a Mac that is already enrolled printsAgent is already registered.and then makes the same offer, which is how to get the pane on a machine that was enrolled before this release, or with a key.
Inside a container it says that nothing is running the agent, and what to run. From agent 0.4.137 the message is Container detected — no service manager here, so nothing starts the agent. Run it with: nobgp agent (for example as the container's entrypoint). There is no service manager in a container, so registering enrols the node and starts nothing; the entrypoint is what runs it. Earlier agents said Container detected — skipping service setup., which left a reader to work out whether anything was now running.
Running nobgp register on a node that is already registered prints Agent is already registered. and converges the service. Since agent 0.4.72 that is no longer true of an expired token: a node whose registration token expired while it was offline cannot authenticate, and it cannot refresh either — every refresh path needs the authenticated connection the expired token can no longer make. Such a node prints Registration token expired <instant> — re-registering. and enrolls again under the same identity key, replacing the stale token. Before 0.4.72 that command reported success and changed nothing, on exactly the node that needed it. nobgp status and nobgp show both name an expired token, so you can tell this case from an ordinary stopped agent.
A node whose noBGP name is not the machine's hostname re-registers as itself, from router 0.4.116. node-name is removed from the config file after the first successful registration, so a later nobgp register presents the machine's hostname — which is not the node's name if it was ever renamed. noBGP matches the machine on its identity key first and its name second, so the node comes back as itself and keeps the name noBGP holds for it, along with its labels, role grants, services and storage. Through router 0.4.115 only the name was matched: the lookup missed, a second node was attempted under the same key, and the command failed with enrollment failed — on exactly the machines an expired token had already stranded. It was not a rare shape: measured on a live fleet before the fix, 74 of 149 nodes carried a name that differed from their hostname. On an older router, pass --name with the name noBGP holds. See enrollment failed.
Registration and service setup are reported separately, because they can diverge:
| Exit code | Meaning |
|---|---|
0 | Registered, and the service is installed and running |
3 | Registered, but the service could not be installed or started — the reason is printed above it. Fix the cause, then run sudo nobgp service install. The node itself is enrolled; nothing needs re-registering |
4 | Registered in a container, where no service was set up and nothing starts the agent. Only when the caller asked for this code — see below |
| other | Registration itself failed |
The install script branches on the same codes, so a machine that enrolled but has no running service says so rather than reporting success.
Exit 4 is opt-in, from agent 0.4.137: the command returns it only when NOBGP_EXIT_NO_SERVICE=1 is set in its environment, and exits 0 in a container otherwise. That keeps the contract every existing caller holds — an entrypoint running nobgp register && exec nobgp agent, a RUN nobgp register line in a Dockerfile — because the registration did succeed and a new non-zero code would break each of them. install.sh sets the variable, which is how it can tell the two apart: run inside a container it now prints Agent registered. No service was set up: run the agent as shown above. where it used to report that the service had started.
The router refuses registration from agents older than 0.3.51, rejecting them with the reason agent version no longer supported. Reinstall the current agent, or run sudo nobgp upgrade -f, before registering such a machine. See Minimum Supported Agent Version.
Login vs Register
The nobgp CLI has two authentication concepts that serve different purposes:
nobgp login | nobgp register | |
|---|---|---|
| Purpose | Authenticate you (the user) | Authenticate a device (the node) |
| When to use | On your personal machine — laptop, desktop, workstation | On a remote server, VM, Raspberry Pi, or container |
| What it does | Stores your user credentials so you can run management commands (network, node, exec, shell, proxy) | Enrolls the machine as a node in a noBGP network and starts the agent |
| Requires | A browser — on this machine, or a phone scanning the QR code it prints | Either a browser or a registration key (--key) |
| Runs as | Your user account | Root / Administrator |
Typical workflow:
- On your laptop, run
nobgp loginto authenticate yourself. This lets you manage your infrastructure from the command line. - On a remote server, run
sudo nobgp register(or use a registration key) to join the machine to your network as a node. - From your laptop, use
nobgp exec,nobgp shell,nobgp proxy, etc. to operate on your remote nodes.
You don't need to install or run the agent on your laptop — nobgp login is enough to use the management commands.
nobgp login
Log in to noBGP from your personal machine. Opens your browser for OAuth sign-in and stores credentials in your system keyring.
⚠ On noBGP's routers this needs agent 0.4.153 or newer, for the reason
nobgp register gives: an older agent cannot tie the page to the
terminal that started the sign-in, so the page reads Sign-in refused and asks for
nobgp upgrade rather than signing anyone in. Run sudo nobgp upgrade -f on that
machine (on Windows, nobgp upgrade -f from an Administrator prompt), or reinstall it with the install script,
and run the command again. Nothing is signed in until you do.
Usage:
nobgp login
Credentials are stored in the platform's native credential store:
- macOS: Keychain
- Linux: Secret Service (GNOME Keyring, KDE Wallet, etc.)
- Windows: Credential Manager
Once logged in, you can use all management commands (network, node, exec, shell, proxy) without the agent running locally.
Example (agent 0.4.154 against a router that sends a code to type):
$ nobgp login
Continue in the browser that opened, or open this URL and enter the code, or scan the QR code below:
https://app.nobgp.com/activate
Code: K7QD
<QR code>
Waiting for authentication...
Signed in as you@example.com.
Login successful!
Where the router sends no such page, the terminal prints the sign-in link and a code to compare instead:
$ nobgp login
Open this URL in a browser, or scan the QR code below:
https://router.nobgp.com/auth/login?...
Check that the browser shows the code W3PN. If it shows another code, deny the sign-in.
<QR code>
Waiting for authentication...
Login successful!
The browser names the machine before you approve, from router 0.4.182. The page
you land on after signing in is the same card nobgp register uses,
and it carries what noBGP knows about the terminal that asked: the hostname the machine
reported, its platform and agent version, the public address the router saw it connect
from, and the approximate place that address resolves to. Under them it says what
approving does — Approve signs in nobgp on this machine with your noBGP account. It
stays signed in until you run nobgp logout there. — and tells you to select Deny
if you did not start nobgp login on that machine yourself. Notes:
- Deny sends nothing to the machine. The page answers Nothing was sent to the
machine. You can close this tab., and the terminal stops with
sign-in denied in the browser; the terminal is not signed in. Earlier routers ended the wait too, withthis registration was cancelled. - There is no code to compare on the page. The code is what found this sign-in —
you typed it at
https://app.nobgp.com/activate, or scanned it — so showing it back checked nothing. The machine's details above are what to read. - The address and the place are the router's observation, not the machine's claim. They are what noBGP saw the terminal connect from, so they are the one thing on the card a machine cannot assert about itself. The place is derived from the address and is approximate; the page says so. Where the router sees no public address for that terminal — behind NAT, or on a private network — both are left out rather than shown as something they are not.
- The hostname, platform and agent version are the machine's own words. They are what the agent reported about itself at the start of the sign-in, and they are shown as such.
- Your organization's SSO policy applies here too, from router 0.4.186. Where an
organization enforces SSO, a password sign-in is
refused here exactly as it is in the app: the browser is sent back to the noBGP sign-in
page carrying
error=sso_required, and nothing reaches the terminal, which keeps waiting until you press Ctrl-C or the sign-in expires. Sign in through the identity provider instead. Doing so for the first time also adds you to that organization, as it does in the app.
The URL is printed whether or not a browser opened, and on a terminal the QR code below it is the same link — so you can sign in from your phone. The sign-in completes there: this command polls noBGP for the result, so nothing has to come back to the machine you ran it on.
The code is agent 0.4.153 with a router that sends one, and it is worth reading rather than skipping. From agent 0.4.154, where the router sends a short-code page, the code is one you type there — that is what ties the page to this terminal, and nothing is compared. From router 0.4.182 the approval page shows no code at all and names the machine instead, so what tells you the page belongs to your terminal is the card described above. Either form is four characters from router 0.4.181, A–Z and 2–7; a login code is entered at https://app.nobgp.com/activate and nowhere else, and the account that enters it is the account this terminal is signed in as. See nobgp register for the rest of what the page does with it.
Signed in as <account>. is agent 0.4.154 with a router that names the account: a sign-in somebody else completed says whose it was before the credentials are stored. Where the router does not say, the line is absent and Login successful! is the whole of it.
From agent 0.4.156 against router 0.4.183 the account is named earlier as well, the moment the code is entered: A browser signed in as <account> entered the code. Approve there. If that is not your account, press Ctrl-C and run the command again. The account that enters the code is the one that can finish the sign-in, so a code somebody else picked up is visible before the approval rather than after it. Ctrl-C cancels the sign-in at the router. See nobgp register for the full note.
Where no browser opened, the first line drops the Continue in the browser that opened, clause. From agent 0.4.136 that wording is used only when a browser really did not open — see nobgp register for what the agent can and cannot tell.
nobgp logout
Clear stored credentials from your system keyring.
Usage:
nobgp logout
Example:
$ nobgp logout
Logged out.
nobgp network
Manage noBGP networks. Requires nobgp login first.
Usage:
nobgp network <subcommand> [options]
Subcommands:
list
List all your networks, nodes, and published services. Nodes are grouped by online/offline status.
nobgp network list [options]
Options:
| Option | Type | Description |
|---|---|---|
--network | string | Filter by network name |
--json | boolean | Output as JSON |
Examples:
# List everything
nobgp network list
# Filter to one network
nobgp network list --network production
# Machine-readable output
nobgp network list --json
Example output:
production
Online:
web-server (debian) 550e8400-e29b-41d4-a716-446655440000
staging-app https://a1b2c3d4.nobgp.link
api-server (debian) 660f9500-f39c-42e5-b827-557766550111
Offline:
old-server (ubuntu) 770a0600-a40d-53f6-c938-668877660222
create
Create a new network.
nobgp network create --name <name> [options]
Options:
| Option | Type | Description |
|---|---|---|
--name | string | Network name (required, DNS-compatible) |
--json | boolean | Output as JSON |
Example:
nobgp network create --name staging
add-node
Generate install commands for adding a node to your network. The output is meant to be copied and run on the target machine.
nobgp network add-node [options]
Options:
| Option | Type | Description |
|---|---|---|
--network | string | Network name (uses default if you have one network) |
--node | string | Node name (defaults to hostname on the target machine) |
--json | boolean | Output as JSON |
Example:
$ nobgp network add-node --network production --node web-server-2
── Run on the target machine ──────────────────────────
Linux/macOS:
curl -fsSL https://downloads.nobgp.com/agent/install.sh | NOBGP_KEY=... NOBGP_NAME=web-server-2 sh
Windows:
powershell -NoProfile -Command "Set-Item env:NOBGP_KEY '...'; Set-Item env:NOBGP_NAME 'web-server-2'; irm https://downloads.nobgp.com/agent/install.ps1 | iex"
───── ───────────────────────────────────────────────────
The registration key shown above (NOBGP_KEY=...) can be reused across multiple nodes and does not expire. You can disable it at any time from the noBGP dashboard.
⚠ Run the Linux/macOS line as an ordinary user, and do not add sudo in front of it (router 0.4.149). The install script elevates itself. The key can be in the system log of each machine installed with it, so rotate the key after a bulk install.
nobgp exec
Execute a command on a remote node and print the output. The remote process's exit code is propagated to the local shell.
Requires nobgp login first.
Usage:
nobgp exec -c <command> [options]
Options:
| Option | Short | Type | Description |
|---|---|---|---|
--command | -c | string | Command to execute (required) |
--node-id | string | Node UUID | |
--network | string | Network name | |
--node | string | Node name | |
--admin | boolean | Run as root. Omit it to run at the identity the node offers: its configured user where it has one, root where it has none (unelevated never means root). Refused where the owner set allow-root: false, and refused with forbidden unless you hold root on that node — its owner, an org Owner/Admin, or a root entry on its access list, from router 0.4.179. The forbidden refusal also applies to an omitted --admin on a node that offers only its root identity. On a node with allow-root: false an omitted --admin runs unelevated. --admin=false forces non-root execution (agent PR nobgp/nobgp-agent#1135; earlier agents do not send it) | |
--workdir | string | Working directory | |
--timeout | int | Max seconds to wait for command to complete (default 300) |
You must specify the target node using either --node-id or both --network and --node.
Examples:
# Run a command by network and node name
nobgp exec -c "df -h" --network production --node web-server
# Run a command by node UUID
nobgp exec -c "systemctl status nginx" --node-id 550e8400-e29b-41d4-a716-446655440000
# Run elevated in a specific directory
nobgp exec -c "git pull" --network production --node api-server --admin --workdir /srv/app
# Run a long command with a custom timeout (default is 300s)
nobgp exec -c "pg_dump mydb | gzip > /tmp/backup.sql.gz" --network production --node db-server --timeout 600
Exit codes:
The remote command's exit code is forwarded. This means you can use nobgp exec in scripts:
nobgp exec -c "test -f /etc/nginx/nginx.conf" --network prod --node web-1 && echo "nginx configured"
nobgp shell
Open an interactive shell session on a remote node. Provides a full terminal experience with raw mode, resize handling, and support for interactive programs (vim, htop, etc.).
Requires nobgp login first.
Usage:
nobgp shell [options]
Options:
| Option | Type | Description |
|---|---|---|
--node-id | string | Node UUID |
--network | string | Network name |
--node | string | Node name |
--command | string | Shell command (default: login shell) |
--admin | boolean | Run elevated; omit to run as the node's configured user |
--workdir | string | Working directory |
You must specify the target node using either --node-id or both --network and --node.
Examples:
# Open a shell on a node
nobgp shell --network production --node web-server
# Open an elevated shell
nobgp shell --network production --node api-server --admin
# Run a specific command instead of the default shell
nobgp shell --network production --node db-server --command "psql -U postgres"
nobgp shell is not yet supported on Windows clients. All other CLI commands work on Windows. You can still open shells on Windows nodes — the limitation is on the client side.
nobgp proxy
Manage published services (proxy and terminal endpoints). Requires nobgp login first.
Usage:
nobgp proxy <subcommand> [options]
Subcommands:
list
List all published services across your networks.
nobgp proxy list [options]
Options:
| Option | Type | Description |
|---|---|---|
--network | string | Filter by network name |
--json | boolean | Output as JSON |
Example:
$ nobgp proxy list
staging-app https://a1b2c3d4.nobgp.link production / web-server svc_abc123
my-terminal https://x9y8z7w6.nobgp.link production / api-server svc_def456
publish
Publish a new service with a public HTTPS URL.
nobgp proxy publish [options]
Options:
| Option | Type | Description |
|---|---|---|
--node-id | string | Node UUID |
--network | string | Network name |
--node | string | Node name |
--title | string | Service title |
--proxy-url | string | URL to proxy (e.g. http://localhost:8080) |
--command | string | Terminal command to run |
--admin | boolean | Run elevated; omit to run as the node's configured user |
--workdir | string | Working directory |
--no-auth | boolean | Disable authentication (public access) |
--share | string | Comma-separated email addresses to authorize |
--json | boolean | Output as JSON |
You must specify the target node using either --node-id or both --network and --node. Provide --proxy-url for a proxy service or --command for a terminal service. Omit both for a default shell terminal.
Examples:
# Publish a web app
nobgp proxy publish --network production --node web-server --proxy-url http://localhost:8080 --title "My App"
# Publish a browser terminal
nobgp proxy publish --network production --node api-server --title "Admin Shell"
# Publish a public (no auth) service
nobgp proxy publish --network production --node web-server --proxy-url http://localhost:3000 --no-auth
# Publish a terminal that runs a specific command
nobgp proxy publish --network production --node db-server --command "psql -U postgres" --title "DB Console"
Example output:
https://a1b2c3d4.nobgp.link svc_abc123 (proxy)
update
Update an existing service.
nobgp proxy update --id <service-id> [options]
Options:
| Option | Type | Description |
|---|---|---|
--id | string | Service ID (required) |
--title | string | Service title |
--proxy-url | string | URL to proxy |
--command | string | Terminal command |
--admin | boolean | Run elevated; omit to run as the node's configured user |
--workdir | string | Working directory |
--enabled | boolean | Enable or disable service |
--no-auth | boolean | Disable authentication (public access) |
--json | boolean | Output as JSON |
Examples:
# Disable a service (the = is required for boolean false values)
nobgp proxy update --id svc_abc123 --enabled=false
# Re-enable a service
nobgp proxy update --id svc_abc123 --enabled
# Change title and make public
nobgp proxy update --id svc_abc123 --title "Production App" --no-auth
# Re-enable authentication on a public service
nobgp proxy update --id svc_abc123 --no-auth=false
Boolean flags like --enabled and --no-auth default to true when present. To set them to false, you must use the = syntax: --enabled=false, --no-auth=false. Writing --enabled false won't work as expected.
delete
Delete a published service.
nobgp proxy delete --id <service-id> [options]
Options:
| Option | Type | Description |
|---|---|---|
--id | string | Service ID (required) |
--json | boolean | Output as JSON |
Example:
nobgp proxy delete --id svc_abc123
share
Manage the authorized email list for a service.
nobgp proxy share <action> --id <service-id> [emails...]
Actions:
| Action | Description |
|---|---|
list | Show authorized emails for a service |
add | Grant access to one or more email addresses |
remove | Revoke access for one or more email addresses |
revoke | Clear all authorized emails |
Options:
| Option | Type | Description |
|---|---|---|
--id | string | Service ID (required) |
--json | boolean | Output as JSON |
Examples:
# List authorized emails
nobgp proxy share list --id svc_abc123
# Grant access to specific people
nobgp proxy share add --id svc_abc123 alice@example.com bob@example.com
# Grant access to an entire domain
nobgp proxy share add --id svc_abc123 '*@company.com'
# Revoke access for one person
nobgp proxy share remove --id svc_abc123 bob@example.com
# Revoke all access
nobgp proxy share revoke --id svc_abc123
nobgp file
Transfer files to and from your network's shared filesystem. Works from any machine where you're logged in — the agent doesn't need to be running locally.
When the shared filesystem is mounted (e.g., /Volumes/nobgp on macOS, /mnt/nobgp on Linux), commands operate on the local mount directly for best performance. Otherwise, they fall back to the noBGP API automatically.
Requires nobgp login first.
Remote paths are relative to the network's share — except on a machine where the drive is mounted, where they are relative to the mount root. Since agent 0.4.54 that root holds node/ and networks/<name>/ rather than a single network's files (see What the mount contains), so nobgp file ls / there lists those two folders. Name the network folder explicitly — nobgp file ls /networks/production/ — to reach the same files the API path would give you.
Usage:
nobgp file <subcommand> [options]
Subcommands:
upload
Upload one or more local files to the remote filesystem. The shell expands globs, so *.txt becomes multiple file arguments.
nobgp file upload <local-path>... [remote-path]
If the last argument starts with /, it's treated as the remote destination. For multiple files, the remote path must end with / (a directory).
Options:
| Option | Type | Description |
|---|---|---|
--network | string | Network name (auto-detected if you have one network) |
-R, --recursive | boolean | Upload directories recursively |
Examples:
# Upload a single file to the root
nobgp file upload report.pdf
# Upload to a specific remote path
nobgp file upload report.pdf /docs/report.pdf
# Upload to a remote directory (note the trailing /)
nobgp file upload report.pdf /docs/
# Upload multiple files (shell expands the glob)
nobgp file upload *.log /logs/
# Upload specific files to a directory
nobgp file upload app.log error.log /logs/
# Upload a directory recursively
nobgp file upload -R ./project/ /backups/project/
download
Download files from the remote filesystem. Supports glob patterns for downloading multiple files at once — quote the pattern to prevent shell expansion.
nobgp file download <remote-path> [local-path]
Options:
| Option | Type | Description |
|---|---|---|
--network | string | Network name (auto-detected if you have one network) |
-R, --recursive | boolean | Download directories recursively |
Examples:
# Download a single file to the current directory
nobgp file download /docs/report.pdf
# Download to a specific local path
nobgp file download /docs/report.pdf ./downloads/report.pdf
# Download to a local directory
nobgp file download /docs/report.pdf ./downloads/
# Download multiple files with a glob pattern (quote to prevent shell expansion)
nobgp file download '/logs/*.log' ./local-logs/
# Download all files matching a pattern
nobgp file download '/backups/db-*.sql.gz' ./
# Download a directory recursively
nobgp file download -R /backups/project/ ./local/
Always quote remote glob patterns ('*.log') so your shell doesn't try to expand them locally.
ls
List files and directories in the remote filesystem.
nobgp file ls [remote-path] [options]
Options:
| Option | Type | Description |
|---|---|---|
--network | string | Network name (auto-detected if you have one network) |
--long | boolean | Show detailed listing (type, size, modification time) |
--json | boolean | Output as JSON |
Examples:
# List root directory
nobgp file ls
# List a subdirectory
nobgp file ls /docs/
# Detailed listing
nobgp file ls /docs/ --long
# Machine-readable output
nobgp file ls /docs/ --json
Example output:
$ nobgp file ls /docs/ --long
d 0 2026-03-15 12:00:00 drafts/
- 10240 2026-03-14 09:30:00 report.pdf
- 256 2026-03-12 15:45:00 notes.txt
rm
Remove files or directories from the remote filesystem. Supports multiple paths and glob patterns.
nobgp file rm <remote-path>... [options]
Options:
| Option | Type | Description |
|---|---|---|
--network | string | Network name (auto-detected if you have one network) |
Examples:
# Remove a single file
nobgp file rm /tmp/old-backup.sql
# Remove multiple files
nobgp file rm /tmp/old-backup.sql /tmp/cache.dat
# Remove files matching a glob pattern
nobgp file rm '/logs/*.log'
# Remove a directory and its contents
nobgp file rm /tmp/scratch/
nobgp file rm deletes files permanently. There is no confirmation prompt or trash/recycle bin.
nobgp config
Create or update the agent configuration file. If the agent is not yet registered and you are running the command by hand, it offers to register the node for you.
Usage:
nobgp config [profile] [options]
Examples:
# Interactive configuration for default profile
sudo nobgp config
# Configure a named profile
sudo nobgp config production
# Enable or disable a profile
sudo nobgp config production --enabled
sudo nobgp config staging --disabled
Options:
All global options from nobgp agent are supported, plus:
| Option | Type | Description |
|---|---|---|
--enabled | boolean | Enable this profile (default: true) |
--disabled | boolean | Disable this profile |
--yes / -y | boolean | Non-interactive; answer the confirmations for settings that can cut off remote access (agent 0.4.52+) |
--yes is deliberately not saved to the config file: it is a statement about
one invocation, and persisting it would leave a node pre-answered for every
future veto.
Values provided are saved to the platform config file (see Configuration File). Setting a value that happens to be the default — --log-level=info, say — leaves that key as a commented reference line rather than writing it, so the node keeps tracking the default. Uncomment the line by hand if you want the value pinned regardless of what a later release defaults to.
From agent 0.4.157, Configuration saved means it: a value written while the agent is recording something of its own — at registration, or when it learns its QUIC endpoint or binds its MCP port — is kept rather than overwritten by the agent's older copy of that key, and the running agent picks it up. Through 0.4.156 such a save could be reported as successful and then be silently absent. See two programs write this file.
On an unregistered node the registration offer is made only to an interactive
caller — one whose standard input and standard error are both terminals. Bare
nobgp register opens a browser and waits out the sign-in, which a script or
provisioning step has no way to answer, so a non-interactive run prints the two
commands and exits 0 instead of blocking:
Configuration saved to /etc/nobgp/default.yml
This node is not registered. To register it:
nobgp register # opens a browser to authenticate
nobgp register --key KEY # unattended, with a network key
That keeps the ordinary provisioning order working — configure first (router,
user, capability keys), then register with a key.
If the saved configuration leaves this node serving no MCP commands or no MCP file
access — an empty --allow-tools, or one that drops command or fs — the command
warns immediately after writing, rather than letting you discover it when the next
call is refused.
Example output:
Configuration saved to /etc/nobgp/default.yml
⚠ This node now serves NO commands and NO file access.
Published terminal services are refused too — they answer the same allow-tools.
The remaining way back in is on-device:
sudo nobgp config --allow-tools=fs,command
Dropping only fs is a warning, not a refusal: narrowing a node is legitimate.
Dropping command is different, and since agent 0.4.52 it is confirmed
before the write — see below. Plan the recovery either way: dropping command
closes the browser terminal along with the MCP tools, so
the way back in is on the machine itself. See Capability keys.
Four settings are vetted before they are written, because each fails far from the mistake otherwise — and because two of them can take away the only remaining way onto the node. Nothing is saved if a check refuses or a prompt is declined:
-
--usernaming an account that does not exist on the machine is refused — left to run, it becomes a refusal on every later unelevated session. Since agent 0.4.41 an account that resolves to uid 0 is refused too,--user rootincluded: unelevated never means root, so that configures a node which refuses every unelevated call while looking like it narrowed them. Name an unprivileged account, or leave--userunset and have callers passadmin. Since agent 0.4.44 the name is accepted on Windows too, where it is the node's second identity — file operations first, and from agent 0.4.46 execution as well (0.4.44 and 0.4.45 refused an unelevated execution call rather than running it as LocalSystem). IfNOBGP_USERis set in the agent's environment and names a different account, the command saves your value and warns that the environment outranks it for as long as it is set.Clearing it is
--user "". That is the one way to say "no account" through the CLI — an empty value with no--userflag is read as "never set" and re-filled with the account that ran the install. Since agent 0.4.52 the clear states what it costs first: on a node whose agent is the superuser, removing the account means everyadmin: falsecall is refused rather than narrowed, so unelevated execution stops and callers must sendadmin: "true"or omitadmin. An interactive run asks for confirmation (--yesanswers it); a scripted one proceeds, having printed the warning. The clear survives restarts but not a reinstall —nobgp service installand the post-registration path re-capture the install account deliberately, since a fresh enrollment has no other way to learn one. -
--allow-toolsthat dropscommandis refused without confirmation, since agent 0.4.52. It is the one guard here that stops a scripted caller: the change takes thecommandtool, dispatched bus commands and published terminal services away together, so unlike--allow-root=falseit leaves nothing to get back in with, and undoing it needs console, RDP or SSH access to the machine. An interactive run asks for confirmation. A non-interactive one must pass--yes— and a command arriving over MCP is non-interactive by construction, which is the case this closes: the warning printed after the write went out on the very session the write had just turned off.- From agent 0.4.153 the same change also prints what it does not close,
where
fsis still served andallow-rootis stilltrue: anfs_writeas root can write a cron job, a systemd unit or a launchd plist, which then runs as root. The line namessudo nobgp config --allow-root=falseas the way to close that door too. It is a warning, not a refusal — an owner may mean it.
- From agent 0.4.153 the same change also prints what it does not close,
where
-
--allow-toolsnaming a value that is not a capability domain is refused since agent 0.4.153, rather than dropped with a log line. The agent now fails closed on an unrecognised value, so a typo written now would be a node serving less than its owner meant, discovered later. -
--local-mcptakesonoroffonly, since agent 0.4.153, and anything else is refused before the write — the agent reads a value it cannot parse asoff, so a typo in an off switch must not be saved silently. -
--allow-root=falseprints what it costs, and it asks the resolver rather than reading the name. With a usable unprivilegeduserconfigured it just states that sessions will run as that account and superuser requests will be refused. Where the unelevated identity would itself be the superuser — nouseron a container or a bare-root install, auserthat no longer resolves,user: root, or a Windows node offering only LocalSystem — it means remote execution is off entirely (command,command_subscribeand terminal sessions all refused), so an interactive run asks for confirmation and a scripted one proceeds with a warning in the log. Both halves close on each other there: the elevated path is vetoed, and the unelevated one has no identity to land on (unelevated never means root). The same warning is repeated at every config load, naming which case the node is in — and since agent 0.4.52 it names a third: on a node whose agent is not itself the superuser,allow-root: falselocks nothing out, because an elevated request resolves to the same unprivileged identity and is served. The agent says so rather than claiming a refusal it will not make.- ⚠ A flag writes both spellings of the key.
--allow-root=falsesavesallow-root: falseandallow-admin: false, so the veto survives an agent downgrade to a release that reads only the old name;--allow-root=true, the default, removes both.--allow-adminis accepted as the old name of the flag and does the same thing. See The veto on root was renamed.
- ⚠ A flag writes both spellings of the key.
-
--lan-namescarrying a pattern that is not a valid pattern is refused, since agent 0.4.128. A bad entry makes the whole list refuse every name, so writing one would silently stop the node answering for any LAN name at all. A bare!— an exclusion that excludes nothing — is refused for the same reason. See Which LAN names this node serves.
nobgp status
Show agent status including system information and per-profile runtime status.
Usage:
nobgp status [options]
Like nobgp show and nobgp config, this command needs root: the agent's local
API socket is mode 0600, owned by the configured user (root when that is
unset), so an unprivileged run by any other account could read no profile at all
and would render an empty profile block that looks exactly like "no agent
configured".
On Linux and macOS it re-executes itself under sudo; on Windows run it from an
Administrator terminal. When sudo has no way to ask for a password — no terminal,
no SUDO_ASKPASS, no cached or NOPASSWD credential — the command fails
immediately with that explanation instead of hanging on a prompt nobody can answer.
For an unprivileged read of the same state, use the status tool on the
node's local MCP server.
Options:
| Option | Short | Description |
|---|---|---|
--json | -j | Output as JSON |
--yml | -y | Output as YAML (default) |
--ping | -p | Probe each target with ICMP and report reachability and round-trip time |
Example output (YAML):
version: "1.0.0"
platform: debian
architecture: amd64
hostname: web-server-1
config_dir: /etc/nobgp
service: running
profile:
pid: 1234
uptime_secs: 3600
registration:
node_id: "550e8400-e29b-41d4-a716-446655440000"
agent_key_id: "a1b2c3d4"
signing_key: "e5f6g7h8"
router:
url: "router.nobgp.com"
connected: true
since: "2026-08-22T09:14:02Z"
transport: quic
remote_addr: "203.0.113.10:443"
quic_endpoint: "router.nobgp.com:443"
quic_endpoint_source: learned
quic_pin: provisioned
quic_fallbacks: 0
control_reconnects: 0
network:
local_ip: "192.168.1.5"
gateway_ip: "100.64.16.1"
tun_device: "nobgp0"
domain: "550e8400-e29b-41d4-a716-446655440000.nobgp.net"
dns: local
peer_access: "as a local client (loopback and every local address); the host firewall sees TCP, UDP and ping from peers as local traffic, not as traffic from the network"
peer_loopback: true
slice6: "fd3a:9c21:4e00::/64"
ipv6: true
lan_names:
- "*"
mdns_announce:
state: "off"
targets:
- name: web-server
node_id: "770e8400-e29b-41d4-a716-446655440222"
address: "100.64.16.2"
local: false
- name: db-primary
node_id: "660e8400-e29b-41d4-a716-446655440111"
address: "100.64.16.3"
local: false
sessions:
- node_id: "660e8400-e29b-41d4-a716-446655440111"
name: db-primary
target: postgres
encrypted: true
compressed: true
- node_id: "660e8400-e29b-41d4-a716-446655440111"
name: db-primary
target: metrics
encrypted: true
compressed: true
mcp:
granted: true
local_mcp: "on"
running: true
port: 51843
allow:
tools:
- fs
- command
- logs
roots:
- /
root: true
root_source: default
without_root: deploy
fs:
type: fuse
mount: /mnt/nobgp
mounted: true
selected: auto
backends:
- "fuse: available"
- "nfs: unavailable (this kernel has no NFSv4 client (no `nfs4` in /proc/filesystems after a module load; OpenWrt: `opkg install kmod-fs-nfs-v4` matching the running kernel))"
- "webdav: available"
host_services:
reporting: true
services:
- type: _ssh._tcp
port: 22
name: SSH
source: listener
When a profile cannot be read at all, it is reported with the reason rather than silently omitted — an absent profile block would be indistinguishable from a profile that does not exist:
unavailable:
production: "agent is not running — start it with 'nobgp service start'"
The other reason is agent did not answer in time — it may be wedged; check 'nobgp service logs'.
Since agent 0.4.72, when the profile's registration token has expired the reason says so instead of leaving you with generic advice that cannot work:
unavailable:
production: "agent is not running — start it with 'nobgp service start'; registration token expired 2026-07-30T09:14:02Z — the router refuses its connections; re-register with 'nobgp register'"
That is the case where starting the agent changes nothing: it starts, the router
rejects the expired token, and the process manager paces the next attempt fifteen
minutes out — so nobgp status almost always catches the node between attempts
and, before 0.4.72, reported an ordinary stopped agent. The token is read
straight off disk, which is what lets the reason appear while the daemon is
down — the only time it is needed. The fix is
nobgp register.
The router block reports the control-channel connection to the noBGP router:
| Field | Description |
|---|---|
url | The router FQDN this node connects to |
connected | Whether the control channel is carrying, as the agent's control loop last observed it — not merely whether a connection object exists. Detection is bounded by the keepalive contract, so a link that dies silently can read true for up to 68 seconds on wss, or up to 120 seconds on QUIC, before it flips; it is never unbounded, and since / offline_for_secs below are what tell you how long the current state has held. ⚠ The QUIC figure was about 60 seconds through agent 0.4.96 and doubled in 0.4.97, when the transport's idle timeout was raised to give the node's own keepalive real margin — the priced trade is that healthy connections stop being closed under load while a genuinely dead router takes up to twice as long to show here. ⚠ Through agent 0.4.95 this field could never report false once the process had connected once: it asked whether a connection object existed, and nothing ever cleared that. A node measured 150 seconds into a total outage still reported connected: true — 84 seconds after the router had already published it as offline. On an agent below 0.4.96, trust network_directory over this field |
since | When the state connected reports began: how long this link has been up, or how long the node has been alone. An RFC 3339 UTC instant, not an age, because the question it is there for is comparing the two sides of one connection — compare it against the same node's offline_at in network_directory while the link is down, and against its online_at (router 0.4.108+) while it is up, which is the row that says whether both sides are talking about the same session. Absent before the control loop has observed anything at all. Agent 0.4.96+ |
offline_for_secs | How long the link has been down, in seconds. Present only while connected is false. Redundant with since on purpose: the reader is usually someone at a terminal on a misbehaving node, and making them subtract two timestamps to learn how long it has been alone is how a diagnostic goes unread. 0 is a real answer — an outage under a second — not an absence. Agent 0.4.96+ |
transport | The control-channel transport: quic or wss. Answers "is this node on QUIC?" — the current connection while connected, the most recent one while not, since it was on QUIC when it dropped is worth more during an outage than a blank. Empty only before this process's first connection. This field only ever describes the running process; since agent 0.4.115 the agent also writes one info line per successful registration — control channel registered over quic (<address>) — so the same answer survives in the log after the fact |
remote_addr | The remote address of the control connection — current or most recent, as transport above. Shows which address family is actually in use (native IPv6 vs IPv4) and which router endpoint is serving the node |
quic_endpoint | The host:port the QUIC leg dials. Omitted when the router URL cannot be parsed |
quic_endpoint_source | Where the quic_endpoint value came from: override (a --quic-router/NOBGP_QUIC_ROUTER setting), learned (supplied by the router at registration), or derived (the router URL's own host, used before an endpoint is learned) |
quic_pin | Trust state of the certificate pin that gates every QUIC attempt: provisioned (set via NOBGP_QUIC_CERT_PIN), learned (received at registration), or none. none means QUIC is skipped entirely until a pin is learned — the usual reason a node stays on wss. From agent 0.4.128 nobgp register records the pin from the registration response itself, so a node that has just registered — a container that registers with a key on every start included — reads learned on its very first connection rather than after a WebSocket one. Against a router too old to send it there, the WebSocket bootstrap is unchanged |
quic_fallbacks | How many times this process attempted QUIC but registered over WebSocket instead (for example, on a network that blocks UDP). 0 means QUIC has been used throughout, or the node is on a wss-only transport |
control_reconnects | How many times this process re-established a dropped control connection, on whichever transport it was using. A rising count means the connection keeps flapping even though recovery succeeds — read it beside transport to know which one is flapping. Named quic_reconnects through agent 0.4.82, when only a QUIC connection was re-established in place; a WebSocket node's dropped connection ended the process instead, so the counter could only ever describe QUIC. From agent 0.4.83 it covers both, which is why it no longer carries the QUIC name |
udp_blocked | Present and true only when the agent has recently observed that UDP is blocked on this network, which makes the next connect skip the QUIC attempt. Omitted otherwise |
tls_intercepted | Present only when something re-signed the control channel's TLS connection — antivirus HTTPS scanning or a corporate proxy. The value names the certificate authority that did it (inspection products identify themselves there). Omitted on a clean connection, and never reported for a self-hosted router, whose certificate authority is yours to choose. See Security software and VPNs |
The mcp block is the on-box answer to "why is my MCP off" — see Local MCP Server:
| Field | Description |
|---|---|
granted | The router's word: true when someone who controls the node has turned its local MCP on with node_grant. The grant is what starts the local server, so false means there is nothing on 127.0.0.1 yet |
local_mcp | The node owner's own switch, on or off — the local-mcp key. off with granted true is granted, refused by the owner: the server does not run, whatever the router grants. Read it before reading running: false as a failed bind. Agent 0.4.153+ |
running | true when the listener is actually up. granted true with local_mcp on and running false means the listener could not bind |
port | The port the server actually bound, which is not always the configured mcp-port — a taken port moves. Shown only while running is true, so a remembered port never reads as a live endpoint |
The allow block is the node owner's veto set as the agent is applying it right
now — what this box will refuse before you dispatch anything to it. It covers every
remote path onto the node: the MCP tools and event-bus sources a
node_grant reaches, and, since agent 0.4.33, a
published terminal service as well. See
Capability keys:
| Field | Description |
|---|---|
tools | The capability domains this node serves: fs, command, logs. All three by default; logs is a domain from agent 0.4.153 |
tools_invalid | Present and true only when allow-tools in the profile names something that is not a domain. The value is dropped and, with nothing valid left, the node serves nothing — so the flag is how a typo is visible rather than mistaken for a deliberate lockout. Agent 0.4.153+ |
roots | The filesystem roots the fs tools and the fs event source may touch. / (the whole filesystem) by default, always excluding the agent's own configuration directory |
root | true when callers may ask for execution and file access as the superuser. false refuses such a request rather than downgrading it. Named admin through agent 0.4.152, and the old name is still reported beside it |
root_source | Which key the value above was read from — default, allow-root, allow-admin (the old name of allow-root), both, or allow-root and allow-admin disagree; the stricter value applies. Agent 0.4.153+ |
without_root | The account a root: false call actually runs as on this node. Since agent 0.4.42, as unelevated, which is still reported beside it. Read from the same gate the real call uses, not from the user key — a configured account that was deleted, or one that resolves to uid 0, reads fine in the config and is refused at the call |
without_root_refusal | Present instead of without_root when work without root cannot run here at all, naming which shape it is: no account on a root-running agent, an account that no longer resolves, or user: root. Its presence is the whole "every call on this node runs as root" signal — see unelevated never means root. Since agent 0.4.42 as unelevated_refusal, still reported beside it |
without_root_is_administrator | Present only when true: the account in without_root is itself administrative — a member of the local Administrators group on Windows — so root: false on this node drops from the ambient identity to Administrator and no further. It is the on-box half of the directory's info.user_is_admin, and its absence is not a claim that the account is ordinary. Since agent 0.4.46 as unelevated_is_admin, still reported beside it |
warning | Present only when the owner has narrowed this node's own remote remedy — an empty tools, or one without command. It names what is off and says plainly that published terminal services are refused along with the tools, so re-enabling is an on-device job |
⚠ The allow block renamed five fields in agent 0.4.153, when the setting itself became allow-root. admin → root, unelevated → without_root, unelevated_refusal → without_root_refusal, unelevated_is_admin → without_root_is_administrator. Every old name is still reported, with the same value, so a script that reads one keeps working — but do not read both and expect them to be two different facts.
The fs block reports the shared drive on this node:
| Field | Description |
|---|---|
type | The backend that actually mounted the drive: nfs, fuse, winfsp or webdav. Empty when nothing is mounted |
mount | Where it is mounted. This is the path in use, which on Windows is not always the configured drive letter — a letter already holding a foreign drive is left alone and the mount moves to the first free one |
mounted | The backend's own liveness answer rather than a generic guess — each backend knows where its mount really is and, on Windows, which logon session owns it. A mount must fail three consecutive probes, about a minute apart, before the agent tears it down and remounts. From agent 0.4.82 an nfs mount is asked two questions rather than one: whether the kernel still has anything mounted at the path, and whether the path answers. An empty directory answers the second, so a mount that vanished while its mount point stayed behind used to read true forever — see a healthy nfs mount on an empty folder |
selected | The fs config key in effect: auto unless the backend is pinned. Agent 0.4.57+ |
backends | One line per backend, in this platform's preference order, best-first, saying whether it could serve on this host and, when it could not, what is missing and what to install. From agent 0.4.65 it holds only the backends that can run on this platform at all, so a Mac lists nfs and webdav (in that order from agent 0.4.66; 0.4.65 alone had webdav first) and a Windows node winfsp and webdav. A backend that could serve but is not finished enough for auto to choose it says both halves and names the pin that would use it. No backend is in that state from agent 0.4.82, which is when winfsp — the last one that was — became what auto picks on Windows. Through 0.4.81 its line named node-local locks and a slow file create, and that reason had been rewritten as each item was closed: agent 0.4.74 alone also named case-sensitivity, which 0.4.74 itself had fixed and 0.4.75 took off the line; through 0.4.72 it named a write path that lost data, already fixed in 0.4.69; and before that one that had never been exercised on a real mount. From agent 0.4.73 a WinFsp older than 1.10 reads as unavailable here, naming the version found and the upgrade, rather than as a backend the node could mount with. Reported even when nothing is mounted, which is when it matters most. From agent 0.4.70 a Linux node's nfs line is about the kernel's client alone, since the mount helper package is no longer required there — see what nfs needs on a host. Agent 0.4.57+ |
no_fallback | Present only when this host can run exactly one backend, and its presence is the warning: if that mount fails there is nothing to switch to. Every other platform degrades — Linux fuse → nfs → webdav, macOS nfs → webdav, Windows winfsp → webdav where WinFsp is installed — so the line names the one backend that can serve and, where the condition is fixable, what would end it: a Windows node without WinFsp is one deep on webdav and is told to install WinFsp, while a Synology, whose only package manager carries neither a WebDAV nor an NFSv4 client, is told plainly that there is no second one and no way to add one. It describes the host, not the mount, so a perfectly healthy node prints it — which is the point, since it is the warning you want before a mount fails rather than after. It counts what could be pinned, not what auto would take, and is absent when no backend is available, which mounted and error already speak to. Absent as well on a node whose local WebDAV proxy is not listening — fs: off, no mount point, or a proxy that failed to bind — because WebDAV then reads as unavailable for a reason that is not the host's, and backends above still reports each backend's own answer. Agent 0.4.83+ |
note | Present only when the mount is up and healthy and a person at the machine will still see something else. Today it has one cause, on Windows: a logon session's own drive mapping shadowing the mount's letter for that user, so the file tools reach the volume and mounted: true is honest while Explorer in that session opens whatever the mapping points at. The line says which case it is — a stale noBGP WebDAV mapping the agent could not remove from that session, or a mapping that is not noBGP's at all and was therefore left alone — and gives the remedy: net use <letter>: /delete inside that session, or mount: set to another letter. It is not an error and not an unhealthy mount, which is why it has its own line rather than joining error. Agent 0.4.94+ |
unreadable | Present only when the drive is mounted and the agent cannot read through it, and its presence is the finding. mounted: true was the whole problem: such a mount is genuinely healthy for everyone else on the machine, while every file tool and command call under it fails. On macOS the line names the consent layer and gives the Full Disk Access grant in full, including that sudo fails identically because root is not special to it; on a Windows webdav drive it says the mapping is scoped to the logon session that made it — so the drive is probably fine in that user's own desktop while nothing arriving through noBGP sees a drive at all — and points at fs: winfsp for a machine-wide volume. A probe that cannot answer within two seconds says the mount may be wedged and states plainly that this is not a permission verdict, because a wedged mount and a refused one need opposite remedies. Absent while the drive reads normally, and absent when nothing is mounted — an unmounted drive has its own line and blaming consent for it would point the wrong way. It is the on-box half of the directory's info.mount_readable, for the operator at the machine who is the only one who can fix it — see When the agent cannot read its own mount. Agent 0.4.93+ |
locking | What a lock taken on this mount is actually worth, in the operator's terms — one sentence naming whether locks reach other nodes and, where they do not, what still holds between programs on this machine. Reported for the backend the node is mounted with, never for the ones in backends above, and absent when nothing is mounted. It exists because the mount itself will never tell you: a lock the drive cannot enforce still returns success, so the program that took it carries on believing it. Every backend answers, including the two whose answer is good news — see what each backend does with a lock. Agent 0.4.79+ |
conflicts | One line per write this mount could not save and no later write has resolved, in path order — the answer to "what is this node holding that never reached noBGP". Each line names the file, where its unsaved bytes were kept, when the write was refused and why. A line that says nothing was kept and nothing was lost is not a failure to keep anything: it is a refusal caught before any of your bytes were accepted, so the write call itself failed and the data is still your program's. Absent on a mount that is holding nothing. A webdav mount joins from agent 0.4.147, when its proxy started conditioning the operating system client's saves and keeping the bytes of one noBGP refuses; that mount keeps no cache, so its lines are read from the notes the proxy wrote beside those bytes. Below 0.4.147 a webdav mount refused nothing and so listed nothing. A record is cleared by exactly one thing: a later write of that same file reaching noBGP. From agent 0.4.95 two more things reach this list: an upload noBGP has been refusing for more than two minutes, which is still queued and still being retried and says so; and a recorded rename, mkdir or delete that noBGP then refused, whose line says the operation was not applied rather than describing a loss of bytes — there were none at stake — and is cleared by a later operation landing at that name. See what a node could not save. Agent 0.4.80+ |
pending | One line per namespace operation this mount has accepted from a program and noBGP has not taken yet — a rename, a mkdir or a delete — oldest first, each naming the operation and when it was accepted. They are recorded in the node's cache, survive a restart, and are replayed in the order they were made. A non-empty list on a node whose connection is healthy is the thing to look at: an operation noBGP refuses is abandoned at once and reported under conflicts above, so anything still waiting here is waiting on the link rather than on a verdict. Absent when nothing is pending, and absent on a backend that does not record them — today a Linux fuse mount is the only one that does. See when noBGP cannot be reached. Agent 0.4.95+ |
error | Why there is no mount, when there is none — a pinned backend the host cannot serve, a failed mount, a backend that declined. From agent 0.4.147 it also names an attempt that was stopped because it did not finish within two minutes, which is what a hung mount helper looks like — on any backend, and see Which backend mounts the drive for what happens next. Empty while mounted and empty before the first attempt, so its absence means nothing has gone wrong yet rather than that nothing is wrong. Without it the block contradicts itself: mounted: false beside a backend reported as available. From agent 0.4.70 an nfs mount point that is busy but is no longer a mount names the processes holding it, which is what must exit before the drive can come back; from agent 0.4.75 that answer comes from webdav too, where a webdav mount previously reported the wedge as a decline and left this field empty, and from agent 0.4.78 from fuse, the leading backend on Linux. From agent 0.4.151 any webdav mount that was attempted and failed on macOS or Linux names its own failure here, and the log says the node will retry; through 0.4.150 it read webdav declined to mount at …, which is the answer reserved for a host that has nothing to mount with — see Which backend mounts the drive. winfsp is the late one: its driver returns no reason at all when a mount is refused, so through agent 0.4.92 this field listed three possible causes and left you to pick between them. From agent 0.4.93 it names the one thing Windows can be asked — whether the drive letter is occupied right now. Occupied means something took the letter after the agent had just found it free, or a stale mapping is still pinned to it: check net use and Get-PSDrive, and clear a leftover with net use <letter>: /delete. Free means this is not a letter conflict at all, which leaves the WinFsp driver not starting or mount options it rejected — a WinFsp older than 1.10. ⚠ It says the letter is taken, never by whom: naming the holding process on Windows is not something the agent can do. Agent 0.4.59+ |
The network.overlay field is present only when this node has no overlay at
all, and its presence is the finding — the value says why. From agent
0.4.112 a node that cannot pick a /20 out of 100.64.0.0/10 comes up
anyway, without one, and from agent 0.4.142 so does a Linux node whose
host will not give it a tunnel adapter at all. Either way: no TUN interface, no
local DNS server and no peer traffic, while the control channel stays up, so
command, file and nobgp status keep working and the machine can still be
repaired from off-box. Before those releases the matching condition ended the
process instead, over and over, which took away the one route anyone had to a box
whose only defect was that it could not build an overlay.
network:
local_ip: "192.168.1.5"
overlay: "no overlay: no free /20 slice available in 100.64.0.0/10 (host is fully claimed)"
dns: unavailable
ipv6: true
Four conditions reach it — the first two are about the overlay address, the last two about the tunnel adapter:
| Cause | What it means |
|---|---|
no free /20 slice available in 100.64.0.0/10 (host is fully claimed) | Another product on the machine claims the whole range — see Peers Are Unreachable on a Machine Running Another Overlay VPN |
cannot auto-pick an overlay slice: enumerating host routes: … | The agent could not read the machine's routing table, so it has no evidence that any /20 is free and will not invent one. Agent 0.4.112+; before that a failed scan read as an empty routing table and the agent took 100.64.0.0/20 on the strength of having seen nothing |
no permission to create a TUN device | The host refused the tunnel adapter — a container or pod without NET_ADMIN, which AWS Fargate, Cloud Run, Azure Container Instances and Kubernetes pods under a restrictive PodSecurity profile all withhold. Linux, agent 0.4.142+; before that the agent exited at startup and kept restarting |
this host has no TUN device | There is no /dev/net/tun for the agent to open, and the agent may not create one — a pod under the PodSecurity restricted profile, for example. Linux, agent 0.4.142+, same history |
The first two have the same shape of remedy: free space in 100.64.0.0/10,
repair whatever broke route enumeration, or pin
overlay-cidr to a /20 you know is free —
a configured slice is honoured even when the route scan cannot verify it. The
last two are fixed where the container is defined: grant NET_ADMIN and pass
/dev/net/tun through, as in the Docker
examples. Then restart the agent.
⚠ The last two reach a Linux node only, from agent 0.4.143. On macOS and Windows a refused tunnel adapter fails startup, as it did before 0.4.142 — there the same refusal can be a passing condition (an antivirus holding the adapter, an adapter still being removed after an upgrade), and the agent does not retry the adapter, so a node that carried on without one would stay without an overlay until somebody restarted it. Agent 0.4.142 alone came up without an overlay on all three platforms. Every platform this mode is for — the container runtimes above — is Linux.
Read this field before reading a missing gateway_ip or tun_device as a fault
of their own; local_ip is the host's own LAN address, which such a node still
has and still reports.
The network.dns field reports how overlay hostnames resolve on this host:
| Value | Meaning |
|---|---|
local | The agent's built-in resolver answers overlay names on this node — resolution stays on the box and peer names resolve normally. |
unavailable | The agent could not configure DNS on this host, so overlay names do not resolve here. You can still reach peers by their overlay IP (see nobgp status targets), and all other functionality is unaffected. ⚠ A node reporting overlay above is always unavailable here for a different reason — there is no overlay to resolve names on at all, so reaching peers by address does not work either. Read overlay first. |
The network.peer_access field is how another node reaches this machine's own
services (agent 0.4.140+), stated by the running agent rather than worked out
from configuration. The value is a sentence; these are the four it can begin
with:
| Value | Meaning |
|---|---|
as a local client … | A peer's TCP, UDP and ping arrive as a local connection the agent opens on this machine, so a peer reaches what a local program reaches — a service on 127.0.0.1 included — and the host firewall does not filter it. This is the ordinary state from 0.4.140. |
none (socket delivery did not start, …) | The agent could not start that delivery, so from agent 0.4.144 this node drops every packet a peer sends it — the agent's log says why. Agents 0.4.140 to 0.4.143 said through the overlay interface, at the primary address only … here and did that instead: peer traffic arrived on the tunnel adapter at the machine's primary address, where the host firewall applied to all of it, as in every release before 0.4.140. |
none (this node has no overlay; …) | The node has no overlay at all, so no peer reaches it. network.overlay above says why. |
unknown (the running agent is an older version …) | The agent predates 0.4.140 and does not report this. The CLI never guesses a value here. |
What the first value costs and what it buys is
How another node reaches this machine's own services;
the short form is that
windows-firewall: respect-windows, ufw, firewalld and pf no longer filter
TCP, UDP or ping from peers on such a machine, and that the agent's own ports
stay refused to them regardless.
The network.peer_loopback field beside it is the owner's
peer-loopback setting as the agent is applying it (agent
0.4.153+): true, the default, is the reach peer_access above describes;
false is the veto, and a peer is then refused every port bound only on this
node's loopback. The field is absent on an older agent rather than false —
such an agent refuses nothing, and a false would say that it does.
The network.slice6 field is this node's IPv6 overlay slice — a unique local /64 inside fd00::/8, beside the IPv4 /20 (agent 0.4.132+). Peer names resolve to an address in it as well as to their IPv4 one.
network.overlay6 appears instead, and only when the node has no IPv6 slice, with the reason — off (overlay-ipv6: false), IPv6 is not available in this kernel (ipv6.disable=1?), IPv6 is disabled (net.ipv6.conf.all.disable_ipv6=1), or a /64 that is already routed on the machine. Such a node runs its overlay on IPv4 alone and is otherwise unaffected: this is never a failure of the node, and overlay above is the field that reports having no overlay at all. Exactly one of the two is present.
The network.ipv6 field is a different question from either of those — it is true when the host holds a usable global IPv6 address, which is what lets the QUIC transport attempt an IPv6 connection to the router. A false value explains why a node only ever connects over IPv4: there is no usable global IPv6 source on this host (a private or link-local–only address does not count). It says nothing about the overlay slice, which is private to the machine and needs no IPv6 connectivity at all.
The network.lan_names field is the lan-names list in
force — which host names on this machine's own LAN it will resolve for the rest
of the network (agent 0.4.128+). It is always present, and [] rather than
absent when the answer is none, because none is the value most worth reading
back. ["*"] is the default and means any name. A lan_names_invalid line
appears beside it when the list holds a pattern the agent cannot parse, which
makes it refuse every name — only a hand-edited profile or an environment
variable reaches that state, since nobgp config refuses a bad pattern before
writing it.
The network.mdns_announce block reports the local-only mDNS
announcer (agent 0.4.128+), so an owner can tell off from on
and not publishing without reading the log:
| Field | Description |
|---|---|
state | off (mdns-announce is false, the default), unsupported (on, but this platform has no announcer — Windows), or on |
registered | Registrations the machine's own responder holds right now — one per peer name, plus one per service announced for a peer. Only while on |
waiting | Registrations not published yet: one retrying after a failure, and on Linux any held while avahi-daemon is off the bus, which is the ordinary state of a headless machine. registered: 12, waiting: 0 on a host with no responder would be a lie, which is what this field exists to prevent |
capped | true when there were more names and services than the 128-registration cap, so some are not announced |
The host_services block is what this node reports to the network about the
services its own host offers (agent 0.4.128+) — see mDNS
keys:
| Field | Description |
|---|---|
reporting | The mdns-report-services setting. false means the owner turned reporting off, so the node looks for nothing and reports an empty list |
services | What it is reporting: type (the DNS-SD service type), port, the instance name, and source — mdns when the host's own responder announces it, listener when the node found something listening on that port and the host announces nothing, in which case the name is a default and the protocol on the port is not confirmed |
pending | true while the first look has not finished — a pass takes a couple of seconds after the agent starts |
error | Why the last pass could not look. The list above is then the last one a pass did find, which is deliberately kept rather than cleared: an empty report would take this machine's shares off every peer |
On macOS and Windows, the network block also includes a route field reporting overlay datapath route health:
| Value | Meaning |
|---|---|
ok | The kernel routes the overlay slice through the noBGP TUN interface — the datapath is healthy. |
missing | Overlay-bound packets would escape via another interface instead of the TUN, so the node is silently unreachable on the overlay. What to do about it depends on the platform — see below. |
On macOS, missing means the route was lost (typically an upgrade-restart race between the old and new TUN). The agent re-installs it automatically within about a minute; if a missing value persists, run sudo nobgp service restart.
On Windows, the overlay route is the TUN adapter's own connected route, so it cannot simply be lost — missing means another product's route is winning, almost always a full-tunnel VPN client. The agent deliberately does not try to repair this: it cannot outbid the other route, and deleting someone else's route is not its call. Fix it on the VPN side (split-tunnel, or exclude 100.64.0.0/10) — see Security software and VPNs.
The field is omitted on Linux, where the overlay route cannot be stranded the same way, and on any platform where the probe itself could not run.
On macOS, a node with an IPv6 slice also reports route6, which answers the same two values for the IPv6 slice's own route (agent 0.4.132+). The two routes are installed separately and are lost separately, so one field cannot speak for both — and a node whose IPv6 route has gone still answers AAAA for every peer, which is what makes it worth reading. It is repaired on the same cadence. The field is omitted on Linux and Windows, where the IPv6 route cannot be stranded that way, and on a node with no IPv6 slice.
Peer targets
The network.targets list shows the peers this node currently holds a live
overlay address for — one row per peer name, with the address your local
applications use to reach it.
| Field | Description |
|---|---|
name | The peer's name |
node_id | The peer's node ID — the identifier that is valid everywhere, and what every MCP tool takes as a target. Omitted for a target with no node behind it |
address | The overlay address to use for this peer from this node. Unlike node_id this is node-local: each node mints its own handles, so the same peer has a different address on every node and an address carried elsewhere points at the wrong machine |
local | true when the target is served by this node itself |
Only peers present in the most recent directory update from the router are
listed. When a peer goes offline (or is deleted), it drops out of targets on
the next update — but its overlay address is reserved, not released: the
agent keeps the address held for that name so a returning peer gets the same
address back, and any app still holding the old answer keeps working. A
reserved address is not shown in nobgp status because the node is no longer
known to be reachable. Reserved addresses are released after 7 days without
the router vouching for the name.
A deletion now reaches a node that was away, from router 0.4.113. noBGP tells a network's members when a peer is deleted, and until this release only the members that were connected at that moment heard it — a node's own directory only ever gains entries, so one that was offline, or a container that rebuilt its directory from empty, could go on listing a peer that no longer exists until something unrelated overwrote the name. It resolved, and sessions opened against it went nowhere. Deletions are now queued for 30 days and replayed to a node immediately after it reconnects, including the ones that happen as a side effect of removing a whole network. Nothing on the node changes and no agent release is involved.
From agent 0.4.104 an address reaches that same reserved state by a second
route: the router stated how long its answer for the name stays authoritative,
that lease has passed, and nothing on this machine has addressed it for 30
minutes. Such an address is likewise held rather than released — the 7-day window
is still the only thing that releases one — so the row simply disappears from
targets until the next lookup re-confirms it. See
Overlay addressing.
If a name a peer used to serve is republished on this node, targets shows
the local row for it, and the peer's reserved address is released as soon as
the router confirms the peer no longer advertises the name — immediately when
that peer is online, on its return otherwise.
Name lookups behave the same way — see nobgp resolve.
Peer sessions
The network.sessions list shows the live mesh sessions this node currently
holds to its peers. Idle sessions that have been recycled are not listed.
| Field | Description |
|---|---|
node_id | The peer node's ID |
name | The peer's name, resolved from the mesh directory (omitted if unknown) |
target | The target this session carries traffic for |
encrypted | true if the session's data channel is encrypted |
compressed | true if the session's data channel is compressed |
Every session is scoped to a single target, so a peer may appear on more than one row — one per target you are actively communicating with.
Probing reachability
Pass --ping (or -p) to send one ICMP echo to each target and report whether
it responded and how long the round trip took. Reachability is an end-to-end
test: the probe routes through the overlay mesh to the peer and back. Because the
first echo to a cold peer triggers mesh-session setup, the probe allows up to a
few seconds per unresponsive target before giving up.
nobgp status --ping
When probing is enabled, each target gains two fields:
| Field | Description |
|---|---|
reachable | true if the target answered the ICMP echo, false otherwise |
rtt_ms | Round-trip time in whole milliseconds (integer); present only when reachable is true |
targets:
- name: web-server
node_id: "770e8400-e29b-41d4-a716-446655440222"
address: "100.64.16.2"
local: false
reachable: true
rtt_ms: 12
- name: db-primary
node_id: "660e8400-e29b-41d4-a716-446655440111"
address: "100.64.16.3"
local: false
reachable: false
Probing each target requires raw-socket privileges (CAP_NET_RAW, root, or
Administrator). If the probe cannot run, targets are returned without the
reachable/rtt_ms fields rather than being reported as down.
Environment interference
Other software on the machine is the most common cause of a node that registers
fine but behaves oddly. The environment block names what the agent found:
environment:
tunnels:
- "Norton VPN Wintun (169.254.12.7)"
security_products:
- "Norton 360"
| Field | Description |
|---|---|
tunnels | Another product's active VPN or tunnel adapter, with the address it currently holds — the usual reason overlay traffic goes missing while the control channel looks healthy. Only adapters holding a real address are listed; the agent's own interface, sibling profiles, and other overlays inside 100.64.0.0/10 are excluded, as are the operating system's own transition adapters (Teredo, ISATAP, 6to4) and PPP-based uplinks such as DSL or mobile broadband, which are the machine's own internet connection rather than someone else's tunnel |
security_products | Third-party antivirus or firewall registered with the operating system. Windows reads Security Center; macOS lists activated security and network system extensions; Linux reports nothing here |
The block is deviation-only: when nothing is detected it is omitted from the
output entirely, so its absence on a healthy machine is the expected result, not
a missing field. Stock Windows Defender is filtered out of security_products
for the same reason — this is a report of what differs from a normal machine,
not an inventory of installed software.
The list is answered from a cache refreshed in the background, so nobgp status
keeps answering promptly even when the operating system's own query is slow, and
a product installed moments ago can take a few minutes to appear.
See Security software and VPNs for what to do about each kind of interference.
nobgp resolve
Resolve a peer name to its overlay address by asking the running agent directly.
The answer comes straight from the mesh directory, bypassing the operating
system's DNS resolver — so you get the deterministic overlay address even on
hosts where a LAN search domain shadows bare names or where overlay DNS is
otherwise unavailable (see nobgp status).
Usage:
nobgp resolve <name>
The name may be a bare peer label (web-server) or the node's own fully
qualified overlay name. The resolved address is printed alone on stdout, so it
composes cleanly into scripts:
ssh admin@$(nobgp resolve web-server)
Options:
| Option | Short | Description |
|---|---|---|
--json | -j | Output as JSON (adds the node_id, fqdn and found fields) |
Examples:
# Print the overlay address for a peer
nobgp resolve web-server
# 100.64.16.2
# JSON output with the canonical FQDN
nobgp resolve web-server --json
JSON output:
{
"name": "web-server",
"node_id": "770e8400-e29b-41d4-a716-446655440222",
"address": "100.64.16.2",
"fqdn": "web-server.550e8400-e29b-41d4-a716-446655440000.nobgp.net",
"found": true
}
node_id is the peer's node ID — the identifier that is valid everywhere, and
what MCP tools take as a target. The address is node-local by design: the same
name resolves to a different address on every other node, so never carry one
between machines.
A name that exists in the directory but has no address assigned yet returns
"found": true with an empty address — retry shortly. A name that is not part
of this node's overlay reports as not found.
Peers that are currently offline. When a peer drops out of the router's
directory, the agent keeps its overlay address reserved (see
Peer targets) but stops treating it as a served answer. On the
next lookup — through nobgp resolve or overlay DNS — the agent asks the router
to arbitrate:
- The peer still exists (it is merely offline): you get its address back — the same one it had before.
- The peer was deleted: the name reports as not found immediately, rather than waiting out the 7-day reservation window.
- The router cannot be reached: the reserved address is served anyway, so a transient control-channel blip never makes a known peer disappear from DNS.
The same three-way arbitration applies to a name whose router-stated lease has lapsed while nothing on the machine was using it (agent 0.4.104) — the address is reserved rather than served, and the next lookup re-confirms it.
A name that has moved onto this node resolves to the local address as soon as the name is published here, without waiting for the peer's reservation to be settled — see Overlay addressing.
Names your own LAN also answers. From agent 0.4.116 a name this machine
reaches on its own LAN is not answered with an overlay address, so a lookup of
such a name through the system resolver reaches the device directly. From agent
0.4.139 overlay DNS hands back the device's LAN address itself, where
earlier releases answered not found and left the system resolver to find it
— see A name this machine reaches itself. nobgp resolve
does not do that: it answers from the mesh directory by design, so it still
prints the overlay address for a name the overlay holds. Use it to ask what the
overlay says, and an ordinary lookup (getent hosts, ping) to see what your
applications will get.
Exit Codes:
0- The name resolved to an address1- The name was not found, is not in this node's overlay, or no agent is running
Like nobgp status, this command queries the agent over its local API socket,
which is owned by root — so it elevates the same way, re-executing itself under
sudo on Linux and macOS.
nobgp peers
List the peer keys this node has pinned, and accept a new key for a peer that was rebuilt. Agent 0.4.98+ — see Peer key pinning for what the pinning does and does not catch.
Usage:
sudo nobgp peers [profile] [options]
Options:
| Option | Short | Description |
|---|---|---|
--forget <ref> | Drop the pin for one peer, named by node ID or name, so its next key is accepted and pinned | |
--forget-all | Drop every pin. Each peer reverts to first contact | |
--json | -j | Output as JSON (mode plus the peers array) |
Example output:
$ sudo nobgp peers
peer-key-pinning: warn
(a changed key is reported and the session PROCEEDS — set `enforce` to refuse)
NODE ID NAME FINGERPRINT FIRST SEEN LAST SEEN
660e8400-e29b-41d4-a716-446655440111 db-primary 9f2a1c74be03d581 2026-08-01 09:14Z 2026-08-23 11:02Z
770e8400-e29b-41d4-a716-446655440222 web-server 4c81d0aa27f6b93e 2026-08-04 15:40Z 2026-08-23 10:58Z
A node with nothing pinned yet says so — one pin is recorded the first time this node talks to each peer, so a node that has not reached any peer has an empty list rather than a problem.
The fingerprint is the peer's own agent_key_id. It is the same value that
peer prints under registration in its own nobgp status, so the
two can be compared character for character. Compare them on that peer's
console — read back through noBGP's own surfaces the comparison proves nothing,
because those are the party the check is aimed at.
Accepting a rebuilt peer. Re-provisioning a node gives it a new key under the same node ID, so an ordinary rebuild looks exactly like a substituted key and nothing on the wire tells them apart. Once you have confirmed the new fingerprint at the peer, accept it here:
# By node ID or by name — whichever the mismatch report gave you
sudo nobgp peers --forget db-primary
forgot 660e8400-e29b-41d4-a716-446655440111 (9f2a1c74be03d581)
The next contact with that peer pins whatever key the router offers.
That has to be done on each node that had already talked to the rebuilt peer — a pin is per node, and there is no fleet-wide accept.
--forget-all is the blunt version and says what it costs: every peer reverts to
first contact, which is the window pinning exists to close. It is not a cleanup
step — deleting the pin file by hand is the same thing without the warning.
nobgp peers reads and writes the pin store directly rather than going through
the running agent, so it works with the daemon down — which is often where you
are when a peer you just rebuilt cannot be reached. A running agent notices the
change itself within one session setup; no restart is needed.
The store sits in the root-owned configuration directory beside the profile's
.key and .jwt, so the command elevates like nobgp status and nobgp show:
it re-executes itself under sudo on Linux and macOS, and must be run from an
Administrator terminal on Windows. Running it unprivileged would report no
pinned peers on a node that has plenty.
nobgp mcp
Connect this node's local MCP server to Claude Desktop or Claude Code.
A registered node can run an MCP server on 127.0.0.1, started by an operator's node_grant (see Local MCP Server). nobgp mcp is how you register that server with the machine's Claude clients, check that it is answering, and take the registrations back out again.
Usage:
nobgp mcp <subcommand> [options]
Persistent option (available on every subcommand):
| Option | Type | Description |
|---|---|---|
--profile | string | Agent profile to bridge to (default: default) |
install
Register this node with the Claude clients installed on the machine.
nobgp mcp install [--yes]
Options:
| Option | Short | Description |
|---|---|---|
--yes | -y | Skip the confirmation prompt (for scripts) |
The command names the account whose Claude configuration it is about to write and asks for confirmation. It only touches configuration files that already exist, and a file it cannot parse is left alone and reported rather than replaced.
From agent 0.4.126 it also refuses to replace a nobgp entry that is not a local node's — the public server is registered under that same name — and stops before the prompt, so --yes cannot skip it. Rename the other entry (to nobgp-public, for example) and run the install again. See one registration slot.
Before writing anything it makes sure there is a server to register — it flips no switch of its own. It starts the agent service if it isn't running, fails fast on the state waiting cannot fix (the node's local MCP is off — turn it on first, install second), then waits up to 30 seconds for an endpoint that answers. If none does, the install fails instead of registering a port nobody holds.
After registering it opens one throwaway session against the live endpoint and reports how many router tools it exposes — 29 from router 0.4.179, where the endpoint is simply on or off, on top of the node's own two local tools. A count it cannot obtain right now is reported as such, not as an endpoint that is off. ⚠ The example output below was captured when the old observe / manage tiers still existed and shows a tier's smaller count.
Requires root: on Linux and macOS the command re-executes itself under sudo; on Windows run it from an Administrator terminal.
Example:
$ nobgp mcp install
Root privileges required, re-executing with sudo...
Installing the noBGP MCP server for user "alice" (profile "default"):
Claude Desktop /Users/alice/Library/Application Support/Claude/claude_desktop_config.json
Claude Code /Users/alice/.claude.json
Continue? [Y/n] y
registered with Claude Desktop (...)
registered with Claude Code (/Users/alice/.claude.json)
MCP server ready at http://127.0.0.1:51843/mcp
This node holds an operator-granted role — 21 network tools available through the proxy.
Restart Claude to pick this up — it reads its server list only at launch.
The entry it writes carries the endpoint and bearer token — a direct HTTP entry for Claude Code, a nobgp mcp stdio bridge with the values in its env for Claude Desktop. See what gets written.
That token belongs to the profile, not to the account: running the install for a second account on the machine hands out the same credential, so access can't be withdrawn from one account alone. See one token per node.
This writes a per-user file, so the wrong account succeeds silently. Under sudo it resolves your real account (SUDO_USER), writes that account's configuration and hands the file back to you. Run as root with no SUDO_USER and it configures root's Claude, so it warns and the prompt defaults to "no".
uninstall
Remove this node's entry from every Claude configuration on the machine.
nobgp mcp uninstall
Everything else in those Claude files is left untouched, and so is the agent: the server keeps running, and the profile keeps its mcp-port and mcp-token — they are the running server's state, and other accounts on the machine may still be registered against them. To stop the server itself, node_revoke the node's grant at the router — the only control that starts or stops it.
A nobgp entry that is not a local node's — a public server registration under the same name — is left in place and reported, from agent 0.4.126. This node never wrote it, so uninstalling this node does not take it away.
No root required — this touches only your own account's Claude configuration. Restart Claude afterwards.
status
Show the local MCP endpoint and where it is registered.
nobgp mcp status
Example:
$ nobgp mcp status
server: http://127.0.0.1:51843/mcp (responding, per the Claude Desktop registration)
Claude Desktop: registered (/Users/alice/Library/Application Support/Claude/claude_desktop_config.json)
Claude Code: not registered (/Users/alice/.claude.json)
No root required: the endpoint is read out of the registration in your own Claude configuration, falling back to the agent's own root-owned state — the line names whichever it used. It then probes that endpoint rather than trusting it, so a registration pointing at a port the agent no longer holds reports — NOT responding (re-run nobgp mcp install to repair it) instead of a false "running". server: unknown means nothing is registered yet and the agent's own state isn't readable by this user.
From agent 0.4.126 a nobgp entry that is not a local node's reads as not registered — found a non-local "nobgp" entry, naming its scheme and host (or, for a command entry, its command name) and nothing else. It is neither probed nor counted as this node: probing the public server with an empty token answers, so it used to be reported as this node's endpoint responding.
It reports the registration, not the grant. For whether the router has granted this node a role — the one thing that starts the server — use the mcp block of nobgp status.
nobgp notify
Publish an event into the subscription this process belongs to. Aliased as nobgp publish.
This is how a script dispatched by command_subscribe reports its own progress: the agent exports $NOBGP_SUBSCRIPTION into every command it runs for the event bus, so the script never has to be told an identifier.
Usage:
nobgp notify [key=value ...] | [message] | [json] | -
Options:
| Option | Description |
|---|---|
--subscription | Subscription id (defaults to $NOBGP_SUBSCRIPTION) — for a process the agent did not start |
--status | ok (the default) is a progress report and ends nothing. Any other value (completed, failed, timeout, cancelled) is this node's last word: it settles this node's share of the subscription and never touches the work running on other nodes |
Payload forms. The shape is chosen by what you actually wrote, so a shell script never has to hand-assemble JSON:
# key=value pairs become a typed JSON object —
# numbers and true/false are not quoted
nobgp notify status=ok count=3 host="$(hostname)"
# a plain message is sent verbatim
nobgp notify "migration finished"
# JSON is passed through untouched
nobgp notify '{"stage":"schema","ok":true}'
# stdin, same rules
./migrate.sh | nobgp notify -
# settle this node's share of the dispatch
nobgp notify --status failed "backup aborted: disk full"
The key=value form applies only when every argument is one, so a message that happens to contain an equals sign is not silently reinterpreted.
Passing no payload and no --status fails with exit 1 and the message nothing to publish. The usual cause is a pipe missing its -:
./migrate.sh | nobgp notify # ✗ exit 1 — stdin is only read when asked
./migrate.sh | nobgp notify - # ✓
A bare nobgp notify --status completed is still valid — settling this node's share of the subscription is the message.
In a dispatched script:
#!/bin/sh
nobgp notify phase=start
pg_dump mydb | gzip > /backups/db.sql.gz
nobgp notify phase=done bytes="$(stat -c %s /backups/db.sql.gz)"
With no subscription in context, or one that has already closed, nothing is sent and the exit status is still 0. A script under the bus is not supposed to fail because the reader went away.
Exit Codes:
0- Published, or there was no subscription to publish into1- Nothing to publish (no payload and no--status), no running agent to publish through, the payload was rejected, or the agent is too old for this command
nobgp events
Show the events this node is publishing to the network event bus.
Usage:
nobgp events [-f]
Options:
| Option | Short | Description |
|---|---|---|
--follow | -f | Keep streaming live events (Ctrl-C to stop) |
Without --follow, the command prints the node's recent events (the newest 256 are kept) and exits. With --follow, it prints those and then keeps streaming.
# What has this node reported recently?
nobgp events
# Watch it live while testing a subscription
nobgp events -f
nobgp events shows what this node is publishing — the half that is invisible from the machine producing it. It is not a subscriber feed: subscribers read events at the router, where the queue lives, with event_tail. Use this to confirm a node is actually reporting what you expect.
nobgp list
List all available profiles.
Usage:
sudo nobgp list
From agent 0.4.158 this command elevates itself, like status,
show and remove: the profiles are the files in the
config directory, and that directory is root's, so an unprivileged run could not read
it at all. Earlier agents ran unprivileged and told you to type the command again with
sudo. On Windows nothing self-elevates — run it from an Administrator terminal.
A config directory that does not exist is No profiles found. — that is a node
with no profiles. Any other reason root cannot read it is a real fault (a damaged or
inaccessible directory), and nobgp list exits 1 naming the directory and the reason
rather than reporting No profiles found., because an empty list from a read error is
not the same claim as a node with no profiles:
$ sudo nobgp list
cannot read the profile directory /etc/nobgp: open /etc/nobgp: input/output error
This is not the same as having no profiles.
nobgp show
Show the configuration and status details of a profile.
Usage:
nobgp show [profile]
If no profile is specified, shows the default profile.
Alongside the profile's configured settings and identity keys, the output
includes the QUIC, IPv6 and MCP state that is otherwise machine-managed. Each
line below appears exactly once, and the settings that resolution can override
are shown as resolved rather than as the raw value the flag or config file
holds — router, insecure and quic-router all follow that rule:
| Line | Description |
|---|---|
quic-router | Your QUIC endpoint override, printed only when it is what the QUIC leg actually dials — that is, when quic-endpoint below reports (override). A value sitting in the config that endpoint resolution ignores prints no line at all, so it can never read as an override the next line contradicts with (learned). Older agents wrote a now-retired built-in value into config files, and nothing removes it, so this suppression is what an upgraded node relies on |
quic-endpoint | The host:port the QUIC leg dials, followed by its source (override, learned, or derived) in parentheses |
quic-cert-pin | Shown only when the pin deviates from the healthy path: none (no pin yet, so this node stays on wss until its next registration delivers one) or provisioned (a NOBGP_QUIC_CERT_PIN value outranking the router's). A normally learned pin prints no line — the (learned) on quic-endpoint already covers it, since both arrive together |
quic-udp-blocked | Shown as true only when the agent has recently observed UDP being blocked on this network |
ipv6 | true when the host holds a usable global IPv6 address (which lets the QUIC transport attempt an IPv6 connection), false otherwise |
token-expired | Deviation-only: printed only when the profile's registration token has already expired, as the expiry instant plus the remedy — 2026-07-30T09:14:02Z — re-register with 'nobgp register'. A live token prints no line. Agent 0.4.72+ |
mcp-port | The loopback port the local MCP server prefers, or any (assigned at start) when none has been recorded yet |
allow-tools | The capability domains this node serves, or none (nothing permitted) for an empty list — a value an operator must never read as "not configured" |
allow-roots | The filesystem roots the fs tools and the fs event source may touch, rendered the same way |
allow-root | Whether this node permits execution and file access as root / Administrator, and which key the value came from — false (read from allow-admin (the old name of allow-root)) on a profile written before the rename, or false (allow-root and allow-admin disagree; the stricter value applies) where both are set and differ. true (default) where neither is. Called allow-admin through agent 0.4.152 |
local-mcp | Whether this node will run its local MCP server at all — on or off. Printed even at its default on, for the reason the allow- lines are: an off switch you cannot read back is one nobody trusts. Agent 0.4.153+ |
peer-loopback | Whether an overlay peer may reach a port bound only on this node's loopback. Printed even at its default true. Agent 0.4.153+ |
mdns-report-services | Whether this node reports the services its own host offers — see mDNS keys. Printed even at its default, for the reason the three allow- lines are: a setting you cannot read back is one nobody trusts. Agent 0.4.128+ |
mdns-announce | Whether peers are registered with this machine's own mDNS responder for local applications. Agent 0.4.128+ |
lan-names | The LAN names this node serves to the network, as the patterns the agent applies rather than the raw text — so a list of only exclusions reads back as *, !router, and an empty or off value as none (no name is resolved for remote nodes). A pattern the agent cannot parse prints as none with the parse error, since that is what it does: refuse every name. Printed even at its default. Agent 0.4.128+ |
windows-firewall | What this node does about its inbound Windows Firewall rule — allow-overlay or respect-windows. Printed on every Windows node, including at its default, since it decides whether the host has an inbound allow rule at all; off Windows it prints only when the profile sets the key, and says it has no effect there. Like fs, it reports what resolution decided, so a value that is neither of the two prints as invalid and names what the agent will do instead — nothing. From agent 0.4.140 respect-windows on a machine delivering peer traffic from its own socket prints with a qualifier, so the line cannot be read as keeping peers off the machine's ports. From agent 0.4.144 that qualifier is respect-windows (does not filter traffic from peers: this node delivers it from its own socket, as local traffic), because the setting now filters nothing from another node; agents 0.4.140 to 0.4.143 printed (does not filter TCP, UDP or ping from peers; filters only traffic on the kernel path: other IP protocols, fragments, ICMP other than ping), which was true of them. It prints plain where that delivery is not running — on 0.4.144 that node drops peer traffic outright rather than filtering it. Agent 0.4.124+ |
fs | The shared drive backend this profile is pinned to — auto (the default), off, nfs, fuse, winfsp or webdav. Printed even at its default, since it decides whether the node mounts anything at all. It shows what resolution decided rather than the raw value: a typo prints auto ("nsf" is not a valid value; ignored), because an unparseable value falls back to auto rather than taking the node's filesystem away |
fs-backend | The backend that actually mounted and where it landed — nfs (mounted at /Volumes/nobgp) — or webdav (mount at /mnt/nobgp is not responding) when the mount is still in the kernel's table but the backend's own probe says it is dead. Agent 0.4.61+ |
fs-error | Why there is no mount, when there is none. Deviation-only, so a healthy node prints no line. Agent 0.4.61+ |
peer-access | How another node reaches this machine's own services, in the running agent's own words — as a local client … on a machine delivering peer traffic from its own socket, none (socket delivery did not start …) where that delivery is not running (agent 0.4.144+; agents 0.4.140 to 0.4.143 said through the overlay interface, at the primary address only … there), none … on a node with no overlay. Like the fs lines it is the running agent's answer, so it is silent when no agent responds. It mirrors network.peer_access in nobgp status. Agent 0.4.140+ |
The QUIC lines mirror the router block of nobgp status; the three allow- lines mirror its allow block and are printed even at their permissive defaults, since a setting you cannot read back is one nobody trusts. The three fs lines read as one block: fs is configuration and appears whether or not an agent is running, while fs-backend and fs-error are the running agent's answer and are silent when no agent responds — the full picture, including every backend this platform could use and why each can or cannot serve, is the fs block of nobgp status. See nobgp status for the full meaning of each value. The mcp-port line is the persisted profile view — it does not say whether the router has granted this node a role, which is what actually starts the server. For that, and for a live port, use the mcp block of nobgp status or nobgp mcp status.
fs-backend, fs-error and peer-access are the running agent's answer, not the config file's, so they are read from the agent over its local socket and are silent when no agent is running — which agent is up is nobgp status's question. The two fs lines are never both silent while something is wrong: a node with no mount says why on fs-error, and both are absent only before the first mount attempt or under fs: off, where the fs line above already says everything that is true. They mirror fs.type/fs.mount/fs.mounted and fs.error in nobgp status, which additionally lists every backend and whether it could serve; peer-access mirrors its network.peer_access.
Like nobgp status and nobgp config, nobgp show reads the profile's
root-owned configuration and therefore requires root privileges. On Linux and
macOS it automatically re-executes itself with sudo (prompting for your
password if needed); on Windows it must be run from an Administrator terminal.
Without these privileges the command would fall back to the built-in default
values rather than the profile's actual settings.
nobgp remove
Remove a profile and all associated files: its configuration (.yml) and the
lock file beside it (agent 0.4.157+ — see Configuration
File), identity (.key, .jwt), the profile's local API
socket, and the per-profile state and runtime files the agent keeps beside them
(remembered overlay addresses, cached transport state, PID and lock files).
Usage:
nobgp remove [profile] [options]
Options:
| Option | Description |
|---|---|
--force | Force removal without confirmation |
From agent 0.4.121 it also removes partly-written copies of those files that a
crash left behind. The agent writes the config, the identity key and the token
by creating a temporary file beside each and renaming it into place, so a power
cut or an unclean reset inside that moment strands a .<profile>.key.tmp-…,
.<profile>.jwt.tmp-… or .<profile>.yml.tmp-…. Nothing read those files and
nothing started an agent for them — but nothing removed them either, because
nobgp remove deletes by exact name and their suffix is random, so the command
whose whole job is to delete a node's identity reported success while the private
key was still on disk. It is swept by nobgp remove and by
nobgp uninstall now, and by the agent itself at startup for
anything more than an hour old (old enough that no live write can own it). The
files are 0600 in a root-owned directory either way, so this is residue rather
than exposure. A sweep that cannot read the directory says so instead of
reporting nothing to remove.
On Windows it also removes that profile's inbound firewall rule
from agent 0.4.126 — noBGP-<profile>, plus any device-named rule from an earlier
release that belonged to the profile. The rule is deliberately kept across an agent
stop, so nothing else would have removed it and a removed profile used to leave one
behind allowing inbound traffic from a network the machine had left.
Every path it deletes is root-owned, so — like nobgp status, nobgp show and
nobgp config — the command requires root and re-executes itself under sudo on
Linux and macOS; on Windows run it from an Administrator terminal. Other profiles
on the machine keep their own files, their own firewall rule, and keep running.
nobgp uninstall
Remove the noBGP agent from the machine. This stops and removes the system service, deletes the installed binaries, and clears runtime files (logs, PID/socket, locks). On Windows it also deletes the agent's inbound firewall rules from agent 0.4.124, so an uninstalled agent leaves nothing allowing traffic in. Up to 0.4.125 that is the device-named rules (noBGP-nobgp0 and so on). From agent 0.4.126 it is every profile's noBGP-<profile> rule, and any device-named rule that an earlier release left on the machine. Requires root / Administrator.
By default, nobgp uninstall keeps the profile's identity files (.key, .jwt, .yml) so that reinstalling the agent later re-enrolls the machine as the same node. Pass --purge to also remove the configuration directory — this deletes the node's identity, so a subsequent reinstall enrolls a brand-new node.
Usage:
sudo nobgp uninstall [options]
Options:
| Option | Short | Description |
|---|---|---|
--purge | Also remove all configuration and credentials (identity is gone; a reinstall enrolls a new node) | |
--force | -f | Skip the confirmation prompt |
Examples:
# Remove the agent but keep the node identity for a later reinstall
sudo nobgp uninstall
# Fully remove the agent, including configuration and credentials
sudo nobgp uninstall --purge
# Skip the confirmation prompt (useful in scripts)
sudo nobgp uninstall --purge --force
Unless --force is given, the command asks for confirmation before removing anything. On Windows the running executable cannot delete itself, so binary removal is scheduled to run just after the process exits.
nobgp uninstall makes removal self-sufficient — you don't need to run nobgp service uninstall or a package-manager uninstall first. If you installed via a package manager and prefer the tidy path, apt purge nobgp / brew uninstall nobgp / etc. still work.
nobgp upgrade
Upgrade the agent to its release-channel version (default) or a specific version.
Without a version, nobgp upgrade installs this node's release-channel version — the same version automatic upgrades install. On a machine that isn't registered, the stable channel is used. A test or pre-release build is never installed unless you name its version explicitly.
Anything off the safe path asks for confirmation first (with a "no" default), and there are two kinds of confirmation answered by two different flags:
- A backwards install — a downgrade, or a reinstall of the version already installed — asks a force prompt. Answer it non-interactively with
-f(--force).-yalone does not authorize it:nobgp upgrade <older-version> -yfails outright, so a stray-yin a script can never downgrade a node. - A forward off-path install — a version ahead of the node's channel, or one that can't be verified against the channel — warns with a "no" default. Either
-yor-fanswers it.
-f answers every prompt (it covers everything -y does); -y answers only the safe/forward ones. Running bare nobgp upgrade -f converges a node that is ahead of its channel back down to the channel version — the manual convergence path for nodes with auto-upgrade disabled.
Usage:
nobgp upgrade [version] [options]
Examples:
# Upgrade to this node's channel version
sudo nobgp upgrade
# Same, without confirmation prompts
sudo nobgp upgrade -y
# Upgrade to a specific version (confirms if off-channel)
sudo nobgp upgrade 1.2.3
# Downgrade or reinstall without prompts (requires -f, not -y)
sudo nobgp upgrade 1.2.3 -f
# Converge an ahead-of-channel node back down to its channel version
sudo nobgp upgrade -f
Options:
| Option | Short | Description |
|---|---|---|
--yes | -y | Non-interactive; answer yes to the safe/forward prompts (a version ahead of the channel, or one that can't be verified). Does not authorize a downgrade or reinstall. |
--force | -f | Non-interactive; answer yes to every prompt, including the force prompt for a downgrade or reinstall. Covers everything -y does. |
If the named version has no package for this machine, the upgrade stops with version X is not published for <platform>/<arch> — usually a typo or a release that was never cut. Run nobgp upgrade without a version to install the one your channel is on. The same message for a channel version means the release is probably still uploading; retry in a few minutes.
That check runs before any confirmation prompt: the command asks the download origin whether the package exists, so a version that isn't there is rejected without first walking you through a downgrade or off-channel warning. Only a definitive answer counts — an unreachable origin, a timeout or no network at all lets the upgrade proceed, so recovering an offline node with sudo nobgp upgrade <version> -f never stalls on the check.
nobgp service
Manage the noBGP agent as a system service.
Most service commands depend on a service manager and do not work in Docker containers. The underlying service manager differs by platform: systemd on most Linux distributions, OpenRC on Alpine Linux, procd on OpenWRT, launchd on macOS, SysV init scripts on the rest, and Windows Service Manager on Windows. The exception is nobgp service logs, which also works for a containerized agent running in the foreground by reading the agent's mirrored log file.
Usage:
nobgp service <subcommand>
Subcommands:
install
Install the noBGP agent as a system service.
sudo nobgp service install
Registers the agent as a system service and enables it to start on boot. On Linux this creates a systemd unit (or OpenRC/procd script depending on the init system); on macOS a launchd plist; on Windows a Windows Service.
From agent 0.4.100 a box running busybox init — SysV-style init scripts with neither Debian's service command nor a boot directory of symlinks (/etc/rc.d or /etc/rcN.d), which is the usual shape of a Buildroot-class image — is recognised as its own case: the agent drives /etc/init.d/nobgp by path and adds the S99nobgp symlink that busybox's rcS reads at boot, so install, start, stop, restart and status all behave normally there. It is decided by probing the host, not by its distribution, so a machine that has either helper keeps the ordinary path.
From agent 0.4.101 the boot link does not always go beside the init script. Where the firmware ships a hook script (/etc/init.d/S99custom) that enumerates a directory of its own, the link is installed there (/etc/kvmd/user/scripts/S99nobgp) — which is what an appliance with an overlay root requires, since its rcS expands /etc/init.d/S??* before the writable layer exists and so can never see a link added later. The hook is read and must name that directory; unfamiliar firmware falls back to /etc/init.d/S99nobgp, and a machine with no such hook is unaffected. See Buildroot appliance runs, but nothing starts after a reboot.
⚠ Below 0.4.100 this did not work on such a host: agent 0.4.99 failed the install with symlink /etc/init.d/nobgp /etc/rc.d/S50nobgp: no such file or directory and left the init script behind with no boot link, and earlier releases reported the service as installed while it could be started neither then nor at boot. On 0.4.100+ both start and restart add a missing boot link before running anything, so a machine left in that state is repaired without reinstalling the service — see Buildroot node registers but the service will not install.
uninstall
Remove the noBGP system service.
sudo nobgp service uninstall
Stops the service, disables it, and removes the service unit file.
On a busybox-init host, agent 0.4.101 also sweeps the S99nobgp boot symlink out of every boot directory it knows about rather than only the one this machine reads today — so a link an earlier release left in the wrong place cannot outlive the service.
start
Start the noBGP service.
sudo nobgp service start
stop
Stop the noBGP service.
sudo nobgp service stop
From agent 0.4.108, on macOS this is the only stop that holds. The agent now exits non-zero when nothing asked it to stop, and the launchd job is configured to restart on an unsuccessful exit — so a stray kill, a sudo launchctl stop nobgp or a launchctl kill TERM system/nobgp no longer leaves the node down: launchd brings it back about 8 seconds later. nobgp service stop, nobgp service restart and nobgp service uninstall all unload the job, and an unloaded job is not restarted whatever it exited with, so they behave exactly as before.
The agent tells the two apart with a marker the CLI writes immediately before it asks — stop-intent.json in the configuration directory — because launchd delivers a plain SIGTERM for an operator's stop, byte-identical to any other. The marker is one-shot: it is consumed by the stop it describes and cleared at every start, so it can never make a later unexpected exit read as intentional. It is not a profile, and the agent will not try to start one for it.
⚠ Linux and Windows are unchanged. systemd's Restart=always and OpenWrt's respawn already restart a clean exit, so there is nothing to gain there — and exiting non-zero would leave a correct systemctl stop nobgp reported as failed rather than inactive (dead). The Windows Service Manager takes no recovery action by default, so a non-zero exit would change nothing except on a machine where recovery actions have been configured, where it would start restarting on a console Ctrl+C.
If a stop was not asked for, the agent says so in its log before exiting — this stop was not asked for by any nobgp command; the exit code will say so — which on macOS lands in /var/log/nobgp.err.log.
restart
Restart the noBGP service.
sudo nobgp service restart
If the service is currently stopped, restart simply starts it rather than failing — in place, and reporting service was stopped; started. A stopped service has no session to kill, so none of the scheduling below applies to it.
From agent 0.4.83, restarting a running service is scheduled rather than performed by this command. It is handed to something outside this process's own tree — a transient systemd unit on Linux, a detached helper elsewhere — and the command returns as soon as that is arranged, about two seconds before the restart fires:
service restart scheduled (transient systemd unit), firing in 2s.
The restart is NOT driven by this process, so a command session that ran this command ends with it — the service comes back on its own.
Confirm with 'nobgp service status'.
So on a running service exit 0 means "scheduled", not "back up" — confirm with nobgp service status.
The reason is that this command is routinely run through noBGP, in a command session or a published terminal. A session's process is killed the moment the agent is told to stop, so an in-process stop-then-start died between its own stop and start: the node went down and stayed down until someone reached its console. Scheduling the restart outside the session's process tree is what makes restarting a node remotely safe.
On the rare host where nothing can take the restart out of tree, the command says so rather than guessing: run from inside a noBGP command session it refuses, naming a local console or a reboot as the way through; run from a local shell it warns and restarts in place, as it always did.
From agent 0.4.147 a Synology DSM node is not such a host. DSM runs systemd but has no systemd-run, so through agent 0.4.146 nothing could take the restart out of tree there, and the command refused inside a noBGP session. It now starts the detached helper instead: the helper's one systemctl restart is a job that systemd finishes after the stop has ended the session, so the service comes back.
From agent 0.4.147 a slow stop no longer leaves a sysv node stopped. On those init scripts — busybox and sysv boxes — the stop is given 10 seconds, after which the script reports a failure and deliberately does not start the service again, while the agent's own orderly shutdown can take about 20. There is no respawn behind it, so such a node went down and stayed down with no remote way back. The command now waits up to 30 seconds for the old supervisor to exit and then starts the service itself. If that supervisor is still running at the end of the wait, the original failure is reported and the service is left running on its old program rather than being stopped a second time.
From agent 0.4.148 the same recovery covers macOS. A restart there unloads the launchd job and loads it again, and anything launchctl writes to its error output is reported as a failure — so a job that was unloaded and then not loaded again was left unloaded, which KeepAlive does not bring back. The command now waits for the old supervisor to exit and starts the service itself, and it tries the start up to three times, two seconds apart: a load immediately after an unload can fail and then work a moment later. This matters on every Mac from this release on, because an upgrade now restarts the whole service there rather than replacing the agent alone.
⚠ On Windows a restart asked for from inside a noBGP session is always that host, and from agent 0.4.130 it is refused rather than reported as scheduled. A session's processes are held in a Windows job object that is torn down with the session, and the helper that would carry out the restart is deliberately not allowed to leave it — letting it out would also let every unrelated grandchild escape the cleanup that kills a session's process tree. Through agent 0.4.129 the helper was started inside the job anyway and the command printed scheduled: it died with the session about two seconds later, having stopped nothing, so a node reported as restarting had simply carried on. Restart such a node from an Administrator terminal on the machine, from its console, or by rebooting it.
From agent 0.4.94 a command dispatched to a cohort with command_subscribe counts as such a session too. Through 0.4.93 a dispatch was not marked as one, so on a host where scheduling failed a fan-out of nobgp service restart took the restart-in-place branch that the marker exists to refuse — killing the dispatch's own process tree, on every node in the cohort at once, with nothing left alive to start the service again.
status
Check the status of the noBGP service.
sudo nobgp service status
Exit codes:
0- Service is running1- Service is stopped2- Service is not installed
logs
View service logs.
sudo nobgp service logs
Like the rest of nobgp service, this requires root — on Linux and macOS it
re-executes itself under sudo, and on Windows it must be run from an
Administrator terminal (see Root privileges). The systemd
journal only shows an unprivileged caller their own messages, so a non-root run
returned a journal slice with none of the agent's lines in it and nothing saying
they were missing rather than absent.
Options:
| Option | Short | Default | Description |
|---|---|---|---|
--lines | -n | 100 | Number of lines to show |
--follow | -f | false | Follow log output in real-time |
# Show last 50 lines
sudo nobgp service logs -n 50
# Follow logs in real-time
sudo nobgp service logs -f
The command reads from the platform's native log source (systemd journal, logread on OpenWRT, OpenRC/launchd log files, or syslog). When no service manager captures the agent's output — inside a container running in the foreground as PID 1, or on Windows, where the Service Control Manager captures nothing — the agent mirrors its output to an on-disk log file (<config-dir>/nobgp.log, capped at 10 MiB with one rotated generation), so nobgp service logs works there too.
From agent 0.4.103 the mirror is also written where the service manager's own capture would not survive a reboot. Some appliance and router firmware puts /var/log in RAM — a tmpfs, or a symlink into one — so the init script faithfully captures the agent's output into a file the next boot erases. Such a node now writes the durable mirror as well, and prefers it to those files: they are still there and are still the current boot's log, but they are tried after the mirror, because a file holding only this boot opens perfectly well and would otherwise be the answer. This is Linux only and is decided by measuring the filesystem, never by the platform's name — a machine whose /var/log cannot be measured is left exactly as it was, since the cost of guessing wrong is flash writes on a device that did not need them. See The agent's log starts empty after every reboot.
⚠ From agent 0.4.105 the mirror is preferred to those files and to nothing else. A ring buffer — the systemd journal, or logread on OpenWRT — is still tried ahead of it, because the mirror begins at the upgrade that started writing it and holds nothing older, while the ring buffer holds everything from before. Agents 0.4.103 and 0.4.104 put the mirror ahead of those too, so on such a box the first read after upgrading could reach the bottom of a nearly empty file and report it as the whole log. A ring buffer that holds none of this agent's lines is skipped as it always was, so nothing is preferred to the mirror on a machine where it is empty.
From agent 0.4.89 it finds that source by trying each one, rather than deciding from the platform which one this machine ought to be using. The candidates are tried in order and the first that actually produces a line of this agent's own wins. What that changes in practice:
- A box where the expected mechanism is present but empty now falls through instead of showing nothing. The clearest case is a container that has
journalctlinstalled and no journal behind it: the old platform guess stopped there and printed an empty log, with the agent's own mirrored file sitting right beside it unread. - When nothing can serve the log, the error names every candidate it tried and why each declined — including "this file exists but you may not read it", which is a different problem from "this machine keeps no log here" and has a different fix.
- What did not change is the narrowing: where the log is a file shared with the rest of the box (
/var/log/syslog,/var/log/messages), only this agent's own lines are shown, on--followas well as on a plain read, so-n 100is a hundred lines of noBGP.
From agent 0.4.86 the node_logs MCP tool reads these same sources remotely — the node picks the mechanism, applies your filters before anything crosses the network, and reports which source it read and, for a file, its absolute path. This command is the on-box view (a live tail -f); that one is the fleet view.
From agent 0.4.89 the two consult one description of where this node keeps its log, so they can no longer disagree about it. Before that each had its own map of the platforms, and the one in this command was the one that could go stale unnoticed — nothing compares them, and only a packaging change on the machine in front of you would ever reveal the drift.
On Windows the Service Control Manager does not capture a service's output, so the agent mirrors its logs to C:\ProgramData\nobgp\nobgp.log (capped at ~10 MiB with one rotated .1 generation). nobgp service logs reads that file back with PowerShell Get-Content, and --follow is supported (via -Wait). The same on-disk logfile is used when the agent runs in the foreground or inside a container, where no service manager is available to capture its output.
nobgp version
Display the noBGP agent version.
Usage:
nobgp version
Example output:
nobgp version 1.0.0
nobgp help
Display help information.
Usage:
nobgp help [command]
Examples:
# General help
nobgp help
# Help for specific command
nobgp help agent
nobgp help config
nobgp help service
Configuration File
Location
The configuration file path depends on the platform:
| Platform | Path |
|---|---|
| Linux | /etc/nobgp/default.yml |
| macOS | /usr/local/etc/nobgp/default.yml |
| Windows | C:\ProgramData\nobgp\default.yml |
Format
YAML, and the agent owns the file — it rewrites it whenever it records something for itself: the port it bound for the local MCP server, the overlay slice it picked, the QUIC endpoint the router told it about, from agent 0.4.59 the loopback port its NFS server bound (nfs-port, so an existing mount survives a restart), and from agent 0.4.78 where the drive was last mounted (last-mount-point, so a mount left behind by a killed agent can still be found).
The agent also watches this file, so an edit you make is applied without a restart. From agent 0.4.78 it tells its own write apart from yours: through 0.4.77 a value it recorded after startup — nfs-port and mcp-port — read back to it as your edit and restarted the agent for a reload, which on a node that had just mounted tore the drive down again about five seconds later. An edit of yours is still picked up exactly as before.
Two programs write this file, and from agent 0.4.157 neither loses the other's change. The agent writes it for itself (the values above, and the ones registration records), and nobgp config writes it for you. Each used to render the whole file from what it held in memory, so a change made between the other one's read and its write was written back to its old value and lost — nobgp config printed Configuration saved, the agent never reloaded, and the setting was simply not there. allow-root: false is the case worth naming: a veto could disappear on a node that had just been told to apply it. Two things close it now. Both programs take a lock on the file around every read-modify-write, so their reads and writes cannot interleave; and before writing, each re-reads the file and keeps any key the other changed, writing only the keys it changed itself. A write that keeps somebody else's key is not treated as the agent's own, so a running agent reloads on it exactly as it does for an edit you make by hand. From the same release, a nobgp config that lands in the moment between the agent reading the file at startup and its watch starting is applied rather than absorbed unnoticed.
The lock is a dot-prefixed file beside the config file — .default.yml.lock for default.yml — holding nothing and read by nobody. Either program creates it the first time it needs it, nobgp remove deletes it with the rest of the profile, and profile enumeration skips it, so it never looks like a second profile. Where it cannot be created at all — a read-only configuration directory — the read goes ahead without it, since a file that cannot be written has no race to lose and a read-only profile must still load.
From agent 0.4.87 one key is the exception: an edit to fs is recorded as pending rather than applied, and while it is pending nothing else in this file reloads either — a reload re-reads the whole file, so applying a neighbouring key would carry the parked backend switch with it. The agent logs fs changed, restart to apply - config not reloaded; a restart applies it, and putting the value back lifts the hold.
Since agent 0.4.35 a key is written only when its value differs from the default. Everything else is emitted as a commented line, so the file reads as the choices someone actually made rather than a frozen snapshot of every default the release happened to ship with:
domain: a1b2c3d4-....nobgp.net
overlay-cidr: 100.64.0.0/20
mcp-port: 51843
mcp-token: ...
user: deploy
## Defaults. Shown for reference, NOT read. Uncomment a line (single #)
## to pin that value; leave it commented and the agent tracks the
## default, which may change between releases.
# auto-upgrade: true
# compress: true
# encrypt: true
# fs: auto
# fs-cache-ttl: 30s
# interface: auto
# lan-names: '*'
# log-level: info
# mdns-announce: false
# mdns-report-services: true
# mount: /mnt/nobgp
# overlay-ipv6: true
# peer-key-pinning: warn
# router: router.nobgp.com
# transport: auto
# transport-family: auto
# windows-firewall: allow-overlay
## event-sources was migrated into allow-tools and is no longer read.
## event-watch-roots was migrated into allow-roots and is no longer read.
(Abbreviated — the defaults block lists every key the agent knows, one per line, in alphabetical order.)
Uncomment one of the single-# lines to pin that value: the agent then treats it as your choice and keeps writing it. Leave it commented and the node tracks the default, including a default that changes in a later release. Explanatory text uses ##, so uncommenting a whole block cannot drag prose into the YAML.
Two consequences worth knowing:
- The file is agent-generated. Comments you add by hand do not survive the next time the agent records something, which can be minutes later.
- Keys the agent no longer reads are swept out on that rewrite, with a note naming the successor where there is one.
Older agents wrote every default into the file as an explicit setting the first time nobgp config ran, which is why a node installed before 0.4.35 lists keys nobody chose — and why a value it froze then would have stayed pinned through a later change of default. Since agent 0.4.36 the agent tidies that when it starts, so an upgraded node is converted on its first restart; a 0.4.35 agent rewrote the file only the next time it had something of its own to record, which on a settled node may never come. Nothing changes about the effective settings, and the file is replaced atomically, so an upgrade restart or a power cut in the middle of a write cannot leave a node without its router, its overlay slice or its MCP token.
The rewrite happens only when the contents would actually differ. A file that already says the right thing is left alone — same bytes, same timestamp — so the startup sweep costs a settled node nothing, and an agent running with no config file at all (the env-only container shape) has nothing to sweep and says nothing about it.
The registration-key and node-name fields are only used during initial registration and are automatically removed from the config file after successful registration. Agent identity is stored separately in .key and .jwt files alongside the config.
Local MCP server keys
These control the MCP server the agent runs on 127.0.0.1 — see Local MCP Server.
local-mcp: on # the owner's off switch: `off` and the server never runs
mcp-port: 51843 # port to prefer; the agent writes back whatever it binds
mcp-token: "..." # bearer token; minted and written back by the agent, one per profile
What starts the local server is node_grant, called at the router by someone who controls the node — its owner, or an Owner or Admin of its organization: a node whose local MCP is off serves nothing on 127.0.0.1, and turning it off stops the server within seconds. It does not expire on its own — it stands until node_revoke turns it off. What the node will serve is bounded by the capability keys below.
⚠ From router 0.4.179 there are no tiers, and what the endpoint may do to a peer is the node owner's current right on that peer, re-checked on every call — so it stops when the owner leaves the organization or loses a right, and a change of the node's owner turns it off. See Local MCP Server.
mcp-port and mcp-token are agent-written state, not settings you edit — nobgp mcp install reads them as root and copies them into the Claude configuration for you. They persist while the endpoint is off, so turning it on again rebinds the same endpoint and registered clients keep working.
local-mcp — the owner's off switch
From agent 0.4.153 the machine's owner has the last word on whether the local endpoint runs at all:
# This machine never serves MCP on 127.0.0.1, whatever the router grants
sudo nobgp config --local-mcp=off
# Back to the default
sudo nobgp config --local-mcp=on
- The router's grant cannot override it.
offwith a live grant is granted, refused by the owner: the server does not start, andnobgp statusreportsmcp.granted: truebesidemcp.local_mcp: offso the two halves are distinguishable from a listener that failed to bind. - It is set on the machine and only there, like the capability keys:
node_config_setrefuses it, because a remote path to an off switch is not an off switch. onandoffare the values.trueandfalseare read too, for a hand edit or a YAML writer that produces them. Anything else is refused bynobgp config, and a value already in the file that the agent cannot parse reads asoff— a typo in an off switch must not leave the server on.- The switch does not touch
mcp-portormcp-token, so turning it back on rebinds the same endpoint and registered clients resume without a re-install.
Capability keys
These are the node owner's say over what this machine serves — for event-bus sources, for the direct command / fs_* tools, and for published terminal services. All three are permissive by default; narrowing them is how you take capability away from a box — see Monitoring & Events.
allow-tools: # capability domains served here: fs, command, logs
- fs
- command
- logs
allow-roots: # where the fs domain may reach
- /
allow-root: true # false refuses root requests instead of downgrading them
allow-admin is now allow-rootFrom agent 0.4.153 the veto on the superuser is the key allow-root, the flag
--allow-root and the environment variable NOBGP_ALLOW_ROOT — the same rename the
router made in 0.4.179, where it is reported as allow_root. It is the same setting
with the same meaning; only the spelling changed.
- The old name still works.
allow-admin,NOBGP_ALLOW_ADMINand--allow-adminare all still read, so a profile or a script written before the rename keeps its veto.--allow-adminis hidden from--helpand prints one line naming--allow-root. - ⚠ When both keys are set, the stricter value wins —
falsebeatstrue, in either order. A rename must never turn a veto off, so a profile sayingallow-admin: falsethat later getsallow-root: truefrom a script that knows only the new name stays at false until the owner changes the old key too.nobgp showsays which key the value came from, and names the disagreement where there is one. - A flag writes both keys.
--allow-root=falsesavesallow-root: falseandallow-admin: false, so an agent downgrade to a release that reads only the old name keeps the veto.--allow-root=trueis the default, so the save removes both keys from the file.
The full treatment is The veto on root was renamed.
The third domain, logs, is new in agent 0.4.153: it is what serves
node_logs, so allow-tools: [] really serves nothing.
A profile that named its domains explicitly before that release — [fs, command],
[fs] — has logs added once on the upgrade and recorded as added, because such
a list never meant to refuse the agent's own log; remove it with nobgp config --allow-tools afterwards and it stays removed. An empty list stays empty: [] is
the owner's "serve nothing".
allow-roots matches a path against each root after resolving symlinks, so a link
cannot smuggle a call outside them, and /etc does not admit /etcetera. The
whole-filesystem value is / — or \, and either spelling is accepted on either
platform, so the permissive default means everything on Windows too. Narrower
Windows roots are drive-qualified, e.g.:
allow-roots:
- C:\ProgramData\app
- D:\logs
One tree is off-limits whatever allow-roots says: the agent's own configuration
directory (/etc/nobgp, /usr/local/etc/nobgp on macOS, C:\ProgramData\nobgp on
Windows). The fs tools and the fs event source refuse every path inside it —
widening allow-roots does not open it and neither does allow-root: true, and a
symlink pointing into it is resolved and refused the same way. That directory holds
the node's key and JWT and the config these keys are read from, so serving it would
put both one fs_read away on a node still carrying the permissive defaults. The
command domain is unaffected: a session that runs as an account with permission to
read those files still can, and allow-roots does not confine a command's workdir
either — the command string is unconstrained, so a confined working directory would
refuse nothing that cd does not reach anyway.
A tool that walks — fs_list --recursive, fs_glob, fs_grep — asks the same
question at every directory it descends into and prunes the subtree on a refusal.
Agents before 0.4.37 vetted only the root they were handed, so a recursive walk
starting at a permitted path reached the agent's own directory beneath it; fs_grep
was not checked against allow-roots at all, since its roots travel in a different
field from the one the guard read.
allow-tools: [] serves no domain at all. The values are capability domains, not tool names — a value that isn't one (fs_read, commands) is dropped with a log line naming it. ⚠ From agent 0.4.153 that fails closed: if nothing valid is left, the node serves nothing, where earlier agents restored the permissive default. allow-tools is the owner's cap on the box, so a list the agent cannot read must not serve more than the owner wrote — [commands] was meant to narrow the node and used to serve fs and command. nobgp config --allow-tools refuses an unrecognised value outright for the same reason, and nobgp status reports allow.tools_invalid where the profile already holds one. These keys were called event-sources and event-watch-roots in earlier agents, where they were empty by default; an upgraded agent migrates the old names without tightening anything the node already served.
allow-root: false drops command sessions, dispatched commands and file operations to the user account, and refuses anything that would run as uid 0 outright rather than downgrading it.
⚠ One thing it stopped covering in agent 0.4.156: the agent's own log. node_logs reads that log as the agent process itself, whatever the call asks for, so this veto does not bind it — it runs no command of the caller's and opens no path the caller chose. Through agent 0.4.155 a node with allow-root: false refused the tool outright, which took the first thing anyone diagnosing that node would ask for. Turning the log off is the logs domain of allow-tools. Details
The user key is the other half of that, from agent 0.4.41. A node resolves what an unelevated call would run as — the configured account if there is one, its own ambient identity otherwise — and refuses the call as failed_precondition when that resolves to uid 0, rather than running the work as root. That covers a node with no user whose agent runs as root (a service install, a bare-root container, and a Windows node offering only LocalSystem, which reports as uid 0), a user that no longer resolves, and user: root spelled out. Either remedy clears it: nobgp config --user <account> on the box, or the caller sending admin: "true", which runs the work elevated. An omitted admin also runs elevated where the node reports no account or user: root. An agent already running unprivileged is unaffected — it serves unelevated calls as itself, since elevating there would reach the same uid. So on a container or a bare-root install, allow-root: false turns remote work off rather than narrowing it: both paths refuse. See Unelevated never means root.
For the fs_* / file tools it is a genuine privilege drop, not an authorization check standing in for one: the agent drops privilege before opening the file, so a file written unelevated is owned by that account, and the destination of an atomic write is checked against that account as well. A user that no longer resolves — deleted, or a config copied off another machine — is refused rather than silently falling back to the agent's ambient identity, which on a service install is root. The refusal names the account it could not resolve, so a deleted account reads as what it is instead of turning every later call into an unannounced root call.
Assuming another account is the superuser's privilege, so an agent that is not running as root cannot do it. On such a node — a foreground nobgp agent started by an ordinary user, say — a file operation that would run as user is refused naming the reason, rather than running as the agent's own account instead. Either run the agent as root or clear user. A service install runs as root, so this does not arise there.
Every remote path onto the node: the MCP tools and event-bus sources a node_grant reaches, and a published terminal service.
Services were exempt before agent 0.4.33, on the reasoning that a terminal keeps a locked-down node recoverable. The exemption also meant allow-root: false could not do the thing its name promises — a service published to run as root handed out root on a node that had refused exactly that — so the exemption is gone and the recovery advice moved to the moment of the change: nobgp config warns when a write leaves the node serving no commands or no file access, and asks before turning off superuser execution on a node where that removes remote execution entirely.
allow-tools without command refuses the browser terminal too, and allow-root: false on a node with no usable user set (a container, a bare-root install, a Windows node that captured no install account) refuses every session outright — the elevated path by the veto, the unelevated one because it would resolve to root. A Windows node that has an account stays operable as that account from agent 0.4.46; on 0.4.44 and 0.4.45 it keeps its file tools and loses execution. On those nodes the only way back is console, RDP or SSH access to the machine — so on a box behind CGNAT, set user to a working unprivileged account rather than relying on a terminal service surviving the change.
allow-root: false on Windows is an off switch for executionA Windows node offers one identity by default, so there is no unprivileged account to drop to. Since agent 0.4.33 the setting is still honoured there — as the only answer the platform can give: false refuses remote execution on that node entirely rather than being ignored. Earlier agents logged that they could not apply it and kept the superuser enabled.
It stops being true on a Windows node that has a user, once its agent reaches 0.4.46: allow-root: false there drops both file operations and execution to that account, exactly as on Unix, so the veto narrows the node instead of switching it off. On 0.4.44 and 0.4.45 only the file half dropped — fs_* and file ran as the account while execution was still refused.
The second identity on Windows
Since agent 0.4.44 a configured user gives a Windows node the second identity Unix nodes have — no separate key, the same one-switch rule as every other platform. The account is obtained from the local security authority by name and impersonated for the operation: nothing prompts for a password and no credential is stored on the machine, which is why an unattended install can capture one.
Since agent 0.4.46 it covers every surface, on the same one-switch rule as Unix:
| Operation | admin: false, agent 0.4.44–0.4.45 | admin: false, agent 0.4.46+ |
|---|---|---|
file and the fs_* tools | runs as user, a genuine privilege drop | runs as user |
command, terminal sessions, dispatched bus commands | refused, failed_precondition | runs as user, with that account's own profile and environment |
Through 0.4.45 execution was refused rather than served because a Windows spawn still happened in the service's own context — serving it would have run as LocalSystem while the node had just answered with the configured account. The refusal named the way out: admin: true, to run as LocalSystem deliberately.
Two consequences of running a process as that account. The first unelevated command for an account that has never signed in at the machine creates its profile directory (C:\Users\<account>), which nobgp uninstall leaves in place; file operations need no profile and create nothing. And because the identity is obtained by name with no stored password, it carries no network credentials — a command that reads a file share, or reaches anything else that authenticates it over the network, works with admin: true and fails with admin: false.
An account in the local Administrators group is the common case, since the installer captures the interactive desktop user. Such an account gets an unfiltered admin token, so admin: false there drops from LocalSystem to Administrator and no further. It is not refused — the two identities stay distinct, unlike a Unix user: root — and it is reported both ways: nobgp status carries allow.without_root_is_administrator on the box (agent 0.4.46+, as allow.unelevated_is_admin through 0.4.152 and still reported under that name), and network_directory reports it fleet-wide as info.user_is_admin.
Agent 0.4.46 also strips such a token of its Windows privileges before anything uses it. Without that, an administrative account's token carries SeBackupPrivilege enabled, and a read taken with backup privilege never consults the file's ACL — so on 0.4.44 and 0.4.45 an unelevated fs_read on such a node could read files the account's own permissions did not allow. It now reaches exactly what that account is permitted to reach.
A user the node cannot obtain a token for leaves it with one identity and logs why, rather than reporting an account it cannot become. Execution fails closed for the same reason: a token, profile or environment that cannot be built refuses the call rather than running it in the service's context.
Agent 0.4.43 shipped this behind a windows-user-identity key. That key is retired and an upgrading agent removes it from the config file; a Windows node that recorded an install account under 0.4.43 gains the second identity when it upgrades.
See A second identity on Windows.
Peer key pinning
From agent 0.4.98 a node remembers the encryption key it was given for each peer the first time the two talked, and reports it when that key changes. This key says what it does about a change — see Peer key pinning for what that catches and what it does not:
peer-key-pinning: warn # warn (default) | enforce | off
| Value | What happens on a changed key |
|---|---|
warn | The change is logged at ERROR, naming both fingerprints and the command that resolves it, and the session proceeds. The pin is left as it was, so the same change is reported again on every session until someone resolves it — a finding that silences itself after one look is one nobody sees |
enforce | That session is refused. One session, never the node's control channel: a changed key is a claim about one peer, and taking the whole node off the network over it would be an outage anyone able to offer a bad key could trigger at will |
off | Nothing is remembered and nothing is checked |
The default is warn, and that is an operational choice rather than the strongest
one. Re-provisioning a node re-keys it under an unchanged node ID, so an
ordinary rebuild and a substituted key are the same event in band. Under enforce
a rebuild therefore takes that node out of reach of every peer that had ever
talked to it until somebody runs nobgp peers --forget on each of
them. Choose enforce on a fleet whose nodes are not routinely rebuilt; the
sessions it refuses are exactly the ones warn would have let through while
saying so.
An unrecognised value is treated as warn with a log line naming the valid ones —
a typo in a security setting must not silently turn it off.
The pins themselves live in a <profile>-peerkeys.json file in the same
directory. It is state, not configuration: deleting it reverts every peer to
first contact, which is the window pinning exists to close, and
nobgp peers --forget exists so there is never a reason to remove
it by hand. A store the agent cannot read or cannot parse keeps whatever pins it
already holds and says so once, rather than quietly starting over; only a store
that is genuinely gone clears them.
This key is not settable remotely. node_config_set
refuses it, because the check it governs is aimed at the same control plane that
serves that surface — set it on the machine with nobgp config --peer-key-pinning.
Address family
From agent 0.4.113 a node can be told which IP address family to lead with.
It is settable three ways — --transport-family, NOBGP_TRANSPORT_FAMILY, or
the key below:
transport-family: auto # auto (default) | v4 | v6
| Value | What the node dials |
|---|---|
auto | Both families at once, the ordinary happy-eyeballs race: on the QUIC leg IPv6 leads and IPv4 follows about 200ms later, and on the WebSocket leg the operating system's own resolver picks the order. This is the default and is byte-for-byte what every node did before this key existed |
v6 | IPv6 first and alone; IPv4 only if that fails |
v4 | IPv4 first and alone; IPv6 only if that fails |
An explicit value is sequential, and that is the point of it. The race starts
the second family a fraction of a second later whatever happens, so a node set to
v4 under a racing dial would still emit IPv6 on every connect and the setting
would mean nothing. The case this exists for is hardware whose own uplink breaks
on IPv6 traffic — a wireless firmware that wedges its radio, say — where the goal
is that the family never leaves the machine, not that it loses a race.
It is a preference and never a force. Both explicit values fall back to the
other family, so a node whose chosen family stops working degrades to the one that
still runs rather than stranding itself offline. Nor does a preference create
capability: asking for v6 on a host with no usable global IPv6 address dials
IPv4, which is the only answer available. An unrecognised value is treated as
auto — a garbled setting falls back to the behaviour that always works.
What it covers: every connection the agent makes through its shared dialers — the control channel on QUIC and on WebSocket alike, mesh and service channels, the event stream, the local MCP proxy, upgrade downloads and the browser login flow. The upgrade download is the reason the scope is that wide rather than the control channel alone: it is the largest transfer the agent makes, and a preference that did not cover it would leave the worst offender in place on exactly the machines the setting is for.
From agent 0.4.142 the shared drive and the CLI
follow it too. The mount's own transfers and the router calls nobgp login,
nobgp file and the other CLI commands make now start from the same dialer, so a
node set to v4 no longer emits IPv6 from the one part of it that moves the most
bytes. Through 0.4.141 those connections raced both families whatever this key
said, and pairing the setting with fs: off or an interface-level block was the
only way to keep a family off such a host.
⚠ The CLI reads the setting from your own environment first. --transport-family
and NOBGP_TRANSPORT_FAMILY always reach it; a value that lives only in a profile
file reaches it for the default profile, and only when the account running the
command can read that file. The agent itself is unaffected — it always reads its
own profile.
Cost when the preferred family is dark: a black-holed family is silence rather
than a refusal, so the leading attempt has to time out before the fallback runs —
a few seconds per connect, against roughly 200ms under auto. A family that
refuses outright, or has no route, fails immediately and the fallback runs at once.
The key is settable remotely with
node_config_set, unlike its sibling
transport: it chooses which address of the same endpoint to dial first, with
the endpoint, the certificate pin and the node's credentials identical either way,
and both values fall back — so it cannot put a node out of reach the way a wrong
transport can. The change applies on the agent's ordinary reload, within about
five seconds.
IPv6 overlay keys
From agent 0.4.132 a node takes an IPv6 overlay slice beside its IPv4 /20, so a peer's name resolves to both families. Two keys:
overlay-ipv6: true # take an IPv6 slice (default). false = IPv4-only overlay
overlay-cidr6: fd3a:9c21:4e00::/64 # agent-written: the slice it picked
| Key | Flag | Environment | Notes |
|---|---|---|---|
overlay-ipv6 | --overlay-ipv6 | NOBGP_OVERLAY_IPV6 | Default true. false is the off switch for the IPv6 datapath on this node |
overlay-cidr6 | — | NOBGP_OVERLAY_CIDR6 | The slice in use. Normally agent-written state, like overlay-cidr — it is recorded so a restart reuses it and peers keep the addresses they had. A value that is not a /64 inside fd00::/8, or one already routed on the machine, is replaced by a fresh pick with a warning |
overlay-ipv6: false does not turn IPv6 off on the machine — it stops the overlay taking a slice of its own. The node keeps its IPv4 overlay, its control channel and everything else, and nobgp status reports network.overlay6: off (overlay-ipv6: false).
A host that cannot take a slice needs no setting. IPv6 disabled in the kernel, or a /64 collision the node cannot dodge, leaves the node on IPv4 with the reason in network.overlay6. The setting is for the case where IPv6 works and you want it off anyway.
overlay-ipv6 is settable remotely with node_config_set and applies on the agent's ordinary reload, within about five seconds — a node whose IPv6 path misbehaves keeps its control channel, so that surface is the one still working when the datapath is not. overlay-cidr6 is refused there, exactly as overlay-cidr is: changing a slice re-numbers every peer on that node.
Local API socket keys
On Unix the agent's local API socket is 0600, owned by the configured user — which registration sets to the installing account, so dispatched commands running as that account can already call nobgp notify. It falls back to root-owned when user is unset or the account no longer exists. These keys open it to a group as well, so an additional user-land consumer can reach the event stream:
api-socket-group: nobgp # group name or numeric gid
api-socket-mode: "0660" # octal; only meaningful with a group set
Unlike nobgp status and nobgp resolve, the commands a dispatched script uses —
nobgp notify and nobgp events — do not elevate.
Run by an account the socket does not admit, they say so specifically ("the agent is
running but its API socket denied access"), naming these two keys and running as root
as the fixes, rather than sending you to check a daemon that is perfectly healthy.
Filesystem keys
These two decide how — and whether — this node mounts the shared drive, and how stale a directory listing it may show you. They arrived separately: fs in agent 0.4.57, fs-cache-ttl in agent 0.4.66, so a node between those two releases has the first key and not the second.
fs: auto # auto | off | nfs | fuse | webdav | winfsp
fs-cache-ttl: 30s # duration; clamped to [0, 10m]. ⚠ ALWAYS write the unit
fs picks the backend, and it is config-only — there is no flag and no environment variable for it, so sudo nobgp config or an edit of the profile is how it is set. auto is the default and is what every existing profile already does: the agent walks its platform's list and mounts with the first backend that can serve on this machine right now. See Which filesystem you get for the per-platform order and what each backend needs.
- An explicit value is honoured or refused out loud, never silently swapped. Someone who writes
fs: nfsis pinning a backend or diagnosing one, and both are defeated by quietly mounting something else — the pin does not hold, and the diagnosis measures the wrong thing. If the named backend cannot serve on this box, the node mounts nothing and says what is missing. fs: offmounts nothing, deliberately. A node that cannot mount warns and keeps retrying; a node told not to mount does neither. From agent 0.4.78 it also clears a mount an agent that was killed left behind, on Linux and macOS — the kernel is asked what is mounted there and only a mount this agent's own backends make is removed (details). Through 0.4.77 such a mount stayed until someone unmounted it by hand, so on those versions restart the service first and then set the key. Everything else about the node is unaffected.- A misspelled value is not a pin. It warns and falls back to
auto, because a typo must not take a node's filesystem away.nobgp showprints what resolution decided, so a value that is not in force never reads as one. - Changing it needs a restart, and from agent 0.4.87 the agent deliberately will not reload on it. Every other key here is picked up by the running agent within about five seconds;
fsis the one key where a reload is not the same thing as a restart, because the backend is chosen by a probe at startup and the mount it makes is held by the kernel after the process that made it is gone. Onefs: winfsp→fs: webdavedit on a live Windows node was measured leaving a node whose control channel was perfectly healthy and which could not serve a single session — recovery needed someone at the console. So the agent logsfs changed, restart to apply - config not reloadedand keeps running on the value it started with; runsudo nobgp service restartto apply it. Through agent 0.4.86 the edit reloaded the agent, which is the failure above. - ⚠ While an
fsedit is pending, no other edit to this file reloads either. A reload re-reads the whole file, so letting a laterlog-levelchange through would apply the parked backend switch with it — an edit touching both applies neither. Puttingfsback to the value the agent started with lifts the suppression at once. The same rule is whatnode_config_setreports asrestart_requiredon keys you would expect to be live.
fs-cache-ttl is a staleness window, not a speed dial: it is how long this node may keep showing you a folder that another node has already changed. It is settable three ways — --fs-cache-ttl, NOBGP_FS_CACHE_TTL, or the key above — and clamped to [0, 10m], with a value past the ceiling reduced to 10m and a negative one to 0.
fs-cache-ttl: 30 is thirty nanoseconds, not thirty seconds — a listing cache that expires before it is read, and a node quietly back on a round trip per lookup. Always write the unit: 30s, 2m.
The agent catches the obvious shape of this: any value below one second that is not exactly 0 is treated as a missing unit, so it warns, tells you to write 30s, and keeps the default rather than acting on it. A value that is unparseable altogether does the same.
0 is a real setting rather than "off": it re-lists every time while still serving unchanged bytes from cache, which is the honest answer for a hot shared tree. On an NFS mount the value is read at mount time, so a change there needs a service restart to take effect.
Shared drive proxy keys
The agent runs a small local proxy for the shared drive on 127.0.0.1:19840, falling back to a port the operating system assigns when that one is taken. It is what lets the machine's own WebDAV client mount the share without knowing anything about noBGP credentials: the proxy stamps the node's token onto every request it forwards to the router. It listens whether or not the drive is currently mounted, and on nodes whose backend is not WebDAV at all. The two cases where it is not started, from agent 0.4.57: fs: off, and a node with no mount point at all (a Windows profile that found no free drive letter). In both the mount is the proxy's only consumer, so leaving a credential-stamping listener up would give away the node's access to the share while serving nothing.
So any local process that can open a loopback socket holds the node's access to everything the drive presents — since agent 0.4.54 that is the node's own storage area as well as each network's share (see What the mount contains) — and on macOS, that reaches past the drwx------ on /Volumes/nobgp.
Since agent 0.4.53 the config file carries a webdav-proxy-owners key intended to bound that to a set of local accounts:
webdav-proxy-owners: # numeric uids allowed to reach the local proxy
- 0
- 501
Setting it takes the shared drive off the node. From agent 0.4.97 the agent refuses every local caller when the key is set, on Linux, macOS and Windows alike, and says so once at ERROR in its log.
The identity check the key relies on asks the kernel which account is on the other end of the connection — and the call it uses only answers for a Unix-domain socket, while this proxy listens on loopback TCP. Through agent 0.4.96 the kernel's unanswered reply was read as an answer, so the observe-only log line printed a number that was not the caller's uid: 4294967295 on Linux against a real caller at uid 107, and 0 on macOS against a real caller at uid 501. Following the guidance these docs used to give — read the uid out of the log, then set it — would therefore have admitted every local process while the log, the config and the docs all said the proxy was restricted.
Agent 0.4.97 makes the gate fail closed rather than silently wrong: the observe-only line now reads WebDAV proxy: peer uid unavailable on this platform (observe-only), and a node whose key is set logs refusing every local caller — webdav-proxy-owners is set, but the peer's uid cannot be determined over a loopback TCP socket on any platform. Clear the key to restore the mount. Nothing about the exposure changed — the proxy is exactly as reachable as it was — only what the agent claims about it.
- An empty list — the default — observes rather than allows, and refuses nothing. It is the only usable setting today.
- A non-empty list refuses everything. Every caller gets
403 Forbidden, because no caller's uid can be determined. Negative entries are still ignored with a log line rather than wrapping into a uid that would match nobody. - It is uids, not account names —
id -u <account>on the node — for whenever the check becomes enforceable. - Nothing on the node needs it to mount. The drive works with the key unset; that is the supported configuration.
There is no command-line flag for this — edit the config file, and the agent reloads.
Peer reach into loopback
From agent 0.4.153 the machine's owner decides whether an overlay peer may reach a
port bound only on this node's loopback. Since agent 0.4.140 a peer's connection
arrives as a local connection the agent opens, so a
peer reaches what a program on the machine reaches — 127.0.0.1 included. This key is
the veto on that half of it:
peer-loopback: true # default: a peer reaches loopback like any other local address
# Keep loopback-only services off the network
sudo nobgp config --peer-loopback=false
trueis the default and the behaviour of every earlier release. It changes nothing on a node that has not set it.falserefuses a peer every port bound only on loopback —127.0.0.1,[::1], and the loopback interface's own link-local (macOS giveslo0anfe80::1, and a service bound only there is bound only on the loopback). A service bound to the wildcard address, or to an address the machine actually holds on a real interface, is unaffected: a peer still reaches it.- The same answer is used for what the node reports. A loopback-only listener the veto refuses is left out of the services this host tells the network about, so the report can never claim a peer reaches a port the node itself would refuse.
- It does not narrow a published service. Publishing a service is a delegation the node's controllers made on purpose, which is a different question from the generic peer reach this key gates.
- It is set on the machine and only there, like the capability keys:
node_config_setrefuses it. The node reports the value to the router, andnobgp statuscarries it asnetwork.peer_loopback.
Windows Firewall rule
From agent 0.4.124 a Windows node decides for itself whether the agent keeps an inbound allow rule in Windows Firewall. The key has no effect on Linux or macOS.
windows-firewall: allow-overlay # allow-overlay | respect-windows
allow-overlay(the default) installs one rule, namednoBGP-<profile>— for examplenoBGP-default— allowing inbound traffic from this node's overlay slice on every protocol and port. This is what every release before 0.4.124 did, so an upgrade changes nothing about what a node can reach.respect-windowsinstalls no rule and, at the next agent start, removes the one an earlier start installed. Windows Firewall and the operator's own rules then decide what a peer may reach on the host.
Taking the rule away does not take noBGP away. The agent's own surfaces — the
command tool, the fs_* and file tools, and published services —
do not need it: they run through the agent rather than through an inbound listener.
What the rule is for is another machine on your network reaching a port this host
serves directly, which is exactly what respect-windows hands back to Windows.
⚠ From agent 0.4.140 respect-windows no longer covers TCP, UDP or ping from
other nodes, and from agent 0.4.144 it covers nothing from them at all. That
traffic is delivered as a local connection made by the agent, which Windows
Firewall does not filter, so a peer reaches this machine's ports — including one
bound to 127.0.0.1 — whichever value this key holds. Agents 0.4.140 to 0.4.143
still governed what arrived on the tunnel adapter — other IP protocols, IP
fragments, and ICMP other than ping — and from 0.4.144 none of that reaches the
machine either: the agent drops it rather than
writing it into the adapter, so there is nothing left for the rule to filter.
nobgp show says so on the windows-firewall line of a machine
that is delivering that way. See How another node reaches this machine's own
services.
Set it with nobgp config --windows-firewall respect-windows from an Administrator
terminal, or by editing the profile. Like the other settings that are the machine
owner's call, it has no remote path:
node_config_set refuses it, because what a host
lets in is its owner's decision about their own machine.
Three behaviours worth knowing, all of them from 0.4.124:
- The rule is created once and kept, including across a stop. Earlier releases added it whenever the tunnel came up and deleted it on a clean stop, which lost any edit an operator had made to the rule and left a duplicate behind after a crash. An existing rule is now left alone: its remote-address list is rewritten only when it reaches outside the node's current slice, so narrowing it by hand sticks, and edits to its profile, ports or interface types survive.
nobgp uninstallremoves it, so an uninstalled agent leaves no inbound allow rule behind.- An invalid value changes nothing — no rule is added and none is removed, and
the agent logs why.
nobgp configrefuses the value before writing it, so only a hand-edited profile reaches that state.nobgp showprints the setting on Windows always, and off Windows only when the profile sets it, where it says it has no effect on this platform.
The rule is named after the profile from agent 0.4.126, and the earlier
device-named rule is migrated to it. Up to 0.4.125 the name came from the tunnel
adapter — noBGP-nobgp0 — and that index is not stable: the agent steps past a busy
one, so a rule kept across a stop could lose its owner, or be claimed by the wrong
profile on a machine running several. The profile name does not move.
- The migration happens at the next agent start and keeps your edits. The device-named rule a profile owns is renamed rather than replaced, so whether it is enabled, which firewall profiles it applies to, its ports, its interface types and a remote-address range narrowed by hand all survive. This is true when one rule has that name and its remote address range is inside the profile's slice, or its name is the adapter that this start opened. A name with duplicate rules (left by a crash), or a rename that fails, gets a new rule without your edits. On a Windows whose display language is not English, the agent cannot read the remote address range, so it sets the range to the profile's slice. Any other device-named rule that profile owns is deleted, so one rule becomes one rule.
- Ownership is decided first by the overlay slice: a rule whose remote addresses fall inside this profile's slice is this profile's, and one inside another profile's slice is left for that profile to migrate at its own next start. A rule left over (remote address Any, an old slice, or not readable) is this profile's on a machine with a single profile. On a machine with several profiles, it belongs to the profile that opened that adapter at this start.
- A failed read of the host's rules changes nothing under
allow-overlay. Windows always has inbound rules, so a listing that comes back with none means the read failed, not that the firewall is empty — the rule is left exactly as it is until the next start rather than adding a duplicate that a later start would delete along with the operator's own rule.respect-windowsstill deletes, by name. nobgp removeremoves the rule of the profile it removes, andnobgp uninstallremoves every profile's rule plus any device-named rule an earlier release left behind. Nothing else removes it: it is deliberately kept across a stop.- An agent that comes up without an overlay still applies
respect-windowsand removes the rule;allow-overlayhas no slice to allow there and leaves it alone.
Which LAN names this node serves
From agent 0.4.128 a node's owner decides which host names on the machine's own LAN it will resolve for the rest of the network — the names a member reaches on its own LAN.
lan-names: "*" # default: any name
# Serve the NAS and the printer, and nothing else
sudo nobgp config --lan-names 'nas*,printer'
# Serve everything except the router
sudo nobgp config --lan-names '*,!router'
# Answer for no LAN name at all
sudo nobgp config --lan-names off
- Comma-separated patterns, with
*and?wildcards. Matching ignores case and a trailing dot, and a name matches on its full name or its first label — sonascoversnas.lanandnas.local, which is what lets the owner write a host name once even though one peer asks for it bare and another asks for it dotted. An IP address matches on the full string only. !patternexcludes, and an exclusion wins whatever the order.*,!routerand!router,*are the same list, so a veto never depends on where it sits. A list of only exclusions starts from every name, so!routermeans "everything but the router", andnobgp statusshows it back as["*", "!router"].- An empty value,
offornonemeans no name. The node then answers for nothing on its LAN — peers reach such devices through another member, or not at all. - ⚠ It is not a DNS firewall. The first-label rule cuts both ways:
nas*also admitsnas.example.com, and!routeralso refusesrouter.example.com. For a refusal, matching too much is the safe side.
What it filters is every lookup the node makes on the network's behalf — the
answer it gives when noBGP asks the members who can reach a name, the delivery
leg it arms for such a name, and the re-arm it does at startup. A name outside
the list is not looked up on the network's behalf, so no other node gets its
address: net_reach reports
resolution.refused: "lan_names" and probes nothing. ⚠ It withholds the address
and not the existence — the node's own lookups are not filtered, and one still
runs on the miss path, so a remote caller can tell a refused name that exists on
the LAN from one that does not, by the shape of the refusal
(agent#1167).
- What this machine's own applications look up is never filtered. The list is
about what the node serves to others;
ping nason the machine itself works exactly as it did. - Narrowing it takes down a leg that was already up. A name the node was delivering traffic for, and that a narrowed list no longer covers, stops being delivered to at the next directory sync rather than at the next restart.
- It is the machine owner's setting, like
allow-roots, so there is no remote path to it:node_config_setrefuses to write it andnode_config_getdoes not report it. Reaching into the owner's own LAN is their call. - A bad pattern is refused before it is written, by
nobgp config. One that reaches a running agent anyway — a hand-edited profile, or the environment variable — makes the node refuse every name, and bothnobgp showandnobgp status(asnetwork.lan_names_invalid) say which pattern and why.
How a node delivers to a LAN device
From agent 0.4.131 there is nothing to set here. A node always carries another member's traffic to a device on its own LAN — a name it serves for the network — from its own socket: the agent ends the flow itself and opens an ordinary connection to the device, so the host's kernel picks this machine's own LAN address as the source and the reply comes back to the socket that asked. It needs no firewall rule and no source rewrite, and it works the same way on macOS, Windows and Linux.
The lan-delivery key that chose between this and kernel forwarding in agent
0.4.129 and 0.4.130 is retired. The agent no longer reads it and removes it
from the profile the next time it writes the file; --lan-delivery and
NOBGP_LAN_DELIVERY are gone with it. A node that had it set to socket behaves
exactly as before; one left on forward moves onto the socket path when it
upgrades.
- ⚠ TCP, UDP and ICMP echo only. Kernel forwarding carried any protocol and a proxy cannot: other ICMP types and anything that is neither TCP nor UDP are dropped. A ping is answered only when the real device answers this node — a reply is never invented for a device that is silent.
- IPv4 and IPv6 from agent 0.4.140. The node dials every address the name resolves to on its LAN, preferring IPv4 where the device has both, and a device that answers with an IPv6 address alone is served by a node that has a route to it — and refused, so that noBGP asks another member, by one that has not. ⚠ A ping to such a device is not answered by a Windows node, whose ICMP interface here is IPv4 only; TCP and UDP work. Through agent 0.4.139 the leg was IPv4 only, which is the limit the forwarding path already had, and a name that answered with an IPv6 address alone was refused.
- Concurrent flows are bounded — 512 relayed TCP flows and 256 UDP flows, counted separately so a burst of DNS queries through the node cannot refuse a TCP connection. Past the cap a new flow is refused, and the node logs it at most once a minute.
- A LAN answer of
0.0.0.0, a loopback, a multicast or the broadcast address is refused. Pi-hole and hosts-file blocklists answer0.0.0.0, and a socket connection to it would reach services this machine binds to127.0.0.1. - Traffic addressed to the node itself takes the same path from agent 0.4.140. Through agent 0.4.139 only LAN devices went this way and a peer's traffic to the node reached it over the overlay interface; now the node's own services are served the same way, with consequences of their own — see How another node reaches this machine's own services. The two have separate flow budgets, so LAN traffic cannot refuse a connection to the node.
- On Linux the firewall rules the old path needed are no longer installed, and
an agent that finds rules an earlier release left behind removes them: the
source-NAT and forward-accept chains named after the overlay interface, the
matching
fw4rules on OpenWrt, and afirewalldbinding of the overlay interface to thetrustedzone.
mDNS keys
Two keys, both from agent 0.4.128, about the same protocol pointing in opposite directions: one is what this node tells the network about its own host, the other is what it announces to this machine's own applications about its peers.
mdns-report-services: true # report what this host offers. Default true
mdns-announce: false # show peers to local apps. Default false
mdns-report-services — what this host offers
The node reports the services its own host serves on its LAN, so the rest of the
network can see them: file sharing (_smb._tcp), screen sharing (_rfb._tcp),
remote login (_ssh._tcp and _sftp-ssh._tcp), AFP (_afpovertcp._tcp) and RDP
(_rdp._tcp). That fixed list is the whole of it — no other service type is ever
reported. The result appears as info.host_services in
network_directory, in host_services in
nobgp status, and in each peer's copy of this node's directory
entry.
- Two ways a service is found.
source: mdnsmeans the host's own responder announces it, which is where the instance name comes from.source: listenermeans the node found something listening on that port and the host announces nothing — so the name is a default and the protocol on that port is not confirmed. - A listener counts only where a peer could reach it: bound to all addresses,
to the address overlay traffic is delivered to, or to a dual-stack
::. From agent 0.4.140 that includes a service bound to127.0.0.1or[::1]alone, because a peer now reaches loopback on the machine; before that release such a service was left out, since no peer could reach it. A machine whose agent is not delivering peer traffic that way applies the older rule. Another loopback address (127.0.0.53) and a specific address that is not the one overlay traffic is delivered to — a container bridge, the tunnel adapter — still do not count. - The node reads a listener table; it never connects to the port. Connecting to
the host's own
sshdevery few minutes would write a line to its auth log each time, which afail2banrule counting those could act on. - On Windows, SMB is reported only when the host actually shares a folder. The Server service listens on 445 on every Windows machine, so the port alone says nothing; the node asks for the share list and reports SMB only if there is a real one. If it cannot read the list, it leaves SMB out rather than guessing.
- Reported, never obeyed. Peers can already reach these ports over the overlay, so this adds discovery and not reach — which is why it is on by default. noBGP checks the shape of the list, caps it at 32 entries and passes it on.
- A pass runs at startup and every five minutes. One that could not look —
a tool missing, the time budget spent — keeps the last report rather than
reporting nothing, since an empty list would take this machine's shares off every
peer.
nobgp statusshowspendinguntil the first pass finishes, anderrorwhen the last one could not look. falseis the owner's veto, and it is thorough: the node runs no discovery, reads no listener table, and reports an empty list — which is also what clears a list it reported earlier. Likelan-names, it has no remote path:node_config_setrefuses it.
mdns-announce — peers on this machine
With mdns-announce: true on macOS or Linux, the node registers each peer as
<name>-nobgp.local with the machine's own mDNS responder (mDNSResponder on
macOS, avahi-daemon on Linux), pointing at the overlay address this node minted
for that peer — and one registration per service the peer reports, so another
node's SMB share appears in Finder without anyone typing an address.
- ⚠ The registrations are local-only, and that is the point. They are made on the loopback interface: applications on this machine resolve them, and nothing is sent on the LAN. An overlay address is meaningful only on the node that minted it, so announcing one to a LAN would be advertising an address that means something else — or nothing — on every other machine there.
- It is off by default. Turning it on changes nothing about what peers can reach; it changes what this machine's own applications can see.
- Windows has no announcer, because it offers no way to register a name that
local applications resolve and the LAN does not see. The key is accepted there
and
nobgp statusreportsmdns_announce.state: unsupportedrather than pretending. - What is announced follows the directory: a peer name this node holds a live
entry for, and the services that peer reports (see
mdns-report-servicesabove). A name this node serves itself, a stale entry or a withdrawn one is not announced, and a name that is not a valid label is skipped rather than mangled. - Capped at 128 registrations, names first, then services.
nobgp statusreportscapped: truewhen there were more. - On Linux it needs
avahi-daemon. While there is none — the ordinary state of a headless machine — registrations are kept and counted aswaiting, and they are published the moment avahi appears. The agent also warns once if the host'savahi-daemon.confexcludes the loopback interface or turns publishing off, since the names are then invisible locally (they are still never leaked).
Configuration Priority
Configuration is loaded in this order (later values override earlier):
- Configuration file (
/etc/nobgp/default.ymlon Linux,/usr/local/etc/nobgp/default.ymlon macOS,C:\ProgramData\nobgp\default.ymlon Windows) - Environment variables
- Command-line options
Example Configuration Files
These show the settings part of the file — the keys whose values differ from the default. The agent adds its own recorded state (domain, overlay-cidr, from agent 0.4.132 overlay-cidr6, mcp-port, mcp-token, nfs-port, last-mount-point, the learned QUIC values) and the commented defaults block described under Format.
Minimal (a stock node after registration): no settings at all — every value is the default, so the file holds only what the agent recorded for itself.
Production:
log-level: "warning"
user: "deploy"
allow-root: false
allow-admin: false # written too, so an agent downgrade keeps the veto
Development:
debug: true
log-level: "debug"
compress: false
Self-hosted router:
router: "router.example.com"
Environment Variables
All configuration options can be set via environment variables:
| Variable | Equivalent Config | Example |
|---|---|---|
NOBGP_KEY | registration-key | export NOBGP_KEY="abc..." |
NOBGP_NAME | node-name | export NOBGP_NAME="server1" |
NOBGP_ROUTER | router | export NOBGP_ROUTER="router.nobgp.com" |
NOBGP_INSECURE | insecure | export NOBGP_INSECURE=true |
NOBGP_TRANSPORT | transport | export NOBGP_TRANSPORT="wss" |
NOBGP_TRANSPORT_FAMILY | transport-family | export NOBGP_TRANSPORT_FAMILY="v4" |
NOBGP_QUIC_ROUTER | quic-router | export NOBGP_QUIC_ROUTER="router.example.com:443" |
NOBGP_QUIC_CERT_PIN | quic-cert-pin | export NOBGP_QUIC_CERT_PIN="3b5d...c1" |
NOBGP_DEBUG | debug | export NOBGP_DEBUG=true |
NOBGP_LOG_LEVEL | log-level | export NOBGP_LOG_LEVEL="debug" |
NOBGP_COMPRESS | compress | export NOBGP_COMPRESS=true |
NOBGP_ENCRYPT | encrypt | export NOBGP_ENCRYPT=true |
NOBGP_IDLE_TTL | idle-ttl | export NOBGP_IDLE_TTL="30m" |
NOBGP_INTERFACE | interface | export NOBGP_INTERFACE="eth0" |
NOBGP_PING_INTERVAL | ping-interval | export NOBGP_PING_INTERVAL="120s" |
NOBGP_OVERLAY_CIDR | overlay-cidr | export NOBGP_OVERLAY_CIDR="100.64.16.0/20" |
NOBGP_OVERLAY_IPV6 | overlay-ipv6 | export NOBGP_OVERLAY_IPV6=false |
NOBGP_OVERLAY_CIDR6 | overlay-cidr6 | export NOBGP_OVERLAY_CIDR6="fd3a:9c21:4e00::/64" |
NOBGP_MTU | mtu | export NOBGP_MTU="1500" |
NOBGP_FS_CACHE_TTL | fs-cache-ttl | export NOBGP_FS_CACHE_TTL="30s" |
NOBGP_USER | user | export NOBGP_USER="nobgp" |
NOBGP_LAN_NAMES | lan-names | export NOBGP_LAN_NAMES="nas*,printer" |
NOBGP_MDNS_REPORT_SERVICES | mdns-report-services | export NOBGP_MDNS_REPORT_SERVICES=false |
NOBGP_MDNS_ANNOUNCE | mdns-announce | export NOBGP_MDNS_ANNOUNCE=true |
NOBGP_RUNTIME_DIR | runtime-dir | export NOBGP_RUNTIME_DIR="/etc/nobgp" |
NOBGP_LAN_NAMES set to an empty value means answer for no LAN name, which
is the same thing lan-names: "" in the profile means — an empty variable is a
choice here, not an unset one. The three mDNS and LAN-name
keys are all settable this way, which is the shape a container
image or task definition uses.
NOBGP_FS_CACHE_TTL takes a duration and needs its unit — a bare number is read as nanoseconds, and the agent treats a sub-second value as a missing unit rather than an instruction. There is deliberately no environment variable for the fs key: which backend a node mounts with is a property of that machine, set on the box or in its profile — see Filesystem keys. windows-firewall is the same shape of setting and is set with its flag or the config key.
NOBGP_QUIC_CERT_PIN hands the router's QUIC certificate pin in as provisioning
material, so a node dials QUIC on its very first connect instead of bootstrapping
the pin over a WebSocket connection — the shape a stateless container wants, since
it keeps no profile file to remember a learned pin in. The value is the SHA-256 of
the router's certificate public key, hex-encoded (64 characters); nobgp show
reports such a node's quic-cert-pin as provisioned and nobgp status its
router.quic_pin the same way. It outranks anything the router advertises: a
router that rotates to a different certificate is not re-learned over this value,
its QUIC handshake simply fails and the node carries on over WebSocket until you
update the variable. A malformed value is read as no pin at all, which leaves the
node on WebSocket rather than trusting an unverified endpoint. Set it only against
a router whose pin you know — an ordinary node learns one at registration with
nothing to configure.
NOBGP_USER is the unelevated identity as provisioning material — an image or task definition ships an unprivileged account and names it here, so the node serves admin: false from its first boot instead of refusing every unelevated call until someone creates an account by hand. Since agent 0.4.44 it is honoured on an installed agent too, not only an env-only one: before that a node silently reverted to no account on its first restart, once registration had written a config file. The environment outranks the config file for the agent's process lifetime, so change it in the service or task definition rather than with nobgp config — which saves your value and warns that the variable is winning. The value is not copied into the profile: a provisioned node's config file stays free of a user it never chose.
Usage:
export NOBGP_KEY="<YOUR_NETWORK_KEY>"
export NOBGP_NAME="server1"
export NOBGP_DEBUG=true
sudo -E nobgp agent
Use -E flag with sudo to preserve environment variables.
Log Levels
The --log-level option controls output verbosity:
| Level | Description | Use Case |
|---|---|---|
error | Only errors | Production (minimal logging) |
warning | Errors and warnings | Production (standard) |
info | Informational messages | Default |
debug | Detailed debug information | Development |
trace | Very detailed tracing | Troubleshooting |
What info includes
Two rules bound the default level, and both exist because a log line is a disk write — on a NAS whose journal sits on a RAID mirror, one line is one write to every drive in the box, and that is enough to stop the drives ever hibernating.
- Nothing periodic is logged above
debug. No task writes a line just because a timer fired. Since agent 0.4.110 that includes the metrics summary the agent pushes each minute, and since agent 0.4.114 the DNS re-arm probe that runs once a minute onsystemd-resolvedhosts. - A node says nothing about another node (agent 0.4.114). Peer registrations, a peer's overlay address being retracted, pruned or reserved while the peer is away, and the overlay datapath's per-flow and per-ping lines are all
debug. Before that release a peer that slept and woke — or simply ranping— made this node write, so an idle machine's log volume tracked its noisiest peer rather than its own activity.
Neither rule makes the log silent about this node's own connection. Since agent 0.4.115 every successful registration writes one info line naming the transport that carries the control channel and the router address serving it — control channel registered over quic (<address>) — and on the auto transport, where both legs dial and only one wins, the leg that lost writes closed the losing wss leg. Both are per registration, not periodic, so a node that stays connected writes neither again. The same answer for the connection in force right now is nobgp status's router.transport and router.remote_addr.
So an idle node has a quiet log, and nobgp service logs returning older lines on a connected, idle node is the node being quiet rather than the log being broken. Every one of these lines is demoted rather than deleted: --log-level debug brings them all back. For peer activity specifically, the authoritative record is noBGP's own directory — read it with network_directory or watch it with presence_subscribe — never this log.
Examples:
# Minimal output
nobgp agent --log-level error
# Standard production
nobgp agent --log-level warning
# Development
nobgp agent --log-level debug
# Detailed troubleshooting
nobgp agent --log-level trace
Exit Signals
The agent handles standard Unix signals on Linux and macOS:
| Signal | Behavior |
|---|---|
SIGTERM | Graceful shutdown |
SIGINT (Ctrl+C) | Graceful shutdown |
SIGHUP | Reload configuration |
SIGUSR1 | Increase log level |
SIGUSR2 | Decrease log level |
Unix signals are not supported on Windows. Use nobgp service stop or the Windows Service Manager to stop the agent — this performs a graceful shutdown that reverts the DNS suffix, unmounts the network drive, and resets firewall rules before exiting, the same cleanup SIGTERM triggers on Linux and macOS. Configuration reloads require a service restart.
From agent 0.4.108, a SIGTERM or SIGINT that no nobgp command asked for makes the agent exit non-zero, and launchd restarts it about 8 seconds later. sudo pkill -TERM nobgp, sudo launchctl stop nobgp and launchctl kill TERM system/nobgp therefore stop the agent only momentarily on macOS. Use nobgp service stop, which unloads the job so the stop holds. Linux and Windows are unaffected — see that section for why.
Examples (Linux/macOS):
# Gracefully stop the agent
sudo pkill -TERM nobgp
# Reload configuration without restarting
sudo pkill -HUP nobgp
# Temporarily increase logging
sudo pkill -USR1 nobgp
Logging
Service Logs
The simplest cross-platform way to view logs is with the built-in nobgp service logs command (see above). Platform-specific methods are also available:
Linux (systemd):
sudo journalctl -u nobgp.service -f
Linux (OpenRC / Alpine):
tail -f /var/log/nobgp.err
Linux (OpenWRT):
logread -e nobgp
macOS (launchd):
tail -f /var/log/nobgp.err.log
Windows (PowerShell):
The Windows Service Control Manager does not capture a service's output, so the agent mirrors its logs to a file that nobgp service logs reads back. To view it directly:
Get-Content -Path C:\ProgramData\nobgp\nobgp.log -Tail 50 -Wait
Containers (no service manager):
# The agent's stdout/stderr
docker logs <container>
# Or read the file the agent mirrors its logs to (also what `nobgp service logs` reads)
tail -f /etc/nobgp/nobgp.log
Standalone Logs
When running standalone, logs go to stdout/stderr:
# Standard output
sudo nobgp agent
# Redirect to file
sudo nobgp agent > /var/log/nobgp.log 2>&1
# Pipe to logger
sudo nobgp agent 2>&1 | logger -t nobgp
Node Targeting
Several commands (exec, shell, proxy publish) require specifying a target node. There are two ways:
By name — specify both the network and node name:
nobgp exec -c "uptime" --network production --node web-server
By UUID — use the node ID directly (useful in scripts or when node names aren't unique across networks):
nobgp exec -c "uptime" --node-id 550e8400-e29b-41d4-a716-446655440000
You can find node IDs with nobgp network list.
JSON Output
Most management commands support a --json flag for machine-readable output. This is useful for scripting or piping to tools like jq:
# List networks as JSON
nobgp network list --json
# Get service details as JSON
nobgp proxy list --json | jq '.networks[].nodes[].services[]'
# Create a network and capture the ID
nobgp network create --name staging --json | jq -r '.network_id'
Platform Support
The management commands (login, logout, network, node, exec, proxy) work on all platforms — macOS, Linux, and Windows. The one exception is nobgp shell, which is not yet supported on Windows clients.
| Command | macOS | Linux | Windows |
|---|---|---|---|
login / logout | Yes | Yes | Yes |
network list / create | Yes | Yes | Yes |
network add-node | Yes | Yes | Yes |
exec | Yes | Yes | Yes |
shell | Yes | Yes | Not yet |
file upload / download / ls / rm | Yes | Yes | Yes |
proxy list / publish / update / delete | Yes | Yes | Yes |
The shell limitation is on the client side only. You can still open shells on Windows nodes using browser terminals or from a macOS/Linux client.
Common Usage Patterns
Personal Machine Setup
# Log in once on your laptop
nobgp login
# Now manage your infrastructure from the command line
nobgp network list
nobgp exec -c "uptime" --network production --node web-server
nobgp shell --network production --node api-server
Remote Server Setup
# Generate install commands from your laptop
nobgp network add-node --network production --node new-server
# Copy the output and run it on the remote machine
# (installs agent, registers with network, starts service)
Development Workflow
# Register interactively and run with debug
sudo nobgp register --name "dev-$(whoami)"
sudo nobgp agent --debug
# Monitor logs in another terminal
sudo nobgp service logs -f
Production Deployment
# 1. Register the agent (opens browser for OAuth login)
# Registration installs and starts the service itself; exit 3 means
# the node enrolled but the service did not come up.
sudo nobgp register --name "$(hostname)"
# 2. Only needed if step 1 reported a service failure (exit 3)
sudo nobgp service install
# 3. Verify
nobgp status
Containerized Deployment
# Use registration key (browser login not available in containers)
docker run -d --restart unless-stopped \
--pull always \
--name nobgp \
--cap-add NET_ADMIN \
--cap-add SYS_ADMIN \
--device /dev/net/tun \
--device /dev/fuse \
--security-opt apparmor=unconfined \
-e NOBGP_KEY="KEY" \
-e NOBGP_NAME="container-$(hostname)" \
nobgp/nobgp
Debugging Connection Issues
# Run with maximum logging
sudo nobgp agent --log-level trace --debug
# Check if router is reachable
curl https://router.nobgp.com
# Check registration status
nobgp show
Configuration Best Practices
1. Register Before Running
Good:
sudo nobgp register --name "server"
sudo nobgp service install
Avoid:
sudo nobgp agent # Running without registration
2. Set Appropriate Log Levels
- Production:
warningorerror - Development:
infoordebug - Troubleshooting:
debugortrace
3. Use Descriptive Node Names
Good:
web-server-prod-1api-server-stagingdb-primary-us-east
Avoid:
server1nodetest
4. Secure Your Configuration File
# Restrict permissions
sudo chmod 600 /etc/nobgp/default.yml
sudo chown root:root /etc/nobgp/default.yml
5. Monitor Service Health
# Add to cron or monitoring system
*/5 * * * * systemctl is-active nobgp.service || systemctl restart nobgp.service
Troubleshooting Commands
Check Configuration
# View current config
cat /etc/nobgp/default.yml
# Validate YAML syntax
python3 -c "import yaml; yaml.safe_load(open('/etc/nobgp/default.yml'))"
Test Connectivity
# Check router accessibility
curl -v https://router.nobgp.com
# Test WebSocket connection
websocat wss://router.nobgp.com # If websocat is installed
Diagnose Service Issues
# Check service status
sudo nobgp service status
# View service logs
sudo journalctl -u nobgp.service -n 100
# Check for errors
sudo journalctl -u nobgp.service -p err
# Restart service
sudo nobgp service restart
Resource Usage
# Check process
ps aux | grep nobgp
# Memory usage
ps -o pid,user,%mem,rss,command -C nobgp
# CPU usage
top -p $(pgrep nobgp)
Advanced Topics
Custom Router
Custom routers are available for enterprise deployments only. Contact sales@nobgp.com for more information.
For enterprise deployments with custom routers:
router: "custom-router.example.com:8080"
Multiple Profiles
Connect a single machine to multiple noBGP networks using profiles. Each profile is a separate YAML file in the config directory:
# Register profiles for different networks (interactive)
sudo nobgp register production --name "server-prod"
sudo nobgp register staging --name "server-staging"
# List all profiles
nobgp list
# Show a specific profile
nobgp show production
# When running as a service, all profiles are managed automatically
sudo nobgp service install
sudo nobgp service start
Interface Selection
By default (interface: "auto"), the agent picks the primary interface
automatically. It first chooses the first up, non-loopback interface that has a
hardware (MAC) address and an IP address in the required family. If no such
interface exists, it falls back to MAC-less point-to-point links — such as
cellular modems (wwan0), PPP, and tun devices — ranking them by carrier
state and whether they hold a global-unicast address. This means nodes that only
have a cellular or point-to-point connection are now selected automatically.
To override auto-selection, force a specific network interface:
interface: "eth0" # Or eth1, wlan0, wwan0, etc.
An explicit interface name always wins: the agent uses exactly that interface and never re-homes to another.
Waiting for an interface at startup
From agent 0.4.107, an agent that starts on a host with no usable interface waits for one instead of failing startup. A usable interface is one that is up, is not loopback, and holds an IP address — an interface that is up but has not been given an address yet is not usable yet either.
Having none is normally temporary: the link is not up yet at boot, or the machine's Wi-Fi dropped while the agent happened to be restarting. Through 0.4.106 that was fatal, and the process exiting turned a temporary condition into an outage the node could not leave on its own — the service manager restarted the agent, the next start ran the same probe, and the node cycled roughly once a minute for as long as the condition lasted. On a Buildroot appliance whose Wi-Fi had died this measured 1,991 identical exits over 31.5 hours, ending only when someone cut power to the box.
The agent now re-probes on a widening interval — first after 2 seconds, doubling to a ceiling of 30 seconds — and continues startup the moment a link appears, logging which interface it took and how long it waited. The first failure is logged as a warning and restated every 5 minutes, so a long wait leaves a readable log rather than a line a minute.
Two things worth knowing while a node is in this state:
nobgp statusdoes not report the wait. The agent's local API socket is opened after this point in startup, so the profile is listed underunavailable— the same answer as a stopped agent. The service log is where the wait is visible.- An explicit
interface:is waited for too. A node pinned to an interface name that is not present on the host waits for that name to appear instead of exiting, so a typo in the setting now waits indefinitely rather than failing fast. The log line names the interface it is waiting for.
Next Steps
- Agent Installation - Detailed installation guide
- Core Concepts - Understand how noBGP works
- MCP Reference - API reference for AI assistants
See Also
- Configuration directory:
/etc/nobgp/(Linux),/usr/local/etc/nobgp/(macOS),C:\ProgramData\nobgp(Windows) - Binary location:
/usr/bin/nobgpor/usr/local/bin/nobgp(Linux/macOS),C:\Program Files\noBGP\nobgp.exe(Windows)