Gaps and Limits
What the sandbox leaves open, what check reports as open, and where
it can't run (Security).
Known gaps
check reports these as open. Behind its proxy instead, the first two
are closed and the others remain.
On macOS, with [network] mode = "host"
Every localhost port, through ::ffff:127.0.0.1, IPv4 localhost through
an IPv6 socket. Seatbelt doesn't count it as localhost, so the rule's
"any address" covers it, and a rule names only localhost or any host,
with (remote ip6 ...) matching IPv4 too, so Seatbelt can't close it.
Also the Mac's own LAN address, which isn't localhost to Seatbelt
either: a server listening on every address is reachable through it.
On Linux, with [network] mode = "host"
Every localhost port, since bubblewrap can't tell ports apart and the
sandbox shares the machine's network. And, where the kernel's Landlock
is older than ABI 6, the machine's abstract unix sockets: X11's
(@/tmp/.X11-unix/X0, which an X server may trust for the same user
without a cookie, to read the screen and type keys) and some session
buses'. ABI 6 and newer scopes them shut.
A localhost port its spec names
A port named with --port, or a harness's own ports, is open to that
service's whole API, when something serves it.
On Linux other than x86_64 and arm64
No seccomp filter: a sandbox's first program still closes every
descriptor it was handed, but the system calls the filter refuses
elsewhere are open to the program, and a [commands] list is refused,
since nothing there holds it.
On macOS, a descriptor left open
A file descriptor what started the sandbox left open: the program keeps it, and Seatbelt doesn't filter writing to a pipe or socket it already holds.
On macOS, a socket in a folder a sandbox writes
A socket Link makes there (an agent's store bridge's) is bound by its
path in a folder of its own beside it: a sandbox swapping a link in for
that folder as it's made has the socket made, a new file named s,
wherever the link points that you can write. Nothing more: it's made
private, given, moved and removed through the folder Link holds open,
never through the link.
Its mode and owner
They're set only once it's checked a socket of yours with one name, so a hard link swapped in for it is refused; on macOS that check is by its name just before, so a swap in between can still have a file of yours made private or given as the socket would be. On Linux it's bound, checked and changed through handles alone.
On Linux, a missing .envrc
It isn't held: a sandbox can make one. direnv runs a new or changed
.envrc only after direnv allow, which shows it.
On Linux, code run around [commands]
Where a spec lists programs, the program loader, which every program
needs, runs any file the sandbox can read (ld.so ./program): it has to
stay runnable for every dynamically linked program, so it can't be
closed. And a listed program loads a library the sandbox wrote when
LD_PRELOAD names it: the variable is cleared when the sandbox starts,
but a program can set it for a child it runs.
What check doesn't report
These are open too, and check doesn't try them.
[commands] limits files, not code
A listed interpreter (sh, node, python3) runs any script it's
given, and on macOS a listed program that isn't Apple's loads a library
the sandbox wrote when DYLD_INSERT_LIBRARIES names it (cleared when
the sandbox starts, but a program can set it for a child).
What is closed
On Linux, running any file descriptor (execveat with AT_EMPTY_PATH)
is closed where a spec lists programs. Memory files are either sealed
non-executable or their creation is refused when sealing or its
listener is unavailable. check requires those paths to be blocked.
Dot files below the top
A project's dot files that run outside are read only at the top of each
folder a sandbox writes, and nowhere else. A sandbox can make a .git
in any folder inside (src/.git, with a core.fsmonitor), and git run
there, by you or by a shell prompt that shows git's status when you cd
in, runs what it wrote, outside.
A repository of its own
A repository it clones or makes in a folder inside is its own, hooks and
settings: running git there runs what it wrote. A folder holding your
projects (--write ~/code) keeps none of theirs. Where
--write-dot-files names a folder, it writes them all. A script of the
project that your git settings already run (a filter's, an alias's) is
the project's, as its Makefile is.
A hook manager's settings
Those that aren't there (.pre-commit-config.yaml, lefthook.yml)
aren't held: a sandbox can make one. A hook manager runs one only where
its stub is in .git/hooks, which is read only, and its install makes
the settings when there are none.
The placeholders, on Linux
Removing one of the empty placeholders made in a dot file's place while
a sandbox runs (git clean -d does) lets that sandbox make it. A
sandbox killed outright leaves its placeholders until the next sandbox
on the folder ends.
On macOS, an [internet] tcp endpoint
It's reached only by a client that connects through a command, as ssh's
ProxyCommand does (git over SSH). A database client such as psql, or
a driver, can't reach one there: with no network of its own, a name
can't be given an address of the sandbox's, and Seatbelt can't send a
connection elsewhere.
On macOS, calls to its proxy
Any program or user of the machine can send the proxy calls without a
browser's marks, such as CONNECT to a host it refuses. Those count as
the sandbox's: 30 refusals in a minute pause its wire until lnk sandbox resume, they use up its calls a minute and its 512 connections, and
they are in the wire's log under its name.
A hosted model that searches by nature
Such as Perplexity's sonar models or OpenAI's *-search-preview on
OpenRouter, it fetches pages for the sandbox whatever its route removes.
With a list of hosts and the key on the wire, the route refuses it
(how);
with the key in the harness, it can't see it, and lnk agent start
says so. Name a model that doesn't.
A URL under another key
The model route knows the ways a provider takes a URL to fetch by their
keys (any ending in url or _data); a provider's new way under
another name isn't refused until Link knows it.
A port [localhost] ports names
--port is reached whatever listens on it, Link's own among them: on
macOS another agent's proxy (each agent's on a fixed port), or a
sandbox's proxy on a free port; an agent's harness served on this
machine's loopback (its gateway); a model lnk model serve serves.
Only 40781
The sandbox can't tell those ports from others without reading other
plugins' settings, so only 40781 is refused: name only ports you know.
A public address not on an interface
The proxy doesn't refuse it. On a cloud box behind the cloud's NAT (AWS, Google Cloud), its public IPv4 is such an address, so a sandbox there reaches the box's own services on it where the box's firewall lets the internet in. So does a home router's public address, where the router answers from inside. Keep a box's firewall closed to what shouldn't be public.
Limits
Where the sandbox runs, and what doesn't work inside it.
macOS and Linux only
On Linux it needs bubblewrap, and user namespaces the kernel allows (Ubuntu's rule).
On Linux
Debuggers and tracers (gdb, strace) don't work inside a sandbox,
since ptrace is refused. Programs that need io_uring use their
fallback, and nothing inside can make a container of its own: Docker,
Podman, or bubblewrap again.
Run by root
What needs one of root's capabilities fails inside, such as listening on a port below 1024, and a folder it's given that another user owns (a project mounted as uid 1000 in a container run as root) can't be written: its user namespace maps only root, so those files belong to nobody inside.