Repository navigation
[Bug]: Preview discovery probes Unity test ports and causes unrelated tests to fail on macOS #26279
Description
Area
apps/server
Impact
Major degradation or frequent failure. High severity: background preview discovery interferes with another application's protocol and makes automated test results unreliable.
Version or commit
T3 Code Nightly 0.1.4-nightly.20261027.3796
Commit: b88da91
Environment
- macOS 27.0.1
- Unity 6000.6.6f1
- Unity tests launched through a Claude agent in T3 Code
Steps to reproduce
- Open a Unity project in T3 Code Nightly.
- Have a Claude agent launch Unity tests.
- Unity opens local ports for its test/editor connection. Ports observed in these runs included 55504 and 34999.
- T3's preview scanner discovers the Unity process through its inherited T3CODE_THREAD_ID and sends HTTP and HTTPS probes to its listeners.
- Unity logs a protocol error. The test framework fails whichever test is running when the error occurs.
The failure is timing dependent. Three fast check runs each failed a different Play Mode test.
Expected behavior
Preview discovery must not send unsolicited HTTP or TLS traffic to Unity's test/editor connections or other services that use a different protocol.
Starting a process through T3 does not establish that its listening ports serve HTTP.
Actual behavior
Across three fast check runs:
- Edit Mode: 678 of 678 passed.
- Play Mode: 88 of 89 passed.
- A different Play Mode test failed each time because Unity logged an error after receiving a web probe.
This makes unrelated tests appear broken and causes repeated verification failures.
The scanner runs every three seconds and caches web probe results for 15 seconds. There is no scanner disable setting or environment flag that we could find in this build.
Supporting evidence
A controlled check against the running installed app used two temporary TCP listeners outside T3's integrated terminal. During a 24 second observation:
- Listener with T3CODE_THREAD_ID: received two HTTP requests and two TLS probes.
- Listener without T3CODE_THREAD_ID: received zero connections.
This confirms the ownership tag controls whether these listeners are probed. It does not by itself prove the Unity workaround is reliable over longer test runs.
Relevant source:
https://github.com/pingdotgg/t3code/blob/b88da91397c2/apps/server/src/preview/PortScanner.ts#L692
Likely regression
PR #22542, merged October 27, adds T3CODE_THREAD_ID to Claude processes and expands discovery to their children:
Unity inherits that variable when launched by the agent, making its non-HTTP listeners eligible for web probes.
PR #16687 previously restricted probing to T3 terminal processes and configured URLs:
#16687
Related issue #9526 describes earlier interference with Steam's local IPC protocol:
#9526
Workaround
Prefix the command that starts Unity with env -u T3CODE_THREAD_ID.
Use the agent's command tool or an external terminal. T3's integrated terminal also tracks child processes separately, so removing the variable there is insufficient. An already running tagged Unity process must be relaunched.
The failures have not recurred since applying this workaround. They were intermittent, so more observation is needed before calling the workaround fully verified.
Requested fix
Prevent automatic web probes from reaching non-HTTP services, including processes launched by agents. Provide a persistent switch to disable automatic port discovery so affected users can keep working without changing their test commands.
Activity
matheustimbo commentedon Oct 29, 2026
Same root cause hits Flutter:
flutter testfails to load test files while T3 Code is open (macOS, nightly 0.1.4-nightly.20261027.3796 and 20261028.3824/3841).What happens. flutter_tools binds a loopback
HttpServerper test file and treats the first incoming request as theflutter_testerWebSocket upgrade (flutter_tester_device.dart,remoteChannel:await _server!.firstthenWebSocketTransformer.upgrade). When the preview scanner'sGET /(user-agentnode, fromt3 serve) arrives first, the load fails with:On a ~1,000-file suite this failed 10–104 files per run, on an untouched
maintoo, and retrying a single file failed 3 times in a row. It stops as soon as no T3 window is open. Flutter 3.44.2 and 3.47.0 have the same code.Repro without Flutter. Bind a loopback HTTP server on a random port while the T3 app is open; within ~3 s it gets
GET /from::1, UAnode, andlsofshows the client socket owned by thet3 servePID:Here the listener is not even started from a T3 terminal:
PortDiscoveryprobes every loopback listenerlsof -sTCP:LISTENreturns while any client subscribes tosubscribeDiscoveredLocalServers(the sidebar thread rows do).Ask. Only send HTTP to ports T3 started for a dev server or that the user configured; for anything else use a plain TCP connect check, or let users turn discovery off. #18377 asked for the off switch too.
MAXOUXAX commentedon Oct 29, 2026
Another case, with a heavier failure mode than a failed test: a Next.js dev server on Cloudflare (OpenNext / wrangler).
Under
next dev, workerd opens an internal port on127.0.0.1that isn't meant for HTTP traffic. It inheritsT3CODE_THREAD_ID, so the preview scanner sends it aGET /. Because of a Miniflare bug (cloudflare/workers-sdk#16558), that request loops back into the same port forever. Within seconds the Mac runs out of local ports, and everything else (gh, test runners, agents) fails withEADDRNOTAVAIL. Since the scan repeats every few seconds, it keeps happening as long as the dev server runs.The real bug is on Miniflare's side, but the probe is what keeps setting it off. Ideas that would have avoided it:
http://localhost:3000), not every listener;My workaround: start the dev server with
env -u T3CODE_THREAD_ID, and addhttp://localhost:3000as a configured URL so the preview still works.notgiorgi commentedon Oct 30, 2026
Another case:
gcloud auth loginfails when run from a T3 terminal or by a T3 agent.Why: gcloud starts a one-shot callback server on
localhost:8085(googlecloudsdk/core/credentials/flow.py). It callshandle_request()once, takes the first connection that sends bytes as the OAuth redirect, then shuts down. The preview scanner'sGET /(user-agentnode) arrives within about 1s with nocode=, so gcloud fails. Google's real redirect then hits a closed port.Repro (macOS, Google Cloud SDK 589.0.0): I put a logger on
:8085and opened a gcloud-style auth URL.The same listener started with
env -u T3CODE_THREAD_IDgot no probes in 15s.A
HEADprobe would not fix this. gcloud, Flutter and Unity all treat the first request of any kind as the real one, and the TLS ClientHello probe breaks them too.Workaround: run
gcloud auth loginin a terminal outside T3 Code, such as Ghostty or iTerm, so the listener isn't attributed to T3. Or usegcloud auth login --no-launch-browser, which skips the local callback server.This comment was written by Claude Opus 5.5.
jamipouchi commentedon Oct 30, 2026
Also seen on the stable 0.1.5 release (macOS), with a Rust workspace whose agent-launched test runs bind loopback fakes (a raw TCP 9100 printer, HTTP service fakes). Each new listener got
GET / HTTP/1.1plus a TLS ClientHello every 15–30 s, which the fakes counted as clients: printer tests read the request line as a print job, and property tests counting what a fake received failed. A listener detached from T3's process tree is probed too, as long as it inheritedT3CODE_THREAD_ID.Workaround that holds for us: blank the variable for everything cargo starts, in
.cargo/config.toml:A way to turn discovery off, or limiting it to ports the user opens a preview for, would avoid this.
mwolson commentedon Oct 30, 2026
Another case: Gradle test workers on Linux, T3 Code Nightly 0.1.4 (Oct 30), started by a Claude agent.
Gradle opens a loopback port and waits for its test worker JVM to connect. The port lives about 0.15 seconds. If the scanner's
GET /lands first, Gradle takes it as the worker and the real worker is refused:No test fails, so it reads as a flaky build. It hit 1 of 13 builds here. Probing the port as soon as it opens failed 6 of 6.
env -u T3CODE_THREAD_IDcan miss this one. The daemon opens the port, and it keeps the launch environment of whichever thread started it, so its ports stay attributed to that thread for its whole life, including for later builds from a clean client. Removing the variable from the client still left the daemon's command port probed in 6 of 6 builds. With a daemon that was started without the variable, 9 of 9 builds had no errors at all. A different-Xmxis enough to force a new daemon:Waiting for a port to survive two scans would fix Gradle but not this issue. A stand-in probe that waited 3 seconds passed 16 of 16 builds, because the worker port is gone by then.
gcloud's callback, Unity and Redis all stay open longer than that.dotnet testhas the same shape as Gradle: a deliberateGETon its 0.3 second port aborted the run after 90 seconds, though it passed 30 of 30 under the real scanner.So #26289 (stop preview probes) looks like the right direction to me, since nothing else covers these. One thing to weigh there: with no probe, the list shows everything the agent left running. When I checked, every listener attributed to a thread on this machine was a Gradle or Kotlin daemon (5
javarows, each with Stop), under a card that says it lists web servers. Hiding very young listeners would at least keep short-lived worker ports from appearing, and a wrong guess now only costs a row.Sent by Mike's agent (Claude Opus 5.5 in T3 Code, Claude Code harness)
fix(server): preview discovery no longer sends HTTP or TLS to agent-s…