My automation worker runs on a Mac mini and does most of its work through Chrome: reading LinkedIn, replying on Threads, following people. Before any browser action it runs a check I call Gate 0. The check proves two things. The Chrome it's about to drive is the mini's own, and the tab in front belongs to the Claude tab group the extension works in. If either one fails, the phase stops. A worker that clicks in the wrong browser is worse than one that does nothing, so this check is supposed to fail closed.
Yesterday it failed three times: the Gate 0 CLI exited 1 at 10:15, 12:30 and 18:30. The fix
went in at 01:36 this morning as commit 4108334, and it's small. The bug is worth writing
up anyway, because the check did nothing wrong. It was offered the wrong tab.
What happened
Chrome's extension only delivers clicks to the active tab. So an earlier version of Gate 0 learned a recovery step: if the verdict fails, list Chrome's tabs over AppleScript, find the one whose title matches the page the phase expects, bring it to the front, and look again. That step had fixed a class of failure I used to fix by hand.
The function that chose the tab was called tabToActivate, and it did this:
const at = w.tabs.findIndex((title) => typeof title === "string" && title.includes(want));
That returns the first match. Yesterday window 1 held several Threads profile tabs, all with the same title. The first one was an ungrouped stray, left over from some earlier page load. Gate 0 activated it, looked again, and said exactly what it should have said: in front but not in a Claude group. Then the CLI stopped. The grouped tab was two places further along and was never offered.
So all three failures looked the same. Gate 0 was correct about the tab it was shown. The picker only ever showed it one tab.
The fix
tabToActivate now takes the list of tabs it has already tried and refused, and returns
the next same-title tab it hasn't tried. The CLI loops: pick one, activate it, look again,
and stop when a verdict passes or no candidates are left. The tab list is finite, so the
loop ends. When every candidate has been tried, it returns null and Gate 0 fails exactly
as it did before.
The part I cared about most was what didn't change. The Claude-group check is untouched. The fix makes the search wider but keeps the standard the same. It's easy to "fix" a failing safety check by relaxing the check. What was broken here was the search, and the commit only changes that.
There are two new tests. One builds a window with a matching tab, a New Tab, and another
matching tab. It passes tab 1 as already tried and expects tab 3 back. The other marks
every candidate as tried and expects null. Both failed on the old code, one returning
null and the other tab 1. After the fix, the gate0 and gate0-cli tests ran 22 passes,
exit 0.
The morning audit was the real test, and Gate 0 passed on the CLI's first run: activated tab 5 of window 1 … in a Claude group. That's the fix working on the live machine, on a tab that wasn't the first match.
The second thing the same night turned up
The overnight review that found this also found something I like less. On a day with three
Gate 0 exit-1s, the check log for gate0 shows five passes and zero failures.
Every check in my worker can print its verdict as JSON so it can be filed in a table. That table is how I tell, a week later, whether a check is catching more or less than before. The failing first attempts yesterday weren't run with that flag. The passes that came after the manual recovery were. So on the one day Gate 0 really struggled, the record said it was perfect.
That isn't a code bug. It's the phase choosing when to run the check with its verdict recorded. But the effect is the same as a bug. Anyone counting failures in that table would conclude Gate 0 never fails, and they'd stop looking at it.
What it taught
Two lessons came out of one night.
First, when a check says no, look at what it was given before you look at the check. The
Claude-group rule never needed doubting, because it was right every time. The mistake
was upstream, in an innocent-looking findIndex that assumed titles are unique. They
aren't, and a browser that has been driven all day collects duplicate tabs.
Second, a verdict counts only if it's recorded whichever way it comes out. A check that files its passes and drops its failures is worse than one that files nothing, because an empty table at least looks empty. If I want to know how often something fails, I have to record it every time it runs, and most of all when it fails.
