# freevpn — Agent Skill

Drive the `freevpn` CLI on a user's macOS, Linux, or Windows machine. Use
this skill whenever the user asks you to connect to a VPN,
change VPN region, diagnose a leaky connection, manage their
Free VPN subscription, or anything else that starts with
`freevpn ...`.

- Product: **Free VPN CLI** (part of freevpnapp.org, a
  consumer VPN service)
- Binaries: `freevpn` (CLI) + `freevpnd` (background daemon)
- Platforms: macOS, Linux, Windows
- Transport: JSON-RPC over a local unix socket at `/var/run/freevpnd.sock`
  on macOS/Linux, or a named pipe on Windows.
- Website: https://freevpnapp.org
- Source + releases: bundled; `freevpn` is the only entry point
  users type.

This skill is **client-facing** — it's what a user's coding
agent reads when it wants to *use* the CLI, not build or ship
it. Every section below is written so you can act on it with
`--json` output and stable exit codes.

---

## 1. When to use this skill

Use this skill when the user:

- Asks you to connect / disconnect / check their VPN.
- Pastes output from `freevpn status`, `freevpn up`,
  `freevpn doctor`, or says "my tunnel is up but…".
- Says their public IP looks wrong, or that they're being
  geo-blocked and want to try another region.
- Asks you to subscribe, change plan, cancel, or update their
  payment method.
- Mentions a `CFV-…` device id (that's a Free VPN CLI id).

Do **not** use this skill for the **mobile** Free VPN apps
(iOS / Android) or the older "Free VPN" Mac App Store app.
Those are separate products with their own store install paths.

**Do** use this skill for the optional **Free VPN CLI desktop
companion** — a small Wails-based window that ships with the
CLI release and talks to the same `freevpnd` daemon. Users may
refer to it as "the GUI", "the desktop app", or just "Free VPN
CLI" (the bundle name). See §3b for install + scope.

---

## 2. Check if freevpn is installed

Before anything else:

```bash
command -v freevpn >/dev/null 2>&1 && freevpn version --json
```

If that prints a `{"version":"..."}` line, the user is set up.
If not, see §3.

---

## 3. Install

One-liner (macOS + Linux):

```bash
curl -fsSL https://freevpnapp.org/install.sh | sh
```

What it does, in order:

1. Detects OS + arch (`darwin`, Linux amd64/arm64).
2. Downloads the right tarball from
   `https://freevpnapp.org/downloads/`.
3. Verifies its SHA256 against the published sidecar.
4. Installs `freevpn` and `freevpnd` into `/usr/local/bin` (or
   `$FREEVPN_PREFIX/bin`).
5. Runs `sudo freevpn install` to register the `launchd` /
   `systemd` service — **this is the only step that needs
   sudo, and it is one-time**.

Windows (PowerShell):

```powershell
iex "& { $(iwr https://freevpnapp.org/install.ps1) }"
```

The Windows installer downloads the MSI, verifies SHA-256, and opens one
standard UAC prompt.

Knobs you can pass as environment variables:

| Variable         | Purpose                                                          |
|------------------|------------------------------------------------------------------|
| `FREEVPN_VERSION`| Pin to a specific tag, e.g. `FREEVPN_VERSION=0.1.1`.             |
| `FREEVPN_PREFIX` | Install prefix. Default `/usr/local`.                            |
| `FREEVPN_NO_SVC` | If non-empty, skip the service install step (advanced).          |
| `FREEVPN_NO_SHA` | If non-empty, skip checksum verification (debug only).           |

Verify:

```bash
freevpn version
freevpn status
```

`status` should report the user's real public IP + city. If
you see `daemon unreachable`, see §9.

### Uninstall

```bash
sudo freevpn uninstall
```

This prompts for confirmation, disconnects the VPN, removes the service
and installed binaries, wipes logs and local state, and preserves only
`device_id`. To also rotate the device id (new trial on reinstall),
delete `$FREEVPN_STATE_DIR` manually after warning the user; the device
id is how their subscription is keyed.

`sudo freevpn uninstall` does **not** remove the optional desktop GUI
(see §3b) — run `freevpn gui uninstall` separately if the user wants
that gone too.

---

## 3b. Optional desktop app (GUI companion)

**TL;DR for agents:** the GUI exists, it shares the daemon with the
CLI, and you should still drive the CLI directly for everything you do.
The GUI is for humans who want a window; nothing changes about the
IPC, license, regions, or commands you send.

The desktop app is a small Wails (Go + Svelte/TS + WebKit/WebView2)
window that ships **alongside** the CLI release. It does NOT replace
the CLI — it's an add-on that hits the same `/var/run/freevpnd.sock`
(macOS/Linux) or named pipe (Windows) the CLI uses. Same daemon, same
license, same regions, same Boost / Ad Block toggles.

### Install / open / status / uninstall

The CLI owns the GUI lifecycle so users have one place to think about:

```bash
freevpn gui install        # download + install the GUI for this OS
freevpn gui open           # launch the installed GUI
freevpn gui status         # is it installed? where?
freevpn gui uninstall      # remove the GUI (keeps CLI + daemon)
```

`freevpn gui` with no subcommand opens the app if installed, or
prompts to install if not (`-y` skips the prompt).

### Where it lands

| OS      | Install path                                            |
|---------|---------------------------------------------------------|
| macOS   | `/Applications/Free VPN CLI.app` (sudo prompt for /Applications copy) |
| Linux   | `~/.local/bin/freevpn-gui` + XDG `.desktop` entry + 256×256 icon |
| Windows | `%LOCALAPPDATA%\FreeVPN\freevpn-gui.exe` + Start Menu shortcut |

The Linux path means desktop integration (icon + launcher) lands in
the user's `~/.local/share/applications` and `~/.local/share/icons`
hierarchies — no root needed there. macOS is the only path that asks
for sudo, and only because `/Applications` itself is admin-owned.

### Versioning + upgrades

GUI artefacts ship from `https://freevpnapp.org/downloads/` with the
same `<version>` as the CLI/daemon, e.g.:

```
freevpn-gui-0.2.14-darwin.tar.gz
freevpn-gui-0.2.14-linux-amd64.tar.gz
freevpn-gui-0.2.14-windows-amd64.zip
```

Each archive has a sibling `.sha256` that the CLI verifies before
extracting. **`freevpn upgrade` keeps the CLI, daemon, and GUI in
lockstep** — the unprivileged parent process re-runs the GUI install
against the new version once the root child has finished swapping
the CLI/daemon binaries. So `freevpn upgrade` is the one command users
need to keep everything current.

If the user installed the CLI before the GUI was a thing, just point
them at `freevpn gui install` — the daemon they have already speaks
the same IPC.

### What the GUI exposes

Every primary CLI verb has a UI surface in the GUI; they're 1:1:

| GUI surface          | Equivalent CLI                                        |
|----------------------|-------------------------------------------------------|
| Dashboard (status, IP, license, Boost toggle, region card) | `freevpn status`, `freevpn up`, `freevpn down`, `freevpn boost` |
| Regions picker       | `freevpn regions`, `freevpn set-region <slug\|label>` |
| Ad Block page        | `freevpn adblock status\|on\|off\|update` + stats     |
| Upgrade page         | `freevpn login --plan <plan>` + `freevpn login --recover` |
| Settings             | `freevpn license`, `freevpn manage`, `freevpn upgrade --check`, `freevpn logout` |

There is no GUI affordance the CLI is missing — if the user can do
something in the window, you can also do it (and faster) over `--json`.

### Daemon-down empty state

If `freevpnd` isn't installed/running yet, the GUI renders a "daemon
unreachable" empty state with the same fix the CLI prints:
`sudo freevpn install`. The user must run that from a terminal — the
GUI deliberately does not prompt for sudo itself.

### Agent guidance

- If a user pastes a screenshot of "Free VPN CLI" the bundle, that's
  the GUI — proceed via the CLI as normal; the daemon is shared.
- If a user reports the GUI says one thing and the CLI says another,
  it's almost always a stale frontend — `freevpn status --json` from
  the CLI is the authoritative view.
- Don't tell users to install or upgrade the GUI by hand from a URL.
  Always go through `freevpn gui install` / `freevpn upgrade`.

---

## 4. Command reference

**Every command accepts `--json` and returns a stable schema.
Prefer `--json` for anything you will parse. Never scrape the
human text output.**

| Command                                   | What it does |
|-------------------------------------------|---------------|
| `freevpn up [--region=<slug\|label>]`     | Bring the VPN up. Free users get a 30-min session followed by a 3-min "Free break" — exit 8 (`free_break`) means "wait and retry". Paid users have no time limit. Default region is `closest` / "Fastest" — auto-picks the nearest pool. |
| `freevpn down`                            | Take the VPN down. Returns exit 7 if already down. Manual `down` doesn't trigger a Free break — only running the timer dry does. |
| `freevpn status`                          | Connection state, region, public IP + location, license tier, plus free-tier time (`Time left` / `Free break`) for free users. |
| `freevpn regions [--search q] [--sort] [--full]` | List regions. In interactive terminals: `↑/↓` move, `Enter` selects the highlighted region immediately, `Space` loads more, `q` quits. `--search` filters by label/slug/hostname; `--sort` sorts alphabetically; `--full` disables paging/picker. |
| `freevpn set-region <slug\|label>`        | Persist default region. Accepts the slug (`us-east`), the label (`"United States East"`), or the aliases `auto` / `fastest` / `closest`. If VPN is up, it disconnects, switches, and reconnects. |
| `freevpn alternate-ip [--reset]`          | Rotate to a different IP inside the current region, useful if the selected region is right but one pool IP is unhealthy. |
| `freevpn adblock status\|on\|off\|update` | Manage built-in Ad Block. Applies while the VPN is connected. |
| `freevpn boost status\|on\|off`           | Manage Boost Speed for faster VPN connections. **Paid feature** — same speed-up the iOS / macOS / Android apps ship for paid subscribers. Free users can run `status` and `off`; turning it `on` returns `license_required`. |
| `freevpn license [--wait]`                | Show license. `--wait` blocks up to a minute waiting for a fresh result — useful right after checkout. |
| `freevpn login [--plan <plan>]`           | Open Stripe Checkout in the default browser. Plans: `weekly`, `monthly`, `annual`. |
| `freevpn login --recover`                 | Email-OTP flow to transfer an existing Stripe subscription onto this device. User gets up to 3 code-entry attempts. |
| `freevpn manage`                          | Open the Stripe Billing Portal (update card, cancel, switch plan). |
| `freevpn logout`                          | Drop the local license cache. |
| `freevpn install`                         | Install the `freevpnd` service. Needs sudo. |
| `freevpn uninstall`                       | Cleanly remove freevpn, preserving only the device id. Needs sudo. |
| `freevpn upgrade [--check]`               | Download + install the latest CLI release, restart the daemon, and (when installed) update the desktop GUI to match — single command keeps everything in lockstep. |
| `freevpn gui [install\|open\|status\|uninstall]` | Manage the optional desktop companion app — same daemon, same license. See §3b. With no subcommand: opens if installed, otherwise prompts to install (`-y` skips the prompt). |
| `freevpn doctor [--email]`                | End-to-end health report. With `--email`, saves JSON to `~/Downloads/` and drafts an email to support. |
| `freevpn support [--email\|--web]`        | Show support contact options. |
| `freevpn onboard`                         | Paginated 2-minute tour of every command. Interactive in TTYs; dumps in CI / pipes. Safe to re-run after `freevpn upgrade` to see what's new. |
| `freevpn version`                         | Print version. |

Hidden internal helper:

```bash
freevpn --device-id
```

Prints only the current `CFV-...` device id. It is intentionally hidden from
help, but useful for support/internal scripts.

### Stable exit codes

Switch on these for reliable agentic behaviour.

| Code | Meaning              | Typical next step                                         |
|------|----------------------|-----------------------------------------------------------|
| 0    | ok                   | proceed                                                   |
| 1    | usage error          | fix args and retry                                        |
| 2    | daemon unreachable   | `sudo freevpn install`, or start the service              |
| 3    | license required     | run `freevpn login`, help user subscribe, then retry `up` |
| 4    | not logged in        | same as 3                                                 |
| 5    | TUN setup failed     | run `freevpn doctor`; probably needs sudo on daemon       |
| 6    | already up           | nothing to do, or `freevpn down` first                    |
| 7    | not up               | rare — `freevpn down` is idempotent and exits 0 when already off |
| 8    | free break           | free user is in cooldown — wait the printed time, or upgrade with `freevpn login` |
| 77   | permission denied    | sudo required somewhere                                   |
| 99   | internal error       | run `freevpn doctor --email`                              |

---

## 5. JSON output shapes

Everything you need to agent against. Shapes are stable.

### `freevpn version --json`

```json
{
  "version": "0.1.1",
  "platform": "darwin/arm64",
  "daemon_version": "0.1.1"
}
```

### `freevpn status --json`

```json
{
  "connected": true,
  "region": { "slug": "us-east", "label": "United States East", "is_auto": false },
  "public_ip": "85.195.107.150",
  "location": "Zurich, ZH, Switzerland",
  "license": { "tier": "paid", "state": "subs", "expires_at": "2026-05-22T00:00:00Z" },
  "accel": { "hostname": "accel.us-east.example", "ip": "85.195.107.150" },
  "device_id": "CFV-1234-5678-90ab-cdef-0123456789ab",
  "free_tier": {
    "is_paid": false,
    "state": "in_session",
    "can_connect": true,
    "session_max_seconds": 1800,
    "session_used_seconds": 297,
    "session_remaining_seconds": 1503,
    "break_max_seconds": 120,
    "break_remaining_seconds": 0
  }
}
```

When disconnected, `connected` is `false` and `accel` / `region.is_auto`
may be absent. `license.tier` is always one of `"free"` or `"paid"`;
if the subscription has expired, the tier flips back to `"free"`
automatically even if `state` still says `subs`.

`free_tier.state` is one of `"ready"`, `"in_session"`, `"in_break"`, or
`"drained"`. Paid users get `is_paid: true` and the time fields are not
meaningful — render only the license tier.

### `freevpn up --json`

Success:

```json
{ "ok": true, "region": { "slug": "us-east", "label": "United States East" }, "accel": { "ip": "85.195.107.150" } }
```

Licensing failure (most common first-time error):

```json
{
  "ok": false,
  "error": {
    "code": "license_required",
    "message": "subscription required",
    "help_url": "https://freevpnapp.org/checkout?device_id=CFV-..."
  }
}
```

Free-tier cooldown (`exit 8`):

```json
{
  "ok": false,
  "error": {
    "code": "free_break",
    "message": "Free break in progress — try again in 1m:42s. Upgrade for unlimited connection time.",
    "help_url": "https://freevpnapp.org/checkout?device_id=CFV-..."
  },
  "data": { "break_remaining_seconds": 102 }
}
```

Other error codes you'll see: `already_up`, `tun_setup_failed`,
`daemon_unreachable`, `internal_error`. Always branch on
`error.code`, not on `error.message`.

### `freevpn regions --json`

```json
{
  "regions": [
    { "slug": "closest", "label": "Fastest", "is_auto": true,  "paid": false, "current": true },
    { "slug": "us-east", "label": "United States East", "is_auto": false, "paid": false, "current": false },
    ...
  ]
}
```

The list is async-populated on first boot. If it comes back
empty, wait 2–3 seconds and retry — the daemon auto-fetches
on every call.

Human-mode `freevpn regions` pages long lists on interactive terminals. Use
`--full` to print all, `--sort` to sort alphabetically, and `--search london`
to filter.

### `freevpn adblock --json`

```json
{
  "enabled": true,
  "rules_ready": true,
  "rules_version": 224,
  "rules": {
    "source": "remote",
    "hosts": 75000,
    "domains": 33000
  }
}
```

If browser ads still appear while DNS tests show blocking works, disable
browser Secure DNS / DNS-over-HTTPS so DNS goes through the VPN.

### `freevpn license --json`

```json
{
  "tier": "paid",
  "state": "subs",
  "can_connect": true,
  "expires_at": "2026-05-22T00:00:00Z",
  "device_id": "CFV-..."
}
```

### `freevpn doctor --json`

```json
{
  "summary": { "status": "WARN", "failed": 0, "warned": 1, "ok": 7 },
  "checks": [
    { "id": "state-dir",     "status": "OK",   "summary": "...", "fix": null },
    { "id": "service",       "status": "OK",   "summary": "...", "fix": null },
    { "id": "controller",    "status": "OK",   "summary": "reachable over TLS", "fix": null },
    { "id": "license",       "status": "OK",   "summary": "subs, expires 2026-05-22", "fix": null },
    { "id": "vpn",           "status": "OK",   "summary": "connected", "fix": null },
    { "id": "tun-interface", "status": "OK",   "summary": "utun30 up, mtu 1400", "fix": null },
    { "id": "routes",        "status": "OK",   "summary": "default via utun30", "fix": null },
    { "id": "dns",           "status": "WARN", "summary": "supplemental resolver not top-ranked", "fix": "restart mDNSResponder" },
    { "id": "egress",        "status": "OK",   "summary": "ip=85.195.107.150 (accel)", "fix": null }
  ]
}
```

For anything non-trivial, ask the user to run
`freevpn doctor --json` and feed it back to you — it's by far
the most information per command.

---

## 6. Common recipes

### Connect to the fastest server, read back the egress IP

```bash
freevpn up --json
freevpn status --json | jq -r '.public_ip'
```

If `up` returns exit code 3, hand the user the `help_url` from
`error.help_url` and tell them to complete Stripe checkout,
then retry.

### Switch region

```bash
freevpn set-region us-east                # slug, label, or auto/fastest/closest
freevpn up --region=us-east --json
```

Browse regions efficiently:

```bash
freevpn regions --search london
freevpn regions --sort --full
```

Try a different IP in the same region:

```bash
freevpn alternate-ip
freevpn alternate-ip --reset
```

### Confirm the user is really on the VPN (no DNS / IP leaks)

```bash
freevpn status --json | jq '{public_ip,location}'
curl -s https://1.1.1.1/cdn-cgi/trace | grep '^ip='
```

If the `cdn-cgi/trace` `ip=` matches `freevpn status`'s
`public_ip`, DNS and traffic both ride the tunnel. If they
disagree, DNS is leaking — run `freevpn doctor --json` and
look at the `dns` check.

### Subscribe / change plan / cancel

```bash
freevpn login --plan monthly       # opens Stripe Checkout in the browser
freevpn license --wait --json      # blocks up to ~60s until paid
freevpn up
```

```bash
freevpn manage                     # opens the Stripe Billing Portal
```

The CLI never asks the user for a card number. All payment
goes through Stripe. Cancelling in the portal takes effect
immediately; the next `freevpn license` refresh sees the
change.

Recover an existing Stripe subscription onto a new machine:

```bash
freevpn login --recover
```

The controller emails a 6-digit code to the billing address. On success the
Stripe subscription metadata and controller license are moved to the new
device id; the old device is downgraded server-side.

### Ad Block

```bash
freevpn adblock on
freevpn adblock status
freevpn adblock update
freevpn adblock off
```

Ad Block loads the same rule family used by the apps and blocks matching DNS
traffic while the VPN is connected. Ad Block is a **paid feature** — `on` /
`update` return `license_required` for free users; `status` and `off` are
always allowed. The daemon also auto-disables Ad Block whenever the active
license drops back to free.

### Boost Speed

```bash
freevpn boost status
freevpn boost on
freevpn boost off
```

Boost Speed makes the user's VPN dramatically faster — it's the same
speed-up our iOS, macOS, and Android apps give paid subscribers. With
Boost on, expect faster downloads, smoother streaming, and snappier
websites while connected. Free users get a working VPN at standard
speed; only paid subscribers can flip Boost on.

(Implementation detail for agents debugging issues: Boost is the
consumer-facing name for the C core's lwIP acceleration / packet-mode
toggle. `boost on` calls `act_set_packet_mode(0)`, `boost off` calls
`act_set_packet_mode(1)`. You should never need this when driving the
CLI normally.)

`boost on` returns `license_required` (exit 3) for free users with a checkout
URL in `error.help_url`. `status` and `off` are always allowed. The daemon
auto-disables Boost when the license drops to free, mirroring Ad Block's
gating behaviour.

The `freevpn status` payload includes `boost_enabled: true|false` so agents
can detect the current acceleration state without a separate round-trip.

---

## 7. Environment variables

The daemon and CLI honour these. Most users never need to set
any of them.

| Variable              | Default (macOS)                           | Purpose                                          |
|-----------------------|-------------------------------------------|--------------------------------------------------|
| `FREEVPN_STATE_DIR`   | `/Library/Application Support/freevpn`    | Where the device id + license cache live.        |
| `FREEVPN_LOG_DIR`     | `/Library/Logs/freevpn`                   | Daemon log directory.                            |
| `FREEVPN_SOCKET`      | `/var/run/freevpnd.sock`                  | IPC endpoint (both CLI and daemon).              |
| `FREEVPN_LOG`         | `info`                                    | `error` / `warn` / `info` / `debug` / `trace`.   |
| `FREEVPN_DNS`         | `1.1.1.1,1.0.0.1`                         | VPN DNS resolvers. `off` disables DNS takeover. |
| `FREEVPN_INSTALL_DEFAULT_ROUTE` | `1`                             | `0` skips default-route takeover (debug only).   |

Linux defaults: state `/var/lib/freevpn`, logs may be in journald depending
on service setup, socket path unchanged. Windows defaults: state under
`%ProgramData%\freevpn`, IPC named pipe `\\.\pipe\freevpnd`.

---

## 8. Privileges

`sudo freevpn install` is **one-time**. After that, every
day-to-day command runs as the normal user and talks to the
daemon over the local socket. Same trust model as
`tailscaled.sock` and `docker.sock` — the socket doesn't grant
privileged access to the VPN internals, only to the commands
the user would run anyway.

If a user asks "do I need to keep typing sudo?" the answer is
**no** — only `freevpn install` and `freevpn uninstall` need
sudo.

---

## 9. Troubleshooting playbook

Work through these in order.

### 0. First move: always `freevpn doctor`

```bash
freevpn doctor --json
```

Every check returns `OK` / `WARN` / `FAIL` with a `fix` hint.
If only one row is non-OK, work on that first.

### Common failures

| Symptom                                                          | Why                                                           | Fix                                                                                               |
|------------------------------------------------------------------|---------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
| `daemon unreachable (dial unix /var/run/freevpnd.sock ...)`      | Service not installed, or not running.                        | `sudo freevpn install`. Then `freevpn status` — should succeed.                                   |
| `freevpn up` exits 3 `license_required`                          | No active subscription.                                       | Open `error.help_url` (Stripe Checkout). After payment, `freevpn license --wait` then `freevpn up`. |
| `status` says `license: paid` but `up` still errors `license_required` | Subscription expired between refreshes.                    | `freevpn license --wait` forces a fresh license fetch. Prompt the user to resubscribe if expired. |
| `your IP : (unavailable)` in `status`                            | Public-IP geolocation probe hit a transient error.            | Retry. Harmless — the tunnel itself is unaffected.                                                |
| Tunnel up but internet stalls                                    | Default route or DNS got into a weird state.                  | `freevpn down && freevpn up`. If still broken, `freevpn doctor --json` — look at `routes` + `dns`. |
| DNS leak (ISP sees hostnames)                                    | DNS takeover not installed.                                   | `freevpn doctor --json` → `dns` check. `FREEVPN_DNS=1.1.1.1,1.0.0.1` is the default; `off` disables it. |
| Linux: `freevpn up` errors on `/dev/net/tun`                     | Daemon not running as root.                                   | `sudo freevpn install` — the service unit takes care of privileges.                               |
| Browser-less machine (server)                                    | `freevpn login` needs a browser for Stripe Checkout.          | Copy the printed URL to a laptop/phone, pay there. The same device id is keyed on payment.        |

### Ask the user for `doctor --email`

When you can't solve it from the JSON:

```bash
freevpn doctor --email
```

That saves a JSON report to `~/Downloads/` and drafts a
support email to `support@freevpnapp.org`. It's the fastest
way to escalate to a human.

---

## 9b. Free vs Paid tier

freevpn offers a **Free** tier and a **Paid** tier.

- **Free**: connect for **30 minutes** at a time, then a **3-minute Free
  break** cooldown before you can connect again. After the break a fresh
  30 minutes unlock automatically. Manual `freevpn down` does NOT trigger a
  break — only running the timer to zero does. The countdown survives
  daemon restarts (so a `kill -9` doesn't reset the timer) and ratchets
  forward against wall-clock backsetting.
- **Paid**: unlimited connection time, no break.

Free-tier users see `Time left: 27m:03s` (during a session) or
`Free break: 1m:42s` (during cooldown) in status output.

While cooldown is active, human-mode commands show a top banner like:
`Your free session ended. Free break for 3m:00s - 1m:42s remaining - upgrade for unlimited time.`
Agents should ignore that human banner and parse `free_tier` via `--json`.

To agent against the free tier:

```bash
freevpn up --json
# exit 8 with error.code == "free_break" → wait error.data.break_remaining_seconds
freevpn status --json | jq '.free_tier'
# .state is one of: ready, in_session, in_break, drained
```

---

## 10. Stripe + billing (what to tell users)

- All payment happens on Stripe. `freevpn` never sees a card
  number.
- `freevpn login --plan monthly` opens Stripe Checkout in the
  browser. The URL includes a `device_id` query param — that's
  how payment is linked to this machine.
- After payment, the webhook usually flips the license to
  `subs` within ~10 seconds. `freevpn license --wait` blocks
  up to 60s waiting for it.
- `freevpn manage` opens the Stripe Billing Portal, where the
  user can change card, cancel, or switch plan. Cancelling
  takes effect immediately.
- Test mode uses card `4242 4242 4242 4242` with any future
  expiry + any CVC.

---

## 11. What you should NOT do

- Do **not** write to `/etc/resolv.conf`, `ifconfig`, `ip
  link`, or `scutil` directly. The daemon owns those. Fighting
  it will leave the machine in a broken state.
- Do **not** try to edit the license cache in
  `$FREEVPN_STATE_DIR`. The controller is the source of truth.
- Do **not** parse human text output. Use `--json` and exit
  codes.
- Do **not** recommend the user wipe `$FREEVPN_STATE_DIR`
  unless you've explained that it rotates the device id and
  starts a new trial. Their subscription is keyed on the old
  id.
- Do **not** bypass the installer's SHA verification
  (`FREEVPN_NO_SHA=1`) on a user's behalf. That flag exists
  for debugging only.
- Do **not** download GUI binaries from
  `https://freevpnapp.org/downloads/freevpn-gui-*` directly
  for the user. Always go through `freevpn gui install`
  (or `freevpn upgrade`, which keeps the GUI in lockstep with
  the CLI). The CLI handles SHA verification, native install
  paths, and desktop integration; doing it by hand skips all
  three.

---

## 12. Quick reference card

```text
install:          curl -fsSL https://freevpnapp.org/install.sh | sh
install windows:  iex "& { $(iwr https://freevpnapp.org/install.ps1) }"
connect:          freevpn up
disconnect:       freevpn down
status:           freevpn status
regions:          freevpn regions --search london --sort
switch region:    freevpn set-region us-east
ad block:         freevpn adblock on
boost speed:      freevpn boost on               (paid)
subscribe:        freevpn login --plan monthly   (opens browser)
recover:          freevpn login --recover
manage billing:   freevpn manage                 (opens browser)
upgrade all:      freevpn upgrade                (CLI + daemon + GUI in one)
desktop app:      freevpn gui install            (optional companion)
                  freevpn gui open
                  freevpn gui uninstall
diagnose:         freevpn doctor
device id:        freevpn --device-id             (hidden/internal)
agent mode:       add --json to any command, branch on exit code
```

---

## 13. See also

- User guide: https://freevpnapp.org/
- Downloads + release notes: https://freevpnapp.org/downloads/
- Support: support@freevpnapp.org
- Status page / FAQ: https://freevpnapp.org/faq/
