Two dead ends in the same two delegates — and a third problem underneath both, which changes what the fixes had to be.
Neither delegate was installed in a shipped app
setupUIDelegateran onlyif (style.dev_tools)(macos.zig:888)setupNavigationDelegatereturned immediately unlessCRAFT_ALLOW_LOCAL_TLSwas set (macos.zig:5335)
Both issues ask for a method on "the existing delegate". Implemented literally, both fixes would have been dead code in every packaged build.
Ungating them also fixes a third bug nobody filed: with no WKUIDelegate, WKWebView denies media capture outright — so getUserMedia() could not work in a packaged craft app at all.
#62 — the content process
WKWebView renders out of process. When that process dies the view does not error, close, or notify the page: it goes white and stays white, and the only route back was a Reload menu item the user had to know to look for.
-reload is Apple's documented recovery and it is wrong for half of craft's windows. HTML loaded through loadHTMLString:baseURL: has no request to repeat, so reloading navigates to the synthetic base URL http://localhost/ — where nothing is listening. A window started from --html would have come back as a connection error instead of the app. So what was loaded is retained per window and re-applied.
The budget is a rate, not a count. "Three tries and stop" is wrong for exactly the case this exists to serve: an app open for a week that loses its renderer once a day is healthy, and would spend its three lives by Wednesday and be a white window for the rest of the week. A window that stays up for a minute gets its budget back, so attempts measures a burst rather than a lifetime.
When a burst does run out, the window loads a small self-contained page saying so, with a Reload button. Bounded retries without that just reach the original white window more slowly.
#64 — window.open() and target="_blank"
WKWebView asks the UI delegate for a child webview; with the method absent, its answer is to drop the request. window.open() returned null, links died, and nothing was logged anywhere. The URL now goes to the system browser and the method returns nil — the Electron-conventional default, until #67 gives craft a real second window.
Which URLs is a security question, and the answer is narrower than shell.openExternal. That bridge allows file://, and should: the app is calling it, on a path it chose. Here a page is calling it, on a URL nobody vetted, and NSWorkspace openURL: is not a browser call — it means "ask LaunchServices to do whatever this URL means":
file:///Some.appis a launchx-apple.systempreferences:opens System Settings panes- every installed app that claims a scheme is one
window.openaway
So http and https only. Everything else is refused with a log, never silently — a link that quietly does nothing is the bug being fixed.
The security detail in ungating
handleAuthChallenge was gated by the delegate not existing. The delegate is now installed in every window, so without an internal allowsLocalDevTLS() check, this change would have started trusting local development certificates in every craft app ever shipped. That check is in the same commit.
How it's tested
Both policies are pure modules — the clock and the URL are passed in — so 22 unit tests cover the drop list and the crash-loop arithmetic without a browser or a crash.
The wiring is where ObjC fails silently, so it was verified by running it:
window.open — three refused schemes in a non-dev-tools, headless build:
warning: refused to open a new window ... craft-probe://payload
warning: refused to open a new window ... file:///Applications/Calculator.app
warning: refused to open a new window ... mailto:x@example.comRenderer crash — killing the real com.apple.WebKit.WebContent process four times:
warning: WebKit content process terminated; restoring the window (attempt 1)
warning: WebKit content process terminated; restoring the window (attempt 2)
warning: WebKit content process terminated; restoring the window (attempt 3)
warning: WebKit content process terminated 4 times in quick succession; leaving the window alone so it stops loopingwith four loads of the app's own HTML (1 initial + 3 restores), which is what proves the restore does not land on the base URL.
Budget reset — two kills 65 seconds apart both report attempt 1. That is also what proves the clock is in the units the budget thinks it is: at ms or s, 65 seconds would not have crossed a 60·10⁹ ns threshold and the second would have said attempt 2.
One link is not proven end to end: the NSWorkspace openURL: call itself, because proving it means launching a browser on the machine running the test. It is one line, character-identical to the shipping bridge_shell.zig:398, and the branch that reaches it is covered by external_link's unit tests.
Verified
zig build · zig build test · zig build test-js · x86_64-windows cross-build · zig fmt --check · tsc --noEmit · 493 bun tests · pickier 38 warnings, unchanged from main.
Not done, deliberately
#62 also mentions didFailProvisionalNavigation/didFail as missing. That is a different failure with a visible cause, and an automatic retry there fights a legitimately offline app rather than helping it. The issue's actual ask was the process-termination handler.