Skip to content

Changelog

What each release actually changed, newest first. Upgrade instructions and what an update does not touch are on Updating; the exact pinned version of every binary is on Pinned versions.

Two Windows bug reports (8 and 9 October) drove almost everything here, plus the fixes the following PUBLISH-TESTING audit found on its own. Nothing new was added; Windows catches up to what macOS and Linux already did, and a handful of Valet/Herd and reliability edges get closed.

Windows: a missing Visual C++ Redistributable is named before anything starts

Section titled “Windows: a missing Visual C++ Redistributable is named before anything starts”

php.net’s PHP and Oracle’s MySQL builds link VCRUNTIME140.dll, and a fresh Windows 11 install does not always have it. Before, that meant MySQL’s first start died with a bare exit code and a Retry could hang on a hidden Windows error dialog. Now rex doctor’s new Runtime line, a Services banner, and Start all itself all check the redistributable’s own registry entry (VC\Runtimes\X64) before touching MySQL or PHP:

  • Missing entirely — blocking: “PHP and MySQL need the Microsoft Visual C++ Redistributable (x64), and this PC does not have it — no database or PHP can start until it is installed. Install it from Microsoft, then Start all.”
  • Registered but older than 14.30 (the Visual Studio 2022 toolset PHP 8.4 and 8.5 need) — a warning only: MySQL and PHP up to 8.3 still start.

The banner, rex doctor and the interactive installer all link Microsoft’s own https://aka.ms/vs/17/release/vc_redist.x64.exe; a silent /S install stays silent and offers nothing. Separately, rex itself used to exit with 0xC0000135 before printing anything on a PC without the runtime — 0.8.12’s rex.exe linked the same DLL dynamically. It is now linked statically, so the CLI runs regardless.

Windows: PHP 7.4, 8.0, 8.1, 8.4 and 8.5 start — not just the default 8.3

Section titled “Windows: PHP 7.4, 8.0, 8.1, 8.4 and 8.5 start — not just the default 8.3”

One extension list, read off 8.3’s official zip, had been written into every PHP minor’s ini. The zips differ — 8.4 and newer moved IMAP to PECL, zip is built in before 8.2, gd ships as php_gd2.dll on 7.4 — and rexenv’s preflight refuses any startup warning, so those five minors could never start on Windows. A user hit it on 8.4, because every earlier proof had run on the 8.3 default. Each minor now loads the extensions its own zip actually ships.

Matching macOS and Linux, where every PHP build compiles it in: the official PECL php_imap build is downloaded alongside the PHP zip for 8.4 and 8.5.

rex doctor stops misreading rexenv’s own PostgreSQL as another program

Section titled “rex doctor stops misreading rexenv’s own PostgreSQL as another program”

Windows starts PostgreSQL through pg_ctl, which writes its own command line with forward-slashed paths (C:/Users/…/rexenv/data/…). The doctor’s port-holder check compared against a backslashed one, so it reported rexenv’s own postgres.exe as a foreign program holding its port — with a Stop-Process command that would have killed it. Fixed in the matcher.

A Caddy reload cut off by its own restart no longer reads as a broken site

Section titled “A Caddy reload cut off by its own restart no longer reads as a broken site”

A reload that changes the Caddyfile (adding a site, not rotating a cert) can have its admin connection severed by the admin server it is in the middle of replacing — the new config IS applied, but the reload command itself reports exit 1. Creating several sites in a row on a Windows VM turned up new sites stuck on setup incomplete while they were already serving. A failed reload is now retried once before it is reported; a second, no-op reload confirms the first succeeded, and a genuine config error still fails with Caddy’s own last line attached.

A repository’s composer.json can name a PHP extension your site’s PHP build does not load, not only a PHP version — rexenv now checks both at the clone phase, against the site’s real php -m, before Composer runs:

this repository needs the PHP extension imagick (required by spatie/image), and PHP 8.4 as rexenv ships it does not load it — composer install would refuse it. Nothing was installed; the project cannot run on this site’s PHP as it is.

A migration that stops half-way used to fail every Retry with Composer’s own 1050 Table '…' already exists and no explanation — MySQL commits each CREATE TABLE on its own, so the half-made tables survive while Laravel never records the migration as done. It is now named instead:

A table this migration creates already exists, so an earlier run stopped part-way: MySQL keeps each table it created, while Laravel never recorded the migration as done. rexenv drops nothing and marks nothing as migrated. Export the database first (Databases → Export), then either drop that migration’s half-made tables or let the migration skip tables that exist, and Retry.

And Stop all (the footer, the tray, rex stop / rex restart, and the MCP tool) now refuses while a site is still being provisioned, naming it, rather than stopping the database under its own install or migration:

Nothing was stopped: shop.rex is still being set up, and stopping the stack now would cut off its install or migrations mid-way — a half-applied migration does not undo itself. Wait for it to finish, or Cancel it on its card, then Stop all.

  • The “your .test sites stopped resolving” banner is keyed to the moment Valet or Herd retakes a TLD, not just the bare TLD name — so dismissing it, re-taking the domain from Import, and losing it again a second time shows the banner again instead of staying dismissed.
  • Settings no longer describes a domain Valet or Herd took back as “rexenv answers this” — it says the domain was taken back.
  • The import’s progress card now shows a row the import queue rejected (its folder moved since the scan, say) as skipped and done, instead of leaving it reading “waiting” forever.
  • An import whose first phase refuses outright before the progress card has subscribed used to settle on “checking the source database · 0%” and sit there; the card re-reads the job after subscribing, so it shows the refusal instead.
  • After a database connect or revert, the Database tab’s import card now drops any of that site’s earlier, settled import jobs — including a neutral “this site already uses rexenv’s own copy” result — instead of showing it as a stuck error with a Retry that could only refuse again.

Reliability: a slow edge is no longer mistaken for a foreign proxy

Section titled “Reliability: a slow edge is no longer mistaken for a foreign proxy”

Under a large database copy, Caddy could take just long enough answering the watchdog’s :443 probe that two polls in a row read as unreachable — folded into “foreign”, with a toast naming “another local proxy” and a sudo kill command that would have killed rexenv’s own edge. The watchdog now tells a slow-but-genuine answer from a different program actually answering, and offers a kill command only when it can name that other program.

macOS: “Remove system changes” deletes the CA itself, not only its trust

Section titled “macOS: “Remove system changes” deletes the CA itself, not only its trust”

Untrusting rexenv’s local certificate authority removed the trust setting but left the rexenv Local CA item itself in your login keychain — inert, but unreachable by brew uninstall --zap --cask rexenv, which cannot see inside Keychain Access. “Remove system changes” now also runs security delete-certificate on every copy of the CA.

A curl inside PHP that finds your other .rex sites on macOS; a one-click repair for WordPress sites an old download left short of files; OpenLiteSpeed as a third per-site server on macOS and Linux — and the fixes the 0.8.11 update runs found, on all three systems.

macOS: a site’s own curl reaches your other .rex sites

Section titled “macOS: a site’s own curl reaches your other .rex sites”

Under PHP 8.1–8.5, curl_init("https://other-site.rex/") failed with “Could not resolve host” while gethostbyname() on the same name answered 127.0.0.1. The builds’ libcurl was linked to c-ares, which reads /etc/resolv.conf and /etc/hosts and never macOS’s /etc/resolver/rex — the file that sends .rex to rexenv. It came in unasked: the build tool turns c-ares on whenever it is in the build, and swoole depends on it. WordPress itself was covered by rexenv’s mu-plugin; a plugin calling curl directly, and every non-WordPress app, were not.

rexenv’s macOS builds of 8.1.34, 8.2.32, 8.3.32, 8.4.23 and 8.5.8 are rebuilt — the same patch versions — with a libcurl that uses the system resolver, and the build now fails if curl links c-ares again. 8.0.30 still has the problem: it is the one PHP rexenv does not build, the mu-plugin stays for it, and rex doctor’s PHP curl note now names 8.0 alone.

The version numbers did not change, so a cached build is told apart by its recorded checksum: one that no longer matches the pin is downloaded again, once. What the source does not tell us is whether a PHP pool already running when you update moves to the new build before it is restarted — so restart once (rex restart, or Stop all and Start all) before testing a curl call. Linux and Windows use other publishers’ 8.x builds, which this release does not change.

WordPress sites created by 0.4.0–0.7.1: told, and repaired in one click

Section titled “WordPress sites created by 0.4.0–0.7.1: told, and repaired in one click”

Those versions downloaded WordPress as a tarball that PHP read with every file name over 100 characters cut short, so the site was created without its long-named files — for 7.1, 25 core classes in wp-includes/php-ai-client and 14 default-theme fonts and patterns — and nothing said so. 0.7.2 fixed new sites; existing ones stayed broken unless somebody suspected it.

A site that is missing them now says so on its Overview, and Repair core files puts them back: it re-downloads that version’s core, and puts back only the missing theme files, each checked against wordpress.org’s MD5 and written only where nothing is. The database, wp-config.php and your own files in wp-content are not touched. rex doctor has a WP core line naming every affected site. The check needs wordpress.org’s file list, so offline it says nothing on the Overview and not checked in rex doctor — never “clean”. Details on WordPress.

OpenLiteSpeed, a third per-site server — macOS and Linux

Section titled “OpenLiteSpeed, a third per-site server — macOS and Linux”

rex site server <domain> openlitespeed, or the New Site dialog’s OpenLiteSpeed (.htaccess + LSCache). It reads .htaccess, ships the LSCache module on — so the LiteSpeed Cache plugin behaves as it does on a LiteSpeed host — and sends .php to the site’s shared PHP pool. It is rexenv’s own build, 1.9.3, on loopback ports 8400–8499; no macOS binary existed to ship. There is no Windows build, and Windows refuses it at create and at switch:

OpenLiteSpeed has no Windows build — it exists for macOS and Linux only. Choose Nginx for this site.

Two limits, both on Web servers: OpenLiteSpeed reads a .htaccess once, so rexenv restarts the site when the root one changes, and a subdirectory edit needs rex site restart; and an environment variable whose value contains both ' and " cannot be passed, so it is refused for an OpenLiteSpeed site.

A switch that cannot happen is refused before it downloads

Section titled “A switch that cannot happen is refused before it downloads”

Switching a Windows site to OpenLiteSpeed, or a Linux site to Apache, used to start the download first — and fail with “1 of 1 downloads failed — fix the connection …” and a failed row in the download panel, instead of the sentence saying the server does not exist on that system. The refusals now come first.

macOS: no more “Background Items Added” pile-up

Section titled “macOS: no more “Background Items Added” pile-up”

Every Start all over a running edge unloaded and re-registered its LaunchDaemon, and macOS posts a “Background Items Added” card for each registration — eleven were counted on one test machine. An edge whose files have not changed is now restarted in place, and the daemon’s keep-alive is a switch that Stop all lowers and Start all raises, so a Stop all/Start all pair registers nothing.

Stop all also stops an edge that was still coming up after a boot. It used to ask only what the app believed, and the app believed it was stopped, so rex stop said done while the edge kept serving.

Windows: a silent setup.exe /S replaces the running copy

Section titled “Windows: a silent setup.exe /S replaces the running copy”

Run over an installed rexenv, setup.exe /S exited 0 and left the old rexenv.exe in place — the DNS agent is rexenv.exe too, always running, and the silent installer skipped the locked file without a word. Now a silent install ends the agent’s task and waits for every rexenv.exe of yours to exit, replaces the files, and starts the agent again on the new binary; the app is not reopened, because silent means silent. A file that still cannot be written fails the install with a non-zero exit code instead of being skipped.

The uninstaller closes rexenv first — a running app put the agent’s task straight back — then ends and deletes \rexenv\dns-agent, so the resolver no longer outlives the app. The .rex rule and the certificate still need Remove system changes in the app; the uninstaller says so. See Uninstall.

Adminer’s import cap reaches PHP on Windows. It was sent in a form the Windows PHP pool ignores, so a large dump was cut by PHP at the default limit after nginx had let it through. It is now also written as a .user.ini in Adminer’s folder, which every OS’s PHP reads.

Linux: a route from an older rexenv is not a missing one

Section titled “Linux: a route from an older rexenv is not a missing one”

Updating from 0.8.10 opened onboarding’s Welcome screen over existing sites, reporting the resolver missing while .rex still resolved — the new build did not recognise the old build’s route as its own. A route that still routes now counts as installed, whoever wrote it, with a notice: “Your .rex route was set up by an older rexenv. It works — re-apply it once (one administrator prompt) so this version’s route is in place.” It shows under rex status’s DNS line and in Settings with a Re-apply button; rex tld --repair rex does the same. The update itself never prompts for it. See DNS not resolving.

  • The update card gave one download two sizes — the header in 1024s, the sentence under it in 1,000,000s. Both use 1024 now.
  • A serving site with an empty log was told to “start the site to see logs here”. It now reads “No recent activity yet — new lines appear here as they are logged.”; the start hint is only for a site that is not serving.
  • relaunch.log. After an in-app update, the helper that reopens rexenv writes what it waited for and how the launch ended to relaunch.log in rexenv’s logs folder. A reopen that did not happen used to leave no trace anywhere.
  • rex status | head — any rex command whose reader closes the pipe early — ended in a Rust panic on stderr. It now ends quietly, exit 0.

The fixes the 0.8.10 release week found, on all three systems; a signed apt repository on Linux — and the first release published from rexenv’s own source repository, which is now public.

The source is public, and the releases come from it

Section titled “The source is public, and the releases come from it”

rexenv/rexenv went public on 30 September 2026, under the Apache License 2.0, and this is the first release drafted there instead of on the Homebrew tap. The downloads lived on the tap until now for one reason: the source repository was private, and a private repository’s release asset answers 404 to the unauthenticated request that both a browser and brew make.

Nothing changes for an installed copy. The in-app update reads the same signed descriptor as before; what moved is the download address inside it, and every build since 0.6.0 accepts both hosts — so 0.8.10 updates to this release from the new one, and a descriptor published before the move still verifies. The Homebrew cask and the one-command installers stay in the tap and fetch from the source repository. 0.8.8, 0.8.9 and 0.8.10 are mirrored there byte for byte; older releases stay on the tap. Bug reports go to the same repository’s issues.

rexenv has a signed apt repository, https://rexenv.github.io/apt, and the one-command installer uses it: it adds the repository — the key travels inside the script — and installs the rexenv package from it, so later versions also arrive with sudo apt upgrade. That is the same package the in-app update installs, so the two never disagree about what is on the machine. The repository carries the three newest releases for amd64 and arm64, and apt refuses one signed by any other key.

Two edges, both handled by the script rather than left to you. The repository is published right after a release, so while it lags a brand-new one the release’s .deb is installed directly and the repository stays configured. And an installed rexenv that has no source line gets the line added and nothing else — the package is not touched. Install has the four lines to set it up by hand, and the key’s fingerprint.

Every OS: a site being created says “Setting up”

Section titled “Every OS: a site being created says “Setting up””

For the whole of a first install, the row behind the New Site dialog read setup incomplete with a Retry button — the list said the setup had failed while the card in front of it was still installing. The row is saved when the job starts and marked finished only when it ends, and the list read that mark alone.

It now asks whether a job is running for that site, whoever started it — the dialog, rex site create or an agent. Running: an amber Setting up, and no Retry. The badge and Retry appear only for a job that died or was cancelled. Not answered yet: neither, so the row never flashes “failed” first.

From Git: the PHP requirement is read before Composer runs

Section titled “From Git: the PHP requirement is read before Composer runs”

Cloning a repository that needs a newer PHP than the site runs used to fail inside composer install, with the reason two screens away in the log. rexenv now reads the cloned composer.json’s require.php first and says which version to switch to:

this repository’s composer.json requires PHP >=8.4.1, and this site runs PHP 8.3 — composer install would refuse it. Nothing was installed; switch the site’s PHP version to 8.4 or 8.5 (Site → Settings) and Retry.

A constraint it cannot read never refuses — Composer’s own check still runs, and its refusal now names the wanted and the running version too. There is no hint before the clone: the requirement is in a file, and the file is not there until the repository is.

A stopped stack: the WordPress and Database tabs say so

Section titled “A stopped stack: the WordPress and Database tabs say so”

With the stack stopped, a site’s WordPress tab spun Loading plugins… for as long as WP-CLI took to give up on a database that was not running, and its Database tab showed Adminer’s address above a blank frame. Both now name the service that is down — MySQL is stopped, or rexenv’s web services are stopped — say in one clause what the tab cannot do without it, and carry a Start all button. Nothing is asked of a service that is down, and the tab comes back by itself once it is up.

rex site list and rex site info say which kind of “not serving”

Section titled “rex site list and rex site info say which kind of “not serving””

Both printed serving about a site whose setup never finished — no server block, no certificate — because they read what the stack believed rather than what answers. The list’s STATE column now has four words, because they have four fixes: serving, stopped (you stopped it), incomplete (setup never finished) and down. site info gives one line per case, with the fix in it — blocked — another server answers :443, not rexenv’s edge (rex doctor).

Watchdogs that reported what had not happened

Section titled “Watchdogs that reported what had not happened”
  • “DNS stopped unexpectedly — restarted automatically”, about a resolver that was answering throughout. The check was one datagram with half a second to reply, and the first miss restarted the agent; a busy machine answers late. It now asks up to three times, and restarts only after two silent polls in a row — about twenty seconds.
  • “Another local proxy” on port 443 that was not there. One missed probe is exactly what rexenv’s own edge reload produces, so every rex site create could raise the toast and clear it again. The edge is called blocked only after two consecutive misses.
  • “Removed outside the app”, twenty seconds after Remove system changes removed the edge itself. The step now tells the watchdog what it did.
  • “The HTTPS edge needs Start all” at login, on macOS and Linux, while the edge’s own boot service was merely still starting — the rough edge 0.8.10 left open. Login-start now waits up to two minutes for an edge the system is already bringing up, and asks for a click only when nothing is going to start it.

macOS and Linux: a power cut no longer leaves an empty service file

Section titled “macOS and Linux: a power cut no longer leaves an empty service file”

Found on Ubuntu: Start all, a power cut about thirty seconds later, and /etc/systemd/system/rexenv-edge.service came back zero bytes — and systemd reads an empty unit as masked, so the edge never started again until the next Start all. Whatever a privileged step writes is now flushed to disk before its prompt returns, and the boot and login files rexenv writes itself go through a write that survives the same cut. Windows is unchanged: the registry, the certificate store and Task Scheduler are the system’s own durable stores.

  • Redis, MariaDB, Apache and Xdebug install without the Xcode Command Line Tools. They come as Homebrew bottles whose library paths must be rewritten, and that step was install_name_tool — which is the tools, so on a Mac that had never seen a compiler all four failed. rexenv rewrites those paths itself now. What is proven: the rewrite on a real bottle library, checked against Apple’s own tools. What is not yet: the four installs on a Mac with no tools at all. Cloning from Git still needs git, which on macOS arrives with those tools.
  • Replacing rexenv.app in Finder no longer produces “rexenv quit unexpectedly”. The .rex resolver is relaunched the instant it exits, including halfway through the copy, and macOS kills a program started from a half-copied bundle. It now waits until the new copy verifies before it starts from it. Login Items lists the resolver as rexenv.
  • “Bootstrap failed: 5: Input/output error” on the first Start all after replacing the app, which left the edge down until a second Start all. The install now waits for its own removal to finish before it installs again.
  • A copy macOS is running from a temporary path is not recorded. An app that was unzipped or copied rather than dragged from the disk image runs from a read-only path that disappears when it quits; rexenv refuses to write that path into its resolver’s or login item’s definition — “Move rexenv.app to Applications and relaunch; DNS is served in-process until then.”
  • A second account never takes the .rex resolver task from the first. The \rexenv\dns-agent task has one name per machine, and registering it again overwrote it silently — so a second administrator’s launch moved it to their own sign-in. The launch now refuses, names the holder, and answers .rex itself while it runs: “rexenv is one account per machine, so this launch will not take it over”.
  • A login start stays a login start when rexenv has to relaunch itself through Explorer to leave a job that confines it — a launcher, a scheduled task. Explorer passes no arguments, so that launch used to arrive as one you made: a window, and no Start all.
  • Re-applying the .rex route keeps the rexenv0 link. Every re-apply deleted and re-created it, and systemd-resolved’s late clean-up of the old link could take the new one’s settings with it: measured, 3 of 50 re-applies came back with no DNS scope, and .rex stopped resolving. The link now stays and only its DNS settings are reset; removing it is the uninstall’s job. And the route counts as installed only while systemd-resolved is actually routing through it — a link that had lost its server used to read resolver installed.
  • The in-app update’s password dialog names the update: “rexenv needs administrator permission to install the update it downloaded — the new rexenv package.” The sentence comes from a file the package installs, so the update to this release still shows the setup sentence, and the one after it shows the new one.
  • A certutil that is there but cannot run says so, with the reason and the fix — sudo apt install --reinstall libnss3-tools — instead of “io error: Permission denied (os error 13)”.
  • “WebKitWebProcess closed unexpectedly” after an update — a mitigation, not a fix. The app’s windows are now closed before the process exits. The report showed on 1 of 2 updates and on none of 4 plain quits, and the change sits in the side that is exiting, so the update to this release can still show it. Whether it is enough is not known yet.
  • Adminer can go back. Once a newer Adminer is chosen, its card offers Back to the version this rexenv ships — the same download-and-check as the update, in the other direction.
  • An update published for another operating system is never offered, downloaded or accepted. Until now that rested on the publisher alone.
  • A new sub-site on a subdomain multisite is created as https://, like the main site. Existing sub-sites are not rewritten.
  • rex site create shop.rex names the site shop, as the dialog does, rather than the whole domain.
  • A plugin that is a single file with no folder — Hello Dolly — no longer offers a terminal in a folder it does not have.

No database migration since 0.8.10.

One login setting instead of two, databases that come back after an unclean reboot, and the fixes the Linux and macOS test runs found after 0.8.9.

Every OS: “Start rexenv at login” is one toggle

Section titled “Every OS: “Start rexenv at login” is one toggle”

Until now the wish my sites are back after a reboot was split across two switches — Open rexenv at login and Start services when rexenv opens — and either one alone did something you did not mean: only the first on gave an idle app at sign-in, only the second on started the whole stack every time you opened the window by hand.

There is one toggle now, in Settings → Services. Its description is each operating system’s own sentence; on macOS:

rexenv opens in the menu bar when you sign in to your Mac (a macOS login item) and runs Start all — your sites come back after a reboot without a click. Never downloads or prompts at login.

On Windows it names the notification area and a startup app, which Task Manager’s Startup tab can also turn off; on Linux the system tray and an autostart entry in ~/.config/autostart. Opening rexenv yourself no longer starts anything. The second setting is retired and swept from the database at launch, and rex config set start_services_on_launch is refused like any key nobody has ruled on. If you relied on it, turn on Start rexenv at login.

The login start survives a race the drafted release lost: with macOS’s Reopen windows when logging back in on — the restart dialog’s default — macOS relaunched rexenv first, without the login flag, and the login item’s launch found it running, handed off a plain “open” and exited with the stack down. The handoff now carries the login, and the running copy runs Start all instead of raising its window. One rough edge is open: a login a few seconds after boot can show “the HTTPS edge needs Start all” while the edge’s own boot daemon is merely still coming up; it adopts the edge by itself within about a minute, and the sentence is wrong to ask for a click.

macOS and Linux: MySQL and PostgreSQL start after an unclean shutdown

Section titled “macOS and Linux: MySQL and PostgreSQL start after an unclean shutdown”

A killed server — a reboot that ended it with SIGKILL, a hard power-off — leaves mysql.sock.lock or postmaster.pid behind with its old process id inside. After the boot that id belongs to some other program, and both servers ask only whether the id exists: PostgreSQL refused with “lock file already exists — is another postmaster running?”, mysqld aborted with “Unable to setup unix socket lock file”, the health watchdog respawned each three times and gave up, and every site stayed down until two files were removed by hand.

A start now asks the stricter question — is that process id alive and is its command line the server that wrote the file? — and removes the lock (and MySQL’s socket) when it is not. A live server’s lock is never touched, so a start on a server that really is running still fails the honest way. The log says what was removed and why: removed a stale lock before starting — pid 2009 is now /usr/bin/rexenv, not the server that wrote it.

Linux: one password dialog for setup, one for Remove system changes

Section titled “Linux: one password dialog for setup, one for Remove system changes”

polkit keeps no grant between rexenv’s dialogs, on purpose — so every root step was its own dialog unless the steps were one command. First-run setup asked twice, two minutes apart: the .rex route, then the certificate authority’s system-store file. The two ride one dialog now, and the store’s leg is skipped when the store already holds the certificate. Remove system changes had the mirror image — a second dialog for the system-store file alone, and until it was answered curl and PHP still trusted the CA. It is one dialog too. First run and Uninstall now count two prompts and one.

WordPress from a repository: Bedrock sites finish and serve

Section titled “WordPress from a repository: Bedrock sites finish and serve”

The first real Bedrock provision — roots/bedrock through New site → WordPress → From Git — cloned, installed core through Composer, wired .env and ran wp core install, and then failed setting the admin password: that step spelled its --path by hand and pointed at web/, and WP-CLI said “This does not seem to be a WordPress installation.” The salts stayed generateme and the site never served. Fifteen places built that flag by hand; there is one builder now, resolving the core under web/wp as the install step already did. rex wp on the same site had failed one level up — the manager was handed the project root where the vhost serves web/ — and works on the served root.

  • A re-run of setup on an install that already has sites — an update whose route script went stale reaches the onboarding pages too — ended with Create your first site over three existing ones. The finish page reads the sites count: Open your sites, and “rexenv will serve your 3 sites instantly”.
  • A build that cannot check for updates at all said “couldn’t reach the update server” when nothing had been reached. It says why instead — on Linux, a copy that is neither the .deb install nor an AppImage: “This rexenv is not a .deb or an AppImage install, so there is no update descriptor for it — nothing here says whether a newer rexenv exists.”

No database migration since 0.8.9. The .deb update from 0.8.9 comes back by itself, as 0.8.9 promised; Ubuntu may put up a “WebKitWebProcess closed unexpectedly” report over the new window at that moment — it is the old app’s web view dying as it exits, not the update failing, and it is open work.

The fixes the first Linux release’s own test runs found — above all, that its .rex route routed nothing.

Linux: .rex is answered by rexenv’s own route

Section titled “Linux: .rex is answered by rexenv’s own route”

0.8.8 sent .rex through rexenv0, a dummy network link with .rex as its only routing domain — and gave the link no address. systemd-resolved gives no DNS scope to a link without a real address, so the route sent nothing to rexenv’s resolver. It looked fine on every machine it was tested on, because each of them asked a host that was itself running rexenv — the test VM’s upstream DNS is a Mac, WSL’s is Windows — and those answered *.rex on rexenv’s behalf. On a machine with no rexenv next door, .rex did not resolve.

The link now carries one address, 192.0.2.53/32 — from TEST-NET-1, a range reserved for documentation that no real network uses. A route written by 0.8.8 reads as not installed, so after updating rexenv offers its setup step once more; accept it and the route is rewritten. The check that had passed now asks systemd-resolved which link answered, not only what the answer was. First run has the whole route.

Linux: the app comes back after an in-app .deb update

Section titled “Linux: the app comes back after an in-app .deb update”

The update installed, and then the app stayed closed: the relauncher was started from the running binary, which dpkg had just replaced — the kernel reports it as /usr/bin/rexenv (deleted). It now starts from the new /usr/bin/rexenv. The fix lives in the side of the swap that is already running, so 0.8.8 → 0.8.9 still needs opening by hand once. An AppImage was not affected.

Every OS: a refused update says it was refused

Section titled “Every OS: a refused update says it was refused”

Two fixes to the same card:

  • Each update document keeps its own replay record. One record served every descriptor, and the number in it only moves up — so a copy that had once read another platform’s document, as early Windows builds and development .deb packages did, then refused its own platform’s releases as replays, indefinitely.

  • A refusal is no longer reported as a network failure. The card read “couldn’t reach the update server” for all of it; a Windows machine showed that sentence for a week while it was in fact refusing every release. It now says:

    The update server answered, but rexenv refused what it published: the reason. Nothing here changes until a descriptor this copy accepts is published.

Windows: helper commands no longer come back empty

Section titled “Windows: helper commands no longer come back empty”

The step that stops services inheriting rexenv’s own handles ran beside other threads while they were starting processes, and cleared the output pipes those had just made — so a command rexenv ran at the same moment could exit having printed nothing. It now runs once, as the process starts, before there is a second thread.

The first release with Linux, and the first built entirely by CI — every platform from one tag.

Ubuntu 22.04 or newer, x86_64 and arm64, as a .deb or an AppImage, each with a .sha256 beside it. 22.04 is the first LTS with the WebKitGTK the app’s window needs, and the release is built on it. Install and First run have a Linux tab each.

  • DNS through a dummy network link, not a systemd-resolved drop-in. The drop-in — the obvious copy of macOS’s /etc/resolver file — was tried first, and it sent every name on the machine to rexenv’s resolver. A link whose only routing domain is .rex sends nothing else.
  • The local CA in both stores. Browsers read their NSS databases and curl and PHP read the system store, so rexenv writes both — including the snap Chromium’s own database and every Firefox profile’s, because a snap cannot see your ~/.pki. CA trusted in Settings means both hold the current CA.
  • polkit for every system change, through your desktop’s own password dialog. From the .deb it names rexenv and what it is about to change; the package depends on pkexec | policykit-1, because Ubuntu 26.04 has no policykit-1 package.
  • In-app updates: the .deb through dpkg behind one password dialog, an AppImage swapped in place with none. Each package kind and architecture has its own signed descriptor.
  • PHP 7.4 is rexenv’s own static build, for both architectures, as on macOS.
  • MySQL and PostgreSQL start on 24.04 and 26.04, where Ubuntu renamed libraries they load (libaio1t64, libxml2-16): a private symlink under rexenv’s own data folder bridges the old name, and the .deb declares every library either engine needs.
  • Not in this build: Apache, Xdebug, Redis and MariaDB, each refused with a message naming Linux. FrankenPHP ships.

The universal disk image, the Windows installer and update zip, and four Linux files now come from one tag on GitHub Actions — one draft release with every artefact, or none. Nothing in a release is built on a developer’s machine any more.

The pin a new machine downloads moved to 6.1.1, the version the update path was already offering — until now a fresh install got a database console one release behind.

rexenv runs on macOS 13 and 14. And the .rex resolver stops second-guessing you.

Until now the floor was macOS 15, because one pinned binary needed it. From this release the app runs on macOS 13 (Ventura) and 14 (Sonoma) — with a different pin for the components whose current build does not load there, and an honest refusal for the two that have no build at all.

What decides it is the macOS the app is started on, worked out at every launch and never stored. On 15 or later nothing changes: every component at its newest version. On 13 the app uses MySQL 8.4.3 / 8.0.40, MariaDB 12.0.2 / 11.4.8, Redis 8.2.1, Apache 2.4.65, Xdebug 3.4.5 (PHP 8.1–8.4 — none for 8.5) and cloudflared 2025.4.0; PostgreSQL and PHP 8.0 are not available there. On 14 only two rows differ from 15: cloudflared 2025.4.0, and PostgreSQL 16.4.0 as the one version. Every number is on Pinned versions, printed on the row.

The first launch on such a Mac says so once, in the wizard:

This Mac runs macOS 13.6 — rexenv works here with older versions of some components and without PostgreSQL and PHP 8.0. Everything is available on macOS 15 or later.

After that, anything not available stays listed with the reason wherever you could choose it — the Databases page, the New Site dialog, rex db versions, the agent tools — all with the same sentence:

Needs macOS 14 or later — this Mac runs macOS 13.6.

Upgrading the Mac later moves you forward on the same data. A 13 → 15 upgrade was done on the test machine before shipping: the next launch started MySQL 8.4.6 on the database 8.4.3 had written (MySQL’s own in-place upgrade ran), MariaDB 12.3.2 on 12.0.2’s, Redis 8.8.0 on the old snapshot, and PostgreSQL and PHP 8.0 became ordinary rows. Nothing moves the other way.

The update path knows about the floor too: each release descriptor carries the macOS it needs, and the updater checks it against the running Mac before installing anything.

The .rex resolver: two things it no longer does

Section titled “The .rex resolver: two things it no longer does”

The resolver that answers .rex runs as a small background agent so your sites keep resolving after you quit the app; the app watches it and steps in if it dies. Two cases where that watch was wrong, both found on the release’s own test runs:

  • It stopped calling a working resolver “down”. When the app handed the port back to the agent and the agent took it a moment after the app last looked, the app latched DNS: down and never looked again — .rex resolved fine for two hours while Settings and rex status said otherwise. A resolver the app has given up on is now re-checked every few seconds and adopted the moment it answers.
  • It stopped resurrecting a resolver you removed. After Settings → Remove system changes, the watch read the uninstalled agent as a crash: three “DNS stopped unexpectedly — restarted automatically” toasts, and then the app quietly served .rex itself — the thing you had just asked it to stop. A removal is now a state the watch leaves alone; rex status reads DNS DOWN (removed) and nothing answers the port.

Browsers check what you type against the public list of top-level domains before they look anything up, so a bare acme.rex went to the search engine while acme.rex/ opened. Firefox has a per-suffix setting for exactly this; rexenv now writes it into each Firefox profile when a domain ending is set up, and never flips one you set to false yourself. Chrome, Safari and Edge have no such setting — for those, Settings and the site page say what works: add a slash, acme.rex/, or https://.

Windows fixes, and a new Adminer on fresh installs.

A Windows release build has no console, so every command-line helper it started got a console window of its own. All 22 of them — the database clients and dumps, WP-CLI, nginx -t, pg_ctl — were written on macOS, where this never happens. On 0.8.4 and 0.8.5, Start all left an empty terminal titled …\pg_ctl.exe on the desktop with PostgreSQL attached to it: close it and the database stopped. Tunnels left one titled …\php.exe.

Every helper now starts hidden, and a source check in rexenv’s own test suite refuses any helper started the old way.

Windows: the installer says what is running

Section titled “Windows: the installer says what is running”

Installing over an installed copy used to say “rexenv is running! Click OK to kill it” — even to someone who had quit the app, because the .rex DNS answerer is the same rexenv.exe. It now says:

rexenv is still running: the app, or the small background resolver that keeps your .rex sites answering after you quit.

Click OK to stop it and continue. The resolver starts again on its own once the new version is in place, and your sites and databases keep running.

The answer is the same as before: OK.

Windows: a missing DNS task is registered again

Section titled “Windows: a missing DNS task is registered again”

The \rexenv\dns-agent scheduled task is what starts the .rex answerer at logon. When it went missing, the health check could only log that it failed to restart it — and .rex would have been dead at the next boot. rexenv now registers it again from its own copy of the definition it wrote. What removed the task in the first place is still not known.

The pin was 5.4.2 while the update path offered 6.1.0, so a fresh install got a console eight releases older than an updated one. Both now get 6.1.0. See Pinned versions.

Two Windows bugs that 0.8.4 shipped, both found on the machine they were reported from within minutes of that release, and both invisible to every check that does not run a real cluster in a real window.

Windows: rexenv stopped killing its own PostgreSQL

Section titled “Windows: rexenv stopped killing its own PostgreSQL”

0.8.4 started the server through pg_ctl, which detaches: the app was left holding a pid and no child, and it recorded that as a process it had merely adopted. An adopted process is never “starting” — so while pg_ctl -w was still taking its seconds to return, the health watchdog fired, decided the engine was dead, tried to respawn it, and the port check refused the app’s own server:

[restart-failed] PostgreSQL: port 15432/tcp … already in use by postgres.exe (pid 2064)

The cluster was serving the whole time; the Databases screen showed it down. The handle now carries the pid and the moment rexenv spawned it, which is what marks the server as ours — so the startup grace applies and the app is allowed to stop what it started.

Windows: the Database Browser renders Adminer

Section titled “Windows: the Database Browser renders Adminer”

0.8.4 fixed the header Adminer sends. Necessary, not sufficient — two more things had to be right, and each one alone was enough to leave the panel blank with nothing logged:

  • rexenv’s own policy blocked rexenv’s own frame. The app’s CSP named frame-src rexdb:, but on Windows the same handler is served at http://rexdb.localhost/, which that directive does not match. Both spellings are named now — connect-src had already carried http://ipc.localhost for exactly the same reason.
  • The frame’s address was the macOS one. The src was built as rexdb://localhost/?… on every platform. WebView2 does not know the scheme form, so the URL resolved to nothing and the frame never asked for anything at all — the nginx log recorded no request for the click. The origin is now a platform fact, beside the path separator.

An update no longer closes the app behind your back

Section titled “An update no longer closes the app behind your back”

Installing an update ended in the app quitting itself, the instant the swap landed. It now installs, stops, and asks: a one-button dialog names the version and says what a restart costs — sites, databases and DNS keep running, open terminals and running jobs do not — and the app closes only when you press OK. Dismiss it and the new version starts the next time you open rexenv; the build is on disk either way.

See Updating.

Windows: PostgreSQL starts on a machine with UAC turned off

Section titled “Windows: PostgreSQL starts on a machine with UAC turned off”

It never had. postgres.exe refuses to run under an Administrators token —

Execution of PostgreSQL by a user with administrative permissions is not permitted

— and with UAC off every process on the machine carries one. The engine died on every start, the watchdog respawned it three times and gave up, and all the screen said was PostgreSQL did not start within 15s. initdb survives the same machine because it re-executes itself under a restricted token; postgres.exe has no such code, and pg_ctl is where PostgreSQL keeps it. So rexenv starts and stops the server through pg_ctl — the way PostgreSQL’s own tooling does — rather than reimplementing the token dance.

Three separate things then had to be right before that worked on a real machine, each found by running it rather than by reasoning about it: the spawn needs an explicit working directory (inheriting one gave “The current directory is invalid” before a single setting was read), the app must wait on the process’s status rather than its output (the detached server inherits the pipes, so waiting for EOF hung the start for three minutes with a healthy server listening), and pg_ctl must write its own log file (both pointing at one file failed every start with “The process cannot access the file because it is being used by another process”).

Windows: the Database Browser stopped rendering empty

Section titled “Windows: the Database Browser stopped rendering empty”

Adminer’s wrapper bakes a frame-ancestors list for the direct HTTPS vhost, and that list was replayed unchanged through the rexdb: proxy — where the parent is the app itself, served at http://tauri.localhost on Windows. The engine refused to frame Adminer, silently. The proxy now substitutes the origins rexenv’s own webview is served from and touches nothing else in the header; the direct vhost keeps its strict list.

Two more causes survived into this release and were fixed in 0.8.5 above: on Windows the panel was still blank after this change.

Section titled “A link in an HTML email does nothing when you click it”

Every link, in every HTML mail, for as long as the Mail screen has existed. The preview frame carried sandbox="" — which reads as maximum isolation and, in the same breath, switches off top-level navigation and popups. The mail rendered perfectly and was inert.

The obvious repair is wrong too, and it was measured rather than reasoned about: sandbox="allow-same-origin" lets the app read the frame’s document but WebKit still never delivers the frame’s clicks to the parent, so the link navigated the preview instead of your browser.

So the isolation moved into the document, where the browser engine enforces it whatever the parser misses. Before a captured mail is shown, rexenv prepends a Content-Security-Policy meta as the first child of <head> — script-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none' — then sweeps script, iframe, object, embed, base, form and meta refresh, strips every on* attribute and every javascript:, vbscript: and data:text/html URL, and drops anchor targets so nothing can ask for the app’s own window. Styles and images stay: that is what makes an email look like the email.

The frame is then unsandboxed, and every click in it is cancelled first — link or not, the preview never navigates. An http or https link opens in your preferred browser, through the same path as every other external link in the app (the OS handler when you have no preference set). Anything else is named rather than opened, because an email is the one piece of HTML in this app that a stranger wrote:

That link isn’t a web address, so rexenv left it alone: mailto:someone@example.test

See Mail.

After an in-app update, Apps & Features shows the new version instead of the old one. The swap, the relaunch and the sweep of the previous copy had all worked; the uninstall entry was rewritten with the version the update came from, freshly — measured on the 0.8.2 → 0.8.3 update, where it moved to 0.8.2, and on the 0.7.9 → 0.8.2 update before it, where it stayed at 0.7.9. The cause is that the version was read back off the executable after the rename, and a path that has just been renamed answers for the file that used to be there.

It is now read from the staged executable before anything moves — the same file the staging check has just verified — and a read that yields nothing is logged rather than passing in silence. The entry is only ever rewritten when it already exists: a dev build or a fixture swap has none, and creating one would put a half-filled rexenv row into a developer’s own Apps & Features. A failure to write it is logged, never fatal — the app is already swapped by then, and a stale number in a settings page is not worth failing an update over.

On every PHP version and both platforms:

Setting rexenv PHP’s compiled default
memory_limit 1G 128M
upload_max_filesize 5G 2M
post_max_size 8G 8M
max_input_vars 5000 1000
max_execution_time 60 30

These are written into every pool config, which is the whole of the change: a key you had not set was previously left to PHP’s compiled value, and 128M / 2M are the numbers every WordPress import trips over. max_input_time stays -1, meaning “use max_execution_time”.

Your own value still wins — the defaults fill in only the keys you never touched — and they reach every pool rexenv starts after the upgrade, so existing sites get them with no per-site action. Leave a field empty in Settings → Services → PHP versions → ini settings and the default is what the field shows you as its placeholder.

nginx follows rather than becoming the new limit: its global client_max_body_size is derived from the same defaults — the larger of the two body numbers, so 8G — and a version with a stored value gets a per-site limit tracking that instead. Neither can 413 a body PHP would have accepted.

Windows: the in-app update swaps files, not the folder

Section titled “Windows: the in-app update swaps files, not the folder”

0.8.1 staged a whole new install directory and renamed the installed one aside, the way the macOS build renames rexenv.app. The first real in-app update is where that broke (0.7.9 → 0.8.1, on a clean VM and on the Dell alike): the install directory is also the root of the app-data tree — the database, the logs and every downloaded binary live under it — and a directory with an open handle anywhere beneath it cannot be renamed. It failed with the folder is not writable, while the fixture, whose install directory held nothing open, had passed.

A running .exe can be renamed, and the process keeps running from the renamed file — the measurement the design rests on. So the swap is now a rename pair per file: each installed file moved aside, the new one moved in, every move undone if one of them fails, and your data tree never touched. Nothing is deleted; the previous copy stays until the new app has launched and confirmed its own version. uninstall.exe is written by the installer rather than by the build, so it is copied in from the installed directory before the swap.

What the per-file window costs: there is a real moment when the directory holds a mixture, so the .rex DNS answerer is re-launched afterwards rather than expected to survive it. A per-machine install is still refused by name, with the per-user reinstall named — never a UAC prompt.

Windows only. Every file this release changes is under src-tauri/src/platform/windows/, so nothing rexenv does on macOS is different — if you are on a Mac there is nothing here you need.

The in-app updater called your own install somebody else’s

Section titled “The in-app updater called your own install somebody else’s”

On Windows, if your account is an administrator, 0.8.0’s Settings → About → Updates refused to update and said:

C:\Users\<you>\AppData\Local\rexenv belongs to another account, so rexenv could not clean up after replacing it. Have that account update rexenv, or take ownership first:

$ takeown /R /F C:\Users\<you>\AppData\Local\rexenv

The folder was yours. The installer put it there under your own account and no UAC prompt — it is a per-user install. What went wrong is a Windows detail rexenv had not accounted for: the owner Windows stamps on a file is the token’s default owner, and for an account in the Administrators group that is BUILTIN\Administrators (S-1-5-32-544), not the account itself. rexenv compared the install directory’s owner against the user’s SID alone, so an admin account’s own install came out foreign and the refusal fired.

It now accepts either: the user’s SID, or the SID that account’s token stamps as owner — read from GetTokenInformation(TokenOwner). Measured on a clean Windows 11 VM, where %LOCALAPPDATA% and the install directory the installer made were both owned by Administrators.

Nothing else in the refusal changed: a genuinely foreign install, an unwritable parent, a per-machine install and a disk without room are all still refused, and none of them has ever turned into an admin prompt.

The first release with a Windows build, and the release that made rexenv installable on a Mac that has never had Xcode’s command line tools. It is also the first that imports sites from Local.

A clean Mac could not install a single component

Section titled “A clean Mac could not install a single component”

Preparing a downloaded binary needs to know what it links against. rexenv asked otool -L. /usr/bin/otool is a Command Line Tools shim: on a Mac that does not have the tools it pops the “install developer tools?” dialog and exits non-zero. Every development machine has them, which is exactly why 0.7.0 through 0.7.2 shipped with it and nobody noticed — it took a clean-VM test to find.

rexenv now reads the Mach-O load commands itself, which needs nothing installed. The one thing that still wants the tools is unchanged and was never part of this: the Homebrew-bottle bundles — Redis, MariaDB, Apache and Xdebug — whose dylibs must be relinked with install_name_tool. Those fail with a message naming xcode-select --install.

Trusting a new CA now untrusts the old ones

Section titled “Trusting a new CA now untrusts the old ones”

Every fresh app-data folder mints a new local CA. The ones earlier folders trusted stayed in your login keychain as trusted roots for keys that no longer exist — four of them on one machine that had been reset by hand. Trusting a CA now sweeps the stale rexenv ones in the same step, while the trust dialog’s authorization is still warm, and Remove system changes removes all of them rather than only the one the current folder holds.

The sweep is best-effort by design: if it fails, the trust still succeeded, and a cancelled sweep never reads as “rexenv could not trust its CA”.

An unrecognised flag used to be dropped, and the command ran with the default the flag existed to override. On site create that quietly built the wrong site. On wp search-replace it was worse: --dry-runn --yes turns a rehearsal into a real replace across the database, with no prompt — the typo removes the dry run and the --yes removes the question.

Every flag-taking command now checks its own list first and stops:

rex: unknown flag `--dry-runn` for `wp search-replace`

It exits 2 and does nothing. See the CLI reference.

rexenv now ships a Windows installer. It is a per-user NSIS install — it lands in %LOCALAPPDATA%\rexenv and asks for no administrator rights to install — and it is unsigned, so SmartScreen shows “Windows protected your PC” and “Run anyway” only appears after you click More info. Windows 11 x64; arm64 is not supported, because the official PHP builds for Windows are x86/x64 only and PostgreSQL’s portable build is x64 only, so there is nothing native to ship.

Five things are deliberately not in the Windows build, and rexenv refuses them by name rather than offering them and failing:

Not on Windows Why
Apache would mean Apache Lounge — a third-party trust decision nobody has made
FrankenPHP no Windows pin yet
Xdebug its DLLs must match each PHP’s NTS build and compiler exactly
Redis no official Windows build exists
MariaDB no Windows pin; MySQL 8.4 and 8.0 both ship, so there is a database either way

First-run asks twice on Windows where macOS asks three times — one UAC prompt for the .rex NRPT rule, and Windows’ own certificate dialog for the local CA, which goes into your user’s Root store and never the machine’s. There is no third prompt: the bundled Caddy binds 80 and 443 on 127.0.0.1 unelevated, which also means no firewall alert.

The import screen — now its own Import page in the sidebar, no longer a card under Settings → DNS & SSL — reads Local (WP Engine’s, formerly Local by Flywheel) beside Valet and Herd. As with those two, Local is only ever read: its folders are linked in place, its databases are copied, and none of its servers is started or stopped by rexenv.

What is different about a Local site:

  • .local is re-homed. rexenv refuses .local — macOS uses it for Bonjour — so shop.local imports as shop.rex, and the row says was shop.local. If that name is taken, the row asks for another rather than choosing one for you.
  • A PHP version rexenv does not ship asks first. Local pins full versions such as 7.3.5; that row offers a picker and stays unticked until you choose. Valet and Herd rows pinned to an unshipped minor get the same picker.
  • The database comes from Local’s own server, over its socket. Local runs one per site, and only while that site is started in Local; a stopped one fails with Local named, and the fix is to start it there and retry. The copy is named after the site — local_shop_rex — because every Local database is called local. app/sql/local.sql is never used: it is a snapshot from whenever Local last exported.
  • The copy’s URLs move; Local’s do not. http://shop.local becomes https://shop.rex in rexenv’s copy only, through WP-CLI’s serialization-aware search-replace.
  • Connecting is ticked by default. Until its config points at the copy, a Local site reads no database under rexenv at all, so also connect Local sites connects each one in the same run, with wp-config.php backed up first. While a site is connected, Local reads rexenv’s copy too.
  • Networks import as networks. A subdomain or subdirectory multisite moves every subsite to the new name, and connecting it moves DOMAIN_CURRENT_SITE. A network with a subsite on its own mapped domain is listed and not imported.
  • Local’s router is named. When it holds :443, rexenv says so, with Local’s own Router Mode → localhost as the way to free the port without quitting Local.

Import is a macOS screen. The Windows build does not look for another tool’s sites, and says it did not look rather than reporting that nothing was found. Migrating from Valet, Herd or Local has the details.

One WordPress fix, and nothing else. A WordPress site rexenv created before 0.7.2 can be missing core files — about 25 of them on a WordPress release that ships wp-includes/php-ai-client — and nothing said so when the site was made.

rexenv asked WP-CLI for wp core download, which fetches WordPress’s .tar.gz. WP-CLI unpacks that with PHP’s PharData, and PharData read the archive with every file name cut at 100 characters: 40 names came out truncated — …/WithRequestAuthenticationInterface.php as …Interface., a default theme’s …Italic.woff2 as …Italic.wof — and six files vanished where two cut names collided. macOS creates those names without an error, so the site was created and the damage was silent. It surfaced because Windows cannot create them at all.

Every core download rexenv makes is now WordPress’s .zip build, which unpacks with every name intact: a new site, a localized build, and Re-install core. The locale and version are validated before they reach a download URL.

What Re-install core repairs, and what it does not

Section titled “What Re-install core repairs, and what it does not”

It reads the installed version from wp-includes/version.php and forces that version’s no-content build over the site folder. wp-content, wp-config.php and the database are not touched — a plugin of yours and an edit to a default theme both survive it. Three limits:

  • The no-content build carries no wp-content, which is also why it cannot put back a default theme’s cut font file.
  • It leaves the cut-name leftovers (…Interface., …Italic.wof) where they are, and Verify core checksums does not report them: a repaired site can pass and still carry them.
  • A site whose version.php names no release — a beta, say — is refused: cannot tell which WordPress version <site folder> runs — wp-includes/version.php is missing or names no release version, so there is nothing to reinstall it from.

On some WordPress builds the check also lists wp-includes/php-ai-client files as foreign, until wordpress.org’s checksum manifest covers them. That is the tool’s honest output, not this bug — the files that doesn't exist are.

Neither control is on the rex CLI; both are in the app.

Licence notices only. Nothing rexenv does changed: apart from the version number, the one thing that differs from 0.7.0 is the text the app shows under Settings → About → Licenses & credits.

That list was incomplete. In 0.4.0 through 0.7.0 it left out the MySQL client libraries mysql_async 0.37.0 and mysql_common 0.37.3 and the thirteen crates they pull in — all built into the app since 24 August 2026 — and two crates in the rex CLI’s own dependency graph, memchr 2.8.3 and zmij 1.0.23, which had never been listed at all. 0.7.1 names all 17, and its table now counts 410 Rust crates across both Apple Silicon and Intel.

Nothing generated the list, so nothing could notice it drifting: the table and its counts were typed, and the documented command for regenerating it read only the app, never the CLI. The app’s own verification now checks it and fails when the table and the real dependency graphs disagree in either direction — Rust for the app and the CLI on both architectures, the npm packages, and the vendored Composer packages.

A site’s database can be PostgreSQL — for Laravel and blank PHP, never WordPress — and making that true meant rexenv now builds PHP 8.1–8.5 itself. Agents also reach every command the app has, and a site’s folder opens in your own terminal app.

Pick PostgreSQL in the New Site dialog, or rex site create <domain> --db postgres. The dialog offers it only where it would work and says why when it does not; everything else gets the app’s refusal, quoted on PostgreSQL, and a refused create leaves no docroot or certificate behind.

A Laravel site’s .env is written with DB_CONNECTION=pgsql and every other connection key, replacing the skeleton’s SQLite default. A blank PHP starter seeds the same starter_items table through a pgsql: DSN. Start all starts PostgreSQL when a site’s database lives there — before, a PostgreSQL site could come up with nothing listening on 15432 — and a delete drops the database WITH (FORCE), so an open connection cannot keep it alive.

Two things to know before choosing it: the server has one passwordless postgres superuser, so a PostgreSQL site’s credentials reach every database on it; and importing from Valet or Herd still does not bring PostgreSQL data.

The PHP 8.1–8.5 builds rexenv used to pin advertised a PDO PostgreSQL driver they did not have. A connection was accepted and then stalled for about a minute until PostgreSQL closed it — and Laravel’s pgsql driver is exactly that call. rexenv’s own builds carry the real driver and are checked with a real connection, plus module names, function names and configure flags against the builds they replace. The one deliberate difference: Swoole’s PDO PostgreSQL and SQLite coroutine hooks are gone, because they cannot coexist with the real driver.

8.0.30 stays static-php.dev’s build and has no PostgreSQL driver. The 8.2 and 8.3 pins move to 8.2.32 and 8.3.32 — the patches upstream had already moved to — so a machine that had updated to them lands on rexenv’s build of the same version rather than upstream’s. Each rexenv build installs its licence texts beside the interpreter, and a signed manifest may now name php-licenses so an update to a rexenv build brings them too. See PHP versions.

Every command in the app is now reachable by an agent or ruled out by name, and a new command fails the build until someone rules on it. Thirty were closed as new actions on existing tools: the mail catch-all, TLD status and policy, opening a site in your terminal, the stack’s health reads, WordPress releases and WP-CLI packages, clearing logs, a Git watch’s log, engine versions, Adminer, downloads and start-at-login. A half-built site_create now tells the agent why it failed, and site_retry answers with the settled outcome instead of running. The Agent access card shows the level and its duration at a glance, and the activity feed folds away. See AI agents.

A chevron beside every Terminal control — the site’s quick tile, each plugin row and theme card, and the Terminal tab’s toolbar — opens that folder in Terminal, iTerm, Warp, Ghostty, WezTerm, kitty or Alacritty. That shell is your plain login shell, with no bundled PHP and no wp on PATH; the built-in tab is still the one that runs the site’s PHP. See Add plugins and themes.

  • The menu-bar menu has About rexenv, and a menu held open updates in place instead of closing itself a few seconds later.
  • The Sites page opens on All again, rather than switching to Running on its own.
  • A failed update check reads as a sentence — “rexenv couldn’t reach the update server. Check your internet connection and try again.” — with a 6-second deadline for Check now.
  • Deleting or renaming a site removes its job logs. Logs left behind by earlier versions are not swept. A site’s Logs list also stopped offering a neighbour’s Git job output — foo.rex could see foo.rex-2.rex’s.
  • First-run downloads no longer stick at queued when you move past the Install step while they are still running. Upstream has not yet re-run the wizard by hand on a fresh install to confirm it.
  • An administrator or Keychain dialog no longer holds the app up. While one was open, the window could freeze and commands that needed rexenv’s database waited for you to answer.
  • A Laravel site whose create-project failed can be retried. Retry used to read the half-made skeleton as installed and fail forever.
  • Adminer’s update button reads Updating to and the version while it works, instead of collapsing to an ellipsis.

The first version an installed rexenv could reach without you downloading anything. It is a small release by content — Settings → About gained a Changelog row that opens this page — and a large one by proof: 0.6.0 shipped the self-update machinery, and 0.6.1 is the release that machinery actually installed, end to end, on a real Mac.

That ordering is not an accident. An offer is a live comparison — strictly newer, by numeric segment — so the release that ships self-update is the one release that can never use it. The first in-app update any install can perform is 0.6.0 → 0.6.1.

The Changelog link points at this site, not at a GitHub releases page: rexenv/rexenv was private at the time (it went public on 30 September 2026) and its release page was a 404 to anyone not signed in, and the tap’s releases page is an artefact list rather than an account of what changed. Pointing at the site is what keeps the link working when the repo’s visibility changes.

Also in this release: the DNS agent reports which build it is, so a stale agent left running by an earlier version stops being invisible when you are trying to work out why a name does not resolve.

The release that gives every later release an update path. rexenv can now update itself: it checks a signed descriptor, offers the new version, verifies its signature and checksum, replaces rexenv.app in one atomic step, then quits and reopens on the new version.

The consent sentence sits above the button, in the order the steps happen:

Downloads rexenv 0.6.1 (29 MB), checks its signature and checksum, replaces rexenv.app in one step, then quits and reopens on 0.6.1. Your sites, databases and DNS keep running throughout — services outlive the app. Open terminals and running jobs close with it, exactly as they do when you quit.

The staged copy is verified before anything is exchanged — version, bundle identifier, executable name, the rex sidecar, both architectures present in every Mach-O, and codesign --verify --deep --strict. The exchange itself is a single atomic swap, so no instant exists where the install path holds nothing. Nothing is written inside the running bundle and nothing is copied over it: replacing a running binary in place invalidates pages the kernel is executing, and a copy-over leaves a hybrid of two builds. The previous bundle stays on disk until the new app has launched and confirmed its own version.

Every refusal names the consequence and, where a fix exists, ends with a command you can read before you run it. No refusal turns into an admin prompt.

Situation What rexenv says
Running from a disk image Drag rexenv.app into Applications, open it from there, then update. Nothing was changed.
Never moved out of Downloads macOS is running rexenv from a temporary read-only copy — move it to Applications first.
Opened through a symlink The copy running and the copy that would be replaced may not be the same one.
Applications not writable sudo chown -R "$USER" /Applications — or ask an admin to update rexenv.
The bundle belongs to another account Have that account update rexenv, or take ownership first.
Not enough free space Names the megabytes needed and the megabytes free. Nothing was downloaded.

The space check wants room for the archive, the extracted copy and the previous bundle — three times the download, checked before the first byte, because a refusal that arrives after a 29 MB download has wasted your morning.

The check is a read; the install is a click

Section titled “The check is a read; the install is a click”

The check rides the launch sweep, plus a six-hour loop for a machine left running — a menu-bar app outlives its window by design. The request carries the rexenv version and nothing else: no architecture, no identifier. Turning automatic checks off in Settings → About stops the request itself rather than what is done with the answer, and Check now still works.

  • Skip this version stores a version, not a flag, so a later release still offers itself.
  • The footer says checked N ago from a value written only after a successful check — a failed check keeps the old timestamp instead of ageing into a lie. Nothing ever claims “up to date”.
  • The menu bar carries an Update to … item exactly when a verified offer exists, and it only opens the card.
  • rex status prints one line — update rexenv <version> can be installed from Settings → About — when there is an offer, and --json and the MCP stack_status tool carry the same field. All three read an in-process snapshot: a status call verifies no signature and touches no network.
  • There is no rex update and no MCP tool that installs one. An agent asking for one would be asking to replace the process that enforces the dial that bounds it.

The descriptor is a second signed document alongside the PHP one — app-manifest.json and its signature, on rexenv/runtimes — verified with an ed25519 key compiled into the app, re-checked on every read, carrying its own monotonic serial as rollback protection, and pointing only at artefact URLs under a compiled-in path prefix. TLS is not the gate: a digest check compares bytes against whoever supplied the digest, so an attacker-chosen URL paired with an attacker-chosen hash matches perfectly.

The release where rexenv grew a menu-bar life and an agent surface. 205 commits since 0.4.0; this entry is written from them, not from the tag.

Closing the window used to quit the app, and quitting took the control plane with it: rex and the MCP endpoint are remote controls for the running app, their sockets are opened by its process, and a public tunnel is supervised by it. So a closed window meant a dead CLI and a stopped share while the sites kept serving. Now the crowned-R icon in the menu bar is home — status line, Start all and Stop all, up to eight recent sites, an MCP on/off checkmark, and Open rexenv. A dock tile appears only while the window is up, so you can Cmd-Tab to it. Start on login goes straight to the menu bar with no window, unless first-run setup is unfinished. rex open brings the window forward. See Where rexenv lives once it is open.

One rexenv per data folder, and the CLI socket is the lock. A menu-bar app made a second copy of itself invisible — it booted fully, adopted the services, and stole the first copy’s socket, after which rex talked to a headless orphan. The second launch now exits after bringing the first to the front.

Every rex surface has an MCP tool — 50 of them — on the user’s own sites, not only on scratch sites: create, configure, restart, delete; the eight wp_* tools; artisan and Composer for Laravel; logs, the inbox, the database; the stack, PHP, settings, the TLD; the git-asset group as one tool; blueprints; sharing; the Valet import and connection rewrite. Each decides its level per action.

What decides is one Agent access dial — Read / Changes / Full — with a duration of this session, 7 days or always. It replaced per-site, per-scope, per-client grants that needed six clicks for one site’s ordinary work, plus the database-access prompt, the mail sub-toggle and the auto-allow switch. Read is on whenever the endpoint is and now covers every site’s mail and a SELECT-only view of its database. Full is the only level that hands over a shell as you, and publishing a site lives there rather than behind a second prompt. Where macOS asks for a password, rexenv does not ask again. A login link — real or scratch — is Read, because a link is not a credential; a password is never handed to an agent. The refusal names the dial, both levels and where to change it, in as many words. The full picture is on AI agents.

The feed is a file too: mcp.log, with its own AI agents (MCP) tab in Logs and an Only this site filter on a site’s Logs. Scratch cap and lifetime are settings now — five sites and 24 hours by default, scratch_cap and scratch_ttl_hours from rex config. The bridge answers a pending call in-band when rexenv quits, so the model is told rather than disconnected. rex mcp gained progress notifications for clients that ask, and tool annotations for every registry.

Every site was running or none was. Now a site has its own switch — Stop site, rex site stop <domain>, --all for every site, and the same for an agent. A default site is not a process, so stopping one changes the serving surface and touches no shared nginx or PHP pool: the site’s nginx block answers 503 with no PHP, the edge keeps the route and certificate and serves rexenv’s own This site is stopped page, and only a FrankenPHP or Apache override backend — a process that serves one site — is actually stopped.

What it cost to get right: the first version emitted no nginx block for a stopped site, and nginx answers a name it has no block for from its default server. A public tunnel proxies straight to that nginx, so sharing a stopped site published a neighbour’s site, found live within a day. Every name rexenv knows now answers for itself in every tier. Sharing a stopped site is allowed with a warning, never silently. The Sites list also filters by type and opens on the sites that are running. Sites.

A site serves, and holds a certificate for, every name it answers on — the Domains card, or rex site domains <domain> --add <name>. A hostname reaches exactly one site, and a name that is already some site’s primary or extra is refused with the owner’s name. A Valet link farm imports as one site with extra domains instead of one site and a wall of overlap errors. An extra domain on a WordPress site reaches it and then redirects to the primary, and the card says so. rex site info answers with every name; every site verb finds a site by any of them; renaming a site keeps its other names working. Two fixes on the way: a certificate reissue forgot the extra domains, and an imported extra domain was recorded but not served.

rex doctor and the DNS card in Settings name every TLD your sites use that this Mac cannot resolve, with the cause. rex tld --repair <tld> puts back a missing resolver file and refuses one another tool owns; rex tld --remove <tld> is the explicit, ours-only, not-in-use-only opposite. The import scan lists a leftover resolver file with no site behind it, so there is something to consent to. A .rex domain won’t resolve.

Catch all outgoing mail in Settings is one switch, on by default and on for sites that predate it. It now reaches the two apps that escaped: a WordPress SMTP plugin — measured, isSMTP() opened its own socket and the shim never saw the message — through a mu-plugin that hooks phpmailer_init last; and Laravel, which never consults sendmail_path, through nine MAIL_* variables in the pool and the artisan runner that outrank .env. Also a FrankenPHP override site. Flipping the switch installs or removes the mu-plugin and restarts the pools, so the toggle is pending until the machine agrees. The card names what it cannot catch: a Laravel app that ran config:cache, a plugin mailing through a provider’s HTTP API, and your own terminal. Mail.

  • nginx 1.30.4 is rexenv’s own build. The floor check found 0.4.0’s nginx declaring a macOS 26 floor on its Intel slice against a stated floor of 15, on the one binary every default site’s requests go through — an Intel Mac on macOS 15 could install rexenv and then not serve a site. Upstream had no way out (Intel is 26 from 1.26.3 up), so rexenv builds nginx the way it already builds PHP 7.4: macOS 12 on both chips, asserted per artefact, PCRE2 compiled in, no ssl module (the edge owns TLS), no gzip, linked against nothing but libSystem. Being the builder makes rexenv the distributor, so the licence tarball rides the binary and a download refuses to complete without it.
  • PostgreSQL 18.6.0 / 17.11.0 / 16.15.0, re-pinned for the floor rather than the patch: the previous pins were compiled for macOS 26 on every binary, which this site said and the app could not test. The new ones are measured at macOS 15 across every executable and library in all six tarballs, so PostgreSQL no longer needs macOS 26. cloudflared is now the only binary at the app’s stated floor. PostgreSQL.

A share ends with the app even when the app is killed: a guard process watches rexenv’s pid and start time and stops the tunnel when it goes, where a crash used to leave a site public until the next launch. A share writes its start and its end to rexenv.log — site, public URL, pid, origin — after one was found running that nobody remembered starting and the only evidence was in a per-tunnel file. An agent’s share stops itself after 30 minutes by default and 60 at most, and its timer ends only the share it started. A share rexenv stopped for you now reaches the screen. Public tunnels.

rex open, site stop|start [--all], site restart [--pool], site domains, service restart, tld --repair|--remove, wp user delete with a required decision about the posts, repo watch --tail, site create --starter-db. Progress streams over the socket for a client that asks, and a client that hangs up mid-stream no longer abandons the command half-way. Three fixes worth knowing: site create ignored a misspelt flag and built the wrong site; wp search-replace ran for real on a typo’d --dry-run; wp user delete could not delete anyone. CLI reference.

  • A WordPress site stays WordPress while its database is down: wp core is-installed exits 1 for “not WordPress” and “database unreachable” alike, so the WordPress tab and magic login vanished on Stop all and did not return on Start all until you left the page.
  • rex doctor names the PHP minors whose bundled curl cannot resolve .rex hosts, instead of leaving it in a plugin’s error. WordPress is patched; raw curl_init() is not.
  • A port held by Valet says Valet and offers valet stop; the Valet /storage mapping is reproduced on import.
  • Schema v44. Cmd+Q meets the same quit gate as the menu.

The release where a build can say what built it — rex --version prints rex 0.4.0 (d363e24) off the mounted disk image, without the app running. Every earlier release’s source commit was an inference from the tag and the build timeline.

A subdirectory network kept losing its sub-site paths: siteurl, home and admin_url were rewritten to the tunnel’s origin without the /sub1, so a sub-site 404’d and a login at /sub1/wp-login.php landed on the MAIN site’s dashboard. Fixed, and only on the host the tunnel actually pins.

A subdomain network cannot work through a quick tunnel at all — one hostname, one pinned Host, no wildcard to ask for — so while it is shared it is served as a subdirectory network: <origin>/s1/… becomes a request for s1.<network>. Local behaviour is unchanged: s1.msd.rex still serves natively and msd.rex/s1/ is still a 404. Sub-sites on their own public hostnames still need a wildcard a quick tunnel cannot issue. Sharing a network through a tunnel.

Themes can also be network-enabled from the app now — the toggle reads the network’s own allowedthemes option, because wp theme list has no network-enabled status to render a position from.

Blank PHP sites open on a page that answers something

Section titled “Blank PHP sites open on a page that answers something”

<?php phpinfo(); answered a question nobody asks on their first page load and none of the ones they do. A blank PHP site now gets a generated index.php naming its PHP version, web server and docroot — and the New Site dialog’s Database field is a real choice for it: MySQL, MariaDB or None. Pick an engine and the site gets a database, a seeded starter_items table and a db.php returning a ready PDO; pick None and both the database and its engine download are skipped. Neither generated file is ever overwritten, so a retry cannot revert your morning.

The starter database is a dialog answer — rex site create has no flag for it yet. What a blank PHP site opens on.

Three fixes that add up to a terminal you can work in:

  • It spawns a login shell. rexenv is launched by launchd, so it inherited a bare /usr/bin:/bin:/usr/sbin:/sbin and a non-login shell read only ~/.zshrc — code, rex, Homebrew’s git and every version-manager shim answered command not found inside rexenv while working fine in Terminal.app.
  • A session survives leaving the tab. Switching tabs unmounted the terminal and killed the PTY: a composer install died on an alt-tab and its output was gone on the way back. Four live shells are kept; an evicted one reopens on the next visit.
  • A plugin row or theme card opens a terminal in that asset’s own folder, as a separate session — not a cd typed into a site shell that may be mid-install.

AI agents (MCP): reading a real site’s database

Section titled “AI agents (MCP): reading a real site’s database”

The agent surface gains db_query. On a scratch site it works immediately; on a site you made it is SELECT-only and needs your approval, given in Settings → AI agents (MCP) → Database access, lasting 7 days, listed and revocable there.

The ask is recorded where the query is refused — there is no “request access” tool, so nothing prompts you about access nothing tried to use. Requests die with the app rather than persisting, because a consent prompt whose context is gone is not consent. The prompt names password hashes and tokens in wp_options rather than saying “read access”.

A session-scoped auto-allow answers that prompt with yes. It skips a prompt, never a boundary — the rule that an agent may not change a site you made is untouched — it lives in memory rather than settings so it dies on quit, and it still writes a grant, flagged auto-granted so “did I approve this?” has an answer. New page: AI agents (MCP).

  • About from the app menu opens rexenv’s own About with a Build card — version, commit, build date, platform, Tauri — rather than the bare macOS panel that can show a name and a version and nothing else.
  • rex site relink <domain> <path> re-points a linked site at a folder you moved: it records and reloads, and touches no file. site move remains the owned-docroot command and still refuses a linked folder.
  • rex site retry <domain> finishes a site whose setup stopped part-way.
  • rex config get|set reaches a small allow-list of settings, deny-by-default, every refusal saying why. The signed PHP update manifest and the MCP toggles are refused outright.
  • rex mail list [--unread] [query] and rex mail mark-read — two backend arms no rex verb had ever sent.
  • rex site delete on a linked site no longer claims it removed files it correctly had not. The confirm prompt had always said the folder is left in place; --yes skipped the prompt, so the user most likely to be scripting saw only the false sentence.
  • The Xdebug refusal now separates its two causes: a static build that physically cannot load any extension (7.4, 8.0) versus a minor nobody has pinned a bottle for.
  • A service that fails to start names the macOS version mismatch when that is the reason.

Requires macOS 15 or later, unchanged.

The release where two components stopped being frozen until the next rexenv build — and both of them stayed a button you press.

8.3.31 is what a build pins. When PHP ships 8.3.32, Settings → PHP versions can now install it without waiting for a rexenv release: one button, one minor at a time, restarting that minor’s pool and putting it back on the previous patch if it does not come up.

The trust anchor moves exactly one step — from a checksum compiled into the app to a signed document whose public key is compiled into the app. Five limits hold the blast radius: only php, php-fpm and adminer may be named (never caddy, which is a root LaunchDaemon), https:// from allowlisted hosts only, the signature is re-checked on every read rather than remembered, only a patch of a minor your build already ships, and the pin is the floor — an update can only move you upwards. An older manifest is refused as a replay.

Details, including what the signature does not buy: Patch updates.

“8.4.24 exists”, said where it is true

Section titled ““8.4.24 exists”, said where it is true”

Separately from the button, the PHP rows read php.net’s release list and say what exists. That is not the same claim as “update available”: the 8.x builds come from static-php.dev, which trails php.net — measured at 17 days on 16 August 2026. The row says exists, never “up to date” (unprovable before the first successful check), and rex php list gained a NOTE column carrying the same two facts — serving 8.3.30 and 8.3.33 exists — because a CLI that prints only the pin is the same silent lie in different clothes.

Adminer updates, and a console that matches your theme

Section titled “Adminer updates, and a console that matches your theme”

The Databases screen carries an Adminer card under the engine table, on the screen the console opens from. Updates come from the same signed manifest, capped at major 6 — the newest major whose plugin API was probed against rexenv’s wrapper — and a downloaded build is run through the bundled PHP in a throwaway directory first: rexenv’s login gate and frame protections live inside Adminer’s plugin API, so a build that no longer binds would turn them off silently. See Updating Adminer.

The console also takes its palette from rexenv rather than from macOS. A light-themed app framing a dark console was a real screenshot, not a hypothetical.

  • Premium plugin updates are visible. Vendors’ updaters register behind current_user_can( 'manage_options' ), and a WP-CLI run has no user at all, so every paid plugin was missing from the update data. The check now grants three named capabilities to that one process — nobody is impersonated, no session or cookie exists. Measured on a 47-plugin site: 2 of 10 paid plugins reported an update before, 5 after.
  • Paid plugins show an icon instead of a letter tile, borrowed from their free half where the directory has one.
  • A zip of something already installed offers Replace instead of dead-ending on WP-CLI’s Error: No plugins installed. A git-managed target gets a danger confirm first, because --force unpacks over the working tree.
  • Rows say the thing’s name, with the slug kept beside it, and the plugin filter matches both — typing “loopback” against a row reading rexenv loopback DNS used to answer “No plugins match”.
  • A successful install card clears itself after three seconds; a failed one waits with an × to dismiss, and opening its log holds the timer.
  • Tunnels have a search box over name, domain and the public URL — and it says how many shared sites the filter is hiding, because a live public URL is not a view preference.
  • rexenv writes its own log in every build (logs/rexenv.log, its own tab, rotated at 2MB). On a release build the app previously logged nowhere, so every bug report arrived without the one artefact that explains it.
  • Your default PHP version stops resetting. Settings → Make default held until the next launch and then snapped back to the pinned minor; a new pinned minor arriving with a release also claimed to be a second default.
  • A settings value the pool dies on can be undone. A memory_limit too small to start a worker passed both parse-time gates, persisted, and then failed every later start with nothing on screen connecting the two.
  • ~360MB of superseded PHP trees stop accumulating. A user’s bin/ had reached 3.3GB with every replaced patch still on disk, growing ~180MB per updated minor.
  • Low-contrast text is gone. 135 text usages moved to an AA-clean token and two tokens were deleted outright; the light theme’s update badges were a raw Tailwind hue that could not follow a theme at all.
  • PHP 7.4.33, as a first-class version: same picker, same pool model, its own pool on 9774, 60 extensions including gd, intl, mysqli and redis. Nobody publishes a portable PHP 7.4, so rexenv builds and hosts it — and therefore ships the licence texts beside the interpreter. No OPcache and no Xdebug on 7.4, stated rather than discovered.
  • End-of-life runtimes say so. PHP 7.4, 8.0 and 8.1 are badged where you pick a version, computed against today’s date rather than a list somebody remembered to update.
  • FrankenPHP stops misreporting the PHP version. It serves every site with its own embedded PHP, so the picker is read-only there and annotated with what actually serves — and a 7.4 site on FrankenPHP is refused rather than silently served by 8.5.
  • Resolver drift is reported. If Valet or Herd reclaims a TLD your sites stop resolving; that now arrives as a banner naming the symptom and the way back, and rex doctor reports it.
  • Repository panel: working-tree row — status, stash, restore, reset.
  • Mail: unread filter, Mark all read, and read state that does not wait for a poll.
  • Browsers: open a site in any browser’s private window, per row.
  • Public sharing works for sites on a FrankenPHP or Apache backend.
  • A foreign proxy already on :443 is reported during onboarding instead of failing later.
  • Security: the WordPress magic-login reads the last X-Forwarded-For hop.
  • rex no longer hangs forever when the app stops answering. An app instance can hold the CLI socket, accept the connection and never reply — rex --version and rex status then blocked with no output and no timeout. --version is bounded at 2s and falls back to the CLI version alone; long-running commands still wait as long as they need, but say so after 10s instead of leaving a dead terminal.
  • A Laravel site’s database is named lv_, not wp_. The name derived from the domain alone, so every site type got a WordPress prefix on a database WordPress never touches. Creation-time only — existing sites keep the name they stored.

The first published build. No release notes were written for it, and none are invented here — what it shipped is what the rest of these docs describe, minus everything listed above.