A macOS background daemon that reads your location via CoreLocation and serves it over a Unix domain socket. Comes with a Python client library.
┌──────────────────────────┐
│ corelocation-socket.app │
│ │
│ CoreLocation ──► JSON │
│ │ │
│ Unix Socket │
└───────────────┬──────────┘
│
~/Library/Application Support/
corelocation-socket/location.sock
│
┌───────┴───────┐
│ Python code │
└───────────────┘
The daemon is packaged as a .app bundle (not a bare CLI) because macOS Ventura and later silently deny location access to non-bundled executables. The bundle contains an Info.plist with NSLocationUsageDescription, which is the only location key recognized on macOS (the WhenInUse/Always variants are iOS-only and ignored).
# Build, bundle, sign, install, and start the daemon
./scripts/install.shThis will:
- Compile the Swift binary via SPM (
swift build -c release) - Package it into
corelocation-socket.appwith ad-hoc code signing - Copy the app to
~/Applications/ - Install a
launchduser agent that starts on login and restarts on crash - Load the agent immediately
On first launch, macOS will prompt you to allow location access.
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.corelocation-socket.plist
rm ~/Library/LaunchAgents/com.user.corelocation-socket.plist
rm -rf ~/Applications/corelocation-socket.appThe server communicates using JSON Lines (one JSON object per line, delimited by \n) over a Unix domain socket at:
~/Library/Application Support/corelocation-socket/location.sock
The protocol is inspired by gpsd's JSON protocol but stripped down for desktop use.
TPV (Time-Position-Velocity) — location data from the daemon:
{"class":"TPV","lat":37.8712,"lon":-122.2628,"horizontal_accuracy":35,"timestamp":"2026-02-25T23:55:05.693Z","mode":2}| Field | Type | Description |
|---|---|---|
class |
string | Always "TPV" |
lat |
float | Latitude in decimal degrees |
lon |
float | Longitude in decimal degrees |
horizontal_accuracy |
float | Accuracy radius in meters |
timestamp |
string | ISO 8601 timestamp of the fix |
mode |
int | 0 = no fix, 2 = 2D fix (Wi-Fi/cell) |
Mac desktops typically report mode: 2 (Wi-Fi-based positioning). There is no GPS hardware, so altitude/speed/course are not included.
POLL — request the current location:
{"class":"POLL"}Send this to get an immediate response with the last known location. The server replies with a single TPV message (or an ERROR if no fix is available yet).
ERROR — returned when something goes wrong:
{"class":"ERROR","message":"No location data available"}- On connect: The server accepts the connection and waits for input. It does not send a banner or greeting.
- POLL request: Client sends
{"class":"POLL"}\n. Server replies with one TPV or ERROR line. - Streaming: Connected clients receive broadcast TPV messages whenever CoreLocation delivers a new fix. On a typical Mac, this is roughly every 5 minutes via Wi-Fi positioning. The client does not need to send anything to receive broadcasts — just stay connected.
- Multiple clients: The server supports concurrent connections. Each client independently receives broadcasts and can send POLL requests.
- Disconnect: Clients can close the connection at any time. The server cleans up automatically.
# One-shot query
echo '{"class":"POLL"}' | socat - UNIX-CONNECT:"$HOME/Library/Application Support/corelocation-socket/location.sock"
# Stream updates (stays connected)
socat - UNIX-CONNECT:"$HOME/Library/Application Support/corelocation-socket/location.sock"Install the client library:
pip install -e .from corelocation_client import get_location
loc = get_location()
print(f"{loc['lat']}, {loc['lon']} (±{loc['horizontal_accuracy']}m)")get_location() opens a connection, sends POLL, reads the response, and closes. It raises ConnectionError if the daemon isn't running, or if no location fix is available yet.
import asyncio
from corelocation_client import stream_locations
async def main():
async for loc in stream_locations():
print(f"{loc['lat']}, {loc['lon']}")
asyncio.run(main())stream_locations() holds the connection open and yields each TPV as it arrives. It sends an initial POLL on connect, then listens for broadcasts.
get_location(socket_path: str | None = None) -> LocationDataSynchronous. Returns the current location or raises ConnectionError / TimeoutError.
async stream_locations(socket_path: str | None = None) -> AsyncIterator[LocationData]Async generator. Yields LocationData dicts as updates arrive. Skips malformed messages automatically.
class LocationData(TypedDict):
lat: float
lon: float
horizontal_accuracy: float
timestamp: str # ISO 8601
mode: int # 0 = no fix, 2 = 2D fixBoth functions default to ~/Library/Application Support/corelocation-socket/location.sock. Pass socket_path to override.
# Check if running
launchctl list | grep corelocation
# View logs
tail -f /tmp/corelocation-socket.log
# Restart
launchctl kickstart -k gui/$(id -u)/com.user.corelocation-socket
# Stop
launchctl bootout gui/$(id -u)/com.user.corelocation-socketThe daemon is configured with KeepAlive: true, so launchd will restart it if it crashes.
"No location data available" — CoreLocation hasn't delivered a fix yet. This is normal for the first few seconds after startup. Retry shortly.
ConnectionError: Failed to connect — The daemon isn't running. Check launchctl list | grep corelocation and the log file.
Location permission denied — Open System Settings → Privacy & Security → Location Services. Find corelocation-socket and enable it. You may need to restart the daemon afterward.
Stale socket file — If the daemon crashed without cleanup, the socket file may be left behind. The daemon removes it on startup, but you can manually delete it:
rm ~/Library/Application\ Support/corelocation-socket/location.sock