1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# vpnchecker configuration (~/.config/vpnchecker/config.yaml)
#
# Claude, Cursor and Rider are ALWAYS protected: while the VPN is down their processes are frozen. For Claude and
# Cursor, lookups for their domains also stay on this Mac (dns shield) and their server addresses are refused by the
# kernel outside the tunnel. Rider is covered by the freeze only.
# `mode` only decides whether the rest of the machine is restricted as well.
# full (default): in addition, ALL outbound traffic is blocked unless it leaves through the VPN interface, goes to
# the allowlist below, or is loopback / DNS / DHCP / NTP / ICMPv6.
# scoped: only Claude / Cursor / Rider are affected. Browser, Slack, Teams, GlobalProtect reconnects, captive portals and
# split-tunnel traffic keep working while the VPN is down.
mode: full
vpn:
# GlobalProtect creates utunN (older versions: gpd0)
interface_pattern: '^(utun\d+|gpd\d+)$'
# VPN address pool (required). The VPN is up when an interface with an address from these subnets exists AND
# the default route goes through it. GlobalProtect hands out a different address on each connect: use the pool.
subnets:
- 10.0.0.0/8
# Also treat the VPN as down when GlobalProtect itself reports a disconnected tunnel (DEM block of its settings).
check_globalprotect: true
protect:
# stop: freeze (SIGSTOP) while the VPN is down, resume (SIGCONT) afterwards: nothing is lost.
# kill: SIGTERM, then SIGKILL.
action: stop
# Lookups for these domains (and their subdomains) never leave the Mac while the VPN is down.
dns_shield: true
dns_port: 5399
domains:
# Server addresses of these hosts are refused outside the tunnel (resolved while the VPN is up).
pf_block: true
hosts:
- api.anthropic.com
- claude.ai
- console.anthropic.com
- statsig.anthropic.com
- cursor.com
- api2.cursor.sh
- api3.cursor.sh
- repo.cursor.sh
# Also learn server addresses from the sockets of these apps. Catches rotating cloud addresses, but also GitHub,
# CDNs and whatever else they talk to, which then become unreachable for every app while the VPN is down.
observe_connections: false
# Regexes against the full command line. A match and all of its children (MCP servers, terminals, extension hosts)
# are protected. Add your own under extra_process_patterns.
process_patterns:
- '/Claude\.app/Contents/'
- '/Cursor\.app/Contents/'
- '/\.cursor/(extensions|bin|cursor-server)/'
- '@anthropic-ai/claude-code'
- '/\.local/share/claude/'
- '(^|/)claude( |$)'
- '(^|/)cursor-agent( |$)'
- '/Rider\.app/Contents/'
extra_process_patterns:
# Wake and relaunch scan period.
interval: 250ms
# A pause between two ticks longer than this means the Mac slept: freeze first, verify the VPN afterwards,
# and stay frozen for at least wake_hold (GlobalProtect can still show a dead tunnel as up right after wake).
wake_gap: 3s
wake_hold: 5s
# Consecutive good checks (spread over interval x (n-1)) required before the apps are resumed.
resume_checks: 4
allow:
# Used in `full` mode, and to keep VPN gateways out of the protected set.
# RFC1918, link-local, multicast and IPv6 ULA
lan: true
# Allow VPN gateways seen as host routes outside the tunnel while the VPN is up
learn_gateways: true
# Allow the portal (GlobalProtect settings), the gateway GlobalProtect reports itself (current connection) and
# gateways from pan_gp_event.log automatically. While the VPN is down the logs are followed, so a gateway the client
# has just started trying is allowed within seconds.
globalprotect: true
# Domains the VPN's gateways live under. Host names that only appear in the user-writable event log are trusted
# when they end in one of these (portals and the gateway GlobalProtect reports itself are always trusted). In
# `full` mode every lookup under them also goes through the local DNS forwarder, so the address the VPN client
# receives is allowed before it connects: other gateways and rotating addresses keep working.
gateway_suffixes:
# Hosts the VPN client probes before it connects, allowed in `full` mode. GlobalProtect tests for a captive portal
# with these three in rotation; two refused probes look like a captive portal to it and it waits instead of
# connecting, so keep all three.
probe_hosts:
# Hosts reachable without the VPN, each with all its subdomains ('*.' prefix optional, quote it).
# Add from the CLI: `vpnchecker allow example.com`. Subdomain addresses are learned by a local
# DNS forwarder (/etc/resolver/<domain>), also VPN portal / gateway hosts the client must reach.
# A domain cannot be listed here and under protect.domains at the same time.
domains:
# - vpn.example.corp
# - jumpcloud.com # identity provider of a SAML / SSO VPN login: its page must load without the VPN
cidrs:
# - 203.0.113.5/32
check:
# fallback polling; routing table changes trigger an immediate check
interval: 2s
timeout: 1500ms
# allowlist domains are re-resolved this often; seen addresses are kept for resolve_retain
resolve_interval: 60s
resolve_retain: 24h
update:
# The installed daemon looks for a newer GitHub release every `interval` while the VPN is up. Installing one means:
# download, check against the release checksum and against this config, swap the binary, restart.
# false: never look; update by hand with `sudo vpnchecker update` or the install command.
auto: true
# Ask before installing: a dialog with Update / Later / Skip This Version appears in your session and nothing is
# downloaded without a yes. No answer, or nobody logged in, means no update. false: install without asking.
confirm: true
interval: 24h
# macOS notification when the protection blocks / resumes the apps
notify: true