Cove gets a face: an official macOS menu bar companion โ created by Robby McCullough and folded into the project with his blessing โ shows the stack's health at a glance, starts and stops services, opens or creates sites, and keeps itself current, all compiled locally in seconds from source that ships inside cove.sh itself. A new cove status --porcelain gives it (and any tooling you write) a stable machine-readable contract. Under the hood, a security-audit pass now validates every hostname that reaches the Caddyfile and /etc/hosts, and first-time installs were fixed on fresh Macs, containers, and minimal RHEL images.
โจ New Features
-
A Menu Bar for Cove (macOS)
Cove now has an official menu bar companion, created by Robby McCullough and folded into the project with his blessing. The icon shows the stack's health at a glance โ full color when Caddy, MariaDB, and Mailpit are all running, light grayscale when only some are, dark grayscale when everything's stopped โ and the menu can start or stop Cove, refresh status, and jump straight to the Dashboard, Adminer, Mailpit, or the logs folder, with an optional launch-at-login toggle. It's a dependency-free native app that stays true to Cove's single-file philosophy: the Objective-C source ships embedded inside
cove.sh itself, and cove menubar enable compiles it locally in seconds (using the Command Line Tools every Homebrew machine already has), installs it to ~/Applications, and launches it โ no separate download, no notarization dance. Strictly opt-in: cove upgrade never installs it on its own, but once enabled it keeps the app current automatically, and cove menubar disable removes it cleanly. If you were running Robby's original standalone build, enabling the official one simply takes its place. Beyond service control, the menu carries a Sites submenu listing every site with one-click open โ hold Option on a WordPress site to generate a one-time admin login instead โ plus quick links to the Dashboard, Adminer, Mailpit, the logs folder, and the Sites folder. The app also watches the stack for you: if a service dies out from under an otherwise-running stack it posts a macOS notification (deliberately staying quiet when everything stops at once, which usually just means you ran cove disable), and it checks GitHub once a day for new Cove releases, offering an "Update Cove" menu item that runs cove upgrade in a real Terminal window. Status polling is battery-considerate โ a 20-second timer with generous tolerance, refreshed instantly whenever the menu opens โ and it reads the new machine-readable status output, so PHP-FPM pools for version-pinned sites are monitored right alongside Caddy, MariaDB, and Mailpit. Larger installs get a "Recent" section at the top of the Sites submenu (top five by the same modified signal the dashboard sorts on), "New Siteโฆ" creates a WordPress site from a name prompt and opens it when ready, and a "Reload Caddy" item covers the regenerate-and-reload you'd otherwise open a terminal for after editing a directive.
๐ ๏ธ Improvements & Changes
-
Dashboard Filter Focused on Load
Opening
https://cove.localhost now puts the cursor in the filter box immediately, so you can start typing a site name the moment the page lands โ type to narrow, arrow to pick, Enter to open, no click required. Esc still clears and blurs the filter for the j/k crowd.
-
The Menu Bar Icon Is Now a Vector
The embedded icon asset switched from a 300ร300 PNG to the same SVG mark the dashboard renders, so there is one logo source of truth โ and the Finder/Login Items app icon is now generated crisp at every size up to 1024px instead of being upscaled from 300px. The embedded asset shrank from 29KB of base64 to under 1KB of text. Because Apple's SVG rasterization is technically undocumented,
cove menubar enable probes it first and fails with a clear message rather than assembling a bundle around a corrupt icon, and if the app itself ever can't decode the logo it falls back to a "Cove" text item in the menu bar instead of an invisible one.
-
Machine-Readable Service Status
cove status --porcelain emits a stable key=value line per service (caddy=running, mariadb=stopped, php-fpm-8.2=running, plus version=) intended for tooling โ it's what the menu bar app consumes. The human-readable output is unchanged and remains free to evolve; the porcelain keys are a contract and won't.
๐ Security & Bug Fixes
-
Domain and Proxy Values Are Now Validated Before They Reach the Caddyfile and /etc/hosts
cove mappings add and cove proxy add accepted their domain (and the proxy target) with only an is-it-empty check, then wrote the raw value straight into the generated Caddyfile and โ for mappings โ into a root-written /etc/hosts line. A value carrying Caddy syntax could inject an arbitrary server block (binding extra ports, proxying elsewhere, serving an attacker-chosen docroot), and because the /etc/hosts block was assembled with echo -e, a literal \n in a mapping could expand into an additional root-written host entry (local DNS spoofing). Both commands now reject anything that isn't a bare hostname via a shared validate_hostname helper, and the /etc/hosts writer uses printf '%s\n' so a backslash sequence can never expand โ the same discipline the Tailscale proxy path already applied to its target. The same check now also covers the Tailscale hostname itself (cove tailscale enable), which is likewise interpolated into Caddyfile server-block labels โ a stray space or brace in a pasted hostname used to take every site down at the next reload. The allowed charset is letters, numbers, dots, hyphens, and underscores, plus an optional leading *. wildcard label (proxy targets also allow a port colon), so dev hostnames like my_site.test and wildcard mappings that worked before keep working. Surfaced by a security audit of the CLI.
-
Menu Bar App Can No Longer Wedge on a Stuck Command
Every
cove call the menu bar app makes now runs under a timeout with a fire-once completion guard, so a subprocess that hangs (a wp-cli or PHP child during site creation that inherits the pipe and outlives cove) can no longer leave the app stuck on "Startingโฆ"/"Creatingโฆ" with every control disabled until relaunch โ the watchdog reclaims it (20s for status, 45s for login, two minutes for start/stop, four for a full site create). Two related fixes rode along: an unreadable status response no longer clears its own error and shows "Checking Cove statusโฆ" forever, and the one-time login now picks the URL that actually points at the target site's wp-login endpoint (a preceding wp-cli/PHP notice URL used to win and burn the token on the wrong page), with a wider token charset so it can't truncate mid-URL. Surfaced by a security/robustness audit of the app.
-
Menu Bar Crash Alerts Wait for a Second Opinion
A "stopped unexpectedly" notification now requires the service to be seen down on two consecutive status checks before it fires, and starting an upgrade from the menu counts as a user action. Previously a status poll landing mid-
cove upgrade โ or during a terminal-run cove enable restart โ could catch a service between stop and start and alert the user about a crash in an operation they launched themselves. A service that genuinely dies still notifies, just one poll cycle later.
-
Menu Bar Login Is Single-Flight
Generating a one-time admin login takes wp-cli a few seconds, and the menu gave no feedback while it worked โ an impatient second click ran a parallel
cove login, which regenerated the one-time token and invalidated the first URL moments before its tab opened, landing on an "invalid token" page next to a working one. The menu now shows a "Logging inโฆ" busy state for the duration and ignores further clicks until the first attempt finishes.
-
Homebrew Prompt Works Under
curl | bash
On a fresh Mac without Homebrew, the installer's "install it now?" confirmation used a bare read โ but under the documented curl https://cove.run/install-cove.sh | bash, stdin is the script itself, so the read swallowed a byte of the script instead of the keypress and the install always aborted as "declined." The prompt now reads from /dev/tty behind the same guard the end-of-install handoff already used, so first-time macOS installs complete. Surfaced by the same audit.
-
Installer No Longer Dies in Non-Interactive Environments
install-cove.sh tested whether /dev/tty was readable before wiring it to cove install's interactive prompts โ but in containers, CI runners, and ssh host 'curl | bash' sessions, /dev/tty exists and passes the readability test while actually opening it fails with "No such device or address", killing the install before Cove ever ran. The installer now tests that /dev/tty can genuinely be opened, so environments without a controlling terminal fall through cleanly to Cove's non-interactive install path. Found by running the installer across six Linux distros in fresh system containers.
-
Binary Fallback Now Works on Minimal RHEL-Family Systems
Installing a dependency from a release tarball (gum on Fedora/RHEL, where no native package exists) piped it into
tar โ which minimal Rocky/RHEL images don't ship. Worse, the extraction's stderr was suppressed, so the only symptom was a generic "Failed to install gum" with no cause. Cove now installs tar via the package manager before its first tarball extraction and lets extraction errors reach the terminal, taking a Rocky Linux 9 container from hard failure to a clean end-to-end install.
Cove learns versions. Pin any site to an older PHP with cove php โ a native Homebrew php-fpm running behind FrankenPHP, switchable from the dashboard with one click โ choose the WordPress release at creation with cove add mysite 6.4.3, and audit or update every site's core at once with the new cove core. cove clone stamps out copies of a seeded site โ files, database, URLs โ in seconds via copy-on-write. The dashboard dropped its last external dependency so it loads without internet, clicking a row now opens its action menu, and the bundled Adminer jumped to 6.0.
๐ ๏ธ Improvements & Changes
-
Adminer 6.0
New installs now ship Adminer 6.0.0 instead of 5.4.2 โ the database manager's first major release in Cove's lifetime, bringing shorter URLs, CSV export of a result without re-running the query, column reordering on touch screens, and a batch of upstream XSS/CSRF hardening. Cove's autologin entry point and version-detection logic carry over unchanged, and existing installs pick the new version up through
cove upgrade's normal Adminer check. The one casualty was dark-mode syntax highlighting: 6.0 wraps SQL keywords in documentation links colored navy by Adminer's own stylesheet, which made SELECT nearly invisible on a dark background โ the Cove theme now has those links inherit the surrounding token color, so SQL statements read correctly in both themes.
-
The Dashboard No Longer Needs the Internet
The site dashboard used to load Alpine.js from unpkg.com on every visit, which meant a machine without a connection โ or with unpkg unreachable โ got a dead page instead of a site list. The framework is gone entirely: the dashboard is now plain vanilla JavaScript baked into Cove itself, with explicit rendering and delegated event handling replacing Alpine's reactivity. Every behavior carries over โ filtering, sorting, pinning, keyboard navigation, the โK palette, right-click menus, undo-window deletes, the add-site flow โ and the context menu now measures its real size before positioning, so its bottom items can't land off-screen when opened near the edge of the window.
-
Pick a Site's PHP Version from the Dashboard
cove php gained a UI. Every row's action menu now shows the site's effective PHP version and opens a picker listing Cove's default (FrankenPHP) alongside every Homebrew PHP the machine could run โ installed versions switch with one click, versions that aren't installed yet are greyed out but still clickable: choosing one kicks off the brew install in the background and pins the site the moment it lands. Picking a version closes the modal instantly and the progress lives on the row itself โ the site's PHP chip pulses with the target version while the change applies (seconds for an installed version, minutes when Homebrew is downloading one), then settles when the site is live on it. Sites pinned off the default wear a small php 8.2-style chip in the list so a mixed fleet reads at a glance, and a pin that lands mid-server-reload is retried idempotently the same way renames are. Versions that left Homebrew core (8.0, 7.4) appear only when a tap already provides them, so the installer never offers something it can't deliver.
-
Click a Row for Actions, Click โ to Open
Clicking a site row used to launch the site in a new tab, which made the row's real actions (rename, log in, view log, delete) a right-click discovery problem. The interaction is now the other way around: clicking anywhere on a row opens its action menu at the cursor โ same menu as right-click โ and opening the site lives on a small always-visible โ icon beside the domain. The icon is a real link, so cmd-click and middle-click open background tabs like anywhere else on the web. The type pill still filters and the login button still logs in, the โฏ button is gone since the whole row now does its job, and on phones every action is one tap instead of hidden behind hover.
-
Sort the Dashboard by Type
The type column header joined name / modified / size as a clickable sort. It groups WordPress sites apart from plain ones and orders the WordPress group by core version โ compared segment-by-segment as numbers, so 6.10 lands above 6.9 instead of between 6.1 and 6.2 โ which makes "which sites are on an old WordPress" a one-click question. Sites of the same type and version fall back to name order, and the choice persists across visits like the other columns.
โจ New Features
-
cove php โ Per-Site PHP Version Switching
cove php mysite 8.2 pins a site to an older PHP; cove php mysite default puts it back on the bundled one. FrankenPHP stays the front door for every site โ TLS, logs, mappings, and custom directives are untouched โ but a pinned site's requests are handed to a native Homebrew php@8.2 php-fpm over a unix socket via php_fastcgi, which keeps the same WordPress rewrite semantics as php_server. Cove owns the whole FPM lifecycle: it generates the pool config (Mailpit sendmail_path, the Whoops error page, the shared error log, and your cove memory limits all carry over), runs each version as a KeepAlive launchd unit, starts a version's service when the first site pins it, and stops and removes it when the last one unpins โ deleting or cloning sites reconciles automatically on the next reload. As a bonus, native php-fpm is a non-thread-safe build with its own per-pool OPcache, so pinned sites are immune to the ZTS OPcache and Imagick crash classes entirely. Also: cove add mysite 6.4.3 --php=8.1 creates a site pinned from the very first request, wp-cli commands for a pinned site (cove login, cove core update, database backups) run under the same PHP the site serves with, cove php alone prints every site's effective version plus the state of each FPM service, cove list gained a PHP column, and cove status shows a row per active FPM version. Requires Homebrew on macOS; missing php@ formulas are offered for install on first pin, and a pin whose binary disappears degrades gracefully back to FrankenPHP rather than 502ing the site.
-
cove clone โ Copy a Site, Database and All
cove clone woo-template poc-acme duplicates an existing site under a new name: files, database, custom Caddy directives, and a search-replace so every stored URL points at the new hostname. The point is fixtures. Building the same scenario by hand โ WooCommerce plus a few users, roles, products, and orders โ costs far more than the five seconds a fresh install takes, so seed it once and stamp copies instead. On APFS and btrfs the file copy is a copy-on-write clone, which is roughly three times faster than a byte copy and means the two sites share disk until one of them is written to. A clone that fails partway cleans up after itself rather than leaving a half-built site and an orphaned database behind.
-
cove core โ See and Update the WordPress Version of Every Site
cove core check reports what each site is running and cross-references wp.org's stable-check data to flag anything outdated or insecure. cove core update <site> moves one site to the latest release, cove core update <site> 6.4.3 pins it to a specific version (a downgrade is a legitimate thing to want when reproducing a bug), and cove core update --all catches up every site that's behind, with --dry-run to preview and --yes for scripts. It's deliberately a separate verb from cove upgrade, which still upgrades Cove itself.
-
WordPress Versions in the Site List
cove list gained a WP column and the dashboard shows each site's version beside its type pill, with a small amber dot on releases wp.org marks insecure. Versions are parsed out of wp-includes/version.php rather than booting WordPress, so reading all 250 sites costs milliseconds and still works on a site too broken to serve a request.
-
Choose the WordPress Version When Creating a Site
cove add now takes an optional second argument saying what goes inside the site โ cove add mysite 6.4.3 pins a release, cove add mysite 6.9-RC1 installs a prerelease, and cove add mysite nightly tracks trunk. Reproducing a bug report against the version it was filed on, or checking a plugin against the next release before it ships, no longer means installing the latest and downgrading by hand. The same slot also accepts plain, so cove add mysite plain is now the natural way to spell what --plain has always done, and latest is the default you get by passing nothing. --plain and --wp-version= remain accepted, so nothing that already worked stops working. On the dashboard, the add-site form gained a version dropdown listing the releases wp.org actually shipped (fetched once and cached for a day, flagging the ones marked insecure) rather than a text box you can typo into a failed install two minutes later โ and it falls back to latest and nightly when the machine is offline.
The dashboard grows up: a โK command palette, right-click and โฏ row menus, keyboard navigation, and pinned sites, so an install with hundreds of sites stays navigable. cove pull and cove push now run on Cove's own backup/restore engine over SSH instead of fetching an external script โ which also means they work against hosts missing zip, unzip, wget, or a MySQL client. Every interactive prompt gained a flag equivalent so scripts and AI agents never stall on a question they can't answer, a new cove health diagnoses crashes and OPcache pressure, and a run of stability fixes clears the OPcache and Imagick segfaults that had been restarting FrankenPHP under load.
โจ New Features
-
cove health โ Read-Only Diagnostic
A new command that aggregates the signals you'd otherwise gather by hand after a crash. It reports service liveness; the FrankenPHP process state and its last exit code (decoding -11 SIGSEGV, -6 SIGABRT/Go-fatal, and -9 SIGKILL as the watchdog's own deliberate restart rather than an unexplained crash); recent hard crashes from the macOS DiagnosticReports, classified as OPcache-shared-memory vs Imagick vs other; live OPcache pressure (interned-buffer fill %, hit rate, cached/max files, OOM and max-files restart counts) with a concrete ~/Cove/php.ini remedy when it detects saturation; and on-disk hygiene (zombie site directories, oversized logs). It changes nothing โ every finding comes with a recommended fix you choose to run. The OPcache probe queries the running server (not a fresh CLI) and retries so a check landing mid-reload doesn't false-alarm.
-
Sortable Column Headers on the Dashboard
The site list now has a proper header row (name / type / modified / size). Click a header to sort by that column, click again to flip direction; the choice persists across visits. Replaces the old "sort:" pill that cycled blindly through modes โ and finally labels the modified and size columns.
-
Undo-Style Deletes Instead of a Confirm Dialog
Deleting a site from the dashboard no longer throws a blocking browser
confirm(). The row disappears immediately and a snackbar offers undo for five seconds; only after the window passes does the backend delete run. Deleting more sites during the window merges them into one batch ("Deleted 3 sites โ undo"). Closing the tab mid-window flushes the staged deletes with keepalive requests so nothing is silently lost.
-
Bulk Delete for Filtered Sites
When a filter narrows the list, a "delete N shown" button appears in the filter row. First click arms it ("sure? delete N"), second click stages every matching site through the same undo window and serialized delete queue. Ideal for clearing out batches of throwaway
poc-* / test sites in one pass.
-
Cove-Themed Empty States
The "no sites yet" and "no filter matches" states now carry small SVG illustrations (a sailboat in the cove, a periscope scanning the waves) that follow the light/dark theme.
-
Command Palette (โK / Ctrl+K)
With a few hundred sites the list is far taller than a screen and scrolling stops being navigation, so search now sits one keystroke away. Type to fuzzy-match a site (ranked so a prefix hit beats a mid-string one) and press Enter to open it, โEnter to jump straight into wp-admin. The same palette runs global commands โ add site, reload server, refresh disk sizes, open the database or Mailpit, database credentials, toggle theme โ so the things buried in the header and footer are reachable without hunting for them.
-
Right-Click and โฏ Row Menus
Each row now has an actions menu with open, log in, copy path, pin, and delete, opened either by right-clicking the row or by a โฏ button. Both drive one component, so the two can't drift apart. This also fixes a real gap rather than just adding polish: the previous
login path delete links only appeared on :hover, which meant they did not exist at all on touch โ and cove tailscale exists precisely so you can reach the dashboard from a phone or iPad, where the dashboard was accidentally read-only.
-
Rename, Reveal, Open Database, and View Log From the Row Menu
Four actions that previously meant dropping to a terminal. Rename wraps
cove rename (directory, database, and URL together) in a modal, and carries pinned/selected state across to the new name so a pinned site doesn't silently unpin itself. Reveal in Finder opens the site directory in the desktop file manager (open on macOS, xdg-open elsewhere), detached so a file manager left in the foreground can't hold the request open. Open database deep-links Adminer straight at that site's own schema instead of the server root. View log tails the site's Caddy log โ the last 64KB rather than an unbounded read โ and folds Caddy's JSON-per-line output down to the fields you actually scan (time, status, method, path, duration), with a toggle back to raw. Actions that don't apply are hidden rather than shown broken: a plain static site offers neither "Log in to admin" nor "Open database".
-
Keyboard Navigation of the Site List
โ/โ (or j/k outside a text field) move a selection, Enter opens the selected site, โEnter logs in, โโซ deletes, Esc clears. Arrows work while the filter is focused, so you can type to narrow and pick without reaching for the mouse. The selected row is scrolled into view and marked distinctly from hover.
-
Pinned Sites
Pin the handful of sites you're actually working on and they float above whatever sort is active, marked with a dot. The choice persists across visits.
-
Service Status Moved Into the Header
The caddy / mariadb / mailpit indicators used to sit only in the footer, which on a real install is roughly 14,000px down the page โ the most glanceable information was the least reachable thing in the UI. They now live in the top bar.
-
The Site List Defaults to Most-Recently-Modified
"What was I just working on" is a better landing state than "what starts with A". Alphabetical is still one click away on the column header.
-
Every Interactive Prompt Now Has a Flag
Commands that used to depend on a TUI can be driven entirely by arguments, so scripts, CI, and AI agents never stall on a prompt they can't answer.
cove pull gains --ssh, --path, and --site (an existing site is overwritten, an unused name is created); cove push gains --site, --ssh, and --path; both take --yes. cove install gains --http/--https (skipping the port menus outright) and --db-root-user/--db-root-pass for the manual-credential fallback. cove directive add|update gains --rules "..." and --file <path> alongside the existing stdin pipe. cove share gains --yes for the on-demand cloudflared install. Where a value genuinely can't be defaulted, the command exits with a message naming the flag to pass instead of aborting with gum's opaque "could not open a new TTY". Prompts still appear exactly as before when you run these interactively.
-
Backup and Restore Are Now Cove's Own
cove pull and cove push no longer pipe https://captaincore.io/do into bash on your machine and on the remote server. That fetched a 7,300-line script to use roughly 490 lines of it, required both ends to be online to a third host, and tied Cove's behaviour to a script released on its own schedule โ a mismatch there is what made cove pull wipe a database it then refused to restore into. Cove now ships one self-contained transfer engine, run locally with bash and piped into bash -s -- over the SSH connection pull/push already hold open, so both ends run identical code and nothing is written to the remote filesystem. cove transfer probe [site] reports what the engine will use on a given host.
-
Transfers Work on Hosts Missing zip, unzip, tar, wget, or a MySQL Client
Every capability now degrades instead of failing. Archiving prefers
zip, falls back to tar, and finally to PHP's own ZipArchive/PharData; the database is dumped and imported with mysqldump/mysql when they exist and through WordPress's $wpdb when they don't. Missing tools are installed first when the host allows it (apt/dnf/yum/apk/zypper/brew, attempted only as root or with passwordless sudo so nothing stops to ask for a password), and a remote with no WP-CLI at all gets wp-cli.phar sideloaded over SSH. Because the target is always a WordPress install, PHP is guaranteed โ which makes the PHP tier a genuine universal fallback and means a bare host needs no package installs whatsoever. wget is no longer needed anywhere: every byte moves over SSH.
๐ ๏ธ Improvements & Changes
-
Backups Are Fetched Over SSH and Kept Out of the Web Root
The old flow asked the remote to build a download URL from its own
home option, then fetched it over public HTTP โ which only worked when the remote's WordPress URL was publicly reachable and mapped to the directory holding the backup. Staging sites behind basic auth, hosts on private networks, and sandboxes whose home still pointed at a provisioning template all failed with "Download failed". Everything now moves over the existing SSH connection, and the archive is written beside the site directory rather than inside it, so a full database dump is never left sitting in a publicly downloadable location.
-
Bulk Transfers No Longer Share the Multiplexed SSH Connection
pull and push open one shared SSH session so credentials are entered once, but sending the archive through that same channel throttled a 30MB upload to roughly 16KB/s against a real host and then stalled outright โ the same file moved in 15 seconds on its own connection. Archive transfers now use a direct connection while the short control commands keep sharing the multiplexed one, and both ends carry keepalives so a genuinely dead link fails instead of hanging forever.
๐ Security & Bug Fixes
-
Dashboard Ran Its Whole Startup Twice
Alpine calls a component's
init() automatically, and the template also carried x-init="init()", so everything in it registered twice โ every keyboard listener, every $watch, the page-hide handler, and the initial site fetch, which meant the dashboard issued two list_sites requests on every load. Idempotent work hid it (focusing an input twice looks the same as once), but anything that toggles cancelled itself out. init() is now guarded so it runs once.
-
cove pull No Longer Empties the Database It's About to Restore Into
Overwriting an existing site ran a DROP DATABASE / CREATE DATABASE before invoking the migration, but restoring requires the destination to still be a working WordPress โ it has to read the site's current URL to rewrite links โ and an emptied database fails that check. The restore aborted having already destroyed the local database, leaving the site with no tables at all. The pre-emptive drop was redundant to begin with: the restore resets the database itself immediately before importing, so the clean slate was already guaranteed at the right moment. The local site now stays intact and servable right up until the restore actually succeeds.
-
cove pull Prompted for a MySQL Password Mid-Run (and Hung Headless)
The pull read its database credentials only after the restore step, so the drop above expanded to a bare -p and stopped at an "Enter password:" prompt โ easy to miss in a script and an outright hang with no TTY. Configuration is now loaded at the top of the command.
-
cove push Only Worked From Inside the Site's Own Directory
The local backup is generated in a subshell that cds into the site, so the archive lands in the site directory while the command is still running from wherever it was invoked. Every later reference โ the existence check, the upload, the cleanup โ used the bare filename against the caller's working directory, so a push from anywhere else failed with "Failed to generate local backup" despite the backup having been created correctly. Those references are now absolute.
-
Mailpit KeepAlive No Longer Thrash-Loops on an Orphan Process
If any leftover mailpit held
:1025/:8025 (a manual start, a brew-services leftover, or a job that outlived launchctl unload), the service-managed instance failed to bind, exited, and KeepAlive/Restart=always respawned it forever โ observed at 51k restarts over ~10 days and a 28MB log of nothing but address already in use. Mailpit now starts through a small wrapper (~/Cove/mailpit-run.sh) that kills orphaned mailpit processes before exec, caps the log at 50MB, and is installed by both cove enable and cove upgrade (via post-upgrade). The macOS plist adds a 30s ThrottleInterval as a backstop; the Linux unit switches to Restart=on-failure with RestartSec=5s. cove disable also reaps leftover mailpit, and cove health warns when launchd's restart counter climbs or mailpit.log balloons.
-
Right-Sized OPcache Stops the Random FrankenPHP Segfaults on Busy Boxes
FrankenPHP embeds a thread-safe (ZTS) PHP, so every site on the box shares one OPcache arena. PHP's stock limits โ 128MB, an 8MB interned-strings buffer, and 10,000 cached files โ are sized for a single small FPM pool, but a real Cove box routinely holds 100+ WordPress sites (hundreds of thousands of PHP files; a single large site can have ~50,000). When the arena or the interned-strings buffer fills, OPcache performs a blocking shared-memory restart, and under ZTS a restart racing concurrent compiles corrupts shared memory โ segfaulting PHP deep inside
cache_script_in_shared_memory (observed crashing in lex_scan, zend_persist_*, and zend_optimize_script). FrankenPHP's Go runtime then turns that SIGSEGV into a whole-process abort โ the same "non-Go code set up signal handler without SA_ONSTACK flag" fatal, but from OPcache rather than Imagick โ which reads as the server randomly crashing while editing a heavy plugin (observed: ~16 hard crashes in one afternoon of plugin development, with the interned-strings buffer pegged at 100% and a 14% hit rate). Cove now sizes OPcache for a multi-site box: memory_consumption=512, interned_strings_buffer=64, max_accelerated_files=100000, and optimization_level=0 (in a validate_timestamps=1 dev environment files recompile constantly, so the optimizer buys almost nothing and its SSA/DFA/call-graph passes were themselves a recurring crash site). Each value is overridable via the corresponding key in ~/Cove/php.ini. Applied on the next cove reload for the file-count/optimizer settings; the two arena-sizing directives take effect on the next full FrankenPHP start.
-
Watchdog No Longer Kills FrankenPHP Mid-Reload
On installs with hundreds of sites, applying a regenerated Caddyfile can hold HTTPS requests longer than the watchdog's 3-probe kill threshold โ so a routine
cove reload (fired by every dashboard add/delete) could get a perfectly healthy server SIGKILLed mid-config-swap, trading a ~45-second pause for a multi-minute cold start while TLS re-provisions for every site. The watchdog now holds fire while cove reload's lock file is fresh, without touching the failure streak; the check is age-capped at 5 minutes so a stale lock can't blind it to a genuinely wedged server. Existing installs pick up the new watchdog via cove upgrade (or a one-time cove post-upgrade).
-
Dashboard Deletes No Longer Race the Background Reload (Second Delete "Not Deleting" / Sites Resurrecting)
Deleting a site in the dashboard fires a background
cove reload, and a second delete issued while that reload was still in flight could fail two ways. First, the reload's Caddyfile snapshot still named the just-deleted site, so when the stale config applied, Caddy's log directive re-created the site's logs/ skeleton โ resurrecting the "deleted" site in the dashboard (and re-adding its /etc/hosts entry). Second, a delete request landing exactly in the FrankenPHP config-swap window could hang and never execute at all. Now: cove delete leaves a tombstone and flags an in-flight reload to re-run against the post-delete Sites listing; cove reload prunes tombstoned skeletons; the Caddyfile and /etc/hosts generators skip directories that aren't servable sites (no public/ and no custom directive); the dashboard retries deletes whose response was dropped mid-reload; and the delete API is idempotent, so a retry that finds the site already gone reports success.
-
FrankenPHP No Longer Crashes on WordPress Image Processing (Imagick vs. Go)
FrankenPHP embeds a thread-safe PHP built with the
imagick extension baked in. The first time any request constructs an Imagick object, ImageMagick's MagickWandGenesis() installs its own SIGSEGV/SIGBUS/SIGABRT handlers without the SA_ONSTACK flag โ but Go, FrankenPHP's runtime, owns SIGSEGV for goroutine stack-growth guard pages. The next stack-growth signal then trips fatal error: non-Go code set up signal handler without SA_ONSTACK flag and the whole server aborts (the watchdog restarts it, producing the ~45s rebind windows and ERR_EMPTY_RESPONSE bursts). Because WordPress uses Imagick as its default image editor, every thumbnail, resize, and upload was a trigger, so image-heavy sites under load crashed repeatedly. The bundled MU-plugin now forces the GD image editor (wp_image_editors โ WP_Image_Editor_GD), so Imagick is never instantiated and MagickWandGenesis() never runs; GD covers every core WordPress image need. A site that genuinely needs Imagick can opt back in with add_filter( 'cove_force_gd_editor', '__return_false' ). Existing sites pick up the fix via cove upgrade (or a one-time cove post-upgrade).
-
cove upgrade Now Self-Locates via $0 Instead of PATH
The upgrade flow resolved its install path with command -v cove, which finds whatever cove happens to sit in PATH rather than the copy actually running. When cove runs through a shell alias or an absolute path, those can differ โ a stale v1.8 binary left in /opt/homebrew/bin answered a v1.11 upgrade's post-upgrade call with "Unknown command" (skipping the Whoops/MU-plugin/watchdog refresh) and ran reload through three-versions-old dashboard and Caddyfile generators. The running script's own $0 is now authoritative, with PATH lookup kept only as a fallback.
๐ Security & Bug Fixes
-
Whoops No Longer Promotes
E_WARNING to a Full-Screen Crash Page
The Whoops bootstrap's silence mask covered notices, deprecations, and E_USER_WARNING, but not plain E_WARNING โ so a harmless warning like PHP's "Cannot modify header information - headers already sent" (core's own Content-Type header colliding with a plugin that echoed output early, as LearnDash's setup wizard does) was converted into a fatal ErrorException and rendered as a full-screen crash, even with WP_DEBUG_DISPLAY off. E_WARNING is now silenced alongside the other non-fatal levels, so the Whoops page is reserved for actual fatals and uncaught exceptions. Existing sites pick up the regenerated bootstrap via cove upgrade (or a one-time cove post-upgrade). Reported in #5.
โจ New Features
-
nanobrew Opt-In Package-Manager Support
On macOS, Cove can now drive services through nanobrew (
nb) instead of Homebrew โ opt in via COVE_PKG_MANAGER=nanobrew, or it's picked automatically when only nb is installed. A new pkg_service helper abstracts brew services / nb services for every caller (including emulating the missing nb services restart), and cove install initializes the MariaDB data directory itself on nanobrew since nb skips Homebrew's Ruby post_install hooks.
-
FrankenPHP Watchdog
A standalone watchdog script (installed by
cove enable, run by launchd/systemd) detects "zombie" FrankenPHP โ process alive and accepting TCP connects, but HTTPS requests hang, as happens on the rare "panic during panic" crash path โ and SIGKILLs it so the service manager respawns a fresh instance. Uses an HTTP-level probe with a startup grace period so it never kills a server that's still provisioning certs for hundreds of sites. Ships alongside FD-limit and signal-handling hardening for large installs.
-
--yes on cove install / cove delete
Both commands now accept --yes for scripted callers, skipping interactive confirmation.
๐ ๏ธ Improvements & Changes
-
Cheap
cove status โ No More Homebrew on the Polling Path
On macOS, cove status checked MariaDB by shelling out to brew services list, which boots a full Homebrew Ruby interpreter (~1โ2s idle, far longer under load). With the menu bar app polling status every few seconds, slow calls stacked into a feedback loop that drove load average past 50 and starved FrankenPHP until the watchdog restarted it. MariaDB is now probed with a new is_mariadb_running helper โ a single mysqladmin ping handshake (with a pgrep mariadbd fallback) โ bringing a full cove status run down to well under a second no matter how often it's polled.
-
Shorter One-Time Login URLs
cove login tokens are now 7 characters (cove_login_token), making magic-login URLs comfortably paste-able.
-
Wider
gum Inputs + Shared SSH Auth Across cove pull / cove push
Remote-sync prompts render at a usable width, and pull/push share a single SSH authentication flow instead of prompting separately.
-
cove enable Reports Success Based on Caddy, Not the Watchdog Install
The green "Services are running" banner was gated on the watchdog install's exit code (almost always 0), so a Caddyfile that kept FrankenPHP from starting still showed all-green. It now polls is_caddy_running and shows the "Caddy failed to start" error when the server never answers.
-
cove reload Surfaces a Failed Reload
cove_reload always returned 0 even when frankenphp reload rejected the regenerated Caddyfile (bad directive, port conflict), so CLI and upgrade callers couldn't tell the new config never went live. It now returns the reload's status and prints an error pointing at caddy-reload.log.
-
cove disable Stays Disabled Across a Reboot (Linux)
cove enable runs systemctl enable on its units, but cove disable only stopped them โ so after a reboot FrankenPHP came back and re-grabbed ports 80/443, defeating the point of disabling. cove disable now also systemctl disables cove.service and cove-watchdog.timer (MariaDB is left enabled as shared infrastructure).
-
cove upgrade No Longer Caps an Unlimited memory_limit
The upgrade memory floor treated memory_limit = -1 (unlimited) as below 1G and silently rewrote it to 1G, OOM-ing large imports that previously worked. -1 is now recognized as unlimited and left untouched.
-
cove status Reflects Real Mailpit Liveness (macOS)
Status checked whether launchd had the Mailpit job loaded, which stays true even while it crash-loops, so a dead Mailpit showed "Running." It now probes the process directly with pgrep.
-
cove delete Drops the Database Named in wp-config.php
Delete derived the database name as cove_<site> rather than reading the site's actual DB_NAME, so a site whose DB was renamed left its real database orphaned โ and could drop an unrelated database that matched the derived name. It now reads DB_NAME from wp-config.php, falling back to the derived name only when that can't be read.
-
Watchdog State Reset on (Re)install
Registering the watchdog now clears its saved
.watchdog.state, so a disable/enable cycle where the OS reuses FrankenPHP's previous PID can't leave a stale "healthy" record that bypasses the never-kill-while-starting guard and SIGKILLs a server that's still provisioning certs.
-
Tailscale Reverse-Proxy Detection Handles Hyphenated Hosts
The regex deciding whether a site is a pure reverse-proxy on its tailnet port omitted hyphens/underscores, so
reverse_proxy app-server:3000 fell through and was served as a filesystem root over Tailscale instead of being proxied. The host character class now includes - and _.
๐ Security & Bug Fixes
-
Whoops Bumped to 2.18.0 โ Fixes Site-Wide 500s Under PHP 8.5
Cove bundled Whoops 2.15.3, which throws a
TypeError from Run::register() under FrankenPHP's PHP 8.5 (web SAPI). Because whoops_bootstrap.php is auto_prepended to every request, that fatal 500-ed every site at once โ and it surfaced the moment a cove upgrade (or Homebrew) moved FrankenPHP onto PHP 8.5. The bundled version is now pinned to 2.18.0 via a shared deploy_whoops helper, and โ critically โ cove upgrade now refreshes Whoops (it previously never did, so upgraders were left on the incompatible copy). The bootstrap is also wrapped so any future Whoops/PHP incompatibility degrades to "no pretty error pages" instead of taking every site down.
-
Dashboard API Cross-Origin (CSRF) Guard
The dashboard's
api.php mutates state (add_site / delete_site / reload) and reads the request body via php://input regardless of Content-Type, so a malicious page the developer visited could drive it with a CORS "simple" text/plain POST that skips preflight โ deleting local sites without ever reading the response. POST requests are now rejected unless their Origin (or, failing that, Referer) host matches the dashboard's own host. Same-origin dashboard traffic (including on a custom HTTPS port) is unaffected.
-
cove upgrade Refreshes the Login MU-plugin Across All Sites
The one-time-login MU-plugin (captaincore-helper.php) is injected per-site at creation and was never updated afterward, so security fixes to it (like the auth-bypass fix) and its runtime option_home/option_siteurl filters stayed stale on every existing site. cove upgrade now sweeps every WordPress site and re-pushes the plugin wherever the deployed Version: header is behind the bundled MU_PLUGIN_VERSION (bumped to 0.4.0) โ idempotent, so unchanged sites aren't rewritten. cove login continues to lazily re-inject for the site you're logging into. As part of this, all of upgrade's component refreshes (Whoops, its bootstrap, the MU-plugin, the watchdog) now run through the freshly-installed on-disk binary via an internal post-upgrade step, so they deploy the new code rather than whatever the still-running pre-upgrade script held.
-
One-Time Login Tokens Now Expire
The
cove login / dashboard magic-login token was stored in user meta and only cleared on a successful login, so an unclicked link stayed a valid standing credential forever. Tokens now carry a mint timestamp and expire 15 minutes after issue, closing the window in which a stale (or brute-forced) token could be replayed against an exposed site.
-
Remote Backup Artifacts Cleaned Up on Failed
cove push / cove pull
Both commands stage a full backup zip (DB dump + wp-config.php) in the remote web root, but only removed it on the success path โ a failed restore left it directly downloadable by filename. The failure paths now delete the local and remote backup artifacts before aborting. Also fixes a misplaced >&2 in push's log_error that sent error banners to stdout.
-
Unauthenticated Admin-Login Bypass in the Injected mu-plugin (Fixed)
The
wp_ajax_nopriv_captaincore_quick_login handler gated on $post->token != md5(AUTH_KEY) with a loose !=, so a JSON body {"token":true} coerced to true != <hash> โ false and slipped past the check, minting a working one-time admin login URL with no credentials. This mu-plugin is injected into every Cove WordPress site, so any site exposed via cove lan / cove share / Tailscale was reachable. Now uses hash_equals() with a string-type guard.
-
cove upgrade Now Installs the FrankenPHP Watchdog
The watchdog was only ever written by cove enable, which upgraders never re-run โ so a 1.10 install could cove upgrade to the new binary yet never get the watchdog, leaving it exposed to the exact zombie-FrankenPHP lockup the watchdog exists to auto-recover from. Watchdog registration is now a shared install_watchdog_service helper called by both cove enable and cove upgrade (via the freshly-installed on-disk binary so the new function runs).
-
Shared
validate_site_name โ Path-Traversal Hardening
cove delete, cove login, and the dashboard's delete_site never validated the site name's character set (unlike cove add), so a name like ../evil escaped the Sites/ tree โ and cove delete would rm -rf it, escalating to sudo -n rm on failure. A single validate_site_name helper (mirroring cove add's rules) now guards every command that turns a name into a filesystem path, and the dashboard applies the same regex it uses for add_site.
-
cove pull No Longer Drops the Local Database Before the Backup Exists
When overwriting an existing site, pull ran DROP DATABASE; CREATE DATABASE before generating the remote backup โ so if the backup step then failed, the local database was already gone with no rollback, leaving the site more broken than a no-op. The destructive reset is now deferred until a valid backup URL is in hand.
-
cove rename Verifies the DB Import Before Dropping the Old Database
The import into the new database was unchecked, yet the old database was dropped unconditionally afterward โ so a failed import (disk full, packet size, name collision) destroyed the site's only good copy. The DROP is now gated on a verified import and wp-config.php update; on failure the rename aborts with the old database and config left intact. Search-replace failures are downgraded to a warning since the data is already migrated.
-
cove add No Longer Deletes a Working Site When Cosmetic Cleanup Fails
The install subshell's exit status was whatever the trailing wp plugin delete hello akismet returned, so if those plugins were already absent (custom WP package, re-run) the non-zero result tripped the "installation failed" path and rm -rf'd a fully-installed site plus its database. The rollback is now gated on the actual core install step, and the plugin cleanup is tolerated with || true.
โจ New Features
-
cove trust Command
A one-shot installer that drops Cove's local root certificate into the OS trust store and every NSS database it can find, so Firefox and Chromium stop showing "Not Secure" warnings on *.localhost without hand-rolling certutil commands.
- Wraps
frankenphp trust for the baseline (macOS login keychain, Linux /usr/local/share/ca-certificates + ~/.pki/nssdb + ~/.mozilla/firefox/*).
- On Linux, auto-installs
libnss3-tools / nss-tools if certutil is missing, then walks ~/snap/*/common/.mozilla/firefox/*/cert9.db and ~/snap/chromium/* to inject the root into snap-packaged browsers โ a path Caddy's built-in scanner doesn't touch.
- Idempotent on re-run: existing "Cove Local Authority" entries are removed before the current root is re-added, so rotating the Caddy CA doesn't stack stale copies.
cove install calls cove_trust at the end of first setup, so a fresh install is trusted everywhere with no extra step.
-
cove memory Command
A cross-cutting view + editor for PHP memory_limit everywhere it can bite you.
cove memory prints a side-by-side report: Cove's own ~/Cove/php.ini, the memory_limit baked into the Caddyfile's frankenphp block, every php binary on PATH (version + loaded ini + live value), and wp-cli's effective PHP.
cove memory set <value> writes memory_limit, upload_max_filesize, and post_max_size into Cove's ini (values accept 512M, 2G, or -1 for unlimited), regenerates the Caddyfile so FrankenPHP picks up the new limits, then walks external inis on PATH and offers to update each one individually.
- New
cove_ini_get helper in main makes Cove's ini the single source of truth: regenerate_caddyfile reads the current values at render time instead of hardcoding 512M.
cove memory set --yes accepts every writable ini non-interactively for scripted fleet updates.
-
Cove-Branded Landing Page for Plain Sites
cove add --plain now writes a designed index.php that matches the dashboard and landing page palette โ Fraunces italic headline, Geist/Geist Mono body, teal accent, and the cove brand mark โ instead of leaving an empty directory that serves a 404. The page reads $_SERVER['HTTP_HOST'] and __FILE__ at request time, so it self-identifies no matter which hostname or mapping serves it, and includes a ~-shortened absolute path so users can jump straight to the file they need to edit.
-
Linux
cove.service Systemd Unit
cove enable now generates /etc/systemd/system/cove.service alongside the existing mailpit.service, so the whole stack survives a reboot without a manual cove enable on each boot.
- Runs FrankenPHP as the invoking user (not root),
Restart=on-failure with a 2s backoff, and passes PHPRC + HOME so php-cli sees the same ini Cove writes.
cove enable stops any ad-hoc FrankenPHP left over from a pre-1.10 install before systemctl takes the listening sockets, and cove disable now stops cove.service first (with a fallback to frankenphp stop for users who never re-ran cove enable).
cove install masks the distro-packaged frankenphp.service that ships with the apt package โ previously that unit would fight Cove for ports 80/443.
๐ ๏ธ Improvements & Changes
-
Adminer 5.4.2 with a Native Cove Skin
The old Catppuccin-derived theme has been replaced with a full redesign matching the dashboard and landing page โ warm cream + teal palette in light mode, dark olive + teal in dark mode, Fraunces/Geist/Geist Mono typography.
- Explicit light/dark toggle in the Adminer header, persisted in
localStorage and applied synchronously via a head-injected inline script so there's no theme flash on load.
- Drag-to-resize sidebar (persisted, clamped 180โ480px) so wide table names don't truncate.
- Cove's
AdminerCoveLogin class now overrides head() to inject the pre-paint theme script and adminer.js via \Adminer\nonce(), so the CSP nonce the Adminer core emits lines up with Cove's asset.
- Stale Catppuccin preview screenshots (
adminer-theme/assets/*.webp) removed from the repo.
-
Dashboard โ Post-Create Alerts, Spinner, and Smart Errors
After creating a site the dashboard now shows a persistent dismissible alert row with either "Log in to admin" (WP) or "Open site" (plain), so the one-time login URL is one click away instead of buried in the toast that fades in 3.5s.
- Inline spinner on the create button plus an animated teal progress stripe across the top of the add-row while
cove add is running (site creation takes 5โ10s; the extra cue makes the wait feel intentional).
- New-site row now carries the correct
size_bytes from the server's inline du -sk measurement, so the size column shows "4.0 KB" immediately instead of โ until the next refresh.
- Backend translates known
cove add failure signatures into user-facing messages ("Site name is taken", "That name is reserved", "Invalid site name", "WordPress installation failed โ check the logs") instead of a flat "An error occurred."
- Dashboard header pills for
db and mail now carry inline SVG icons (barrel database, envelope) that inherit currentColor so they tint correctly in both themes.
- Inline
data-theme bootstrap runs in <head> before any CSS paints, eliminating the first-frame FOUC on cold loads.
-
cove upgrade Now Refreshes the Full Cove Surface
Pre-1.10 cove upgrade only replaced cove.sh + frankenphp + adminer-core.php, which left upgraders stuck with the old Catppuccin CSS, the old Adminer index.php, and whatever memory_limit they'd set in the past.
- Extracted a shared
deploy_adminer_theme helper used by both cove install and cove upgrade. Upgraders now pick up the new Cove skin, the theme toggle, and the resizable sidebar in a single cove upgrade โ no reinstall required.
- Floors Cove's
memory_limit/upload_max_filesize/post_max_size at 1G (the fresh-install default) but leaves any user-set value >= 1G untouched, so a user who bumped to 2G stays at 2G.
- Runs
cove reload at the end of the upgrade (via the on-disk binary so the newly-replaced script's functions are in use), so dashboard and Caddyfile changes apply without a second manual step.
-
Scripted Flags on Confirmation-Only Prompts
Following the
cove ports --yes fix, the rest of the confirmation-only prompts now accept --force / --yes and auto-promote when stdin isn't a TTY, so dashboard-backed and CI-driven use never deadlocks on gum confirm.
cove directive delete <name> [--force]
cove proxy add <name> <domain> <target> [--force] (skips the overwrite confirm)
cove proxy delete <name> [--force]
cove memory set <value> [--yes] (accepts every writable ini in one shot)
cove upgrade [--yes] (skips the prompt shown when the local Adminer version can't be parsed)
๐ Bug Fixes
-
Linux โ Pre-1.10 Sites and Reload Lock Were Root-Owned
Before v1.10, Cove sudo-started FrankenPHP on Linux so it could bind 80/443. Every site created from the dashboard, every
/etc/hosts edit, the reload lock dir, and the caddy.pid file ended up owned by root โ which meant a later user-run cove delete silently failed (with a success message) and a user-run cove reload couldn't reclaim its own lock. v1.10 flips this entirely:
cove install runs setcap 'cap_net_bind_service=+ep' on the FrankenPHP binary so it can bind low ports as the invoking user, without sudo.
cove enable writes cove.service with User=$current_user, so the systemd-managed process is user-owned going forward.
- A new
heal_cove_state_ownership helper runs at the top of cove_reload and repairs ownership of the reload lock, caddy.pid, and site directories left behind by a pre-1.10 install.
cove delete now falls back to sudo -n rm -rf when the site dir is root-owned, and surfaces a real error (with the exact recovery command) instead of silently skipping the delete.
-
Concurrent Reloads Deadlocking Caddy's Admin Server
The dashboard fires
reload_server in the background after every site add/delete (shell_exec 'โฆ&'), so bulk actions would spawn many concurrent cove reload processes. Two parallel frankenphp reload calls reliably deadlock Caddy's admin endpoint with its 10s shutdown timeout, wedging further reloads and HTTP requests. v1.10 serializes reloads with an atomic lock: first caller does the work, subsequent callers touch a pending marker and exit, and the holder re-runs once if the marker is set โ so the final state always converges on the latest Sites listing.
-
Reload Lock TOCTOU Allowing 3+ Concurrent Reloads
The initial
mkdir + echo > pid implementation of the reload lock left a window where a competing reload could cat an empty pid file, decide the holder was dead, and stomp the lock โ letting two or more reloads run in parallel and race create_gui_file's .tmp writes. Switched to a single lock file opened with set -C (noclobber) so the pid is written atomically with the lock's creation. The cleanup trap is also paired with an explicit rm -f at function end, since the trap was observed not firing reliably under shell_exec('cove reload &').
-
Dashboard Deletes Queued During Reload Were Lost
processDeleteQueue would shift() every item once, await apiPost('reload_server'), and then return โ but deletes queued during that await would sit forever, because the next deleteSite() call short-circuited on isProcessingQueue. Wrapped the drain in an outer loop so post-reload enqueues are picked up in the same batch.
-
Adminer Session Warnings on Every Request
The apt
php-zts package defaults session.save_path to /var/lib/php-zts/session, owned by the frankenphp user. Since Cove now runs FrankenPHP as the invoking user, that path is unwritable and Adminer emits a session_start(): Failed to read session data warning on every request. The Caddyfile now pins session.save_path to ~/Cove/cache/sessions/ (auto-created by regenerate_caddyfile), so sessions land somewhere the process can actually write to.
โจ New Features
-
Redesigned Dashboard
The built-in web dashboard at
https://cove.localhost has been fully rewritten to match the Cove landing page.
- Warm-dark-first palette with a teal accent (
oklch(72% 0.12 190) in dark mode), Fraunces/Geist/JetBrains Mono typography, and a new SVG cove/bay brand mark with disc/land/horizon/water/waves/ring layers and theme-aware colour overrides.
- Single rounded card with a soft elevation shadow, three-tier backgrounds (
--bg / --panel / --bg-sunk), and subgrid columns so every WP/STATIC pill, modified stamp, size, and action row lines up across rows instead of sizing independently.
- Per-site size cache at
~/Cove/cache/site-sizes.json, populated by a new refresh_sizes API action. The dashboard shows cached bytes, auto-refreshes on an empty cache, and exposes a manual โป button in the footer.
- "Last modified" column backed by a
modified_at field in list_sites (mtime of public/ falling back to the site dir), rendered as relative time with the full timestamp on hover.
- Filter input with
/ focus shortcut (GitHub-style), Esc to clear, and clickable WP/STATIC pills that set a dedicated type: chip for exact-type matching rather than substring.
- Sort pill cycles name / size / modified.
- Matched substrings in domain names are wrapped in
<mark> so the letters you typed are highlighted as you filter. The site-name portion of each domain is accented in teal while the .localhost suffix stays dim.
- Optimistic single-flight delete queue: rows pop out instantly, the backend runs deletes sequentially so concurrent deletes can't race on Caddyfile regeneration or
/etc/hosts writes, and a single reload fires at the end.
- Click anywhere on a row to open the site โ skipped when text is selected so drag-to-copy still works.
- Login button uses an inline-grid label/spinner stack so the loading state can't shift neighbouring actions.
- Snackbar fades in/out (no sliding) and is centered without transform so Alpine's transition styles don't fight CSS centering.
- Theme defaults to
prefers-color-scheme on first visit; the moon/sun toggle cross-fades between two stroked SVGs in a 32ร32 bordered button.
- Inline SVG favicon ships with the dashboard โ no extra asset to deploy.
-
Installer
--main Mode
install-cove.sh now accepts --main to pull cove.sh from the main branch instead of the latest GitHub release, giving testers a one-liner for verifying unreleased changes.
๐ ๏ธ Improvements & Changes
-
cove.sh PATH Hardening
cove.sh now prepends /opt/homebrew/bin, /usr/local/bin, and ~/.local/bin to PATH at the top of the script. Callers with a minimal PATH โ launchd plists, systemd units, and the dashboard's shell_exec โ can now find gum, wp, frankenphp, and mariadb without per-platform shims.
-
Shared Caddy Start Helpers
New
is_caddy_running and start_caddy_service helpers in main. regenerate_caddyfile probes localhost:2019 before reloading and starts Caddy if it's down (the start reads the fresh Caddyfile, so no separate reload is needed). cove enable delegates to the shared helper instead of duplicating the macOS/Linux start block.
๐ Bug Fixes
-
cove add No Longer Lies About Success on a Stopped Stack
On a stopped stack, cove add used to print "Caddy configuration reload initiated" while the reload silently failed, leaving the new site unreachable until the user noticed and ran cove enable. regenerate_caddyfile now detects a down Caddy and starts it, and the reload itself is no longer backgrounded โ we wait for the admin API to return and surface a real error if it fails. The deadlock that originally motivated backgrounding is now handled at the PHP layer (the dashboard's reload_server already backgrounds cove reload via shell_exec 'โฆ&').
-
First TLS Request After
cove add Hitting tlsv1 alert internal error
On a busy running stack, the old backgrounded reload plus sleep 0.25 returned before Caddy had issued the new hostname's internal-CA cert. cove add now polls the site's HTTPS URL for up to ~2s after reload so the cert is warmed up before the command returns. Happy-path cost is one ~5ms probe.
-
cove db backup Overwriting Previous Snapshots
Backups were always written to ../private/database-backup.sql, so a second run silently clobbered the earlier snapshot. Each backup now includes a YYYYMMDD-HHMMSS suffix so repeat runs accumulate instead of overwriting.
-
Orphan
/etc/hosts Entries After cove delete
cove reload appends 127.0.0.1 entries for every site hostname and custom mapping, but cove delete previously only removed the site directory and the custom Caddy directive file โ the /etc/hosts lines lingered forever. Delete now reads the mappings file before rm -rf, then uses sudo sed -i.bak -E to strip the matching 127.0.0.1 lines in one pass, only prompting for sudo when entries actually need removing.
-
Dashboard Delete Hanging on Sudo Prompt
The
/etc/hosts cleanup inside cove delete used plain sudo sed, which hangs the caller when there's no TTY โ notably the dashboard's PHP shell_exec, which was locking up the UI waiting for a password prompt nothing could answer. cove delete now checks [ -t 0 ] and drops to sudo -n otherwise, so it fails fast with a warning instead of blocking the whole delete flow.
-
cove rename Temp File Collisions and Symlink Risk
The old database dump was written to a predictable /tmp/${old_db_name}.sql path, which collides if two renames run in parallel and is a symlink-attack vector on shared systems. Switched to mktemp with a trap EXIT cleanup so the temp file is removed on any exit path, not just the happy path.
-
Shell Injection in
cove pull and cove push
Interpolating $remote_path into ssh "cd $remote_path && โฆ" passed shell metacharacters โ spaces, semicolons, $(โฆ) โ straight through to the remote shell. A path with a space would break the cd; a path with a ; or $(โฆ) would execute arbitrary commands on the remote server. A new shell_quote helper wraps values in single quotes and rewrites interior ' as '\'', and pull/push now route $remote_path and $backup_filename through it at every ssh call site.
-
Installer Prompts Silently Falling Through Under
curl | bash
When the installer is run the documented way โ bash <(curl -sL https://cove.run/install-cove.sh) โ bash's stdin is the piped script, so every gum confirm/choose/input during cove install received a closed stdin and silently fell through. install-cove.sh now wires the child cove install call to /dev/tty when it's readable so the interactive port-conflict and option prompts actually work during the fresh install.
โจ New Features
-
Alternative HTTP/HTTPS Ports
Cove can now run alongside other local WordPress tools (Local, WordPress Studio, DevKinsta, MAMP) that already bind 80/443. The installer detects conflicts and offers a menu of alternatives.
- IPv4 and IPv6 port probing via bash
/dev/tcp, so the check works even against root-owned listeners (which lsof can't see from a regular user on macOS).
- Recommended fallback ports
8090/8453 โ picked specifically to avoid the well-trodden 8080/8443/8888/8881 range used by Docker, Lando, wp-env, MAMP, and WordPress Studio.
- Chosen ports persist to
~/Cove/config and are emitted into the Caddy global block as http_port/https_port. Site blocks continue to use their plain site.localhost {} form โ Caddy handles the rewrite automatically.
- Re-running
cove install on a machine with non-default ports saved presents a "Keep current / Switch to default / Pick custom" menu so you can migrate in either direction.
-
cove ports Command
A new top-level command to reconfigure ports at any time, not just during install.
- Interactive menu by default; accepts
--http PORT --https PORT for scripted use.
- After a port change, walks every WordPress site under
~/Cove/Sites/ and runs wp search-replace --all-tables --skip-plugins --skip-themes to rewrite stored URLs (siteurl, home, serialized content, custom mappings) so existing sites keep working on the new port.
- Iterates each hostname a site answers on โ base domain plus any entries in
site/mappings โ so extra domains don't get left stale.
--dry-run previews the port change and per-site replacement counts without committing anything.
--skip-urls changes ports without touching databases, for power users who want to migrate manually.
- Shows a confirmation list before running (ask-once, not per-site).
cove install now also runs this DB migration step when a re-install changes ports with pre-existing WordPress sites on disk.
-
FrankenPHP-Backed
wp-cli
Cove now uses FrankenPHP's bundled PHP for both the web server and wp-cli invocations, removing the standalone brew install php dependency entirely.
get_wp_cmd routes all wp-cli calls through frankenphp php-cli โ one PHP runtime for everything Cove touches.
- A dedicated
~/Cove/php.ini is written at install time and exported via PHPRC, giving Cove full control of memory_limit, display_errors, and error_reporting without fighting any system-wide /opt/homebrew/etc/php/*/php.ini.
cove list, cove db, and cove upgrade also use frankenphp php-cli -r for their inline PHP helpers, so Cove works on a fresh Apple Silicon Mac with zero standalone PHP installed.
๐ ๏ธ Improvements & Changes
-
Tailscale Access Serves Sites Directly
cove tailscale enable now serves site files directly from the Tailscale-scoped server block instead of reverse-proxying through the local site.localhost block. This fixes CSS/JS/image loading when a site is accessed from a remote device.
-
Dynamic siteurl/home Override
The bundled
captaincore-helper.php MU-plugin now filters option_home and option_siteurl at request time when a site is accessed via a non-.localhost host (Tailscale, LAN, or cove share). Assets resolve against the current host so the page renders correctly, and wp-cli is unaffected.
-
Smarter FrankenPHP Upgrade
cove upgrade now detects how FrankenPHP was installed and uses the right upgrade path โ apt or dnf for distro-packaged installs, direct download for static binaries.
-
macOS Services via
launchd
Caddy, Mailpit, and the Cove-managed services now run via custom launchd plists on macOS instead of brew services, giving Cove precise control over process arguments and log routing, plus proper auto-restart on crashes.
-
Shared Port Helpers
port_is_free, port_is_own, port_has_conflict, prompt_custom_ports, port_url_for, and update_wp_site_urls_for_port_change are now top-level helpers in main, shared between cove install and cove ports.
-
Cleaner
cove add Output
cove add now writes WP_DEBUG_DISPLAY = false into wp-config.php so WordPress doesn't force display_errors back on mid-install, keeping the command output clean. An additional stderr filter strips any remaining Deprecated: lines that leak out of wp-cli's colorizer on PHP 8.5.
-
Readme Overhaul
Major readme refresh, including a new Quick Start section, a "Running Alongside Local, Studio, or DevKinsta" walkthrough, a Troubleshooting section (cert warnings, WSL systemd, port conflicts, DB recovery), and a rewritten Features list that now covers LAN/mobile, Tailscale, Cloudflare share, WordPress migration, and
/etc/hosts automation.
๐ Bug Fixes
-
"Installation Cancelled" No Longer Reads as "Successful"
The outer
install-cove.sh installer used to print SUCCESS: Cove has been installed successfully! even when the user explicitly cancelled from Cove's interactive prompts. The installer now lets set -e handle the non-zero exit from cove install cleanly, so a real cancel no longer ends with a contradictory success message.
-
Mailpit Install on Fresh Apple Silicon Macs
The upstream Mailpit installer hardcodes
/usr/local/bin as its install directory, which doesn't exist on a fresh Apple Silicon Mac (Homebrew lives at /opt/homebrew). Cove now uses brew install mailpit on macOS instead, sidestepping the issue entirely. The upstream installer is still used on Linux, where /usr/local/bin is always present.
-
FrankenPHP Install on Fresh Apple Silicon Macs
The official FrankenPHP installer had the same
/usr/local/bin problem โ it would silently drop the binary in the current working directory instead of on PATH. Cove now runs the installer from a tempdir and, if the binary ends up there, moves it into $BIN_DIR (e.g., /opt/homebrew/bin) before continuing.
-
PHP 8.5 Deprecation Noise
wp-cli 2.12.0's bundled vendor code (
react/promise, php-cli-tools/Colors.php) emits Deprecated: warnings on PHP 8.5 that previously flooded every cove add run โ ~50 lines per install, polluting captured output like the one-time login URL. A combination of PHPRC display_errors=0, WP_DEBUG_DISPLAY=false, and a precision stderr filter on the install subshell now keeps output clean while preserving real errors.
-
IPv6-Only Port Listeners Not Detected
The initial version of
port_is_free only probed 127.0.0.1, which missed IPv6-only listeners like Python's http.server (which binds :: by default). The helper now probes both IPv4 and IPv6 loopback so a service on either stack is seen.
-
wp --version Validation Loop
install_dependency used to run wp --version as a sanity check after install, which failed when no standalone php was on PATH (wp's shebang is #!/usr/bin/env php). The check now skips that validation step for wp, matching the existing special case for mariadb.
-
Adminer Version Detection on macOS
cove upgrade used Perl-regex grep -oP ... \K to read the installed Adminer version, which only works on GNU grep. macOS ships BSD grep, so detection silently fell back to "unknown" and the upgrade prompt always asked to re-download even when the current version was up to date. Switched to a portable LC_ALL=C sed -nE extraction that pulls the version cleanly from both VERSION="x.y.z" and the @version docblock.
โจ New Features
-
Linux & WSL Support
Cove now runs natively on Linux distributions including Ubuntu, Debian, Fedora, CentOS, and RHEL. It also includes full support for Windows Subsystem for Linux (WSL).
- Automatic OS and package manager detection (
apt/dnf/brew).
- Smart MariaDB service name detection across different distros.
- WP-CLI
--allow-root support for Docker and WSL environments where running as root is common.
-
LAN Access for Mobile Sync
A new
cove lan command enables LAN access to your sites for mobile app testing and sync.
- Assigns a unique port and broadcasts via Bonjour/mDNS for easy device discovery.
- Includes
cove lan trust instructions for installing Caddy's CA certificate on mobile devices.
-
Log Viewer
A new
cove log command provides quick access to site logs or the global error log.
- Supports
--follow (-f) flag for real-time log tailing.
-
Public Site Sharing
A new
cove share command creates temporary public tunnels using Cloudflare Quick Tunnels.
- Uses
cloudflared (installed on-demand via Homebrew if missing).
- Generates a random public URL that works until you press Ctrl+C.
-
Tailscale Integration
A new
cove tailscale command exposes all Cove sites to your Tailscale network.
- Auto-detects your Tailscale hostname or accepts a manual override.
- Assigns unique ports to each site, plus fixed ports for Mailpit (9901), Adminer (9902), and the dashboard (9900).
-
Reverse Proxy Management
A new
cove proxy command manages standalone reverse proxy entries.
- Useful for exposing local services (like AI coding tools) via Tailscale or custom domains.
-
Domain Mappings
A new
cove mappings command allows a single site to be served from multiple domains.
- Mappings are automatically added to
/etc/hosts and the Caddyfile on reload.
-
WSL Hosts Helper
A new
cove wsl-hosts command (WSL only) displays PowerShell commands for updating the Windows hosts file so you can access Cove sites from your Windows browser.
๐ ๏ธ Improvements & Changes
-
Catppuccin Adminer Theme
A new custom Adminer theme has been bundled with Cove, featuring the beautiful Catppuccin color palette.
- Automatic light/dark mode switching based on system preferences (Latte for light, Mocha for dark).
- Full SQL syntax highlighting with Catppuccin colors.
- Modern UI with improved typography, spacing, and styled action buttons.
-
Adminer Auto-Upgrade
The
cove upgrade command now also checks for and installs the latest version of Adminer.
-
Improved Site Listing
The
cove list command output has been refined for better readability.
-
Smarter Tailscale Detection
Tailscale hostname auto-detection has been improved for more reliable setup.
-
Cleaner Error Display
PHP's
display_errors is now disabled by default. The Whoops error handler has been refined to silence noisy E_DEPRECATED and E_NOTICE warnings (common in older plugins) while still displaying fatal errors with full stack traces.
-
Enhanced Share Command
The
cove share command now displays a real-time access log showing timestamp, HTTP status (color-coded), client IP address, method, and path for each request. Connection loss is now detected and reported, and shutdown no longer displays terminal noise.
โจ New Features
-
Remote Site Pushing
A new
cove push command has been introduced to migrate a local Cove site to a remote server via SSH.
- It features an interactive TUI to guide you through selecting a local site and providing remote credentials.
- The command creates a local backup, securely uploads it, and then executes a migration script on the remote server to overwrite the destination site's content.
โจ New Features
-
Remote Site Pulling
A new
cove pull command has been introduced to migrate a remote WordPress site into Cove via SSH.
- It features an interactive TUI to guide you through providing remote credentials.
- It can create a new local site or overwrite an existing one.
- Includes a powerful
--proxy-uploads flag that skips downloading the wp-content/uploads directory and instead configures Caddy to reverse_proxy media requests to the live site, saving significant time and disk space.
-
Piped Directives
The
cove directive add <site> command now accepts input from stdin, allowing you to pipe complex, multi-line Caddy rules directly into a site's configuration. This is ideal for scripting and is used by the new pull command to set up the upload proxy.
๐ ๏ธ Improvements & Changes
-
FrankenPHP Auto-Upgrade
The
cove upgrade command is now more powerful. In addition to upgrading the Cove script itself, it now also checks for the latest version of the FrankenPHP binary on GitHub and will automatically download and install it if a newer version is available.
-
Correct Directive Order
The Caddyfile generation logic has been updated to place custom directives before the
php_server directive. This ensures that custom rules like reverse_proxy are evaluated first, which is critical for the new upload proxy feature to function correctly.
-
Automatic Directive Cleanup
When a site is deleted using
cove delete, any associated custom Caddy directive file is now also automatically removed, ensuring no orphaned configuration files are left behind.
โจ New Features
-
Self-Healing Login Command
The
cove login command is now "self-healing." If the command fails, it will automatically check for and inject a required Must-Use (MU) plugin into the WordPress site, then retry the login process. This ensures the command works reliably even on sites created with older versions of Cove or if the plugin was manually deleted.
-
Integrated MU-Plugin
Cove now uses a dedicated MU-plugin (
captaincore-helper.php) which is automatically added to new WordPress sites. This plugin provides the core functionality for one-time logins via a custom WP-CLI command (wp user login <user>) and also disables WordPress's plugin and theme auto-update email notifications for a cleaner local experience.
๐ ๏ธ Improvements & Changes
-
Global PHP Memory Limit
The global PHP
memory_limit has been increased to 512M in the main Caddyfile configuration. This helps prevent errors when working with memory-intensive plugins or operations across all sites.
-
Refactored Plugin Injection
The logic for creating the MU-plugin has been moved into its own dedicated function (
inject_mu_plugin), cleaning up the cove add command and allowing the new self-healing cove login command to utilize it.
-
More Robust Dashboard Logins
The web dashboard's "Login" button is now significantly more reliable. It delegates directly to the
cove login command, inheriting its new self-healing capabilities and simplifying the dashboard's backend logic.
-
Non-Blocking Server Reloads
Server reloads triggered from the web UI (or the
cove reload command) now run as a background process. This fixes a potential deadlock issue, preventing the dashboard from freezing and providing a much smoother user experience when adding, deleting, or modifying sites.
โจ New Features
-
Admin Login Command
A new
cove login <site> [<user>] command has been added to generate a one-time login link for a WordPress site. This works by finding the first available administrator or by specifying a user ID, email, or login.
-
Dashboard Login Button
The web dashboard now includes a "Login" button for WordPress sites, allowing for one-click access to the admin area. This is powered by a new
get_login_link API endpoint.
๐ ๏ธ Improvements & Changes
-
Automatic
/etc/hosts Management
The reload command now automatically checks for and adds required entries for all Cove sites to the /etc/hosts file, ensuring local domains resolve without manual setup. This requires sudo privileges upon first run.
-
Smarter Installation Script
The main installer (
install-cove.sh) is now architecture-aware, correctly using /opt/homebrew/bin on Apple Silicon and /usr/local/bin on Intel Macs. It will also offer to install Homebrew if it's not detected and attempt to create the installation directory if it doesn't exist.
-
Robust MariaDB Setup
The
cove install command now first attempts an automatic, non-interactive sudo mysql command to create the database user. If this fails, it falls back to the interactive prompt for root credentials, improving the initial setup experience.
-
Resilient Site Creation
The
cove add command for WordPress sites is now more robust. It will automatically clean up the site directory and database if the installation process fails, preventing partial sites. It also now deletes the default "Hello Dolly" and "Akismet" plugins for a cleaner start.
**New Features**
-
Site Renaming
A new
cove rename <old-name> <new-name> command has been added to fully rename a site. This includes updating the directory name, database name, and running a search-and-replace on the site's URL within the database.
-
Path & URL Commands
- Added
cove path <name> to quickly get the full system path to a site's public directory.
- Added
cove url <name> to print the full https://<name>.localhost URL for a site.
**Improvements & Changes**
-
Increased Upload Limits
The default PHP
upload_max_filesize and post_max_size have been increased to 512M to allow for larger file and database imports.
-
Enhanced
list Command
The cove list command now includes a "Path" column, displaying the path to each site's public directory.
**New Features**
-
Interactive Web UI
A new web-based dashboard has been introduced at
cove.localhost for managing sites. This interface allows users to:
- Add and delete sites directly from the browser.
- View a list of all managed sites with links to each.
- See the current database user and password configuration.
- Toggle between light and dark themes.
-
FrankenPHP Support
The script now detects and prefers a
frankenphp installation, falling back to caddy if it's not found. This allows Cove to leverage the performance benefits of FrankenPHP.
-
One-Time Login URLs
When creating a new WordPress site, a one-time login URL is now generated and displayed, allowing for quick and easy access to the new site's admin area without needing to manually enter the generated password.
-
Database Listing
A new command,
cove db list, has been added. It provides a formatted table of all WordPress sites and their associated database credentials and size.
-
Sizing Information in Site List
The
cove list command now includes a --totals flag to display the disk usage of each site's public directory.
-
Upgrade Command
A new
cove upgrade command allows users to automatically fetch and install the latest version of Cove from GitHub.
**Improvements & Changes**
-
Enhanced
list Command
The cove list command now outputs a neatly formatted and styled table for better readability, replacing the previous plain text list.
-
Persistent Mailpit Storage
Mailpit is now launched with a persistent database file (
mailpit.db), ensuring that emails are not lost when the service is restarted.
-
Adminer Auto-Login
The Adminer setup now includes an auto-login feature, pre-filling the database credentials from the Cove configuration file for a more seamless experience. A custom theme has also been applied.
-
Improved Output and Styling
The use of
gum has been expanded across various commands to provide more consistent and visually appealing feedback, including styled tables, prompts, and messages.
-
Refined Site Creation and Deletion
- The site creation process now validates against a list of protected names (
cove, mailpit, adminer).
- Site names are now restricted to lowercase letters, numbers, and hyphens.
- The
cove add command now accepts a --no-reload flag to prevent the server from reloading, which is used by the new web UI to manage the process.
-
Better Service Management
- The
cove enable command now ensures that any running instances of Mailpit are stopped before starting a new one to prevent conflicts.
- The
cove status command now provides more readable, color-coded output.
-
Robust Dependency Checks
The installation script now checks for conflicting services running on ports 80 and 443 and warns the user.
-
Help Command Enhancements
The help text for all commands has been updated to be more descriptive and now includes subcommand details for
db and directive.
**Bug Fixes**
-
MariaDB Connection Wait
The installation script now correctly waits for the MariaDB service to be fully available before attempting to create the database user, preventing a common installation failure.
Added
-
Initial Release of Cove, a command-line tool for local development.
-
Core Service Management: Commands to
enable, disable, and check the status of background services (Caddy, MariaDB, Mailpit).
-
Site Management:
cove add <name>: Create new WordPress sites.
cove add <name> --plain: Create new plain/static HTML sites.
cove delete <name>: Delete sites and their associated databases.
cove list: List all currently managed local sites.
-
Web Dashboard: A GUI at
https://cove.localhost to view, add, and delete sites. It also provides quick links to Adminer and Mailpit.
-
Database Features:
cove db backup: Command to create a .sql backup for every WordPress site.
- Integrated Adminer for web-based database management.
-
Caddy Integration:
cove reload: Regenerates the master Caddyfile and gracefully reloads the Caddy server.
cove directive: Sub-commands (add, update, delete, list) to manage site-specific Caddyfile rules.
- Automatic HTTPS for all local sites using internal certificates.
-
Development Environment:
cove install: Installs and configures all required dependencies like Caddy, MariaDB, and Mailpit using Homebrew.
- Built-in Mailpit service to catch all outgoing application emails.
- Integrated Whoops for informative PHP error pages.
-
Build System:
compile.sh: Script to combine all source files into a single, distributable shell script.
watch.sh: A helper script using fswatch to automatically re-compile the project on file changes.
-
Versioning:
cove version command to display the current version of the tool.
-
License: The project is licensed under the MIT License.