A route typed after each connection solves the moment and not the week. On macOS the durable choice is usually a prefix stored with the VPN service, because the service reinstalls it on every connect and removes it when the service is down. The alternatives are a transient route add for testing and a connect hook for addresses that are only known after negotiation.

Decide whether the route belongs to the service or the session

Ask what the prefix depends on. If it is a fixed private network behind a native VPN service, the route belongs to that service. If it depends on a peer address that changes per connection, a hook or a status-driven job may be the better owner. If you are only checking a path for the next five minutes, a transient route is enough.

The service-bound route has the cleanest lifecycle. It appears with the service and disappears when the service is not connected, so it cannot outlive the tunnel it points at. That property is worth more than the convenience of a one-line command.

A session-bound route belongs to the interface, and that number changes without notice. A reconnect may hand the tunnel a different utun name than the previous session used, which turns a hardcoded command into a route that points at nothing or, worse, at a different tunnel.

Read the current route and service state

Start with the names the system actually uses. The service name in Network settings is the identifier for the commands below, and the current route table shows what the connected client installed. Read both while the tunnel is up.

scutil --nc list
networksetup -listnetworkserviceorder
netstat -rn | head -30
route get 10.20.5.5

That lookup shows which gateway and interface the kernel would pick for this address at this moment. If it already selects the tunnel, the prefix is covered and a new route would be redundant. If it selects the local router, the tunnel is missing that prefix.

Note the peer address while connected. It becomes the gateway value in the service-bound route, and it is usually the destination shown for the tunnel's default route in netstat -rn.

Store the prefix with networksetup

The networksetup tool attaches additional routes to a network service. For a service named OfficeVPN and a private prefix reached through the tunnel peer, the form is:

sudo networksetup -setadditionalroutes OfficeVPN 10.20.0.0 255.255.0.0 10.8.0.6
networksetup -getadditionalroutes OfficeVPN

Each triple after the service name gives a destination, its netmask and the tunnel peer as gateway. One call can carry several prefixes, and an empty call clears the stored list. The getter is the verification step; if the list is empty, the set did not apply to the service you think it did.

The gateway must be the tunnel peer, not the physical router. A route that names the local gateway sends the private prefix out the physical interface, which is the original failure with extra steps. A stale peer is the most common reason a correct-looking route never carries traffic. Re-read the peer from the connected state rather than copying an address from an old note.

Use route add only for a test

A direct route is useful to prove that a prefix works before you make it permanent. The interface form avoids a gateway lookup:

sudo route -n add -net 10.20.0.0/16 -interface utun4

The route vanishes on the next reconnect and depends on the interface number that exists today. That makes it a diagnostic, not a deployment. If the test works and the service-bound route does not, compare the gateway value first; that is the field most often wrong.

Be careful with advice that replaces the VPN's default route to force split behavior. A client that enforces a full tunnel can reinstall its routes after a reconnect or a policy refresh, so the manual change is undone silently. A handful of private prefixes is a routing problem; keeping all internet traffic local against a full-tunnel policy is a server-side decision.

Choose a hook only when the address is negotiated

For PPP-based links such as L2TP, macOS executes /etc/ppp/ip-up once the link is established. The script receives the interface name and the negotiated local and remote addresses, so it can add a route that depends on the peer the server assigned. That is exactly the case a fixed service route cannot express.

IKEv2 and vendor clients may not use that PPP path at all, which means the script can stay silent for the entire session. When that happens, a launchd job that polls scutil --nc status and acts on a state change is the equivalent mechanism, with a small delay.

Whichever hook you use, make it idempotent and make it remove only routes it created. A script that deletes every route mentioning the tunnel will eventually delete the route that keeps the VPN server reachable, and the connection will drop as soon as it succeeds.

Finish by checking the private destination, ordinary browsing, and a reconnect or sleep cycle. Then inspect scutil --dns, because name resolution is a separate policy and a working route does not prove that the private names resolve.

Nsɛm a ɛfa ho