Run xAI's Grok CLI from any project on your machine, with all its traffic routed through a VPN.
Grok isn't available in every region. This repo puts the Grok CLI in a small Ubuntu container whose only network path is a gluetun VPN tunnel, then gives you a vpngrok command that works from any directory. If the tunnel drops, the container loses network entirely instead of leaking your real IP.
Mullvad is the default provider, but gluetun supports 40+ providers (NordVPN, ProtonVPN, Surfshark, PIA, ...) — see Other VPN providers.
- Docker Desktop (or any Docker with compose v2)
- A VPN subscription with WireGuard or OpenVPN support
- macOS or Linux
-
Clone the repo:
git clone https://github.com/dedene/vpngrok.git cd vpngrok -
Copy the env template and fill in your VPN credentials:
cp .env.example .env
For Mullvad: generate a WireGuard config at https://mullvad.net/en/account/wireguard-config and copy the
PrivateKeyand IPv4Addressvalues into.env. Careful: Mullvad rotates the keypair on every config download, so the values in.envdie the next time you regenerate a config.Also set
WORKSPACE_ROOTto the directory where your projects live, unless that's~/Development(the default).vpngrokonly works inside this tree — see Workspace root for why. -
Start the stack and check that traffic exits through the VPN:
make up make verify
-
Install the
vpngrokwrapper on your PATH:make install
cd ~/Development/some-project
vpngrokThat's it. The wrapper starts the VPN and dev containers if they aren't running, installs the Grok CLI on first use, and drops you into Grok with your current directory as the working directory. Host paths map 1:1 inside the container.
First run asks you to log in. If the browser flow doesn't cooperate from inside the container, set XAI_API_KEY in .env instead. Login state persists in .docker/dev-home, so you only do this once.
Your personal skills work too: the container mounts ~/.agents, ~/.grok/skills, and ~/.claude/skills read-only, which covers every home-level directory Grok scans for skills. Project-level skills (.grok/, .agents/, .claude/, .cursor/ inside a repo) come along with the workspace mount. If you don't have these directories, Docker creates them empty — harmless.
Sessions are shared with the host: ~/.grok/sessions is mounted read-write, and because host paths map 1:1 inside the container, /resume shows the same sessions whether you run Grok natively or through vpngrok. Just don't resume the same session from both at the same time.
vpngrok works in any directory under WORKSPACE_ROOT — see below.
The container mounts one directory tree from your machine, and vpngrok only works inside it. It defaults to ~/Development; set WORKSPACE_ROOT in .env if your projects live somewhere else:
WORKSPACE_ROOT=/Users/alice/ProjectsWhy not mount all of $HOME? Because whatever is mounted is readable and writable by the container, and by the Grok agent running inside it. Mounting your whole home directory hands over ~/.ssh, ~/.aws, browser profiles, documents, everything. Scoping the mount to your projects tree means a misbehaving agent (or a compromised dependency it runs) can only touch code you already intended to share with it.
Performance is not the reason: Docker's VirtioFS mounts are lazy, so a broad mount costs nothing until files are actually accessed. It's purely about blast radius. If you truly want your whole home directory, set WORKSPACE_ROOT to its absolute path (e.g. /Users/alice) and own the tradeoff (Docker Desktop will show a warning, and macOS may prompt for access to personal folders).
After changing WORKSPACE_ROOT, recreate the container: docker compose up -d --force-recreate dev.
The vpn container is plain gluetun, so switching providers is an .env change. Look up your provider in the gluetun wiki and set the matching variables:
# NordVPN over OpenVPN, for example:
VPN_SERVICE_PROVIDER=nordvpn
VPN_TYPE=openvpn
OPENVPN_USER=...
OPENVPN_PASSWORD=...The compose file passes through VPN_SERVICE_PROVIDER, VPN_TYPE, WIREGUARD_PRIVATE_KEY, WIREGUARD_ADDRESSES, OPENVPN_USER, OPENVPN_PASSWORD, SERVER_COUNTRIES, and SERVER_CITIES. If your provider needs a variable that isn't in that list, add it to the vpn service in compose.yaml.
After changing providers: make down && make up && make verify.
Two containers:
vpnruns gluetun with your provider's credentials. It owns the network.devis an Ubuntu box that shares the vpn container's network namespace (network_mode: "service:vpn"). It has no network of its own, so everything it does goes through the tunnel or nowhere.
The vpngrok script is a thin wrapper around docker compose exec that maps your current directory into the container.
All projects share one container, one Grok login, and one VPN exit server. There is no per-project isolation.
With the stack running, stop the VPN container and confirm the dev container can't reach the internet:
docker compose stop vpn
docker compose exec dev curl --max-time 10 https://am.i.mullvad.net/ipThe curl should time out rather than show your real IP. Bring the tunnel back with:
docker compose up -d vpn| Target | What it does |
|---|---|
make up |
Build and start the vpn + dev containers |
make verify |
Confirm traffic exits through the VPN |
make install |
Symlink vpngrok into your PATH |
make shell |
Open a shell in the dev container |
make logs |
Tail the vpn container logs |
make down |
Stop everything |
MIT