Start a Saved Mac VPN From Terminal With scutil --nc
Use scutil --nc to start, inspect and stop a saved macOS VPN service, keep credentials out of argv, and prove traffic actually uses the tunnel.
You have a saved VPN service in System Settings, and you want the terminal to bring it up. On macOS the supported path is scutil --nc, but only for services that the system's own network connection manager owns. The menu bar is not the API: third-party clients usually have their own automation, and a profile pushed by a management service may refuse a local start. This is the checklist I run: find the exact service, read its state, start it, wait for the state to change, then prove traffic actually uses the tunnel. Stop at the first step that disagrees, because every later command assumes the one before it succeeded.
Find the service before you try to start it
Start with the inventory rather than the profile name you remember:
scutil --nc help
scutil --nc list
Each line pairs a status with a service identifier, a type and a display name. The identifier is the value to script against, because display names repeat and can be edited. Apple's scutil manual documents the network-connection mode, while the installed --nc help is the authoritative list of subcommands on that Mac; do not assume a flag from an older guide still exists in your macOS version.
An empty list is a result, not an error. A service created by a vendor client or a management profile can be active while invisible here, or visible with a type that the native controller cannot drive. In that case, use the vendor's tool or the profile documentation. Creating a parallel native profile just to have something to start is a bad trade.
Read the current state and confirm the selector
Query the service before starting it, so you know what a successful start should change:
scutil --nc status "Office VPN"
If two services share a display name, the name is ambiguous. Pass the identifier instead; the command accepts either, but only the identifier is guaranteed to select one service. Check that the status word is one you recognize, typically Connected, Disconnected, Connecting or Disconnecting, before you parse it in a script. Also check whether the profile is managed, because a locked configuration can accept the command and still ignore or prompt for it.
This state call is cheap, so use it as the guard in automation: start only when the status is Disconnected, and do not treat Connecting as a failure. If the service is already Connected, skip the start rather than interrupting a working tunnel.
Start the connection and wait for the state to settle
The start request is asynchronous. The command returns when the request was accepted, not when authentication finishes or routes are installed. Run it, then poll the state with a bounded wait and bail out if the service falls back to Disconnected:
scutil --nc start "Office VPN"
n=0
while [ "$n" -lt 30 ]; do
state=$(scutil --nc status "Office VPN" | head -n 1)
[ "$state" = "Connected" ] && break
[ "$state" = "Disconnected" ] && { echo "start failed"; exit 1; }
sleep 2
n=$((n + 1))
done
A status of Connecting that never resolves usually points at credentials, MFA or the server, not at scutil. The command cannot supply an interactive login, so a profile that expects a token or a hardware key will sit in Connecting and eventually fall back. Read the status word before you add retries; looping against an authentication prompt cannot succeed.
Do not treat the shell's exit status as readiness. The useful signal is the state string, followed by evidence that the tunnel is carrying traffic. A start command that returns quietly can still leave you with a connection that never authenticates.
Prove the tunnel carries traffic, not just that it says Connected
Connected describes the connection, not the routing decision. Native PPP and IKEv2 services create a ppp or utun interface and install routes, while a split tunnel deliberately leaves the default route alone. Inspect the interfaces and both address families:
ifconfig -l
netstat -rn -f inet
netstat -rn -f inet6
scutil --dns
Find the interface the service created rather than guessing a number, then ask the routing table which interface a specific destination uses: route -n get 10.0.0.10, substituting an internal host. If it reports the physical interface, the tunnel is up but not carrying that flow. Check split-tunnel routes, resolver order, and any local firewall rule that allows the tunnel interface but not the process.
Name resolution is the usual false negative. Internal names can resolve through the wrong resolver while the tunnel is perfectly healthy, which looks like a VPN fault and is not one. scutil --dns shows the order macOS will use; after a resolver change, flush the cache with dscacheutil -flushcache and sudo killall -HUP mDNSResponder.
Keep credentials and privilege out of the command line
Never put a password, pre-shared key or token in the arguments. Arguments are visible in process listings and land in shell history, and a copied support transcript can leak them. The profile's own credential handling, whether a Keychain item, a system prompt or the client's authentication flow, is the place for secrets.
Running the command with sudo does not make an interactive login unattended. It changes the effective user, not the availability of a GUI prompt, a hardware token or the login keychain. A LaunchAgent can start a service after the user logs in and the keychain is unlocked; a LaunchDaemon usually cannot reach those user-session pieces. If an organization manages the service, its lifecycle rules win over any local script.
Stop the service and verify it stayed stopped
Stop with the same selector you used to start, then read the state back instead of assuming the tunnel is gone:
scutil --nc stop "Office VPN"
scutil --nc status "Office VPN"
A stop that returns to Disconnected still leaves one thing to check: whether the profile is set to connect on demand, which can bring it back without your script. If another client owns routes on the same Mac, those routes can survive a native stop. Re-run the route check before you conclude the machine is off the VPN.
For a wrapper you run by hand, keep the order fixed: list, status, start, poll, route test, stop, verify. Do not parse the friendly name out of free-form output when the identifier is available, and do not treat the start command's exit as readiness. The connection manager reports state; your proof is the state plus a route that points at the tunnel.