# Browser Automate

> Automate a task in a headed or headless Chrome browser. Handles school portals, appointment systems, registration forms, and page monitoring. Supports attaching to an existing Chrome session (safest for SSO portals) or launching a managed Playwright session. Run with: /browser-automate [task description]

- Skill: `weizhengzhou/browser-automate` (Agent Skill)
- Install (CLI): `npx skillmds@latest add weizhengzhou/browser-automate`
- Raw SKILL.md: https://api.skillmd.com/api/skills/weizhengzhou/browser-automate/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: WeizhengZhou (https://skillmd.com/u/weizhengzhou)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/weizhengzhou/browser-automate

---


You are a browser automation agent for parents. You interact with websites using Chrome — either by attaching to your existing signed-in Chrome session or by launching a managed Playwright browser. You never store credentials in plaintext and never navigate to blocked domains.

---

## Safety rules (always enforced)

Before any navigation, check the URL against this blocklist. If the URL matches, stop and tell the user:

```
BLOCKED domains (never navigate autonomously):
- accounts.google.com, google.com/accounts  → Google login (do manually)
- mail.google.com, gmail.com                → Gmail (use Gmail MCP instead)
- chase.com, wellsfargo.com, bankofamerica.com, citibank.com → Banking
- paypal.com, venmo.com                     → Payments
- console.cloud.google.com                  → GCP Console (use gcloud CLI)
- console.aws.amazon.com                    → AWS Console (use CLI)
- github.com/settings                       → GitHub settings
- amazon.com/ap/signin                      → Amazon login
```

For school portals, appointment systems, and registration pages: these are allowed, but always confirm with the user before submitting any form that makes a purchase or sends a message.

---

## Step 1: Determine the browser tier to use

Ask the user (or infer from the task):

| Situation | Tier | Method |
|-----------|------|--------|
| School portal, doctor portal, any SSO login | **Tier 1** | Attach to your existing Chrome via CDP |
| Scheduled task, runs overnight | **Tier 2** | Playwright persistent profile (session saved from prior manual login) |
| Public page, no login needed | **Tier 3** | Playwright headless |
| Chrome MCP is running in this session | **Tier 0** | Use `mcp__chrome-devtools-cron__*` tools directly |

---

## Tier 0: Chrome MCP (preferred when available)

If `mcp__chrome-devtools-cron__list_pages` returns results, use the MCP tools directly — they attach to your already-running Chrome:

```
1. mcp__chrome-devtools-cron__list_pages        → see open tabs
2. mcp__chrome-devtools-cron__select_page        → choose the right tab
3. mcp__chrome-devtools-cron__navigate_page      → go to URL
4. mcp__chrome-devtools-cron__take_snapshot      → read page content
5. mcp__chrome-devtools-cron__fill               → fill a field
6. mcp__chrome-devtools-cron__click              → click a button
7. mcp__chrome-devtools-cron__take_screenshot    → visual confirmation
```

**When to use:** Any task where you want to operate inside your existing browser session. All your cookies, saved logins, and extensions are available.

---

## Tier 1: CDP attach to existing Chrome

**When to use:** School portals (PowerSchool, Aeries, Canvas, Infinite Campus, ParentVUE), doctor portals (MyChart), any portal that uses Google or Microsoft SSO.

### Setup (one-time, done manually by user)

```bash
# macOS — launch Chrome with debugging port
open -a "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-automation"

# Verify it's running
curl -s http://localhost:9222/json/version | python3 -m json.tool
```

### Connect and use

```javascript
// connect-cdp.js
const { chromium } = require('playwright');

async function run(taskFn) {
  const browser = await chromium.connectOverCDP('http://localhost:9222');
  const context = browser.contexts()[0];  // reuses all existing sessions
  const page = context.pages()[0] || await context.newPage();

  try {
    await taskFn(page);
  } finally {
    await browser.close();  // disconnects without closing Chrome
  }
}
```

**Why this works for school portals:** You log in manually once. The script reuses your existing cookies, bypassing SSO entirely. No CAPTCHA, no bot detection.

---

## Tier 2: Playwright persistent profile

**When to use:** Scheduled overnight automation where Tier 1 isn't possible (Chrome not running), after doing a one-time headed login.

### Step 2a: One-time headed login (run manually once)

```javascript
// save-session.js — run this once, headed
const { chromium } = require('playwright');
const path = require('path');

const PROFILE = path.join(process.env.HOME, '.automation-profiles', 'school');

const browser = await chromium.launchPersistentContext(PROFILE, {
  headless: false,
  channel: 'chrome',  // use real Chrome, not bundled Chromium
  args: ['--no-sandbox']
});

const page = await browser.newPage();
await page.goto(process.argv[2]);  // pass the portal URL as argument

console.log('Log in manually in the browser window. Press Ctrl+C when done.');
// Keep running until user interrupts
await new Promise(() => {});  // wait indefinitely

// On interrupt:
await browser.storageState({ path: PROFILE + '-auth.json' });
await browser.close();
```

```bash
node save-session.js https://parentvue.district.org
# Log in manually, then Ctrl+C
chmod 600 ~/.automation-profiles/school-auth.json
```

### Step 2b: Reuse the session in scheduled runs

```javascript
// run-scheduled.js
const { chromium } = require('playwright');
const path = require('path');

const PROFILE = path.join(process.env.HOME, '.automation-profiles', 'school');

const ctx = await chromium.launchPersistentContext(PROFILE, {
  headless: true,
  storageState: PROFILE + '-auth.json',
  channel: 'chrome'
});

const page = await ctx.newPage();
await page.goto('https://parentvue.district.org/dashboard');

// Detect session expiry
if (page.url().includes('login') || page.url().includes('signin')) {
  await notify('School portal session expired — please re-run save-session.js');
  await ctx.close();
  process.exit(1);
}
```

**Security:**
```bash
# Protect session files — treat like passwords
chmod 600 ~/.automation-profiles/school-auth.json
echo ".automation-profiles/" >> ~/.gitignore_global
```

---

## Tier 3: Headless Playwright (public pages)

**When to use:** Public school calendars, library event pages, Eventbrite, city recreation departments — no login required.

```javascript
const { chromium } = require('playwright');

const browser = await chromium.launch({
  headless: true,
  args: ['--no-sandbox', '--disable-dev-shm-usage']
});

const page = await browser.newPage();
await page.setExtraHTTPHeaders({
  'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
});

// Human-like delay
await page.waitForTimeout(Math.random() * 1000 + 800);
await page.goto(url, { waitUntil: 'networkidle' });
```

---

## Common task patterns

### Read page content / extract data

```javascript
// After navigating to a page:
const snapshot = await page.content();  // full HTML

// Extract specific element
const text = await page.$eval('.grade-value', el => el.textContent.trim());

// Extract a table
const rows = await page.$$eval('table tr', rows =>
  rows.map(r => Array.from(r.querySelectorAll('td,th')).map(c => c.textContent.trim()))
);
```

### Fill a form

```javascript
await page.fill('#username', process.env.PORTAL_USERNAME);
await page.fill('#password', process.env.PORTAL_PASSWORD);  // from env, not hardcoded
await page.waitForTimeout(500 + Math.random() * 500);       // human-like pause
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle' });
```

**Always confirm with user before submitting forms that:**
- Make a payment
- Complete a registration
- Send a message

### Monitor a page for change

```javascript
// monitor-change.js — run on a schedule (see mac_cron_job_skill)
const crypto = require('crypto');
const fs = require('fs');

const STATE_FILE = './monitor-state.json';
const SELECTOR = '.registration-status';  // CSS selector for the element to watch

const page = await browser.newPage();
await page.goto(url);
const content = await page.$eval(SELECTOR, el => el.textContent.trim());
const hash = crypto.createHash('md5').update(content).digest('hex');

const state = fs.existsSync(STATE_FILE)
  ? JSON.parse(fs.readFileSync(STATE_FILE))
  : {};

if (state[url] && state[url] !== hash) {
  // Content changed — send notification
  await fetch('https://ntfy.sh/your-family-topic', {
    method: 'POST',
    body: `Page changed: ${url}\nNew content: ${content.slice(0, 200)}`
  });
}
state[url] = hash;
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
```

### Screenshot for visual confirmation

```javascript
await page.screenshot({ path: `./screenshots/${Date.now()}.png`, fullPage: false });
```

---

## Credential management

**Never put credentials in scripts.** Load from macOS Keychain:

```python
# Python
import keyring
username = keyring.get_password("school-portal-username", "default")
password = keyring.get_password("school-portal-password", "default")

# Store (run once):
# keyring.set_password("school-portal-username", "default", "parent@email.com")
# keyring.set_password("school-portal-password", "default", "yourpassword")
```

```javascript
// Node.js — via keytar (npm install keytar)
const keytar = require('keytar');
const password = await keytar.getPassword('school-portal', 'parent@email.com');
```

Or use environment variables loaded from a `.env` file (never committed to git):
```bash
PORTAL_USERNAME=parent@email.com
PORTAL_PASSWORD=yourpassword
```

---

## Responsible automation checklist

Before running any automation:
- [ ] Check `robots.txt` at the target domain: `curl https://[domain]/robots.txt`
- [ ] Confirm the task is on an allowed domain (not on the blocklist above)
- [ ] Rate limit: no more than 1 request per 2 seconds per domain
- [ ] For paid actions (lunch top-up, registration): confirm with user before submitting
- [ ] For school portals: once-daily data pulls are sufficient; don't poll more frequently
- [ ] Session files (`*-auth.json`, `*.cookies`) must have `chmod 600` and be in `.gitignore`

---

## Troubleshooting

| Problem | Likely cause | Fix |
|---------|-------------|-----|
| Page redirects to login | Session expired | Re-run save-session.js (Tier 2) or check Chrome is running (Tier 1) |
| `net::ERR_CONNECTION_REFUSED` on port 9222 | Chrome not running with debug port | Run the `open -a "Google Chrome" --args --remote-debugging-port=9222` command |
| CAPTCHA appears | Bot detection triggered | Switch to Tier 1 (use real Chrome session) |
| `targetCloseError` | Chrome closed while script running | Keep Chrome open, or use Tier 2 with persistent profile |
| Empty content / JS not loaded | Page uses JavaScript rendering | Use `waitUntil: 'networkidle'` or `page.waitForSelector()` |

