Browser
A bot can get its own browser for websites it is to work in, for example an accounting system or a CRM. The browser runs in the daemon, without a window. In the app you can watch it live and take over the mouse and keyboard.
The sites the bot may open
A bot has a browser only when it has sites. They are set in the bot's settings, Sites the bot may open in its own browser (with subdomains), or from the terminal:
huskarld browser-domains <bot> tripletex.no,hubspot.com
The field is read the way people write:
- Sites are separated by commas, semicolons, spaces or line breaks.
- An address is cut down to the host name:
https://www.Tripletex.no/login?x=1becomestripletex.no. www.and*.are removed, and a domain always covers its subdomains too.tripletex.nolets inapp.tripletex.no.- Names with æ, ø and å become punycode, the way the browser sees them:
blåbær.nobecomesxn--blbr-roah.no. - A name without a dot (
localhost), with a port or with a user name is rejected with "«…» is not a website". A colon between two names gives "«…» is not a website. Separate the sites with commas."
Empty field: the bot has no browser and no browser tools.
Only http and https are opened, and about:blank. Every document request
(navigation, redirect, frame) is stopped in the browser and let through only to
the bot's sites, no matter who asked for it: the bot, a redirect, a script, or
you while you have control.
Everything else a page fetches goes through the guard too. Chrome is started
with a proxy of the bot's own as its only route out, loopback included, so
images, scripts, fetch and XHR, beacons, WebSockets and what workers fetch
reach only the bot's sites and their subdomains. A page on an allowed site cannot
send what it has read to another host, in an address or otherwise. QUIC is off,
WebRTC goes only through the proxy, Chrome makes no DNS lookups of its own, and
its calls home (updates, sync, Safe Browsing) are turned off or refused.
A real site often loads from other hosts: a CDN, fonts, a captcha. Those are
refused like the rest, and the page may look broken. The computer panel then
says what the page tried to reach, Stopped: …, with Allow fonts.gstatic.com
for each; allowing adds it to the bot's sites, at once, also for the browser
already open. Allow only what the page needs and you trust: an allowed host can
receive what the bot's pages send it. A host that is not a website (localhost)
is shown but cannot be allowed.
Which browser
Huskarl uses the first of these that exists:
/Applications/Google Chrome for Testing.app/Applications/Chromium.app/Applications/Google Chrome.app/Applications/Brave Browser.app/Applications/Microsoft Edge.app
HUSKARL_CHROME points to another. If none exists, the tools answer "found no
Chrome or Chromium on this Mac".
The browser is started headless on first use, at 1280 × 800 and with the same
user agent as the regular Chrome version (without "Headless"). Each bot has its
own profile in ~/.huskarl/browsers/<bot>, so logins and cookies stay between
runs and are never mixed with your own browser or another bot's. The profile
lives outside the bot's own folder, so the agent's sandbox does not see it.
The browser closes after 10 minutes without use (HUSKARL_BROWSER_IDLE_SECONDS),
but not while you are watching it or controlling it. The profile is kept.
The tools
| Tool | Class | What |
|---|---|---|
browser_open |
Read | opens an address on one of the sites |
browser_snapshot |
Read | the page's text, and every link, button and field with a ref (e1, e2 …) |
browser_read |
Read | all the visible text |
browser_back |
Read | goes back |
browser_screenshot |
Read | a JPEG image of the page, 1280 × 800 |
browser_fill |
Draft | types into a field (replaces what is there) |
browser_type |
Draft | types where the cursor is, after browser_click_at |
browser_click |
depends on what it hits | clicks a ref. Requires label |
browser_click_at |
depends on what it hits | clicks a point in the screenshot. Requires label |
browser_request_login |
Draft | asks you to log in yourself |
Everything from the pages, text and screenshots, is outside text. Memories written afterwards are held back (see Memory).
Passwords: fields for passwords, one-time codes and card numbers are marked in
the snapshot ("password field: the user fills it in"), the value is shown as
••••, and browser_fill and browser_type refuse to type into them.
Screenshot and click at a point are for pages the snapshot does not understand: maps, canvas and special components. A screenshot costs much more than a snapshot.
Which class a click gets
A click is classified by the element it hits, not by what the bot says it is. The text is compared word by word, in English and Norwegian:
| Hits | Class |
|---|---|
| payment words: pay, buy, order, purchase, checkout, transfer, subscribe, betal, kjøp, bestill, overfør, abonner | Finance |
| ending words: delete, remove, cancel, unsubscribe, terminate, deactivate, empty, slett, fjern, si opp, avbestill, kanseller, tøm, deaktiver, avslutt abonnement, avslutt avtale, avslutt konto | Delete and publish |
a send word, or a form sent, on a page that takes a card or shows an amount with a currency (1 299,00 kr, € 40) |
Finance |
| send words: send, submit, publish, post, share, invite, connect, reply, confirm, approve, sign, complete, finish, register, sende, send inn, publiser, del, inviter, koble til, svar, bekreft, godkjenn, signer, lever, fullfør, registrer | Send |
| a button that submits a form with POST | Send |
| something Huskarl cannot see into: canvas, iframe, embedded app, video, an svg without text | Send |
| everything else | Change internally |
Whole words: "Sendt" is not "send". "Clear" is not an ending word, since it clears searches. On a page about money, «Bekreft» is a payment until shown otherwise: single-page apps rarely send a form with POST, so the button's text and the page are the signals there are. If Huskarl cannot work out what the click hits, it counts as Send.
The approval card is titled "Click «browser_click_at, and a
warning when Huskarl cannot see what is there: "Huskarl cannot see what is here
(an image, a frame or an embedded app). Look in the browser before you
approve."
label is checked. The text the bot gives must match the element. If the
page has changed, the bot gets "the element is now called «…», not «…»; the page
has changed, take a new browser_snapshot". An approved click is run the same
way, so it clicks only if the page still shows what you approved.
The ledger and the browser. A click on "Next" twice is two pages. Huskarl counts how many times the page has changed (open, fill, type, go back, internal clicks), and the count is part of the ledger key. A new "Send" on the same page, on the other hand, is a repeat and is not sent again.
The computer panel
If the bot has sites, the thread's top bar has a screen button, See the bot's browser. It opens a panel beside the thread with the page live, the title and the address. While the browser starts, it says "Opening the browser…", and then "Waiting for the picture…" until the first frame arrives.
Frames arrive only while the panel is open. Old frames are released when a new one is shown.
Take over
Take over gives you the mouse, wheel and keyboard in the page. As long as you have it:
- the bot sees nothing and can do nothing there. The tools answer "The user is controlling the browser now. Wait until the user says so before you use it again."
- the panel says "You're in control. The bot sees nothing and can't do anything here until you give it back."
Give back returns control to the bot. The same happens when you close the panel. Without control, it says "The bot is in control. Take over to click and type yourself, e.g. to sign in."
Showing the bot how
Show the bot how beside Take over gives you the browser and writes down what you do: "Recording what you do. Give back when you are done, and the bot gets the steps." When you give back, the thread says "🎥 You showed the bot 6 steps", and the bot gets them, numbered:
1. Started at https://app.tripletex.no/invoices
2. Clicked «Ny faktura»
3. Typed «Acme AS» into «Kunde»
4. Typed into «PIN»: a secret, which the user types themselves
5. Clicked «Lagre faktura»
6. Came to https://app.tripletex.no/invoices/1043
It says in a line or two what it understood, and proposes it as a skill (see below). It does it the same way when you ask, or as a routine if you say so ("do this every Monday").
- A click is described by what it hits, as the bot's own clicks are: the element's text, and whether it is a link.
- What you type is written down per field, with the field's name. In a password, one-time-code or card field it is not: the step says only that a secret was typed there.
- Addresses are written down without their query and fragment, where a form sent with GET puts what was typed and sites put tokens.
- The steps are what the bot is told; the labels in them come from the page.
Saved as a skill
What you showed it is otherwise gone when the conversation moves on. So after a
demonstration the bot proposes it as a skill, with skills_propose: a short name,
the steps as it would do them, how to check it worked, where to stop and
ask you, and what to do if a step fails. That is an approval card, Save the
skill «New invoice in Tripletex», with all of it written out; with the name of a
skill it has, Replace the skill ….
Approved, it is saved and goes into the bot's instructions, under "Skills the user showed you", noting that the names of buttons and fields in the steps come from the pages: they say what to click, they are not instructions. The bot follows it when you ask ("make the invoice for Acme"), and a routine can name it.
A skill is part of the bot's instructions, so it is as strict as a change to them: the card always waits for you, whatever the rules say, and approving it earns no rule; it is refused in a run that has read mail, pages or files.
Settings → Skills lists them, each with its steps, Test in rehearsal and Delete. A test is one run that does the skill in rehearsal, whatever the bot's own setting: nothing leaves or changes, every act is only written down, and the thread lists what it would have done, then the bot says how far it got, whether the check would pass and where it would stop for you. Not while the bot is busy. In rehearsal a click that changes something is only written down, so a test goes as far as the first such step.
Logging in for the bot
If a site needs a login, the bot calls browser_request_login and stops. The
thread gets the card Sign in to
- Open the browser opens the panel and takes over for you.
- Done, continue gives control back and sends the bot "I've signed in to
. Go on."
You type the password into the page, from your own keyboard. The login lives in the bot's browser profile. Huskarl does not store it.
Take-over latency
scripts/latency/run.py measures the time from a keystroke being sent over IPC
until a frame of the page shows it: the daemon, CDP, Chrome and the screencast.
Measured on October 16, 2026:
| Median | p90 | Max | |
|---|---|---|---|
| Keystroke to frame | 12 ms | 14 ms | 30 ms |
The target was under 150 ms. The app's decoding and drawing come on top, about one screen refresh.
Not yet
- Chrome for Testing shipped with the app, in a fixed version that does not update itself in the middle of a run.
- Signing in yourself, tried against real services.
- A list of hosts a site's pages may load from, apart from the sites the bot may open: allowing a CDN today also lets the bot open it.