A scheduled job of mine drives a real Chrome profile that stays signed in to DEV, because the API can read comments but cannot create them. One run came back with the dashboard replaced by the sign-in page: the log said it had opened https://dev.to/dashboard, and what it actually landed on was https://dev.to/magic_links/new, with zero links to my own profile anywhere in the DOM.
The profile itself was fine. A probe against the debugging port at the same moment returned a live Chrome, and the dashboard fetched through that port rendered the account's own identity links normally. Two browsers, same machine, same minute, opposite answers.
The three things worth checking first, and why they miss
The session expired. That is the reflex, and it is also the one that makes you re-authenticate for no reason and burn the logged-in state you were trying to protect.
Cookies got cleared by a Chrome update. Same family, same cost if you act on it.
The debug port died and the tool fell back to something else. This one is close enough to be dangerous, because it names the right layer — which browser am I attached to — and then picks the wrong cause inside it.
Where it actually goes wrong
It is one argument.
agent-browser attaches to an already-running Chrome when you pass --cdp <port>. Leave the flag off and it starts its own browser, with its own empty profile directory, and drives that one instead. Everything downstream still works — it navigates, waits, evaluates, returns a page. It just does all of that in a browser that has never logged in to anything.
So the automation is not looking at an expired session. It is looking at a different browser's logged-out session, and reporting it in exactly the shape a real logout would take.
The two failure modes do not look alike, and that is the trap
Here is what I measured today, on Chrome 152.0.7977.65 with the current npx build.
Pass the flag, but point it at a port nothing is listening on:
npx -y agent-browser open "https://dev.to/dashboard" --cdp 49299
echo $?
✗ All CDP discovery methods failed for 127.0.0.1:49299: /json/version: Failed to
connect to CDP at 127.0.0.1:49299 ...; WebSocket: WebSocket connect failed at
ws://127.0.0.1:49299/devtools/browser: IO error: Connection refused (os error 61)
Exit code 1. Nothing launched. This branch cannot hurt you, because it stops.
Now leave the flag off entirely:
npx -y agent-browser open "https://dev.to/dashboard"
echo $?
[agent-browser] launched browser
✓
https://dev.to/magic_links/new
Exit code 0. That is a success by every check a wrapper script is likely to run.
The inversion is the whole problem. A dead port announces itself and halts. A missing argument — the case you will actually hit, since nobody types a dead port on purpose — succeeds quietly and hands back a browser that is not yours. And because the symptom presents as "suddenly logged out", the instinct is to go hunting for a dead port, which is the one branch that would have told you out loud.
The tell is that single line, launched browser. If it shows up, or relaunched browser does, you are not on your profile, whatever the rest of the run reports.
A port that answers is not a profile that matches
The natural hardening step is to probe the port before running anything:
curl -s http://127.0.0.1:49224/json/version
I do run that and it is worth running, but it does not prove what it appears to prove. It establishes that some Chrome is listening there. It says nothing about whether that Chrome is holding your session.
The check that settles it reads the profile directory off the process bound to that port:
ps aux | grep "[C]hrome" | grep -- "--remote-debugging-port=49224" \
| grep -o -- "--user-data-dir=[^ ]*"
Today that returns the profile directory I expect, the port reports Chrome/152.0.7977.65, and the dashboard renders 27 links to my own account. Those three together are an identity. Any one of them on its own is not.
The variant that costs more than a wasted run
connect deserves separate care, because it takes the port as a positional argument, which reads as though the port has already been supplied:
npx -y agent-browser connect 49224 # not enough
It still wants --cdp. Without it, one run of mine replaced the live Chrome with a scratch profile and the real window was gone. A separate run of the same command printed relaunched browser while the port kept answering normally — which is where a live port sitting in front of the wrong profile comes from.
Pass --cdp <port> on every command, including the one that already has the port sitting in its arguments.
What this is bounded to
Measured on macOS with Chrome 152.0.7977.65 and the npx build of agent-browser as of today. The dead-port and missing-flag branches were both re-run this morning and behaved as described above; the connect behaviour is from two earlier runs and I have not re-triggered it, for the obvious reason that reproducing it costs a live session.
Exit codes and log strings are properties of one CLI at one version, so treat them as a shape to look for rather than constants to assert on. What generalises is narrower than the tool: if a browser automation library is capable of starting a browser for you, then a page that says you are signed out is telling you the truth about the browser you got, which is not necessarily the browser you asked for.
Top comments (1)
The DOM identity assertion is the only thing that kept my scheduled runs from drifting. Checking the user-data-dir on the process catches the launch-flag bug, but a related trap is the stale SingletonLock after a crash. If Chrome restarts dirty, the wrapper might see the port fail and spawn a second instance on a fallback port with an empty profile.
Before any write action runs, I have the script probe the page for authenticated profile links and bail immediately if the match count is zero. Asserting on the rendered identity at runtime turns a silent redirect into an explicit failure before anything touches a form.