Hermes just asked you to clear a browser check? Here is what the 428 challenge on the free tier is

It is two in the morning. Your cron job is running on a server, using the free tier for a round of research. There is no error in the logs — just a link someone printed, and then the job fails quietly. In the morning you read it and have no idea what that link was, why it appeared, or whether you were supposed to click it.
Nothing in your setup is broken. The free tier simply has a gate in front of it now. PR #133824, merged October 6, teaches Hermes to handle that path properly — and, more importantly, makes sure it never sits there waiting when there is nobody to wait for.
When the gate appears
Free anonymous inference goes through POST /api/anonymous/token for a token exchange. The portal can be configured to require a browser check from chosen IP ranges or countries before handing out anonymous capacity. When such a rule fires, that endpoint answers HTTP 428 with the code challenge_required and a portal URL.
The important part: this is portal-side policy, adjustable at any time without a Hermes release. So “it worked fine last week” usually has nothing to do with your machine.
One primitive, three steps
Hermes implements exactly one primitive for this, called browser-v1:
- open the URL the portal handed over;
- poll
/api/anonymous/challenge/statusuntil it clears; - exchange for a token again.
Everything the page itself does — bot detection, in-browser proof of work, an interactive fallback, all of its copy — belongs to the portal and can change without you updating anything. And the mint stays the authority: Hermes never decides locally that a check passed.
The exchange sends a self-reported client block (surface, version, platform, plus capabilities: ["browser-v1"]) and a hermes-agent/<version> user agent.
Who opens the window
Behaviour differs per surface, which is the answer to “why did I see nothing”:
- Desktop: that process opens nothing itself. It publishes the challenge (a
free_tier.challengeglobal event, plus thechallengefield onfree_tier.statusfor a client that connects later); the renderer hands it to the Electron main process, which loads it in a hidden, sandboxed, origin-pinned window. That window is only revealed if the page explicitly asks for a human. - Terminal: print the link, and open it in the system browser when there is a graphical one.
- The wait runs outside the locks: the exchange that hit the 428 already unwound its exception out of the
withblocks, so a slow page never stalls a sibling profile or process. One challenge is worked at a time per process; a second thread hitting the same 428 waits for the first and then simply exchanges again.
When it absolutely will not hang you
The riskiest spots were in the first version of this work, and fixing them is most of what this PR does. They are all familiar shapes:
- A gateway never waits. Gateways resolve Nous credentials synchronously from async code, so a 428 parked the event loop for up to 120 seconds — and nobody sits at a bot’s console to clear a browser check. The gateway surface now records, announces, and raises a retryable error immediately.
- The proxy adapter does not stall the lock. It resolves credentials while holding the lock every proxied request queues on, so a 120-second pause blocked everything. It now resolves as a background caller.
- Background readers never open anything. Keepalive ticks and status paints receive the challenge but never block, print, or open a browser.
- The stdio TUI finally tells you. It used to keep the gateway child’s stderr as a log only, so a challenge became a silent 120-second hang with no link. Required foreground challenges are now announced over JSON-RPC and the TUI prints the link.
Required and optional are different things
A challenge can also arrive on a successful exchange with required: false — the service is measuring, not enforcing. That one is only ever announced to a desktop client (so a hidden window can clear it early); it is never opened in your browser and nothing waits on it.
A required challenge does make an interactive surface wait: up to 120 seconds per attempt, which bounds the “a human is solving a captcha” case without parking a request for the ticket’s full ten minutes. A failed announcement (say, a problem on standard output) falls back to printing the link rather than losing it and failing the run.
What happens when it does not clear
- A challenge that never clears ends with retryable copy and a fail-fast cooldown instead of endless retrying.
- A second
challenge_requiredis not looped on — it surfaces as a retryable error with an explanation. That is deliberate: a loop would be worse than a failure. - A
pow_*error code on a non-428 status keeps its original meaning (it is no longer treated as a retryable server error across every endpoint), so sign-up and promotion flows are unaffected.
What to do about it
If your machine is unattended (a server, a container, CI):
- open that link in your browser and clear the check; the ticket belongs to that exchange, so let Hermes retry afterwards.
- if this keeps happening, consider running scheduled work on signed-in Nous account capacity instead of the anonymous free tier — the account path does not go through the anonymous check. See this write-up on the free search route for the related diagnostics.
- On desktop, do nothing: the hidden window handles it in the background and only appears if you are needed.
The short version
The check is not a bug; it is portal policy doing its job. Hermes’ responsibilities are narrow on purpose: put the link in front of you, never block where nobody is watching, and never conclude locally that a check passed. Once you know that division of labour, a link in your logs is a five-second fix rather than a lost morning.
If you would rather not depend on the free tier at all, compare five free search channels.