Every public share so far went through somebody else's tunnel service. Point a wildcard you control at the machine, set the base domain once, and lerd answers on <site>.<domain> through the reverse proxy or VPN you already run, with the site's ordinary .test vhost untouched beside it. Pinggy joins the SSH tools as a table entry rather than another special case, managed engines reach the LAN as a deliberate opt-in, and the dashboard actions that touch the host sit behind a switch of their own. Underneath, the watcher stopped rewriting snapshots identical to the ones already on disk, stopped re-running the same composer install every minute forever, and went from 62 wakeups a second to 39 at roughly a third of the idle CPU. The boot scan that left the last worktree unroutable for minutes now lands six of them in 24 milliseconds.
A LAN-exposed install served the sites and nothing behind them, so a phone on the WiFi could open the app while a colleague's machine could not reach the database it runs on. Managed engines can be published to the LAN now, and because that is a decision about the network rather than about a service, the switch lives under the LAN exposure it depends on instead of in a card of its own offering something it cannot deliver.
Host header is deliberately not part of that decision, since a browser on the machine may legitimately arrive as the hostname or an /etc/hosts alias, and turning those away leaves the local user with no way in and no credential that helps.Rebinding converges rather than reporting success optimistically. Every affected unit is attempted and the failures reported together, and which units to restart comes from comparing each container's published ports against the policy it should follow, so a toggle interrupted halfway, quadlets rewritten while containers still run the old bind, is noticed and cleared.
internal/podman/quadlet.goRaw .env reads, filesystem browsing, database drops, terminals and shutting lerd down stay local unless you grant them. Both the CLI and the dashboard toggle refuse to run from a remote session, so a session can never widen its own authority, and remote-control off clears the setting with the credentials rather than leaving it armed for the next password.
The dashboard stated "loopback only, LAN devices cannot reach it" directly above a switch offering those devices access to every database, and flipping it only stored a preference. It is hidden while lerd is loopback-only and refused by the CLI and the API too. Turning it off always works, and a setting already on keeps showing, so re-exposing cannot quietly republish databases.
The watcher rewrote the request-timing snapshot every ten seconds and the idle activity map every thirty, for the life of the process, whether or not anything had changed, which on a quiet machine is over ten thousand write-and-rename cycles a day to persist bytes identical to the ones already on disk. The cost that matters there is not CPU, it is that the disk and the writeback path never get to settle, which is exactly what a laptop feels.
A worktree is seeded with a copy of the main repo's vendor directory, so its install marker arrives carrying the main repo's older timestamp against a freshly checked out lockfile. The recovery rescan therefore re-ran composer install on that worktree every minute, indefinitely, and would have gone on forever. Markers are stamped once the install that owns them succeeds. #1271
The host-gateway watch forked podman every tick to read an address that only moves when the container is recreated. The idle engine asked systemd for each suspended site's timer through a call that fetches the unit's entire property dictionary to read one field, where the batched snapshot the dashboard keeps warm already had it. #1271
The source-file watcher re-walked every watched tree each pass to discover new directories, which a checkout of any size makes thousands of readdirs, but new directories already arrive as create events on their parent. Worker detection re-read and re-decoded a site's composer.json once per rule per site. #1271
All three daemons gave themselves dozens of threads on a many-core machine and turned every wakeup into futex traffic across all of them. They are event handlers waiting on sockets, timers and subprocesses, so GOMAXPROCS caps at four; goroutines blocked in syscalls still get threads of their own, so subprocess concurrency is unchanged. The MCP server gets the same cap, since it lives as long as the session that spawned it. #1271
A bare git worktree add from any tool is set up automatically, and a tool that then wanted to act on the new tree had no way to ask whether that pipeline had finished. Probing the tree is a trap: composer's extraction phase fills existing vendor directories and node_modules exists from the very first extracted package, so file counts and mtimes sit still through the longest stretch of an install and read as finished.
lerd worktree wait # blocks on the current directory # exit 0 the tree is safe to touch # exit 1 timed out, still going # exit 3 not a worktree lerd manages, ask someone else
Two installers writing one tree is how vendor ends up holding packages but no autoload.php, which presents as a Composer bug rather than as the race it is. The probe takes a shared non-blocking flock rather than a try-lock, so it neither overwrites the pid the real holder recorded nor claims a lock a starting installer would have to poll for.
Its add action ran bare git and handed the tree straight back to the assistant. It waits by default now and reports whether the tree is provisioned, a wait action covers a worktree created outside the tool, and the reference injected into assistants states that readiness must never be inferred from the tree's contents.
Deleting a checkout outside lerd leaves systemd retrying a command that fails at CHDIR before it runs. Coding agents create and destroy their own worktrees and have no reason to know lerd exists. Such a unit never settles into failed, it flaps between activating and active for as long as the machine is up, so the missing checkout is what decides it. macOS reads the path back out of the guard script, since launchd exposes none.
The boot scan walked a site's worktrees one at a time and, for each, ran the dependency install before writing the vhost, so a subdomain waited not only on its own composer and npm run but on every worktree ahead of it in the list. What that looks like is a worktree that was added successfully, has its vendor and its env in place, and 404s, with nothing in status or doctor pointing anywhere. Nothing about routing depends on the install: six worktrees on a four-core machine went from the last vhost landing seventeen seconds in to all six landing in twenty-four milliseconds. The vhost that scan wrote also named the parent site's PHP container rather than the version the worktree pins in its own .lerd.yaml, so a branch held back on an older PHP was served by the newer one after any watcher restart.
The client shims exec their tool inside a throwaway container joined only to the lerd network, so a hostless psql -l routed correctly while an explicit -h 127.0.0.1 at the port lerd published always failed, because inside that container, loopback is its own. That is also the documented way to reach a lerd database from the host, so it hit anyone running psql, pg_dump, mysql or mysqldump by hand or from a script.
| what you typed | where it went before | where it goes now |
|---|---|---|
psql -l | lerd-postgres, internal port | unchanged |
psql -h 127.0.0.1 -p 55432 | the throwaway container's own loopback → refused | lerd-postgres, internal port |
psql -h db.example.com | straight through | unchanged |
psql postgres://user@host/db | read as no host → local default, carrying lerd's admin PGPASSWORD | straight through, host intact |
The host decision had moved to a flag-only test, so a connection URI or a libpq conninfo string read as no host at all and fell into the local-default branch, arriving at an external server with lerd's own credentials. One function owns the whole question now, answering for both shapes before it looks at a single flag.
internal/shims/PhpStorm runs its quality tools against a temp copy of the buffer under /tmp, which lerd deliberately never mounts, so phpstan started fine and reported the path did not exist. Such arguments are staged into a scratch dir and the paths rewritten back in the output, in both the plain and the json_encode escaped shape, and a copy the tool edited in place is written home, which is what keeps php-cs-fixer working rather than silently discarding its fixes.
Some prefer a bare php pointed at a host install, and the only way was deleting the rc line by hand, which the next install wrote straight back. lerd path:disable persists it. Making the promise true took a fix: lerd run and MCP commands_run handed framework command strings to a shell without prepending the shim dir, so they died on php: command not found while the dashboard ran them fine.
The npm shim forced npm_config_prefix to lerd's node-global dir for every run, so npm install -g landed inside ~/.local/share/lerd, invisible the moment the PATH shim came off, and deleted outright by an uninstall with data removal, which nobody expects from removing a dev tool. A prefix you configured yourself now wins, and globals that did land in the managed prefix are listed as name@version specs and offered back rather than lost. Separately, the wrappers activate Node through fnm or nvm, which clobbers the prefix, so it is injected after activation and shell-quoted, since a data directory containing a space split into two words and env took the tail as the command to run.
A certificate can sit in the macOS keychain with its trust settings cleared independent of the certificate item itself, which is most commonly what a system update does. security find-certificate reports that identically to a genuinely trusted CA, so lerd treated a reinstall as a no-op, skipped the sudo announcement, and left every site's certificate invalid in the browser while podman and the vhost looked completely healthy.
Per SecTrustSettings an absent or empty trustSettings array means trust as a root, which is how macOS records everything add-trusted-cert installs, and only mkcert writes an explicit one. Reading an absent array as untrusted would have had every later install re-run the repair and prompt for authorization each time.
A machine with nss-tools installed but the CA never in the NSS store got a clean bill of health while every browser refused the certificates lerd signs. It compares what the store exports against the CA lerd signs with, rather than accepting any certificate wearing the mkcert name, so a CA regenerated after a profile imported the old one is reported.
internal/certs/browser_trust.goAn export that could not be read reports nothing rather than untrusted, so a failing security cannot drive a privileged repair on its own. A failed repair is named rather than swallowed, since macOS draws the authorization dialog itself and a run with no window server behind it fails there.
Serving a dev server on the site's own domain shipped in 1.31.0. Three things it read exactly once, when it started, and never revisited: the scheme, the domains, and the address it tells a plugin to publish from.
Unsecuring a site left the server still advertising https, so every asset URL named a scheme the site no longer serves and the page arrived unstyled, the exact symptom the base prefix exists to remove. The config follows from the HTTPS toggle and the vhost regeneration every domain change already goes through, deciding on the config's contents rather than on the event, so a vhost rewrite that moves no address never bounces a server somebody is working against.
internal/siteops/devserver.goA site with several domains serves all of them from one origin, which is inherent, since a dev server can advertise only one. What was not inherent: naming an origin at all makes some framework plugins adopt it as their entire allowlist, in place of a default that accepted any domain of the site, so a page opened on a second domain had every module script refused.
A plugin released before Vite grew server.origin ignores it and publishes whatever address the server bound to, which with host: true is the IPv6 wildcard. Nothing can route there, and on a secured site the browser refuses the plain request as mixed content. The one URL a browser actually reaches the server at is watched for on the config's way to disk.
It answers on the site's own domain under lerd's prefix, so everything it served arrived in the request-timing feed looking like a route the app had served. The static-asset test never stood a chance: the modules a dev server rewrites carry extensions the asset list has no reason to know, and the ones rebuilt on every save ended up reading as the busiest routes on the site. The site usage query had drifted from the shared predicate too, testing only the extension while its comment claimed otherwise, so a dev server made a site look busy in the sites list as well. Both decide through one test of the URI now.
internal/reqstats/1.31.0 was tested across distro guests and reviewed end to end, and both passes are in here. What is listed as fixed is what a 1.31.0 install actually runs into, and anything broken and repaired inside this cycle folds into the feature that carried it, since no released version ever had it.
The dump was piped into gzip, which succeeds regardless of what happened upstream, so a failed dump became a successful snapshot holding an empty stream that meta.json recorded as restorable. Restore drops and recreates before it reads the dump, so it would have replaced live data with nothing. The guard runs on the restore side too, before the drop, since verifying only at creation leaves every earlier snapshot on disk.
systemd calls a unit active well before a database accepts connections, so the first request to a database-backed site returned 500. lerd start waits for the engines it started, bounded so a wedged one cannot hold the start hostage. A PHP version switch and secure/unsecure had the same shape: both printed a URL and returned while the old vhost was still answering.
The guard compared canonical path strings, and resolving symlinks only folds the spellings that pass through a link, and a case-insensitive volume, which is what macOS formats by default, spells one directory many ways. Identity is asked of the filesystem now, os.SameFile, which settles case, symlinks and bind mounts together and needs no knowledge of the volume.
A caller that lost the build lock got the cache, which on a cold cache is nil, written out as a zero-byte body, twenty-four of twenty-five concurrent reads after a restart. The client's catch kept the initial value and the dashboard stayed empty for minutes. A caller with nothing to show waits for the build in flight, because answering from a cold cache is a claim about the registry nobody checked.
The watcher signalled readiness only after its boot work, which provisions every worktree needing it. An install slower than the unit's start timeout earned a SIGTERM, systemd restarted, and the fresh process began the same install to be killed at the same point. Readiness goes first; raising the timeout only moves the cliff.
Array properties were emitted without the items the spec requires. Two of them are not string arrays, and declaring string elements there would have been worse than declaring none: a strict client sends strings, the handler's map assertion skips every element silently, and the framework comes back saved with no setup and no logs in it.
podman forwards none of the host environment, so Symfony Console never saw COLORTERM. TERM stays out of it deliberately: the images carry terminfo only for the xterm family, and Symfony matches it against a pattern that foot and wezterm fail.
A data dir that outlived its quadlet fell through to whatever the bundled YAML calls canonical, mounting 16 over a cluster written by 18. A preset declares where its engine records the version that wrote its data, and that reading wins over every pin.
A repo or org rename inserts an extra redirect hop, and the checker parsed the tag out of the first Location alone, so every pre-rename binary failed. It follows the chain up to five hops until a /tag/ URL appears.
The docs shipped walkthroughs for Laravel, Symfony and WordPress while the store supports nine frameworks. Drupal, CakePHP, CodeIgniter, Statamic, Tempest and Magento close the gap, and writing them from real installs is what surfaced the things a reader trips on: CakePHP ships .env loading switched off, so config/.env is never read and the failure surfaces as a PDO constructor error rather than as anything pointing at the file that needs changing. Site Management, 550 lines answering six questions from one sidebar entry, splits into focused pages with the old URL kept live #1308, and pointing a project at a host-installed database gets its own recipe with the Linux prerequisites spelled out #1288.