Turn a live browser session into a fast headless monitor. Use when the user says "monitor this website / page / dashboard", "watch X for changes", "tell me when Y updates", "keep an eye on this", or wants a login-gated page polled cheaply. The agent drives the site once with Playwright MCP (open, look, log in), finds the real HTTP request behind the data, and generates a standalone curl_cffi Python script (real Chrome TLS fingerprint, persisted cookie jar, reverse-engineered/refreshing login, change-diff, hook notification) that replays that request forever without a browser. Runs with uv.
Goal: the agent should not babysit a browser to watch a page. It drives the
site once with Playwright to learn the request, then emits a small curl_cffi
script that replays that request headless, forever, and pings back on change.
Slow, expensive, human-shaped (Playwright, once) → fast, cheap, machine-shaped
(curl_cffi loop). Same pattern the shipped scrapers in ~/git/google and
~/git/ebay use; this skill applies it to whatever page the user names.
Tooling rules (do not skip)
Python via uv only. Generated scripts have a PEP 723 header, so
uv run monitor_x.py fetches curl_cffi itself. Never pip install, never
bare python.
curl_cffi, not requests — impersonate="chrome". This is baked into the
generated script; the reason is in references/curl_cffi.md. Read it.
Secrets in env, never in the file. Passwords, API keys, bearer/refresh
tokens go in the spec as ${ENV_VAR} and live in the environment. The
generated .py gets committed and copied between agents — nothing secret in it.
Write monitors into the project, not a temp dir.
The workflow — run every step, in order
1. Drive the site once (Playwright MCP)
browser_navigate to the page the user named.
If it needs login: log in. Prefer the human doing it once (VNC / the user types
credentials), or type known credentials with browser_type. Either way, get
to the logged-in view that shows the data.
browser_snapshot to confirm you see the target data on the page.
2. Find the real request behind the data
browser_network_requests — list what the page fetched. Sort by size: the
data is almost always the biggest non-asset response. Ignore analytics
beacons (gen_204, /log, /collect).
Pick the request that returns the data as JSON (an XHR/fetch to an API),
not the HTML document. That is the one to replay. If the data is only in
server-rendered HTML, replay the page URL and extract with a regex rule.
Note its method, full URL (incl. query string), and the headers that matter:
authorization, x-api-key, x-csrf-token, x-requested-with, referer,
content-type, and any custom x-*. For a POST, capture the body.
Before you commit to an endpoint, run the JS-isolation test (see
references/curl_cffi.md): fetch the URL with page.request.get inside
browser_run_code_unsafe. If that raw fetch already has the data, curl_cffi
will get it too. If it returns a stub while the page shows the data, that
endpoint is JS-assembled — pick another or fall back to Playwright for it.
When a replay later returns a smaller "stub" payload, that is a gate: bisect
URL → headers → cookies → TLS, cheapest first (same reference).
3. Export the session (cookies incl. httpOnly)
Run scripts/pw_export.js via browser_run_code_unsafe. It returns the full
cookie jar (name -> value) and the dominant cookie_domain. document.cookie
is not enough — it misses the httpOnly session cookie, which is usually the
one that authenticates you.
4. Reverse-engineer the login (so it self-heals)
The cookies from step 3 will expire. Decide how the script gets fresh ones:
Capture the login request. Log out and back in (or watch the network at
login) and grab the login POST — url, headers, body fields. Put it in the spec
as relogin, with ${ENV} for the username/password.
Token refresh: if the site uses a short access token + a refresh token,
capture the refresh call instead (or as well) and put it in relogin. The
monitor replays relogin whenever it hits a wall, then retries once.
If you genuinely cannot replay login (MFA, captcha every time), skip relogin
— the monitor will fire a needs_login event so a human re-seeds cookies. Tell
the user this is the case.
5. Set the wall markers
List the strings that mean "logged out / blocked" in walls (a login-URL
fragment, a JSON flag like "authenticated":false, Just a moment,
Pardon Our Interruption). Without this the monitor reports the login page as a
"change". Get these from what you actually saw when a session dropped, or from
references/curl_cffi.md.
6. Write the spec and generate
Write a capture spec (see references/spec.example.json) and generate:
uv run scripts/gen_monitor.py my-spec.json # -> monitor_<name>.py
extract.kind: json (dotted path, [*] fans a list out) · regex
(pattern) · text (whole body) · contains (needle → bool). Prefer json
on a stable field (ids, counts, a status) so cosmetic HTML churn does not look
like a change.
7. Verify before trusting it
ACME_USER=... ACME_PASS=... uv run monitor_<name>.py --once -v
Check: HTTP 200, no wall, the extracted sample is the real data (not a login
page), a .state.json baseline got written. Run it a second time → it must say
unchanged and fire nothing. If it fires on an unchanged page, your extract
is too broad (a timestamp/nonce in the signal) — narrow it.
8. Wire the trigger (how it crawls back to the agent)
The monitor exits 10 on an event, 0 on no change — a hook can branch on that.
Pick the delivery:
file (default): appends a JSON line to ~/.claude/monitor-events.jsonl.
A backend / Claude hook tails it and wakes the agent. Best for the platform.
stdout: prints @@MONITOR@@ {json}. Best under /loop — the loop reads
the marker.
webhook: POSTs the event JSON to a URL.
command: runs an argv with the event JSON on stdin.
Then schedule the poll. Prefer --once on a timer over the built-in loop, so a
crash can't silently kill the watch:
cron: */5 * * * * cd <dir> && uv run monitor_<name>.py --once
or the built-in loop uv run monitor_<name>.py (foreground, honours interval).
Pre-ship checklist (tick every item)
uv used, PEP 723 header intact — no manual pip/venv.
impersonate="chrome" (curl_cffi), not requests/httpx.
Replaying the API/JSON request, not scraping rendered HTML (unless forced).
Cookies exported via pw_export.js (httpOnly included), correct cookie_domain.
extract reads a stable field — re-running on an unchanged page fires nothing.
walls set — a dropped session is a needs_login event, not a false "change".
Login/refresh reverse-engineered into relogin, OR user told it needs manual re-seed.
Every secret is ${ENV}, nothing sensitive baked into the .py.
--once -v verified: 200, real data, baseline written, second run unchanged.
Hook + schedule wired so a change actually reaches the agent.
Poll interval matches the data's real cadence (minutes, not seconds).
Files
scripts/gen_monitor.py — spec → standalone monitor_<name>.py (embeds the runtime).
scripts/pw_export.js — Playwright snippet to dump cookies + cookie_domain.
references/spec.example.json — a filled-in spec.
references/curl_cffi.md — the anti-bot lessons the runtime relies on.
1---2name: browser-to-curl3description: Turn a live browser session into a fast headless monitor. Use when the user says "monitor this website / page / dashboard", "watch X for changes", "tell me when Y updates", "keep an eye on this", or wants a login-gated page polled cheaply. The agent drives the site once with Playwright MCP (open, look, log in), finds the real HTTP request behind the data, and generates a standalone curl_cffi Python script (real Chrome TLS fingerprint, persisted cookie jar, reverse-engineered/refreshing login, change-diff, hook notification) that replays that request forever without a browser. Runs with uv.4---56# browser-to-curl78Goal: the agent should **not** babysit a browser to watch a page. It drives the9site once with Playwright to learn the request, then emits a small `curl_cffi`10script that replays that request headless, forever, and pings back on change.1112Slow, expensive, human-shaped (Playwright, once) → fast, cheap, machine-shaped13(curl_cffi loop). Same pattern the shipped scrapers in `~/git/google` and14`~/git/ebay` use; this skill applies it to whatever page the user names.1516## Tooling rules (do not skip)1718- **Python via `uv` only.** Generated scripts have a PEP 723 header, so19 `uv run monitor_x.py` fetches `curl_cffi` itself. Never `pip install`, never20 bare `python`.21- **curl_cffi, not requests** — `impersonate="chrome"`. This is baked into the22 generated script; the reason is in `references/curl_cffi.md`. Read it.23- **Secrets in env, never in the file.** Passwords, API keys, bearer/refresh24 tokens go in the spec as `${ENV_VAR}` and live in the environment. The25 generated `.py` gets committed and copied between agents — nothing secret in it.26- Write monitors into the project, not a temp dir.2728## The workflow — run every step, in order2930### 1. Drive the site once (Playwright MCP)31- `browser_navigate` to the page the user named.32- If it needs login: log in. Prefer the human doing it once (VNC / the user types33 credentials), or type known credentials with `browser_type`. Either way, get34 to the logged-in view that shows the data.35- `browser_snapshot` to confirm you see the target data on the page.3637### 2. Find the real request behind the data38- `browser_network_requests` — list what the page fetched. Sort by size: the39 data is almost always the **biggest non-asset response**. Ignore analytics40 beacons (`gen_204`, `/log`, `/collect`).41- Pick the request that **returns the data as JSON** (an XHR/fetch to an API),42 not the HTML document. That is the one to replay. If the data is only in43 server-rendered HTML, replay the page URL and extract with a `regex` rule.44- Note its method, full URL (incl. query string), and the headers that matter:45 `authorization`, `x-api-key`, `x-csrf-token`, `x-requested-with`, `referer`,46 `content-type`, and any custom `x-*`. For a POST, capture the body.47- **Before you commit to an endpoint, run the JS-isolation test** (see48 `references/curl_cffi.md`): fetch the URL with `page.request.get` inside49 `browser_run_code_unsafe`. If that raw fetch already has the data, curl_cffi50 will get it too. If it returns a stub while the page shows the data, that51 endpoint is JS-assembled — pick another or fall back to Playwright for it.52 When a replay later returns a smaller "stub" payload, that is a gate: bisect53 URL → headers → cookies → TLS, cheapest first (same reference).5455### 3. Export the session (cookies incl. httpOnly)56- Run `scripts/pw_export.js` via `browser_run_code_unsafe`. It returns the full57 cookie jar (`name -> value`) and the dominant `cookie_domain`. `document.cookie`58 is **not** enough — it misses the httpOnly session cookie, which is usually the59 one that authenticates you.6061### 4. Reverse-engineer the login (so it self-heals)62The cookies from step 3 will expire. Decide how the script gets fresh ones:63- **Capture the login request.** Log out and back in (or watch the network at64 login) and grab the login POST — url, headers, body fields. Put it in the spec65 as `relogin`, with `${ENV}` for the username/password.66- **Token refresh:** if the site uses a short access token + a refresh token,67 capture the refresh call instead (or as well) and put it in `relogin`. The68 monitor replays `relogin` whenever it hits a wall, then retries once.69- If you genuinely cannot replay login (MFA, captcha every time), skip `relogin`70 — the monitor will fire a `needs_login` event so a human re-seeds cookies. Tell71 the user this is the case.7273### 5. Set the wall markers74List the strings that mean "logged out / blocked" in `walls` (a login-URL75fragment, a JSON flag like `"authenticated":false`, `Just a moment`,76`Pardon Our Interruption`). Without this the monitor reports the login page as a77"change". Get these from what you actually saw when a session dropped, or from78`references/curl_cffi.md`.7980### 6. Write the spec and generate81Write a capture spec (see `references/spec.example.json`) and generate:82```bash83uv run scripts/gen_monitor.py my-spec.json # -> monitor_<name>.py84```85Fields: `name`, `impersonate`, `requests`, `cookies`, `cookie_domain`,86`extract`, `walls`, `relogin` (optional), `interval`, `hook`.8788`extract.kind`: `json` (dotted `path`, `[*]` fans a list out) · `regex`89(`pattern`) · `text` (whole body) · `contains` (`needle` → bool). Prefer `json`90on a stable field (ids, counts, a status) so cosmetic HTML churn does not look91like a change.9293### 7. Verify before trusting it94```bash95ACME_USER=... ACME_PASS=... uv run monitor_<name>.py --once -v96```97Check: HTTP 200, no wall, the extracted sample is the real data (not a login98page), a `.state.json` baseline got written. Run it a second time → it must say99`unchanged` and fire nothing. If it fires on an unchanged page, your `extract`100is too broad (a timestamp/nonce in the signal) — narrow it.101102### 8. Wire the trigger (how it crawls back to the agent)103The monitor exits **10 on an event**, 0 on no change — a hook can branch on that.104Pick the delivery:105- **`file`** (default): appends a JSON line to `~/.claude/monitor-events.jsonl`.106 A backend / Claude hook tails it and wakes the agent. Best for the platform.107- **`stdout`**: prints `@@MONITOR@@ {json}`. Best under `/loop` — the loop reads108 the marker.109- **`webhook`**: POSTs the event JSON to a URL.110- **`command`**: runs an argv with the event JSON on stdin.111112Then schedule the poll. Prefer `--once` on a timer over the built-in loop, so a113crash can't silently kill the watch:114- cron: `*/5 * * * * cd <dir> && uv run monitor_<name>.py --once`115- or the built-in loop `uv run monitor_<name>.py` (foreground, honours `interval`).116117## Pre-ship checklist (tick every item)118119- [ ] `uv` used, PEP 723 header intact — no manual pip/venv.120- [ ] `impersonate="chrome"` (curl_cffi), not requests/httpx.121- [ ] Replaying the **API/JSON** request, not scraping rendered HTML (unless forced).122- [ ] Cookies exported via `pw_export.js` (httpOnly included), correct `cookie_domain`.123- [ ] `extract` reads a **stable** field — re-running on an unchanged page fires nothing.124- [ ] `walls` set — a dropped session is a `needs_login` event, not a false "change".125- [ ] Login/refresh reverse-engineered into `relogin`, OR user told it needs manual re-seed.126- [ ] Every secret is `${ENV}`, nothing sensitive baked into the `.py`.127- [ ] `--once -v` verified: 200, real data, baseline written, second run `unchanged`.128- [ ] Hook + schedule wired so a change actually reaches the agent.129- [ ] Poll `interval` matches the data's real cadence (minutes, not seconds).130131## Files132- `scripts/gen_monitor.py` — spec → standalone `monitor_<name>.py` (embeds the runtime).133- `scripts/pw_export.js` — Playwright snippet to dump cookies + `cookie_domain`.134- `references/spec.example.json` — a filled-in spec.135- `references/curl_cffi.md` — the anti-bot lessons the runtime relies on.
Run npx skillmds@latest add chuk-development/browser-to-curl in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Turn a live browser session into a fast headless monitor. Use when the user says "monitor this website / page / dashboard", "watch X for changes", "tell me when Y updates", "keep an eye on this", or wants a login-gated page polled cheaply. The agent drives the site once with Playwright MCP (open, look, log in), finds the real HTTP request behind the data, and generates a standalone curl_cffi Python script (real Chrome TLS fingerprint, persisted cookie jar, reverse-engineered/refreshing login, change-diff, hook notification) that replays that request forever without a browser. Runs with uv. It is listed under AI & ML on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Independent scanners report: SkillSpector: CAUTION, Skill Scanner: PASS. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
chuk-development (@chuk-development) published this skill. Their other Agent Skills are listed on their SkillMD profile.