Get Started
Run any program so it changes only the folders you give it, reaches only what you allow, and never reads Link's settings. Every Link agent runs in it, and so can anything else.
Needs: macOS (Seatbelt, built in) or Linux with bubblewrap. The
sandbox plugin comes with every harness; lnk plugin add sandbox
installs it alone. Free, no account.
Quickstart
lnk sandbox ready # can the sandbox run here? (Linux: offers what it needs)
lnk sandbox run --write . -- bash # a shell that can change only this folder
lnk sandbox run --internet-host '*' -- curl -sI https://example.com # the internet, but nothing on this machine or its network
lnk sandbox run --spec app.toml -- node app.js # a sandbox written down: only what its file opens
lnk sandbox check --write . # try from inside what it shouldn't allow
lnk sandbox log # every call on the sandboxes' proxies
lnk sandbox stop # close every sandbox's way out at onceRun a program
Run a program in the sandbox
lnk sandbox run --write ~/Projects/site -- npm run build
lnk sandbox run --read ~/Documents/reports --write . -- python3 summarize.py
lnk sandbox run --read '~' --port 5432 -- ./toolThe program reads the system's programs and libraries, and nothing else
unless you open it. It has no network. Its environment is cleared but
for PATH, HOME, TERM, COLORTERM, TZ, the locale and who you are, so no tokens or
SSH_AUTH_SOCK from your shell. On Linux it also gets an empty home
folder, an empty /run and a private /tmp, and sees only its own
processes. run exits with the command's status.
--write <dir>: a folder it may read and write.--read <dir>: one it may only read;--read '~'reads your whole home folder, but Link's settings. Both repeat.--write-dot-files <dir>: a--writefolder whose.git,.envrc,.vscodeand.ideait may change too (below).--hide <dir>: a folder it may never touch, even with--read '~', but for the--writeand--readfolders inside it. Agents' folders,~/Link/Agents, are always hidden this way.--port <port>: a port of this machine's loopback it may connect to.--command <program>: the only programs it may run (below).
Every flag is a key of a spec, the file the next section writes; lnk sandbox permissions lists each section and its flags. run also
reads --allow and --proxy, which its help hides.
Run by root on Linux, the program inside is still root's user but has
no capabilities: it can't override a file's permissions or reach into
other processes, so it writes only folders root owns. It sets TAR_OPTIONS=--no-same-owner, so tar keeps
unpacked files as root's instead of failing.
Describe a sandbox in a file
lnk sandbox spec --write ~/code/app --internet-host '*' --command node > app.toml # a spec to start from
lnk sandbox run --spec app.toml -- node app.js # shows what a new spec opens, and asks
lnk sandbox run --spec app.toml --yes -- node app.js # without asking
lnk sandbox check --spec app.toml # try from inside what it shouldn't allow
lnk sandbox spec --spec app.toml # the spec, paths absoluteA spec is a TOML file whose sections are the sandbox's only grants:
[files], [internet], [lan], [localhost], [tools], [browser]
and [commands]. A section left out stays closed, so a spec with no
sections reads none of your files, reaches no network and runs no
program. Every key and what it takes: spec.md.
name = "app"
version = 1
[files]
write = ["~/code/app"]
[internet]
hosts = ["registry.npmjs.org", "api.openai.com"]
[commands]
allow = ["sh", "node", "npm"]- A spec you haven't run here, or one that opens something else now,
shows what it opens and asks first, since a spec can come from
anyone.
--yesanswers for you. - A spec can't open Link's settings, agents' folders, or what would let a program reach outside the sandbox (each).
- A flag beside
--specreplaces that key of the file's, but--hide, which adds to it. [<section>.macos]and[<section>.linux]hold one OS's keys.
Let it commit
lnk sandbox run --write . -- git status # reads the repository, can't change it
lnk sandbox run --write . --write-dot-files . -- git commit -qam fix # commitsAt the top of each folder it writes, a project's dot files that run
outside the sandbox (.git, .envrc, .vscode, .idea, git's hooks
and the rest) are read
only: what's written there would run as you, outside it, the next time
you run git, cd into the folder or open it in your editor. So git status, diff and log work, and a commit, git config or a new
hook doesn't, until --write-dot-files names the folder; in a spec,
[files] write_dot_files.
Only the top of each folder is kept: a repository it clones or makes in a folder inside is its own. Give it the project's folder, not the one holding all your projects.
Choose what it can reach
Choose the programs it may run
lnk sandbox learn --spec app.toml -- npm test # every program it runs, to add
lnk sandbox run --write . --command sh --command git -- sh -c 'git status'
lnk sandbox run --spec app.toml -- npm test # only what [commands] allow listsWith a spec, nothing runs that [commands] allow doesn't list, and the
program given to run must be on it, or the run is refused before it
starts. Without a spec, any program runs unless --command names them.
A program started from inside that isn't listed gets "Permission
denied", and the sandbox says which:
lnk sandbox: refused /usr/lib/git-core/git-remote-https: add it to [commands] allowlearn runs the command once under the spec's other grants with every
program open, and prints the entries to add to [commands] allow,
writing nothing. It never offers a program in a folder the sandbox may
write or in /tmp, and marks interpreters. A helper a program starts
only now and then is found only if this run starts it.
- An entry is a name found on
PATH, a path, a folder ending in/for every program in it, or*for any. - A script needs itself and what it starts with:
#!/usr/bin/env nodeneedsenvandnode. - Listing an interpreter (
sh,node,python3) lets it run any code it's given. The list limits which programs run, not what they do. - An entry in a folder it may write, or in
/tmp, is refused.
On Linux this needs Landlock, Linux 5.13 and newer; without it a spec
listing programs is refused. On macOS it's Seatbelt's, and learn and
the refused program's name come from what Seatbelt reports in the
system log, which log stream reads only for an admin user: for
another user, a refused program isn't named, and learn says it can't
run.
Give it the internet through Link's proxy
lnk sandbox run --internet-host '*' -- python3 fetch.py
lnk sandbox run --internet-host api.anthropic.com --internet-host '*.telegram.org' -- ./bot
lnk sandbox run --internet-host github.com --tcp github.com:22 --write . -- git clone git@github.com:me/repo.git
lnk sandbox run --internet-host '*.abc.mongodb.net' --tcp '*.abc.mongodb.net:27017' -- node app.js # mongodb+srv:// (Linux)
lnk sandbox run --internet-host '*' --upstream socks5://127.0.0.1:1080 -- curl -sI https://example.comThe internet comes without the rest of your network. The program's
only way out is Link's filtering proxy, set as its HTTPS_PROXY, which
refuses this machine, your network and a cloud's metadata service.
Every agent with network runs this way, unless it has lan.
--internet-host <host>: a host it may reach;*for any.*.telegram.orgcovers the names under it, nottelegram.orgitself.--tcp <host:port>: an endpoint beyond HTTPS on 443 and HTTP on 80, such asgithub.com:22for git over SSH, or*.abc.mongodb.net:27017for every name under one domain (not a shared one). Its host must be one it may reach.--upstream <upstream>: connections leave from here by default (direct), or through a SOCKS5 proxy on this machine's loopback.--per-minute <calls>: the most calls it may make within a minute. The rest get a 429, withRetry-After, until the minute has room.
What ignores HTTPS_PROXY has no network. Python follows it, and Node
gets NODE_USE_ENV_PROXY=1. Git over SSH works once its endpoint is
named with --tcp, using the sandbox's own key in its ~/.ssh; for
ssh itself, run $GIT_SSH_COMMAND user@host. On Linux, a --tcp host
with a port from 1024 is reached by its name too, and the sandbox gets
DNS from its proxy, so psql "host=db.example.com" and
mongodb+srv:// work as they are (its
DNS). Other
endpoints, and every one on macOS, are reached only by a client that
takes a ProxyCommand, like ssh: on a Mac, psql and database drivers
can't reach a --tcp endpoint.
Reach your network
lnk sandbox run --lan-host nas.local -- curl http://nas.local:5000/
lnk sandbox run --lan-host 192.168.1.0/24 --internet-host '*' -- ./sync--lan-host lets it reach machines of your network, on any port,
through its proxy: a name, an address, a range, or * for the private
ranges. Your network is reached from here, whatever --upstream says.
Which addresses a host reaches, and what it never does whatever is
listed: Security.
Let it use a model
lnk sandbox run --model 11434 --write . -- python3 chat.py # Ollama, inference only--model <port> makes a local model runtime (Ollama, LM Studio,
llama.cpp) reachable for inference only: chat, generation, embeddings
and the models' list. Pulling, creating or deleting a model gets a 403.
On Linux the program keeps the runtime's address. On macOS it reaches
the runtime at $LNK_LOCAL_MODEL_<port>, on its proxy, behind a secret
of the run's.
A hosted model's key, and a service's secret such as a Telegram bot's
token, can be added by the proxy, so the program never holds them. lnk agent keys wire sets this up for an agent, and every
agent on Link Harness, or on a harness whose adapter says it takes the
proxy's address, has it by default. The program calls
http://127.0.0.1:<proxy port>/_link/model with a placeholder key,
adding /v1 for OpenAI and OpenRouter. The proxy port is 40781 on
Linux. Works with Anthropic, OpenAI and OpenRouter. A service reaches
its secret's service at http://127.0.0.1:<proxy port>/_link/secret/<name>
with a placeholder, link-wire- and a tag of the secret; a Telegram
bot's token keeps its id in front (<id>:link-wire-...).
Call a tool that runs outside
lnk sandbox tool add gh --allow list_issues --allow get_issue -- github-mcp-server stdio
lnk sandbox tool add docs --url https://example.com/mcp --header 'Authorization: Bearer <token>'
lnk sandbox tool calls gh # its calls
lnk sandbox tool list # every tool, and which calls ask
lnk sandbox run --tool gh -- my-agent
lnk sandbox tool remove ghA tool is an MCP server over stdio that runs outside the sandbox, with
your logins, desktop and SSH agent, or a server at a URL, reached from
outside with its headers. Use it for what the sandbox keeps
out, such as gh or aws with your credentials. A sandbox reaches only
the tools it names, at $LNK_TOOL_<NAME> ($LNK_TOOL_GH).
- Every call asks you first, but those
--allownames.--ask noneasks for none. --env NAME=valueadds a variable;--env NAMEcopies its value now.- It runs in the folder it was added from. Adding a name again replaces it.
--url <url>: a server over streamable HTTP,https://or on this machine.--header 'Name: value'sends a header, such as a token, which no sandbox sees.- Removing a tool ends its sessions.
Open a page in your browser
lnk sandbox run --browser-host github.com -- python3 -m webbrowser https://github.com/login # asks you, then opens itWith --browser-host, a program can open an https:// page in your
browser, such as a login, on the hosts given (* for any). It runs through the sandbox's proxy, and every page asks you
first, showing its whole URL, since it opens with your cookies and
logins. $BROWSER in the sandbox does it, so Python's webbrowser,
gh and xdg-open work as they are.
A login that answers on localhost (its redirect_uri) gets that port
until the answer arrives, or for five minutes at most. On Linux the
proxy passes it into the sandbox.
- Only
https://pages, written in plain ASCII. Never this machine or its network by address. - Its hosts are apart from
--internet-host's: a sandbox reaching one host may still ask to open a page on another, and a URL can carry what it read. So every page asks. - With no desktop, such as a box, nothing opens. The program's own fallback, a URL to copy or a code to type, still works.
Answer what a sandbox asks
A call that asks you first, a tool's or a page to open, shows a dialog on this computer's screen. With no desktop, it asks on the terminal the sandbox started from, with a four-letter code to type back. With neither, such as an agent in the background on a box, it waits for you:
lnk sandbox asks # what's waiting
lnk sandbox answer k7m2pq yes # allow that call, once
lnk sandbox answer k7m2pq noTwo minutes without an answer is no. The first answer counts. To wait longer, set it in the sandbox's settings, which no sandbox reaches:
mkdir -p ~/.config/lnk/sandbox
# ten minutes; silence is still no
echo 'ask_wait = "10m"' >> ~/.config/lnk/sandbox/settings.tomlThe same file takes tool_starts, the tool servers starting at once
(Link's 2), and tool_sessions, the tool sessions one sandbox keeps
(Link's 8).
Check it, and watch it
Check the sandbox
lnk sandbox check # the bare sandbox
lnk sandbox check --write . --internet-host '*' # as a program would run
lnk sandbox check --spec app.toml # as a spec would run, an unlisted program too
lnk agent link check # your agent's sandbox, as Link Harness has itcheck plants bait (secrets in your home folder, in Link's settings and
in each --hide folder, a server on another port, and more), tries to
reach each from inside, then removes it. Each try is:
blocked: the sandbox stopped it.allowed: a folder or permission you gave lets it through.open: a known gap.LEAK: something nothing allows got through. It exits 1; please report it (Security).
See every call on the wire
lnk sandbox log # the latest 40
lnk sandbox log -n 200 --sandbox main-link
lnk sandbox log --refused # only what was refused
lnk sandbox log --json # one JSON object per lineEach line is a call on a sandbox's proxy: when (UTC), which sandbox,
the verb, where to, and what happened. A sandbox is named
<agent>-<harness>, guest-<name> or run-<pid>. The log never holds
a request's path, headers or body.
- Verbs:
connect(HTTPS),tcp(another port),http,mcp(a tool's call),model(hosted),local-model,secret(a service's call, its secret added, such as a Telegram bot's),port(this machine's, Linux),dns(a name looked up, Linux, where it names endpoints),open(a page in your browser),callback(a login's answer let in, Linux),browser(a web page's request, refused, once a minute at most),pause,unknown. - What happened:
allowed, thenendedwith the bytes each way; orrefused, orfailedwhen allowed but nothing answered.
See what each sandbox sent
lnk sandbox traffic # each running proxy: bytes up and down
lnk sandbox traffic --jsonWhat each running sandbox's proxy has carried since it started, every
call it let through: up, from the sandbox, and down, to it. lnk measure reads it as an agent's network.
Stop or pause a sandbox
lnk sandbox stop # close every wire and stop every agent here
lnk sandbox stop --off # open the wires again; agents stay stopped until `lnk agent start`
lnk sandbox pause main-link # close one sandbox's wire; its program keeps running
lnk sandbox resume main-link
lnk sandbox paused # which wires are closed, and whyClosing a wire refuses every new call and ends open ones within a
second. stop works however an agent behaves, and the log stays. A
sandbox without a wire, such as an agent with lan, stops only when
its program does.
A proxy also pauses its own wire when its calls look like no normal
work, and notifies you: 30 calls refused within a minute, 20 different
targets refused within 10 minutes, or any call to a cloud's metadata
service. lnk sandbox paused and lnk sandbox log show which wires
are paused and why, and lnk agent status shows its own, for a box
where no notification shows.
Let someone SSH into the sandbox
lnk sandbox guest add alice --key ~/Downloads/alice.pub
lnk sandbox guest add bob --key 'ssh-ed25519 AAAA...' --proxy # the internet, not your network
lnk sandbox guest add carol --key carol.pub --spec carol.toml # what a spec opens, shown first
lnk sandbox guest list
lnk sandbox spec --guest carol # the spec their sandbox is
lnk sandbox guest remove alice # --delete-folder deletes their files tooThe guest connects as you, with their own key: ssh <you>@<this machine>. They get a shell, a command, scp or sftp, all in their
sandbox. They write only their folder, ~/Link/Guests/<name>, and have
no network unless you allow it. This machine must accept SSH: a box
does, and a Mac needs Remote Login on.
--key <key>: their public key or its.pubfile. A key that already opens this machine is refused.--folder <dir>: another folder for them.--read,--port,--model,--per-minute: as forrun.--proxygives them the internet through Link's proxy,--hostlimits its hosts, and--allowadds a permission (network,read-home,open,localhost,developer-tools).--spec <file>: a spec of what they may do beyond their folder, in place of those. It's checked as a run of it is, shown, and asked (--yesanswers), then kept with its paths absolute; editing the file later changes nothing until you add them again. Its[commands]must let/bin/bashrun, which their session is.--upstreamsays where their internet traffic leaves from.
Make the sandbox work on this machine
# Linux
sudo apt install bubblewrap
lnk sandbox readyOn Ubuntu 23.10 and newer, ready offers to add an AppArmor profile,
/etc/apparmor.d/lnk-bwrap, with sudo, or prints the two commands.
lnk agent start, run and check run ready first. On a Mac there's
nothing to do.
Where things live
~/.config/lnk/sandbox/: your tools, guests, the wire's log (wire.log, rotated at 8 MB) and waiting questions. No sandbox can read or write it.~/.config/lnk/specs/sandbox.toml: the specs run here, so a new one asks first (how).~/Link/Guests/<name>: a guest's folder andHOME.guest removekeeps it unless--delete-folder.~/.ssh/authorized_keys: one line per guest, endinglnk-guest-<name>.
Troubleshooting
| Symptom | Fix |
|---|---|
| "the sandbox needs bubblewrap" | sudo apt install bubblewrap |
| Ubuntu: bubblewrap can't make the sandbox | lnk sandbox ready, and let it add the profile |
| Can't write its cache or temp files | --write a folder; point TMPDIR there |
| Can't resolve names or connect | Add --internet-host '*', or the hosts it needs |
| "Permission denied" running a program | Add it to [commands] allow, or --command |
A commit or git config fails, read only | --write-dot-files ., or [files] write_dot_files |
| "... is a link" (Linux) | Make the dot file a folder or file, or --write-dot-files |
| A spec "hasn't run here before" without a terminal | Read it, then add --yes |
--internet-host program still offline | It ignores HTTPS_PROXY; set its proxy setting |
git over SSH fails with --internet-host | Add --tcp github.com:22 |
| A call hangs, then is refused | Answer it: lnk sandbox asks |
| A login can't open a browser | Add --browser-host <host> ('*' for any) |
| Every call refused | lnk sandbox log: stopped or paused? stop --off or resume |
gdb, strace or Docker fail (Linux) | Refused inside; run them outside |
What the sandbox guarantees, and its known gaps:
Security. Why it works this way:
Decisions. How a plugin uses it:
docs/en/dev.
