How It Works
The idea. Seal each agent in a sandbox whose only way out is vpnw. vpnw checks every connection against a policy, sends it down the path you chose and records it. The agent doesn’t need to know any of this.
A VPN usually works at the level of the machine. Once it is up, every program on the machine goes out the same way, under the same rules, and nobody can say afterwards which program sent what. That was fine when the programs were a browser and a mail client. It fits badly when one of them is an agent that takes instructions from whatever it reads.
vpnw works at the level of one program. You name the program on the command line, and vpnw gives it a path, a policy and a record, while everything else on the machine carries on as before. It does this with four commands:

| Command | What it does | Example |
|---|---|---|
| run | Runs one program through the path you choose: direct, a SOCKS5 or HTTP proxy, and WireGuard in the Beta | vpnw run --config office.toml --via office -- ./agent |
| trace | The same, and prints every connection: the name, the addresses, the decision, the bytes | vpnw trace -- ./agent |
| guard | The same, under a policy. What the policy doesn’t allow never leaves the machine | vpnw guard --policy agent.toml -- ./agent |
| learn | Reads the last trace and writes a draft policy for a person to review | vpnw learn --name agent > agent.toml |
The commands combine. vpnw guard --config office.toml --via office --policy agent.toml -- ./agent routes the agent through the office, enforces its policy and records every connection, all in one run. vpnw needs no service in the background and no account.
The Sealed Sandbox
A proxy setting is only a request. Most tools honor HTTPS_PROXY, but a program can ignore it and connect directly, and an agent that has been talked into leaking a token might well do that. So vpnw doesn’t rely on the setting. It puts the program where there is no other way out:

- A network of its own. vpnw starts a helper in a new Linux network namespace, inside a new user namespace, so no root is needed. The namespace has one interface, loopback, and no route anywhere. The helper checks that and refuses to go on if it finds anything else.
- Two doors. On that loopback the helper listens on two ports: 3128 for HTTP proxy requests and 1080 for SOCKS5. Each connection to them is handed over a Unix socket to vpnw, which runs outside the sandbox.
- No way around them. The helper clears the privileges the program could inherit and installs a seccomp filter. From then on the program can’t open a Unix socket, so it can’t ask a local service such as Docker to connect for it. The filter also refuses io_uring, which can open sockets without calling socket(), and system calls made through a second system call table.
- The program starts. It gets the usual proxy variables pointing at the two ports, with any NO_PROXY setting removed. Its children inherit all of this, so the whole process tree is inside.
A program that honors the proxy settings works as usual. A program that ignores them gets “network is unreachable” for a direct connection, a failed lookup for a name and “operation not permitted” for a Unix socket. Nothing it sends gets out. The Alpha’s tests try 14 such ways out, and the Alpha page lists them.
For HTTPS, vpnw sees only where each connection goes: the name or address, and the port. It never opens TLS, so HTTPS stays encrypted from the agent to the server. A plain http:// request passes through vpnw as through any web proxy, which reads the request’s first line and headers to forward it; none of that is kept in the record.
How a Decision Is Made
Every request goes through the same five steps, in the same order. The first step that decides wins:

Two details matter more than they look. A denied name is never looked up, and with a default of deny neither is a name that no allow rule matches, because a DNS query is a way out too: data can be spelled into a name and sent to the attacker’s DNS server. Before a decision, vpnw looks a name up only when an address could change the answer. And when a name has several addresses, every one of them has to pass, so a DNS answer that slips one private address in among public ones is refused.
deny_private refuses loopback, the private ranges, the link-local range where cloud metadata services live, and more than a dozen other special ranges, in IPv4 and IPv6. It also sees through IPv4 addresses dressed as IPv6 (NAT64, 6to4, IPv4-mapped) and judges them by the IPv4 address inside. A host name made only of digits, such as 2130706433 (which is 127.0.0.1 written as one number), is refused outright.
Paths
A path is how an allowed connection leaves the machine. The Alpha has two kinds. Direct connects from the machine itself. Proxy goes through a SOCKS5 or HTTP proxy, such as the exit into an office network. Named paths live in a small file:
version = 1
name = "office"
[paths.office]
type = "proxy"
url = "socks5h://10.8.0.1:1080" # names are resolved at the exit
Through a proxy, names can be resolved at the exit, which is how internal names such as tracker.office.internal work. vpnw then can’t see which address a name points to, so it refuses policies that would count on it: deny_private with a default of allow is rejected on such a path, with a message that says why.
vpnw fails closed. If the chosen path is down, it checks before the program starts and stops with exit code 123. It never falls back to a direct connection.
The Record
Every run produces a record. While the program runs, vpnw prints each step as a line of text. Three lines from the demo’s runs:
vpnw 14:44:36.244 #1 → api.github.com:443 http-connect
vpnw 14:44:36.245 #1 open 140.82.112.6 via direct (0 ms)
vpnw 14:44:36.387 #5 DENY evil.example:443 no allow rule matches evil.example:443; default is deny (set in the policy)
The same events go to a file as JSON Lines, one event per line, in a versioned format that tools can read:
{"v":1,"ts":"2026-09-29T11:44:36.754728731Z","type":"policy.deny","run":"r-36e0ba","pid":16919,"path":"direct","conn":1,"fields":{"reason":"169.254.169.254: link-local address (169.254.0.0/16), where cloud metadata services live","rule":"deny_private"}}
There are twelve event types, from run.start to run.end, and every connection ends as opened, denied or failed. The record keeps names, addresses, ports, byte counts and decisions. It never keeps the data sent, URL paths, headers, environment variables or proxy passwords.
A Policy in a Few Lines
A policy is a short text file that people can read and keep next to the code:
version = 1
name = "agent"
[policy]
default = "deny"
deny_private = true
allow = ["api.github.com", "*.pythonhosted.org"]
Nobody has to write the first version by hand:
vpnw trace -- ./agent # run it once and watch
vpnw learn --name agent > agent.toml # a draft from that run
vpnw guard --policy agent.toml -- ./agent # from now on, enforced
learn allows what the program did, including anything it should not have done. That is why the draft says “Read it before you use it” at the top. In the live demo the draft allows the attacker’s server, because the agent went there during the trace, and a person takes it out.
The Hard Part
A boundary is only worth something if it holds. The Alpha handles the difficult cases like this:
- Programs that ignore proxy settings. They have no route out, so nothing they send gets out. Each bypass test lists the errors it accepts and fails on anything else, success included. Most tests aimed at this machine also make the same attempt outside the sandbox, to show the attempt itself works.
- DNS as a way out. A denied name is never looked up, nor, under a default of deny, a name no allow rule matches. A program inside the sandbox has no resolver to talk to.
- Local services. The seccomp filter refuses Unix sockets, so the Docker socket and its kind are out of reach.
--allow-unix-socketslifts the filter for a program that needs one, and says so in the record. - Addresses in disguise. IPv4 inside IPv6 is judged by the IPv4 address it carries, a host name made only of digits is refused, and a DNS answer that points inside the network is caught because every address is checked.
- When enforcement is not possible. If the machine can’t create the sandbox, guard refuses to run the program (exit code 122) instead of running it unprotected.
vpnw doctorsays what the machine supports.
What VPN Works Leaves to Others
vpnw is small on purpose. It doesn’t read or filter content, inspect TLS or scan for secrets in what an agent sends. It limits where a program can connect; it doesn’t limit which files the program reads, which is Beta work. It isn’t a VPN service with servers of its own, and it doesn’t replace the VPN a company already runs: it lets one agent use it. It isn’t a sandbox for running malware either. Its job is narrower: one program’s network, with a path, a policy and a record.