Every environment variable and settings file Link reads, the relay's
flags, and the deploy scripts' flags and variables. Flags of lnk
commands aren't here: run lnk <group> <command> --help.
Must be absolute, or commands that delete or trust a folder refuse.
XDG_CONFIG_HOME
~/.config
Settings go in $XDG_CONFIG_HOME/lnk (mode 700). A relative value is ignored.
LNK_EXE
set by lnk
The lnk that started a plugin, which it runs for other commands.
LNK_KEYCHAIN
on, on a Mac
off or 0: keep secrets in settings files, not the Keychain.
LNK_SERVICES
on
off or 0: ask launchd or systemd nothing and start no model runtime; services read as stopped. Tests set it, since a service's name doesn't depend on HOME.
LNK_SECURITY
/usr/bin/security
The Keychain tool (tests). A stand-in turns the Keychain on anywhere.
NO_COLOR
unset
Set: no colors. Colors show only on a terminal.
CLICOLOR_FORCE
unset
Set: colors even in a pipe or a file.
File
What it holds
~/.config/lnk/machine.toml (600)
This machine's id and name; unsandboxed = false forbids running any agent without a sandbox here, whatever asks (a deployment's), and a file holding only that gets its id and name added; keep_moved is how long a moved agent's files stay ("30d", the default, or "never").
Link's plugins
Agents (lnk agent)
lnk agent start passes each LNK_<NAME>_API, _CLI and _ENGINE
variable it's run with (never one naming a key, token, secret, password
or auth) to the agent's bridge service, for a channel plugin's tests.
The provider's own key variable, read as --key is by lnk agent start, new and move. With no model yet, the one that is set (several set: --key); with a hosted model, its provider's, to renew the key. Never passed to a harness or to a box.
LNK_TELEGRAM_TOKEN
asked by lnk agent connect telegram
The bot token from BotFather.
LNK_TELEGRAM_API
https://api.telegram.org
Telegram's Bot API (tests).
LNK_SLACK_BOT_TOKEN, LNK_SLACK_APP_TOKEN
asked by lnk agent connect slack
Bot token (xoxb-...) and app-level token (xapp-...); both or neither.
LNK_SLACK_API
https://slack.com/api
Slack's Web API (tests).
LNK_DISCORD_TOKEN
asked by lnk agent connect discord
The Discord bot's token.
LNK_DISCORD_API
https://discord.com/api/v10
Discord's HTTP API (tests); the Gateway's address comes from it.
LNK_WHATSAPP_NUMBER, LNK_WHATSAPP_OWNER
asked by lnk agent connect whatsapp
The agent's number and yours, with country code. The same twice for your own WhatsApp.
LNK_WHATSAPP_ENGINE
lnk-whatsapp engine
On Link Harness, the program that holds the WhatsApp device and speaks JSON lines (tests).
LNK_SIGNAL_NUMBER, LNK_SIGNAL_OWNER
asked by lnk agent connect signal
The agent's number and yours, with country code; two different numbers.
LNK_SIGNAL_CLI
the signal-cli Link installs
On Link Harness, a signal-cli of your own to carry Signal with (tests); the account stays in ~/.config/lnk/agents/<agent>/channels/signal.
LNK_NO_BROWSER
unset
Set: lnk agent connect opens no browser pages.
LNK_PORT
set by lnk-harness
The agent's base port, for harness adapters. Unset for main.
LNK_AGENT
set by lnk-harness
The agent's name, for harness adapters.
LNK_AGENT_ID
set by lnk-harness
The agent's ID, for harness adapters.
LNK_OWNER
set by lnk-harness
The agent's owner, for harness adapters.
LNK_MEASURES
set by lnk-harness
For harness adapters: the file in the harness's home it may append its own measures to, as OTLP (.link/measures.jsonl), which lnk measure reads.
LINK_FILES
set by lnk-harness
The agent's files folder, for harness adapters.
LNK_MEMORY
set by lnk-harness
In the harness's environment: its memory's address, an MCP server on its proxy.
LNK_MEMORY_MODEL
set by lnk-harness
In a memory engine's environment: the address of the embedding model Link serves for it outside its sandbox, empty when no runtime here has it.
LNK_TOOL_<NAME>
set by lnk sandbox wrap
In the harness's environment: each tool it may call, an MCP server on its proxy. Link Harness calls these and LNK_MEMORY.
File
What it holds
~/.config/lnk/agents/<name>.toml (600)
An agent's settings (below). This machine's is main.
The bridge's way in: messages for the agent from its schedules and subagents.
~/Link/Agents/<name>/Scheduler/
Its scheduler's jobs, written by its tool, fired by its bridge.
~/.config/lnk/agents/<name>/owed/ (700)
Where an answer to a message from others goes (its chat, another conversation, or nowhere), kept by the bridge until it went, so an answer finished after a crash goes there too; a subagent's conversation's is kept for its own subagents' answers.
~/.config/lnk/teams/<team>.toml
A team's file: its members, and the opinions they share (agent spec).
~/.config/lnk/agents/<name>/ (700)
Its log, its foreground lock and its proxy's sockets.
~/.config/lnk/agents/<name>/sandbox.toml (600)
What you grant the agent, whichever harness runs it (below).
~/.config/lnk/harnesses/<harness>.toml (600)
What a harness said it needs here and you allowed, for every agent that runs it (below).
~/.config/lnk/agents/.net/<id>/url.secret (600)
The agent's proxy secret, in its tool and local model URLs, by the agent's ID.
~/.config/lnk/agents/.net/<id>/<harness> (700)
Sockets between the harness's network and its proxy; <harness>-<step> for a setup step's (install, configure, health, ...).
~/.config/lnk/agent-defaults.toml (600)
The proxy upstream each new agent here starts with.
~/.config/lnk/moved/<id>.toml (600)
A note left when an agent moved away.
~/Link/Agents/.moved/<name>-<date>
A moved agent's files folder, kept 30 days, or as machine.toml's keep_moved says.
~/Link/Agents/.incoming (700)
An arriving agent, before it's put in place.
~/.config/lnk/agents/<name>/bridge.json (600)
For a harness with an endpoint, such as Link Harness: its endpoint, key, proof and channels. Made at each start.
Its memory, as a tool (lnk agent memory use), by the agent's ID. Defined again at a start only when something it's made from changed.
~/.config/lnk/agents/<name>/memory.json
What the memory tool was last defined from, so a start with nothing changed leaves it as it is.
~/.config/lnk/agents/<name>/ports.json
The harness's ports at its last start, which lnk agent status shows.
<harness home>/.lnk-configured
A hash of the settings, versions and files the last configure left, so a start with nothing changed skips it. Delete it to configure again.
<harness home>/...
What a harness from outside Link keeps there: its repository's README says.
An agent's settings (agents/<name>.toml)
Each key is changed by an lnk agent command (start, connect,
exit, use, memory); edit them there, not in the file.
Key
What it holds
id
The agent's ID, which never changes and moves with it.
owner
Whose it is: the account signed in (github-<user id>), or before you sign in, <user>@<machine>, which the account replaces at its next start. Left out, the latter.
port, block
Its first port and how many from there are its harness's (left out: 200); none for main.
An agent's grants (agents/<name>/sandbox.toml)
What you grant the agent, whichever harness runs it. Changed by lnk agent allow and deny, or written whole by lnk agent start --sandbox <file>; lnk agent permissions shows them. An agent without
a file has Link's defaults. A key or permission Link doesn't know is an
error.
Key
What it holds
permissions
Its permissions; left out, network and developer-tools.
hosts
The only hosts its proxy lets it reach; none, any.
tcp
Endpoints beyond HTTPS and HTTP, host:port.
tools
Tools that run outside it may call.
calls
The most calls a minute on its proxy; none, no limit.
A harness's grants (harnesses/<harness>.toml)
What the harness said it needs (lnk-<harness> needs) and you allowed,
held while it runs, for every agent here that runs it. Changed by
answering what it asks, by lnk agent <harness> allow and deny (only
for what it asks for), and by repermit;
lnk agent <harness> permissions shows them. A harness without a file
needs nothing.
Key
What it holds
permissions
The permissions it asked for that you allowed.
unsandboxed
true: it runs with no sandbox at all.
risks
Risks of its own it named that you allowed (browser).
answered
The needs it asked for that you answered, not asked again until repermit.
Sandbox (lnk sandbox)
A program run by lnk sandbox run keeps only PATH, HOME, TERM,
COLORTERM, LANG, LC_ALL, LC_CTYPE, TZ, USER, LOGNAME and
SHELL from your shell.
Variable
Default
What it does
LNK_MCP_HEADER_<n>
set by lnk sandbox tool add --header
In a tool at a URL's environment, outside: a header it sends, Name: value.
LNK_TEST_ON_CLOUD
unset
1: the sandbox takes this machine for a cloud's, refusing [lan] what it refuses there (tests). Nothing makes a cloud's machine not one.
LNK_SANDBOX_LEARN_FD
set by lnk sandbox learn
Into the sandbox, for its first program, which removes it: the descriptor it writes each program started to. A program inside that sets it only records what it runs itself.
File
What it holds
~/.config/lnk/sandbox/ (700)
The rules, one Seatbelt profile per policy.
~/.config/lnk/sandbox/<name>.model.json (600)
A proxied policy's model provider and key, which its proxy adds.
A proxied policy's secret (a Telegram bot's token) and its rule, which its proxy adds.
~/.config/lnk/sandbox/<name>.url-secret (600)
The secret a proxied policy's tool and local model URLs carry, which its proxy requires.
~/.config/lnk/sandbox/wire.log (600)
lnk sandbox log: one JSON object per proxied call.
~/.config/lnk/sandbox/traffic/<pid>.json (600)
lnk sandbox traffic: the bytes a running proxy has carried each way, written every 5 seconds while it changes; removed once its proxy is gone.
~/.config/lnk/sandbox/asks/<id>.json (600)
A question a proxy is waiting on (lnk sandbox asks).
~/.config/lnk/sandbox/tools/<name>.json (600)
A tool that runs outside (lnk sandbox tool add): its command, or mcp-remote and its URL, and its environment, headers included.
~/.config/lnk/sandbox/settings.toml
The sandbox plugin's settings, over Link's: ask_wait, how long a question waits for your answer ("120s"; silence is no), tool_starts, the tool servers starting at once (2), and tool_sessions, the tool sessions one sandbox keeps (8). A file that can't be read is said, and Link's are used.
~/.config/lnk/sandbox/stopped
While it exists, every proxy refuses every call.
~/.config/lnk/sandbox/paused/<sandbox> (600)
While it exists, that sandbox's proxy refuses every call.
~/.config/lnk/sandbox/placeholders.json (600)
Linux: the empty placeholders made where a project's dot file isn't, while sandboxes run, and which runs hold each; removed with the last.
~/.config/lnk/sandbox/bwrap-version
Linux: whether this machine's bubblewrap can refuse user namespaces inside, so a sandbox doesn't ask it each run.
~/.config/lnk/sandbox/guests.toml (600)
lnk sandbox guest: each guest's settings, or their spec (--spec), its paths absolute.
~/.config/lnk/specs/sandbox.toml (600)
The sandbox specs run here, each by its path and a hash of what it opens (spec).
Models (lnk model)
No environment variables. The models plugin finds runtimes at their
default ports and at the addresses you add, each time; --upstream
points one command at any other.
File
What it holds
~/.config/lnk/models.toml (600)
lnk model add: each added runtime's name and base_url.
~/.config/lnk/model-facts.toml
Yours, optional: models you describe or correct ([model."<name>"]: window, answer, thinking, price), over Link's own, field by field (lnk model facts).
Accounts (lnk auth)
Variable
Default
What it does
LINK_RELAY
the relay you logged in to (login: https://local.link)
Relay to use (--relay), e.g. http://localhost:7080.
LINK_TOKEN
your login's
Token for the relay, from which lnk bucket share gets a short-lived one.
LNK_CONFIG
~/.config/lnk/config.toml
Where the saved login lives.
LNK_NO_BROWSER
unset
Set: lnk auth login prints the URL without opening it.
LNK_LOGIN_TICKET
unset
A confirmation given ahead by a machine signed in (lnk auth ticket --json): lnk auth login, once approved in the browser, needs no lnk auth approve. lnk box start sets it on the box.
File
What it holds
~/.config/lnk/config.toml (600)
The relay, username, token and GitHub user_id from lnk auth login. Only the accounts plugin reads it.
Tunnel (lnk tunnel)
Variable
Default
What it does
LINK_RELAY
the relay you logged in to, else https://local.link
Relay to use (--relay), e.g. http://localhost:7080.
LINK_TOKEN
your login's (lnk auth)
Token for the relay (--token).
LINK_AUTH
none
user:password visitors must give (lnk tunnel open --auth).
RUST_LOG
link=info
Log level of lnk tunnel open, e.g. link=debug.
Buckets (lnk bucket)
Variable
Default
What it does
LNK_PERSONAL_DIR
~/Link/Personal
Your files folder, an absolute path.
LNK_RCLONE
the pinned rclone in ~/.config/lnk/bucket/engine
Run this rclone instead, unchecked (tests).
RCLONE_*
—
Removed before running rclone.
AWS_*, GOOGLE_APPLICATION_CREDENTIALS
—
Removed before running rclone on a bucket that signs in as a lnk cloud account (aws, gcp).
Also read, for such a gcp bucket set up with env_auth: the key file it signs in with.
LINK_AUTH
none
user:password for lnk bucket share --auth and lnk bucket pull <link>.
File
What it holds
~/.config/lnk/bucket/clouds.toml
The connected clouds and each folder's bucket.
~/.config/lnk/bucket/rclone.conf (600)
Each cloud's settings and encryption passwords, and the keys of those that don't sign in as a lnk cloud account (s3, rclone types, a box's key for one agent).
~/.config/lnk/bucket/rclone-keychain.toml
On a Mac: which settings of which section of rclone.conf are in the Keychain.
What went where: a cache tree reads, and what clean proved of S3 copies, so it reads each back once.
Boxes (lnk cloud, lnk box)
Variable
Default
What it does
AWS_PROFILE, AWS_REGION, AWS_DEFAULT_REGION
—
Read once by lnk cloud connect aws: the login to reuse and its region.
CLOUDSDK_CONFIG
~/.config/gcloud
gcloud's own settings, as gcloud reads them: lnk cloud connect gcp asks gcloud where its login is.
LNK_AWS, LNK_GCLOUD
aws, gcloud on the PATH, else the ones lnk cloud connect installed
The cloud CLIs the adapters run (tests).
LNK_SSH, LNK_SSH_KEYGEN
ssh, ssh-keygen on the PATH
What lnk box runs (tests).
LNK_BOX
set by lnk box setup
In a setup script's environment: the box it sets up.
File
What it holds
~/.config/lnk/cloud/accounts.toml (600)
The connected accounts, and what each cloud's CLI needs to act as them.
~/.config/lnk/aws/, ~/.config/lnk/gcp/ (700)
The AWS CLI and gcloud, when lnk cloud connect installed them.
~/.config/lnk/gcp/credentials/<name>.json (600)
A Google Cloud account's credential file: a service account's key, or your login's application default credentials.
~/.config/lnk/box/ (700)
The boxes, Link's SSH key for them, their host keys, and lock files.
~/.config/lnk/box/boxes.toml (600)
This computer's boxes, each with the spec it was made from (lnk box spec).
~/.config/lnk/box/mux/ (700)
The sockets of the SSH connections to boxes kept open for a minute after a lnk box run.
~/.config/lnk/machine.toml on a box
Its machine ID and name, written by lnk box start.
In the cloud, each box's machine is named lnk-<machine id> and tagged
(labelled on Google Cloud) lnk-box=<name>, lnk-machine=<machine id>
and lnk-owner=github-<your GitHub user id>. The host keys pinned at its
first boot are kept on it: metadata lnk-host-keys on Google Cloud, a
lnk-host-key tag on AWS. A spec's own tags are added to those, at most
40; on AWS its disk has them too, and a spot box's request.
An image lnk box image save makes is named lnk-<name> in the box's
account and tagged (labelled) lnk-image=<name> and
lnk-owner=github-<your GitHub user id>, and lnk-hash=<hash> when
saved with --hash; on AWS its snapshot has them too. Nothing about
images is kept on your computer.
The exits this computer keeps: each machine, how it's reached, who holds it, and who takes it on the socket rather than the port.
~/.config/lnk/vpn/<machine>.log (600)
Each exit's log.
~/.config/lnk/vpn/exit.sock (600), on the machine
The exit's SOCKS proxy, while it's up; left behind when it drops, and removed when it connects again.
Measures (lnk measure)
No environment variables.
File
What it holds
~/.config/lnk/measure/settings.toml (600)
Your settings (lnk measure settings): every, fine and minutes, each a duration (10s, 1d), processes (whether each agent's processes are kept too), and [export], your endpoint's url and its headers. What it leaves out is Link's: a look every 10s, each kept 1d, a minute's summary kept 30d, processes not kept.
Where the watcher stopped reading each harness's file.
~/.config/lnk/measure/watch.log, watch.lock
The watcher's log, and the lock that keeps it to one.
.link/measures.jsonl in a harness's home
What the harness reports of itself (LNK_MEASURES); moved aside to measures.jsonl.1 past 1 MiB.
The core (lnk) and install.sh
Variable
Default
What it does
LNK_RELEASES_URL
https://github.com/woodpav/link-releases/releases
Where releases are downloaded from (tests). Signatures are still checked.
LNK_GITHUB_URL
https://github.com
Where a plugin from outside Link is downloaded from in place of GitHub (tests). Its pinned key is still checked.
File
What it holds
~/.config/lnk/plugins.toml (600)
Each plugin added from outside Link (lnk plugin add <url>): its source, the repository, and the key its releases must be signed with. Also noted: programs beside lnk that lnk upgrade has said once how to keep updated.
install.sh also reads:
Variable
Default
What it does
LNK_INSTALL_DIR
~/.local/bin
Where lnk goes; plugins go next to it.
LNK_VERSION
latest
A version, e.g. 0.11.0. Below 0.11 gives an lnk without plugins.
LNK_INSECURE
unset
1: install without ssh-keygen, so without checking the signature.
The relay and its deploy
The relay (link-relay)
Each setting is a flag, and most can also come from a variable. On a
server, bootstrap.sh writes them to /etc/link/relay.env (root-only),
which the systemd unit loads. Change one with bootstrap's flag, or edit
the file and run systemctl restart link-relay.
Variable
Flag
Default
What it does
LINK_DOMAIN
--domain
localhost:7080
Public base domain, with a port if non-standard. Tunnels are <app>.<user>.<domain>.
LINK_USERS_FILE
--users-file
none
File of <username> <token> lines. Required unless GitHub login is on, or the relay runs locally (a localhost domain on a loopback address).
LINK_STATE_DIR
--state-dir
none
Where login users, blocks, share links and the hosts approved for certificates are saved.
LINK_GITHUB_CLIENT_ID
--github-client-id
none (login off)
A GitHub OAuth app with device flow. Turns on lnk auth login.
LINK_GITHUB_CLIENT_SECRET
--github-client-secret
none (device flow)
The app's secret: web flow and the relay's approval page.
LINK_GITHUB_ALLOW
--github-allow
empty (nobody)
GitHub usernames who may log in, comma-separated, or *.
LINK_GITHUB_MIN_AGE_DAYS
--github-min-age-days
30
Minimum GitHub account age for signups through *. 0: off.
LINK_SIGNUPS_PER_DAY
--signups-per-day
100
Signups through * per 24 hours, for everyone. 0: off.
LINK_WEBSITE
--website
none (404)
Where browsers on the bare domain go, e.g. https://www.local.link.
LINK_NOTICE
--notice
none
A message shown to every lnk that connects.
LINK_TOKEN
--token
dev-token
Local development only: the single user dev's token.
LINK_GITHUB_API
--github-api
https://api.github.com
For tests (hidden).
LINK_GITHUB_URL
--github-url
https://github.com
For tests (hidden).
—
--listen
127.0.0.1:7080
Address to listen on. Keep it on loopback behind Caddy.
—
--public-scheme
https, or http for localhost
Scheme of public URLs.
RUST_LOG
—
link=info
Log level, e.g. link=debug.
On a server, the systemd unit (src/server/deploy/files/link-relay.service)
also limits the relay's open files, memory, threads, system calls and
sockets: the server's security.
Bootstrap installs it; re-run bootstrap after changing it.
Off-site backups (/etc/link/backup.env)
Read by link-state backup, which runs nightly. Without this file,
snapshots stay only on the server, in /var/backups/link. On your Mac,
the same file is what fetch-backup.sh --env reads. Setup is in
the runbook.
Variable
Default
What it is
LINK_BACKUP_URL
—
<S3 endpoint>/<bucket>, e.g. https://storage.googleapis.com/backups
LINK_BACKUP_REGION
auto
The bucket's region: auto for Google Cloud Storage, us-east-1 for AWS, ...
LINK_BACKUP_KEY_ID
—
Access key id, for that bucket only.
LINK_BACKUP_SECRET
—
Its secret.
Uploads are encrypted to the public keys in /etc/link/backup-recipients,
one ssh-ed25519 …, ssh-rsa … or age1… key per line. Without a key
there, the backup refuses to upload.
Deploy scripts
The scripts in src/server/deploy. When to run each is in
the runbook.
bootstrap.sh, on the server as root, piped from your checkout
(ssh root@host 'bash -s -- <flags>' < src/server/deploy/bootstrap.sh).
Settings marked "kept" are saved in /etc/link/relay.env and stay on
later runs.
Flag
What it does
--domain D
Public domain. Required on the first run; kept.
--email E
Let's Encrypt contact for Caddy. Required on the first run, and to install a changed Caddyfile.
--user NAME
Create this user if there's no users file yet; prints its token once.
--github-client-id ID
Turn on lnk auth login with this GitHub OAuth app; kept.
--github-allow LIST
GitHub usernames who may log in, comma-separated, or *; kept.
--website URL
Redirect browsers on the bare domain to this site; kept.
--state FILE
Restore a link-state export snapshot (users, settings, certificates) first. It keeps the server's release key and version floor.
--min-version V
Refuse a release whose relay is older than V, and keep the highest given in /etc/link/min-relay-version as the updater's floor.
--from DIR
Install the deploy files in this folder on the server, a copy of src/server/deploy/files, with no signature check.
create-droplet.sh, on your Mac. Needs doctl, logged in, and an
SSH key on the DigitalOcean account. It creates a droplet and runs
this checkout's bootstrap on it, with --min-version set to the
checkout's version.
Flag
Default
What it does
--domain D
local.link
Public domain.
--email E
—
Let's Encrypt contact. Required unless --state.
--user NAME
—
First user; its token is printed. Required unless --state.
--state FILE
—
Restore users, settings and certificates from a snapshot.
--reserved-ip IP
—
Point this DigitalOcean reserved IP at the new droplet.
--name N
link-relay-<date>
Droplet name.
--region R
nyc1
Droplet region.
--size S
s-1vcpu-512mb-10gb
Droplet size.
--ssh-keys IDS
every key on the account
Comma-separated key ids or fingerprints.
rebuild.sh, on your Mac: --old IP (the droplet to replace) and
--reserved-ip IP, both required. Any other flag goes to
create-droplet.sh.
fetch-backup.sh, on your Mac. Needs age.
Flag
Default
What it does
--env FILE
—
The same settings as /etc/link/backup.env. Required.
--identity FILE
~/.ssh/id_ed25519
Private key matching a line of backup-recipients.
--name NAME
state-latest.tar.gz.age
A specific backup, e.g. state-20260926T031500Z.tar.gz.age.
Environment variables, mostly for testing the scripts:
Variable
Used by
Default
What it does
LINK_RELEASES_URL
bootstrap.sh, create-droplet.sh (passes it on), link-relay-update
the relay-latest release of woodpav/link-releases
Where relay builds and deploy files come from. They must still be signed by the release key.
LINK_RELEASE_SIGNERS
link-relay-update
/etc/link/release-signers
Public keys a release must be signed with.
LINK_RELAY_BIN
link-relay-update
/usr/local/bin/link-relay
The binary it replaces.
LINK_UPDATE_STATE
link-relay-update
/var/lib/link-update
Releases it skips: failed health checks (bad), older ones (older).
LINK_ALLOW_DOWNGRADE
link-relay-update
unset
1: install relay-latest even if older than the installed relay or the floor, or if the installed version can't be read.
LINK_MIN_RELAY_VERSION
link-relay-update
/etc/link/min-relay-version
File with the oldest relay version it installs, written by bootstrap.