WiFiSnitch is a small macOS helper app that exposes Wi-Fi and network status over a local Unix socket.
It ships with a CLI client, wifisnitch, so shell scripts, status bars, and local automation can query the current network state without talking to private macOS APIs directly.
Wi-Fi-specific fields require Location Services permission. Network and authorization fields remain available without Wi-Fi permission unless you explicitly lock them down.
Install from Homebrew:
brew tap gi8lino/tap
brew install gi8lino/tap/wifisnitch
brew services start wifisnitchThis installs:
WiFiSnitch.appwifisnitch
Useful service commands:
brew services stop wifisnitch
brew services restart wifisnitchIf macOS blocks the app or CLI with a quarantine warning:
xattr -dr com.apple.quarantine "$(brew --prefix)/opt/wifisnitch/libexec/WiFiSnitch.app"
xattr -d com.apple.quarantine "$(command -v wifisnitch)"
brew services start wifisnitchOn first launch, macOS should ask for location access. WiFiSnitch needs that permission to read SSID, BSSID, RSSI, channel, and related Wi-Fi details.
To reset permission and trigger the prompt again:
tccutil reset Location io.github.gi8lino.wifisnitch
open "$(brew --prefix)/opt/wifisnitch/libexec/WiFiSnitch.app"Then allow location access in:
System Settings → Privacy & Security → Location Services
Check the current state with:
wifisnitch fetch auth.location_authorized --format=text
wifisnitch fetch auth.location_permission_state --format=textWithout location access:
wifi.*fields are unavailablenetwork.*andauth.*fields still workfetchreturns only the fields the agent can currently provide
Default config path:
~/.config/wifisnitch/config.toml
The repository includes config.default.toml with every current default value.
Runtime defaults:
socket: /tmp/wifi-snitch/wifi-snitch.sock
lock dir: /tmp/wifi-snitch
log dir: ~/.local/state/wifisnitch
refresh interval: 60
log level: info
file logging: false
allow unauthorized fields without location: false
Supported environment variables:
WIFISNITCH_CONFIG_PATHselects the config file.WIFISNITCH_LOG_LEVELtemporarily overrides log verbosity.
Example config:
[logging]
enabled = false
level = "info"
directory = "~/.local/state/wifisnitch"
[agent]
socket_path = "/tmp/wifi-snitch/wifi-snitch.sock"
refresh_interval_seconds = 60
allow_unauthorized_fields_without_location = false
[app]
lock_dir = "/tmp/wifi-snitch"Example overrides:
WIFISNITCH_CONFIG_PATH=~/.config/wifisnitch/config.toml wifisnitch version
WIFISNITCH_LOG_LEVEL=debug open "$(brew --prefix)/opt/wifisnitch/libexec/WiFiSnitch.app"
wifisnitch --socket /path/to/wifi-snitch.sock pingShow help:
wifisnitch --helpCommon commands:
wifisnitch ping
wifisnitch version
wifisnitch fields
wifisnitch formats
wifisnitch fetch wifi.ssid --format=text
wifisnitch fetch wifi --format=json
wifisnitch fetch all --format=json
wifisnitch fetch wifi.ssid,wifi.bssid,wifi.channel --format=lines
wifisnitch fetch network.primary_interface,network.active_tunnel_interface --format=lines
wifisnitch fetch auth.location_authorized,auth.location_permission_state --format=linesOverride the default socket for one CLI command:
wifisnitch --socket /path/to/socket pingBuilt-in commands:
pingversionfieldsformatsfetch
Supported output formats:
textjsonlines
Supported fetch selectors:
all- namespaces:
wifi,network, andauth - aliases:
<namespace>.and<namespace>.*
Bare namespace selectors are the preferred form. The dotted and wildcard aliases expand the same way.
Available fields:
network.generated_atwifi.ssidwifi.bssidwifi.interfacewifi.hardware_addresswifi.powerwifi.service_activewifi.rssiwifi.noisewifi.snrwifi.link_qualitywifi.tx_ratewifi.channelwifi.channel_bandwifi.channel_widthwifi.securitywifi.phy_modewifi.interface_modewifi.country_codewifi.roamingwifi.ssid_changed_atwifi.interface_changed_atnetwork.primary_interfacenetwork.active_tunnel_interfacenetwork.active_tunnel_interfacesnetwork.primary_interface_is_tunnelnetwork.ipv4_addressnetwork.ipv6_addressnetwork.default_gatewaynetwork.dns_serversnetwork.internet_reachablenetwork.captive_portalauth.location_authorizedauth.location_permission_state
Selectors expand to concrete fields before the request is sent to the agent. Unavailable fields may still be omitted from the response at runtime, for example when Wi-Fi fields are blocked by missing location permission.
fetch ... --format=json returns typed values, not stringified ones. Booleans stay booleans, integers stay integers, and lists stay lists.
Example JSON payload:
{
"auth.location_authorized": true,
"auth.location_permission_state": "authorized_when_in_use",
"network.active_tunnel_interface": null,
"network.active_tunnel_interfaces": [],
"network.captive_portal": false,
"network.default_gateway": "192.168.1.1",
"network.dns_servers": ["1.1.1.1", "9.9.9.9"],
"network.generated_at": "2026-03-17T12:34:56Z",
"network.internet_reachable": true,
"network.ipv4_address": "192.168.1.42",
"network.ipv6_address": "fe80::1234",
"network.primary_interface": "en0",
"network.primary_interface_is_tunnel": false,
"wifi.bssid": "aa:bb:cc:dd:ee:ff",
"wifi.channel": 44,
"wifi.channel_band": "5ghz",
"wifi.channel_width": "80mhz",
"wifi.country_code": "CH",
"wifi.hardware_address": "11:22:33:44:55:66",
"wifi.interface": "en0",
"wifi.interface_changed_at": "2026-03-17T12:34:56Z",
"wifi.interface_mode": "station",
"wifi.link_quality": 68,
"wifi.noise": -90,
"wifi.phy_mode": "802.11ax",
"wifi.power": true,
"wifi.roaming": false,
"wifi.rssi": -63,
"wifi.security": "wpa3_personal",
"wifi.service_active": true,
"wifi.snr": 27,
"wifi.ssid": "ExampleWiFi",
"wifi.ssid_changed_at": "2026-03-17T12:30:00Z",
"wifi.tx_rate": 780
}Quick checks:
brew services list | grep wifisnitch
pgrep -fl WiFiSnitch
wifisnitch ping
wifisnitch versionUseful logs:
tail -n 200 ~/Library/Logs/Homebrew/wifisnitch/*.logCommon cases:
- If
wifisnitch pingfails, the agent is usually not running, was blocked by macOS, or never finished startup. - If
wifi.*fields fail, location permission is usually denied, restricted, or not yet granted. - If manual launch works but
brew servicesdoes not, restart the service and compare logs. - If two instances are fighting, stop the service, kill manual copies, and start cleanly again.
Clean restart:
brew services stop wifisnitch
pkill -x WiFiSnitch || true
brew services start wifisnitch
wifisnitch pingShow the current Wi-Fi name:
local handle = io.popen("wifisnitch fetch wifi.ssid --format=text")
local output = handle and handle:read("*a") or ""
if handle then handle:close() end
local ssid = output:gsub("^%s+", ""):gsub("%s+$", "")
if ssid == "" then
ssid = "WiFi"
end
print(ssid)Read Wi-Fi plus tunnel info:
local handle = io.popen(
"wifisnitch fetch wifi.ssid,network.active_tunnel_interface,network.primary_interface_is_tunnel --format=lines"
)
local output = handle and handle:read("*a") or ""
if handle then handle:close() end
local values = {}
for line in output:gmatch("[^\r\n]+") do
local key, value = line:match("^([^=]+)=(.*)$")
if key then
values[key] = value
end
end
print(values["wifi.ssid"] or "")
print(values["network.active_tunnel_interface"] or "")
print(values["network.primary_interface_is_tunnel"] or "")swift build
swift build -c release
make bundle
make run
make stopRun the packaged app:
open dist/WiFiSnitch.appThis project is licensed under the Apache 2.0 License. See the LICENSE file for details.