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 once

Run 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 -- ./tool

The 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 --write folder whose .git, .envrc, .vscode and .idea it may change too (below).
  • --hide <dir>: a folder it may never touch, even with --read '~', but for the --write and --read folders 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 absolute

A 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. --yes answers 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 --spec replaces 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  # commits

At 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 lists

With 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] allow

learn 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 node needs env and node.
  • 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.

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

The 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.org covers the names under it, not telegram.org itself.
  • --tcp <host:port>: an endpoint beyond HTTPS on 443 and HTTP on 80, such as github.com:22 for git over SSH, or *.abc.mongodb.net:27017 for 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, with Retry-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 gh

A 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 --allow names. --ask none asks for none.
  • --env NAME=value adds a variable; --env NAME copies 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 it

With --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 no

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

The 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 it

check 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 line

Each 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, then ended with the bytes each way; or refused, or failed when allowed but nothing answered.

See what each sandbox sent

lnk sandbox traffic                          # each running proxy: bytes up and down
lnk sandbox traffic --json

What 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 why

Closing 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 too

The 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 .pub file. A key that already opens this machine is refused.
  • --folder <dir>: another folder for them.
  • --read, --port, --model, --per-minute: as for run. --proxy gives them the internet through Link's proxy, --host limits its hosts, and --allow adds 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 (--yes answers), then kept with its paths absolute; editing the file later changes nothing until you add them again. Its [commands] must let /bin/bash run, which their session is. --upstream says where their internet traffic leaves from.

Make the sandbox work on this machine

# Linux
sudo apt install bubblewrap
lnk sandbox ready

On 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 and HOME. guest remove keeps it unless --delete-folder.
  • ~/.ssh/authorized_keys: one line per guest, ending lnk-guest-<name>.

Troubleshooting

SymptomFix
"the sandbox needs bubblewrap"sudo apt install bubblewrap
Ubuntu: bubblewrap can't make the sandboxlnk 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 connectAdd --internet-host '*', or the hosts it needs
"Permission denied" running a programAdd 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 terminalRead it, then add --yes
--internet-host program still offlineIt ignores HTTPS_PROXY; set its proxy setting
git over SSH fails with --internet-hostAdd --tcp github.com:22
A call hangs, then is refusedAnswer it: lnk sandbox asks
A login can't open a browserAdd --browser-host <host> ('*' for any)
Every call refusedlnk 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.