The gul CLI
gul is a single self-contained binary - the client half of Gul. It logs in once via your browser, opens a tunnel over one SignalR connection, and forwards public requests to a port on your machine until you press Ctrl+C. No runtime to install, no config beyond a server URL.
Install
One-liner - downloads the right binary for your OS and architecture and puts gul on your PATH:
curl -fsSL https://raw.githubusercontent.com/PianoNic/Gul/main/install.sh | shirm https://raw.githubusercontent.com/PianoNic/Gul/main/install.ps1 | iexPortable - prefer no install? Grab the standalone single-file binary for your platform from the latest release, drop it anywhere on your PATH, and run it:
| Platform | Asset |
|---|---|
| Windows x64 | gul-win-x64.exe |
| Windows ARM64 | gul-win-arm64.exe |
| Linux x64 | gul-linux-x64 |
| Linux ARM64 | gul-linux-arm64 |
| macOS Intel | gul-osx-x64 |
| macOS Apple Silicon | gul-osx-arm64 |
On macOS / Linux, mark it executable and put it on your PATH:
chmod +x gul-linux-x64
mv gul-linux-x64 ~/.local/bin/gulVerify it runs:
gul --helpFirst run
Two one-time steps: point the CLI at your server, then log in.
gul remote https://gul.example.com # store the server URL
gul login # opens your browser for the OIDC logingul remote <url>sets the Gul server URL and stores it (rungul remotewith no argument to print the current one). This is the apex URL your operator gave you (https://gul.example.com), not a tunnel subdomain. Invalid URLs are rejected.gul loginfetches the server's OIDC settings fromGET /config, runs Authorization Code + PKCE against your identity provider, and saves the resulting tokens. A browser tab opens; approve, and it redirects to a local loopback listener that shows a "You can close this tab" page.
TIP
You rarely call gul login by hand. Opening a tunnel checks your token first and logs you in automatically if it's missing or expired (refreshing silently when it can).
Open a tunnel
gul 3000Tunnel live: https://happy-otter.gul.example.com -> http://localhost:3000Share that URL. Every request to it is forwarded to http://localhost:3000 on your machine and the response streams back. Press Ctrl+C to close the tunnel - the subdomain is released the moment you disconnect.
Custom subdomain
Ask for a specific name with --name:
gul 3000 --name myapp # -> https://myapp.gul.example.com (if free)If the name is already taken or invalid (names must be lowercase a-z, 0-9, -, 1-63 chars), the server falls back to a random friendly name and prints whatever you actually got. Without --name you always get a random one like happy-otter.
Commands
| Command | What it does |
|---|---|
gul remote [<url>] | Set the server URL, or print it when run with no argument. |
gul login | Run the browser OIDC login and save the tokens. |
gul logout | Clear the saved tokens (keeps the server URL). |
gul <port> [--name <sub>] | Ensure a valid token, open a tunnel to localhost:<port>, and forward until Ctrl+C. |
gul / gul --help | Print usage. |
Configuration file
Everything the CLI remembers lives in a single JSON file:
~/.gul/config.json(On Windows that's %USERPROFILE%\.gul\config.json.)
{
"ServerUrl": "https://gul.example.com",
"AccessToken": "eyJhbGciOi...",
"RefreshToken": "def502...",
"ExpiresAtUtc": "2026-07-03T18:42:00Z"
}ServerUrlis set bygul remote.- The token fields are written by
gul loginand refreshed automatically before a tunnel opens.gul logoutclears them. - Delete the file to start completely fresh, or re-run
gul remote <url>to repoint at a different server.
WARNING
The file holds live access and refresh tokens in plain text. It's created under your home directory with your user's permissions - treat it like any other credential file and don't commit it.
How forwarding behaves
A few things worth knowing when a tunnel is live:
- Redirects aren't followed. The CLI forwards your app's
3xxresponses as-is, so the visitor's browser sees them - relative redirects keep working through the public URL. - The local app must be running. If nothing answers on the target port, the visitor gets a clean
502with a short text body instead of a hang. - HTTP(S) only. Plain WebSocket upgrades on the tunneled app aren't proxied, and bodies are buffered in memory rather than streamed - Gul is built for developing and demoing web apps, not high-throughput file transfer.
See also: Self-host Gul · What is Gul?