On this page
lokal keeps almost everything in your home folder, owned by you. It changes two system settings, both during
lokal setup: it trusts its certificate authority in the System keychain, and it installs /etc/resolver/lokal.
Nothing else outside your home folder changes.
On this page, <base> stands for lokal’s data directory, ~/Library/Application Support/lokal. lokal creates it
with mode 0700, so only you can read it.
Overview#
| Path | What it is | Removed by |
|---|---|---|
<base>/state.toml | Your projects and services | uninstall --purge |
<base>/ca/root.pem | The CA certificate (mode 0644) | uninstall --purge |
<base>/ca/root-key.pem | The CA’s private key (mode 0600) | uninstall --purge |
<base>/ca/bundle.pem | The macOS system roots plus lokal’s CA, for lokal env | uninstall --purge |
<base>/bin/lokal | The background service’s own copy of the lokal binary | uninstall |
<base>/lokal.sock | The socket the CLI uses to talk to the background service (mode 0600) | uninstall |
<base>/install.json | A record of what setup did, which uninstall follows | uninstall |
<base>/resolver/lokal | The resolver file as setup prepares it, before copying it to /etc/resolver/lokal | uninstall |
<base>/state.toml.lock, <base>/daemon.lock | Lock files | uninstall --purge |
~/Library/Logs/lokal/ | The background service’s logs | uninstall --purge |
~/Library/LaunchAgents/dev.lokal.daemon.plist | The LaunchAgent that runs the background service | uninstall |
/etc/resolver/lokal | Sends .lokal lookups to lokal. Owned by root | uninstall |
/Library/Keychains/System.keychain | lokal’s CA, trusted as a root. Changed as root | uninstall |
user.js in each Firefox profile | One preference line, only if Firefox is installed | uninstall |
The two root-owned changes are the only reason setup and uninstall ask for your password. The security model lists the exact commands.
The state file#
<base>/state.toml holds your projects and services. The first lokal project create writes it, and every
project and service command updates it.
version = 1
current_project = "myproject"
[projects.myproject]
domain = "myproject.lokal"
created_at = "2026-10-10T12:00:00Z"
[projects.myproject.services.api]
host = "localhost"
port = 4000
host_header = "preserve" # or "upstream"
[projects.myproject.services."@"]
host = "localhost"
port = 3000
host_header = "preserve"current_projectis the project thatservicecommands use when you don’t pass-p.- Each
[projects.<name>]table has the project’sdomainand when it was created. - Each
[projects.<name>.services.<subdomain>]table routes one name tohost:port."@"is the project’s own domain.host_headerdefaults topreserveif it’s missing.
Only the CLI writes this file, and the background service only reads it. Each write takes an exclusive lock on
state.toml.lock, writes state.toml.tmp, syncs it to disk and renames it into place, so the file is never
half-written, and parallel commands never lose a change.
Editing it by hand#
You can edit state.toml by hand. The background service watches the file and applies your changes without a
CLI command. Some things to know:
- Hand edits skip the checks the CLI makes: the naming rules, overlapping domains, and ports 443 and 80. Prefer the CLI when you can.
- If the file doesn’t parse, the background service logs the error (see
lokal logs) and keeps serving what it had. - lokal ignores fields it doesn’t know. But the CLI rewrites the whole file on each change, so comments and
unknown fields don’t survive the next
projectorservicecommand. - A file with a newer
versionthan your lokal supports fails withstate file was written by a newer lokal; run a newer lokal.
The certificate authority#
<base>/ca/ holds lokal’s CA, which setup creates as you:
root.pemis the CA certificate, mode 0644.lokal ca pathprints its path.root-key.pemis the CA’s private key, mode 0600. lokal never copies, logs or sends it anywhere.bundle.pemis the macOS system roots plus lokal’s CA. Setup writes it, andlokal envrebuilds it when it’s older than 30 days.lokal envpointsSSL_CERT_FILE,REQUESTS_CA_BUNDLEandCURL_CA_BUNDLEat it.retired/appears only while setup replaces an outdated CA. It holds the old certificate (never its key) until the old CA is out of the keychain. Setup deletes it once that’s done, and so does uninstall.
Leaf certificates, one per service, are issued in memory by the background service. They’re never written to disk or to the keychain.
The background service#
The LaunchAgent#
~/Library/LaunchAgents/dev.lokal.daemon.plist defines the background service, a per-user LaunchAgent with the
label dev.lokal.daemon. Setup writes it, and lokal upgrade rewrites it if it changed. It tells launchd to:
- run
<base>/bin/lokal daemon runas you, when you log in (RunAtLoad), and keep it running (KeepAlive); - restart it within a second if it crashes (
ThrottleInterval1, instead of launchd’s default of 10 seconds); - send its stdout and stderr to
~/Library/Logs/lokal/launchd.out.logandlaunchd.err.log; - open these sockets and hand them to it:
| Socket | Address | Protocol | Used for |
|---|---|---|---|
https4 | 127.0.0.1:443 | TCP | HTTPS |
https6 | [::1]:443 | TCP | HTTPS |
http4 | 127.0.0.1:80 | TCP | Redirects to HTTPS |
http6 | [::1]:80 | TCP | Redirects to HTTPS |
dns4 | 127.0.0.1:53535 | UDP | lokal’s DNS server for .lokal |
Because launchd binds the ports, the background service never needs root, and every listener is on loopback.
The binary copy#
The LaunchAgent doesn’t run the lokal you installed. It runs <base>/bin/lokal, a copy that lokal setup and
lokal upgrade make of the binary you run them with. So the CLI can live anywhere (/opt/homebrew/bin,
~/.local/bin), and nothing depends on its path.
It also means updating the package doesn’t update the background service. After brew upgrade lokal, run
lokal upgrade. Until you do, lokal warns that the background service runs a different build.
The IPC socket and the lock#
<base>/lokal.sockis a Unix socket, mode 0600, that the CLI uses to ask the background service for its status and to reload after a change. Only your user account can connect to it.<base>/daemon.lockmakes sure only one copy of the background service runs at a time.
Logs#
~/Library/Logs/lokal/ holds:
daemon.log, the background service’s log, with one line per proxied request. It rotates daily todaemon.log.1anddaemon.log.2, so three files are kept.lokal logsshows it, andlokal logs -ffollows it.launchd.out.logandlaunchd.err.log, the service’s stdout and stderr as launchd captures them.
System files#
The resolver file#
/etc/resolver/lokal tells macOS to send every lookup under .lokal to lokal’s DNS server. Setup installs it
with sudo /usr/bin/install -m 0644, so it’s owned by root and readable by everyone:
# Installed by lokal: .lokal names are answered by lokal's DNS server.
nameserver 127.0.0.1
port 53535.local names need no file. lokal registers them with mDNSResponder instead.
The System keychain#
Setup adds lokal’s CA certificate to /Library/Keychains/System.keychain and trusts it as a root for
HTTPS only (security add-trusted-cert -d -r trustRoot -p ssl). That’s the only keychain change lokal makes, and after setup
nothing touches the keychain again. Uninstall untrusts the CA and deletes it (security remove-trusted-cert and
security delete-certificate).
Firefox#
Firefox keeps its own certificate store. If /Applications/Firefox.app exists, setup appends one line to
user.js in each profile under ~/Library/Application Support/Firefox/Profiles/:
user_pref("security.enterprise_roots.enabled", true); // added by lokalUninstall removes exactly that line. The security model explains what the preference does.
What lokal doesn’t touch#
lokal doesn’t change /etc/hosts, pf, the Application Firewall, other launchd jobs or any other keychain item.
lokal doctor reads /etc/hosts, but only to warn about entries that conflict with your lokal names.
What uninstall removes#
lokal uninstall | lokal uninstall --purge | |
|---|---|---|
Background service: the LaunchAgent and <base>/bin/lokal | removed | removed |
| CA trust and the CA certificate in the System keychain | removed | removed |
/etc/resolver/lokal | removed | removed |
| lokal’s Firefox line | removed | removed |
install.json, lokal.sock, resolver/, ca/retired/ | removed | removed |
state.toml (your projects and services) | kept | deleted |
CA files: root.pem, root-key.pem, bundle.pem | kept | deleted |
Logs in ~/Library/Logs/lokal/ | kept | deleted |
A plain uninstall keeps your projects and the CA files, so running lokal setup later restores everything with
the same CA. Once the CA is no longer trusted, its key on disk can’t make anything trusted. --purge deletes the
whole data directory, ~/Library/Application Support/lokal/, and the log directory.
The lokal command itself stays where you installed it. Remove it with brew uninstall lokal, with
make uninstall if you built from source, or by deleting the file. Run lokal uninstall first: removing the
package doesn’t undo setup. Upgrade and uninstall has the full steps.
The test sandbox#
LOKAL_HOME=<dir> moves every path on this page that’s under <base> into <dir>, and the logs into
<dir>/logs. There’s no LaunchAgent in a sandbox, and the commands that manage the real background service
(setup, uninstall, start, stop, restart, upgrade) refuse to run. lokal’s integration tests use it. It
isn’t meant for daily use.