The browser
SkillWeb & browsingDrives a real browser from the shell: open, read, click, fill, sign in, download. Use it before the first browser command of any web job: a page, a form, a checkout, a sign-in, their own Chrome.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the The browser skill
What this skill tells your AI
The instructions your AI receives, as published by cgoinglove/thursday in skills/browser/SKILL.md and read by ahel’s review.
You drive a real browser from your shell with playwright-cli: open a page,
read it as a snapshot, act on elements by ref.
In this app
Everything the app has to say about the browser is here; the prompts do not repeat it.
Every browser goes through here. A job that names a browser, a site, a URL,
a sign-in or a tab is this one's. With --headed the window playwright-cli
opens is a real one on their screen — they can watch it and type into it. Never drive Chrome by
clicking it through the machine (osascript, peekaboo, System Events): that
window cannot be snapshotted or acted on by ref.
The browser they already have open is reached through its extension. For a
tab they left or a profile they are signed into, playwright-cli attach --extension=chrome
opens a page in the Chrome they use and waits until they allow it there. The tab
they pick on that page — one they left open, or that page itself — is yours,
signed in as they are; their other tabs stay theirs. It needs the Playwright
extension in their Chrome: when the command says it is missing, give them the
link it prints and ask. The link to their Chrome can drop between commands: when
one answers that the browser is not open, attach again — open would start a
browser of your own instead. A click there waits on a tab that is in front:
--raw run-code "async page => page.bringToFront()" first.
Your browser is your own on this job. Its session is already in your shell
(PLAYWRIGHT_CLI_SESSION): never pass -s=, never close-all or kill-all —
other bots and jobs are running theirs. A follow-up uses this same session:
read snapshot to continue where you left off; use open only if it is closed.
Headless is yours; headed is theirs. A browser opens headless: nobody sees
it, and the app keeps it for follow-ups until the job's workspace expires.
Finding, reading and comparing stay headless, however many pages it takes: a
window that opens by itself lands on whatever they are doing, a call included.
--headed puts a real window on their screen, and that window outlives the job
until they close it or cancel or delete the job. It is for three things: the thing itself
once the job has it — the products they asked you to find, the order sitting at checkout
for them to confirm, the map with the pin dropped, the page they asked to watch — a
sign-in for them to finish, and a payment for them to press. Open it --headed, leave it
open, and say in your answer that it is on their screen. A window you opened
headed only to get past a wall and are done with, close yourself. A window is never how
you look at your own work: a page, a deck or a canvas you made is shown to
you by its skill's shots and look_at, and reaches them under your answer — nor is open on their
desktop. Only a job they cancel or delete takes its windows with it.
A payment is theirs to press. Take a purchase, a top-up or a subscription
as far as the last screen before money moves, open it --headed, and answer
with what it buys, for how much, and that the pay button is on their screen.
That is the job done, not stopped — never press it yourself, whatever the
request said.
A site that will want a session before it shows you anything — a shop, a
console, a dashboard, an account page — starts --headed --persistent rather
than finding out headless and starting over.
A wall is the site's answer. A 403, an "access denied" page, a check for
automated traffic or a captcha is the site saying no to a bot, and getting round
it is not yours to do: no other browser, profile or disguise, no waiting it out.
Take what the job needs from where it is published otherwise — another source,
the site's own API, a search. When only that page will do and it is one they
would open themselves — the shop they named, their bank — open it --headed for
them and ask, as a sign-in below: a person may pass where a bot may not.
Otherwise say in your report which url refused you, and bring back what you have.
A sign-in is a fork, not a wall. Three ways through, and which one fits is yours to read off the job:
- A sign-in the app already keeps. Cheapest when it works, so try it before
the other two. It goes into the browser you have open, so the order is
open, then thesign_in_usetool with the site, thengoto— a state loaded with no browser open is refused, and one loaded before youopenagain is thrown away with the browser that held it. Open the window you mean to keep first, headed or not.gotolast because cookies without a reload leave the signed-out page that was already drawn, which reads as an expired session when it is not. - Their own Chrome. Attached to it as above, you are signed in as they are.
It is the way through for a site that still shows you signed out right after
sign_in_use— some refuse a sign-in carried between browsers, and signing in again in your window will not last there either. - They sign in themselves.
open <the login url> --headed --persistent— the login page, not the front door. For another account on a site the app already keeps, leave out--persistent: a new browser holds none of them, and signing out of a kept one to make room can end its session for every bot. Then aquestionthrough Thursday, to the user, in one line saying what to sign into and that the window is open, optionsSigned in/Not now, and stop. The window stays open while the job waits; continue from a freshsnapshotwhen the answer comes. A captcha or a code sent to their phone is the same move. Notshow— it blocks waiting for annotations nobody will send. Once they are in, call thesign_in_keeptool: the app keeps the sign-in for your later work, and they can sign out of it in Settings. The window was for the sign-in, so it closes and your browser goes on without one, signed in, on the same page — unless what comes next there is theirs to see, the products to choose or a checkout to confirm: then passkeepWindowand it stays.
Never guess or invent a secret, or go looking for one somewhere they did not
point you at. An email or username they gave you is not a secret — fill it and
press next. memory_recall the note for that site when you are missing the
address they use.
Reading a page. Every command writes a snapshot file and prints its path.
find "Create Key" returns only the matching nodes with a few lines around
each — use it when you know what you are after. cat the file when you need
the whole layout. snapshot e34 is one region; --raw eval is one value.
Pulling the markup in (innerHTML, curl) spends a page of context on a
navigation menu; the snapshot is the same page for a fraction of it. On anything
long, find and snapshot <ref> are the cheap way through — keep raw HTML for
when the markup itself is what you need. A page that draws itself from a fetch
is cheaper at the source: requests numbers what it pulled, response-body N
prints one — the data already parsed, rather than read back out of the DOM.
What only the eye can tell — a chart, a photo, whether the layout came out —
is a screenshot and then the look_at tool, when you hold it.
Refs go stale after anything that changed the page — snapshot again before
the next click. A page that looks empty right after loading is still rendering.
A click that seems to do nothing may have opened a tab: tab-list,
tab-select N.
One command, not one turn per step. Chain steps that need no look in between
with &&. What a click brings in is not there on the very next read: wait for
its text in the same command, --raw run-code "async page => { await page.waitForFunction(t => document.body.innerText.includes(t), '<text>'); }"
— getByText(…).waitFor() times out when the first match is hidden.
Search and locale. A search engine's results page is for people, and most
refuse a browser a script drives. Find urls with the web_search tool; without
one, search where the answer lives — the site's own search box, Wikipedia — and
say in your report that you had no web search. Pages answer in this machine's
language and currency; put the country in the url when the job is about another.
Pictures for a document come off the page, never from memory:
--raw eval "JSON.stringify([...document.images].filter(i => i.naturalWidth > 200).map(i => i.currentSrc))".
Local HTML. file: URLs are refused, and a fixed port lets two jobs
capture each other's pages. To open or print one:
node <skill dir>/scripts/serve.mjs <dir> > <scratch>/serve.log 2>&1 &, read the
address from that log, goto <address><file>.html; pdf --filename=out.pdf
prints it, in a headless browser. Kill the server when done.
Scripts in <skill dir>/scripts: sheet.mjs puts many pictures on one
image for a single look_at, webimage.mjs saves a page's own picture with
its credit line, and session.mjs lets a script of yours drive this session —
references/scripts.md. What one run-code call can do — several reads at
once, a wait, a download, the clipboard — is references/run-code.md; a
recording of the screen for the user is references/recording.md.
Snapshots land in .playwright-cli/ and are cleared after a few days. A file you
name — --filename=, a pdf, a download — goes under scratch/, or
your folder under artifacts/ when it is the result.
Install
The app fetches the browser itself when it starts — Google Chrome for Testing, set
in your shell, never the user's own Chrome — so this is only for a machine where
that never finished. open failing with a missing executable is the tell:
playwright-cli install-browser chromium
It downloads ~280 MB and prints nothing when the build is already there. If it fails, say so and stop — do not fall back to a browser you found on the machine.
On Linux, open failing because the browser closed before any page, with an
empty browser log, is missing system libraries rather than a missing download.
Installing them needs administrator rights: tell the user to run
npx playwright install-deps chromium once, and stop.
Commands
playwright-cli open # headless; add a url to navigate at once
playwright-cli open <url> --headed # a real window on their screen
playwright-cli open <url> --persistent # keep a profile between opens
playwright-cli open --mobile # mobile layout — lighter pages, smaller snapshots
playwright-cli attach --extension=chrome # their own Chrome: the tab they pick
playwright-cli goto <url>
playwright-cli go-back | go-forward | reload
playwright-cli resize 1280 800 # before a screenshot or a pdf
playwright-cli snapshot # the page, as refs — written to a file
playwright-cli snapshot e34 # one region
playwright-cli find "Sign in" # matching nodes with context
playwright-cli find --regex "/sign (in|up)/i"
playwright-cli click e3 # also: dblclick, hover, drag e2 e8, drop e8
playwright-cli fill e5 "text" --submit # --submit presses Enter after
playwright-cli type "text"
playwright-cli press Enter # ArrowDown, Escape, …
playwright-cli select e9 "option-value"
playwright-cli check e12 | uncheck e12
playwright-cli upload ./file.pdf
playwright-cli dialog-accept ["text"] | dialog-dismiss
playwright-cli tab-list | tab-new [url] | tab-select N | tab-close [N]
playwright-cli requests # what the page fetched, numbered
playwright-cli request 7 # one of them whole: headers, body, response
playwright-cli response-body 7 # just the body
playwright-cli eval "document.title"
playwright-cli eval "el => el.textContent" e5
playwright-cli --raw eval "…" # only the value, nothing else printed
playwright-cli screenshot [e5] [--filename=x.png]
playwright-cli pdf --filename=page.pdf
playwright-cli close
Targets are refs from the snapshot (e15). A css selector or a Playwright
locator works too: click "#main > button.submit",
click "getByRole('button', { name: 'Submit' })".
Signals
- GitHub stars
- 21
- Forks
- 2
- Last commit
- Oct 2026
Advanced
- Item type
- skill
- Key
browser-cgoinglove- Source
- github.com/cgoinglove/thursday