The Browser That Works While I Do

There is a Chrome window on my machine that I do not use. It is signed in to everything I am signed in to. It has my cookies, my sessions, my history, my saved logins. All day, a coding agent opens tabs in it, fills forms, reads pages, uploads files, and closes the tabs behind itself. I am working in a different window, on a different thing, and the only evidence that any of this is happening is a log file that grows.

People who see this for the first time ask two questions. The first is “how is that not just headless Chrome,” and the second is “how do you get any work done while it is doing that.” The answers are the whole method, so here they are, in the order I learned them.

Headless is the wrong tool, and it took me a while to admit it

Every browser-automation tutorial starts with a headless browser. Fresh profile, no window, no cookies, driven by a test framework. For testing your own app, that is correct. For an agent doing real work on the real internet, it is wrong in three separate ways.

It is not signed in to anything. Half the work an agent can usefully do for me is behind a login I already have. Recreating those sessions in a clean profile means storing credentials somewhere an agent can read them, which is a worse security posture than the problem I started with.

It is detectable. A surprising number of sites can tell a headless browser from a real one, and their response is not an error, it is a quietly degraded page. Empty search results. A form that submits to nowhere. You will spend an afternoon debugging your selectors before you consider that the site is lying to you on purpose.

And it cannot ask for help. The thing that makes an unattended run survivable is that when it hits a captcha or a login wall, a human can open the window and solve it in ten seconds. A headless browser hits the same wall and has nowhere to go.

So the rule is: the agent drives my actual Chrome, launched with a remote debugging port, and never a headless one. That single decision is the foundation. Everything below is what it took to make it livable.

The focus problem, measured

The first week of this was miserable, and it took me embarrassingly long to say why. Every time the agent opened a tab, Chrome jumped to the front of my screen. Mid-sentence, mid-thought, the window I was typing in was suddenly behind a job board. Dozens of times an hour.

I assumed this was just what browser automation did and that I would have to live with it. Then I got annoyed enough to measure it. I forced my terminal to the front, issued each kind of browser command the agent uses, and recorded which application was frontmost afterwards.

What the agent did Did Chrome steal the screen?
Open a new tab, the ordinary way Yes
Open a new tab through the protocol, default options Yes
Open a new tab through the protocol, marked background No
Navigate an existing tab somewhere new No
Type into a page, click, scroll No
Take a screenshot of a tab No
Run JavaScript inside a tab No

One row explained the whole week. The thief is tab creation. Nothing else. A tab that already exists can be navigated, filled, screenshotted and scripted all day without the window ever moving. Only the act of creating a tab, done the default way, brings Chrome forward.

So there is one small wrapper on this machine that the agent uses for every tab it opens, and the only thing that wrapper does is open the tab in the background. Three of the obvious ways to open a tab are on a written blocklist that the agent reads at the start of every session. I have not had Chrome jump at me since.

A second consequence, which I did not expect: because screenshots and scripts work perfectly on a background tab, the agent never needs to bring a window forward at all. It reads the page by asking it questions, not by looking at it. The window could be minimised for the whole run and nothing would change.

The zero-tab trap

Here is a failure that will make sense of a whole class of mysterious errors. Most tooling that connects to a running browser assumes there is at least one open page to attach to. Close every tab in the debug Chrome and the next connection dies with a message about browser contexts, which tells you nothing.

The instinctive fix is to open a scratch tab first. The instinctive way to open that scratch tab is the default way, which is the one that steals focus. So the preflight step that runs before every batch was, for a while, the single most frequent interruption on the machine.

The fix is the same wrapper. Open exactly one scratch tab, in the background, reuse it for the whole session, close it at the end. It sounds trivial written down. It was not obvious in the moment, because the error message is about contexts and the cause is about focus.

One writer per tab

The next thing that went wrong is the one I would warn anyone about, because it does not look like a bug. It looks like the website changed.

I had, at one point, three separate agent processes that all believed they owned the same tab. Each one would navigate it, wait for a page to settle, and read the result. Except by the time it read, another process had navigated the tab somewhere else. The symptom was that forms which had worked for weeks suddenly reported “no submit button found.” The button was there. The page it was looking at was a different page.

The rule that came out of it: one writer per tab, enforced with a lock file, and every process identified by its own id, not by pattern-matching its name. The second half matters more than it sounds. My first attempt at cleanup killed processes by searching for a name, and it missed the ones that mattered because their name was a language runtime, not a script. Locks by id. Kill by id. Nothing else.

Trusted clicks and the dropdown that ignores you

A lot of modern web forms are not forms. They are JavaScript components pretending to be forms, and they only respond to input that looks like it came from a person. A dropdown that opens for a real mouse will not open for a programmatic click, and it will not tell you why.

What works is to send the event the way a person produces it: a press, a pause of about a fifth of a second, a release. The pause is the part everyone omits. Without it the component sees a press and a release in the same tick and discards them. With it, the dropdown opens, and everything downstream of the dropdown starts working.

Two more of these, because they each cost me an afternoon:

The human in the loop is a feature

None of this makes the browser fully autonomous, and that is the point. The agent runs a batch. When it hits something it genuinely cannot do, a captcha, a second-factor prompt, an expired session, it writes a line to a file that I read when I sit down, and it moves on to the next item. When I have two minutes, I open the tab, solve the thing, and the next run picks it up.

This is the difference between a system that runs unattended and one that pretends to. An unattended system is not one that never needs a person. It is one that needs a person for two minutes a day instead of two hours, and knows the difference.

The pieces, in one list

If you want to build the same lane, this is what it consists of. None of it is exotic. All of it is the result of something going wrong.

  1. Your real Chrome, launched with a debugging port. Never headless.
  2. A wrapper for opening tabs that only ever opens them in the background, and a written ban on the three default ways of opening one.
  3. One scratch tab per session, opened at the start, reused, closed at the end.
  4. A lock per tab, held by process id.
  5. Human-shaped input for components that ignore programmatic events.
  6. A needs-a-human file that the agent appends to and never blocks on.
  7. A closing rule: every tab the agent opened gets closed, and the browser is handed back exactly as it was found.

The wrapper, the lock, the rules file the agent reads, and the pacing that keeps a site from noticing the agent at all are the parts I keep to myself, or install for people. The shape of it is above, and the shape is most of the value.