Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
-
In a T3 Code thread, ask the agent to start a listener on a random local
port. For example:import http.server, socketserver, time class H(http.server.BaseHTTPRequestHandler): def do_GET(self): print(time.strftime("%X"), self.command, self.path, dict(self.headers), flush=True) self.send_response(404); self.send_header("Content-Length", "0"); self.end_headers() s = socketserver.ThreadingTCPServer(("127.0.0.1", 0), H) print("listening on", s.server_address[1], flush=True) s.serve_forever()
-
Don't open a preview, and don't point any client at the port.
-
Watch the listener's output for a minute or two.
The listener doesn't have to be a dev server, or anything the user means to
preview. It can be a child of anything the agent runs, such as an HTTP fake
inside a test suite.
Expected behavior
No requests arrive. Starting a process isn't asking for a preview of it, and
nothing outside the user's own tools should be talking to the port.
If T3 needs to detect ports for previews, it should:
- only probe when the user opens or asks for a preview;
- offer a setting to turn automatic port detection off;
- rely on the
lsof/netstatlistener scan it already runs, and not send
HTTP at all. Where an HTTP check is unavoidable, use a dedicated user agent
such asT3Code-PortProbe/<version>, so servers can recognise and ignore it; - back off on a port that has answered 404, rather than retrying every 16
seconds indefinitely.
Actual behavior
Requests arrive about 1.6 s, 17.3 s and 33.0 s after the port opens, and then
every ~16 s for as long as the port stays open. Each one looks like this:
GET / HTTP/1.1
host: localhost:<port>
connection: keep-alive
b3: <trace id>-<span id>-1-<parent span id>
traceparent: 00-<trace id>-<span id>-01
accept: */*
accept-language: *
sec-fetch-mode: cors
user-agent: node
accept-encoding: gzip, deflate
The same trace id is reused across every probe and every port. In one
observation window, eight idle ports each got 6 probes in 90 seconds, 48
requests in all. Running lsof on the connecting socket's source port while a
probe was in flight showed it belonged to the t3 serve process.
Which ports get probed: the server's PortDiscovery lists listeners with
lsof -iTCP -sTCP:LISTEN every 3 seconds, while a client is subscribed to
discovered local servers. It then probes over HTTP only the listeners it
counts as owned. A listener is owned if it was started in a T3 terminal, or if
the listening process has T3CODE_THREAD_ID in its environment (read with
ps -E). Every command an agent runs inherits that variable, and so does every
child process those commands start. I tested this by running two listeners
side by side from an agent's shell for 40 seconds. The one with the variable
was probed; the one started under env -u T3CODE_THREAD_ID was not. A
listener started from a plain Terminal window doesn't have the variable either.
Any local server that logs, counts or reacts to requests sees these as real
traffic:
- Test suites: fake HTTP servers that record every request fail at
random, because a probe landing mid-test is a request the test never made.
In one project, every full run of a ~960-test suite failed a different 1–4
tests until the fixtures were taught to ignore the probe (see Workaround). - Dev servers: the probes show up in access logs, request counters,
hot-reload logs and rate limiters. - Side effects: a server whose
GET /does something, such as a one-shot
OAuth callback listener or a webhook receiver, has it triggered.
Impact
Major degradation or frequent failure
Version or commit
0.1.5
Environment
macOS 27.0.1
Logs or stack traces
Screenshots, recordings, or supporting files
No response
Workaround
There is no setting to turn the probing off. Settings has no port-detection
entry, and the server's discovery code reads no setting before it probes.
There are two workarounds:
-
Start the process without T3's thread id, so T3 doesn't count its
listeners as owned:env -u T3CODE_THREAD_ID pytest
This stops the probes completely. But it only works for commands you can
wrap, and anything else T3 uses that variable for no longer sees the
process. -
Recognise the probe in the server and ignore it. A probe is a
GET /
whoseuser-agentis exactlynode. Match the header name
case-insensitively, because Node sends every header name in lower case,
and some servers' header lookups are case-sensitive. Answer the probe with
an empty 404 and leave it out of whatever the server logs or records. For
example, in a Pythonhttp.serverhandler:def is_port_probe(handler) -> bool: headers = {k.lower(): v for k, v in handler.headers.items()} return handler.command == "GET" and handler.path == "/" \ and headers.get("user-agent") == "node"
This has limits:
- It only helps servers you control. Third-party dev servers still log and
count the probes. - It also ignores any real client that sends
GET /as
user-agent: node, such as a barefetch()from a Node script. - It depends on the probe's current headers, so it breaks if T3 changes
them.
- It only helps servers you control. Third-party dev servers still log and
Activity
juliusmarminge commentedon Nov 2, 2026
Thanks for the detailed report. This is fixed on main by bf8e132 (
fix(server): preview discovery stops sending HTTP and TLS to agent-started ports, https://git.cafe/pingdotgg/t3code/pulls/27893), which first shipped in v0.1.6. Your 0.1.5 build predates it.Preview discovery still lists listeners it attributes to a thread, through the same
lsof//procscan and theT3CODE_THREAD_IDenvironment. It no longer sends them any request. The only URLs it still requests are preview URLs you configured yourself.What I ran, on Linux with a fresh dev database for each build. A thread's agent started your listener script, with the thread details panel open so a client was subscribed to discovered servers:
GET /withuser-agent: nodeand the same trace id at +0.4 s, +16.0 s and +31.5 s, then every ~15.5 s. That was 7 requests in 94 s, matching your capture.T3CODE_THREAD_ID. One was started by the agent; the other was a grandchild process that inherited the variable, like a test-suite HTTP fake. Both appeared under Servers as "Agent · python3". Neither got a single request in ~2 minutes.The regression test
discovers owned listeners without sending HTTP or TLS probesinapps/server/src/preview/PortScanner.test.tscovers this. Updating to v0.1.6 or later should stop the probes, so I'm closing this. Reply if you still see them on a current build.dgregorybm commentedon Nov 2, 2026
Thank you Julius! Sorry for the unnecessary report.