cv-2026/.claude/skills/chrome-session/SKILL.md

9.4 KiB
Raw Blame History

name description
chrome-session Use this skill to drive a real Chrome instance that is signed in to Oleg's accounts (LinkedIn, Gmail, GitHub, HR platforms, etc.) via the chrome-devtools MCP. The browser is launched against a forked copy of his Chrome profile, so authenticated pages, cookies, and saved sessions are available — letting you read job postings on auth-walled HR platforms, scrape logged-in LinkedIn feeds, inspect application-form fields, and pull data from any site where Oleg is already signed in. Also use this skill as the automatic fallback when WebFetch/curl/standard tools fail to retrieve a page (bot blocking, 403, JS-only render, login wall).

Chrome Session

This skill operates a real Chrome browser, attached via the chrome-devtools MCP, using a local copy of Oleg's signed-in Chrome profile. It exists so you can fetch information from services where Oleg is logged in — without him having to copy-paste content manually.

When to use

Use this skill when any of the following apply:

  • The user asks you to open / read / search something on LinkedIn (feed, profiles, jobs, messages, notifications).
  • The user asks to read job descriptions or application forms on HR platforms (Greenhouse, Lever, Workable, Ashby, SmartRecruiters, Workday, LinkedIn Jobs, Wellfound, YC Work at a Startup, company career pages, etc.).
  • The user asks to search for jobs by criteria, scroll a feed, or pull a list of postings.
  • Any page is gated by login, and Oleg is likely already signed in (Gmail, GitHub private repos, Notion, Trello UI, Slack web, etc.).
  • A standard fetch tool failed — WebFetch returned bot-blocking errors, 401/403, empty body because JS isn't executed, captcha, "please log in", or similar. In that case switch to this skill immediately, without asking for confirmation.

Do NOT use this skill for:

  • Pages reachable anonymously where WebFetch works fine.
  • Anything destructive (don't submit forms, don't send messages) unless the user explicitly asked.

Setup overview

  • Launcher script: scripts/launch-chrome.sh — starts Chrome with --remote-debugging-port=9222 against the local profile copy at .chrome/. The script is idempotent: if Chrome is already running it exits immediately.
  • Profile copy: .chrome/ is a fork of Oleg's Profile 1 (the usulpro@gmail.com profile). Logged-in cookies live here. The directory is gitignored.
  • MCP server: chrome-devtools is configured in .mcp.json with --browserUrl=http://127.0.0.1:9222, i.e. it attaches to the already-running Chrome rather than launching its own.

Standard workflow

1. Ensure Chrome is running and attached

Before any browser action, confirm the debug port is live, and launch Chrome automatically if it isn't:

curl -sf http://127.0.0.1:9222/json/version >/dev/null && echo OK || echo DOWN

If DOWN — launch the script in the background and poll until Chrome is ready:

# Launch Chrome in background (script uses exec, so backgrounding the script is correct)
nohup /projects/my-projects/cv-2026/scripts/launch-chrome.sh >/tmp/chrome-launch.log 2>&1 &

# Poll up to 15 seconds
for i in $(seq 1 15); do
  sleep 1
  curl -sf http://127.0.0.1:9222/json/version >/dev/null && echo "Chrome up after ${i}s" && break
  echo "Waiting... ${i}s"
done
curl -sf http://127.0.0.1:9222/json/version >/dev/null || echo "FAILED"

If Chrome does not respond after 15 seconds, stop and report:

Chrome не запускается. Посмотри /tmp/chrome-launch.log и проверь, что .chrome/ существует.

If the launcher prints Error: .chrome/ does not exist, stop immediately — there is no profile to use and you cannot create one.

Do NOT try to sign Oleg in, copy profiles, or work around a missing .chrome/ directory.

2. Open / select the right page

  • Use mcp__chrome-devtools__list_pages to see open tabs.
  • Use mcp__chrome-devtools__new_page with the target URL to open a new tab. If navigation times out (10s default), it's usually fine — the tab still opens; check list_pages to confirm the final URL.
  • Use mcp__chrome-devtools__select_page to switch context if you need to drive a tab other than the selected one.

3. Verify the page actually loaded what was expected

After navigation, always confirm you landed on the expected URL/state before extracting content. Check:

  • Final URL — if LinkedIn redirected /feed//login or /checkpoint/..., Oleg is not signed in on that site.
  • A small evaluate_script to check document.title / a known DOM marker.

If you detect a login wall, a "session expired" page, a captcha, or any other "you are not signed in" state — stop and tell Oleg immediately:

Не получается открыть <URL> — похоже, я не залогинен (<observed state>). Залогинься в этом Chrome-инстансе вручную и скажи, когда готово.

Same rule for soft-fail cases (bot wall, "are you human?", 429, sudden empty body).

4. Extract content

Prefer evaluate_script for structured extraction. Snapshots (take_snapshot) give you the a11y tree with uids and are useful for finding clickable elements and form fields.

Scrolling — important detail for LinkedIn and similar SPAs

window.scrollTo / PageDown / End often do nothing because the real scroll container is an inner element (LinkedIn uses <main>; some apps use a div with custom overflow:auto). To trigger lazy loading:

  1. Find the scrollable container:
    const scrollables = [...document.querySelectorAll('*')].filter(el => {
      const s = getComputedStyle(el);
      return (s.overflowY === 'auto' || s.overflowY === 'scroll')
        && el.scrollHeight > el.clientHeight + 50;
    });
    
  2. Increment its scrollTop in steps (e.g. +1200px), dispatch a wheel event for safety, and await ~700-900ms between steps so lazy chunks can fetch.
  3. Re-check scrollHeight after each step — it grows as new content streams in.

For LinkedIn feed specifically: the container is <main>, posts are marked by <h2> with text "Feed post", and the post container is the nearest ancestor whose innerText.length exceeds a few hundred characters.

Form-field inspection (job applications, etc.)

When the user asks "what fields does this application form want?", use take_snapshot to enumerate inputs by their accessibility labels — that maps cleanly to "Name / Email / Resume / Cover letter / Custom question 1 / ...". Don't fill or submit anything unless Oleg explicitly tells you to.

5. Be efficient with content

  • Cap extracted text per item (e.g. .slice(0, 2500)) — LinkedIn posts and job descriptions can be very long and you'll usually summarize anyway.
  • Deduplicate by container element when scanning lists.
  • Return data as JSON from evaluate_script — it's structured and easy to summarize afterwards.

Failure modes and how to react

Symptom Likely cause Action
curl http://127.0.0.1:9222/json/version fails Chrome not launched with debug port Run ./scripts/launch-chrome.sh in background and poll (see step 1)
new_page times out but the tab appears in list_pages Slow site / heavy SPA Continue — check the final URL via list_pages
Final URL is /login, /checkpoint, /signin Not signed in Stop. Tell Oleg. Don't try to log in for him.
Feed/list shows only the first few items Lazy loading hasn't fired Scroll the inner container, not window
evaluate_script returns count: 0 for known content Wrong selectors (sites change markup) Dump document.body.innerText.slice(0, 500) and a structural probe before guessing more selectors
Captcha / "verify you're human" / 429 Bot detection or rate limit Stop. Tell Oleg. Don't retry in a loop.

Don'ts

  • Don't launch Chrome interactively — always use nohup ... & so it detaches from the terminal.
  • Don't try to sign Oleg in when you hit a login wall — just report and wait.
  • Don't submit forms, click "Apply", send messages, or post anything unless explicitly instructed.
  • Don't loop on bot-detection pages — one detection means stop.
  • Don't commit anything from .chrome/ — it's gitignored, keep it that way.
  • Don't write secrets, cookies, or tokens anywhere outside the running browser. If evaluate_script returns sensitive data, summarize, don't echo it back verbatim.

Example: "read first N LinkedIn posts"

  1. curl -sf http://127.0.0.1:9222/json/version → if DOWN, launch via nohup ./scripts/launch-chrome.sh & and poll until up.
  2. list_pages — is LinkedIn already open? If not, new_page(https://www.linkedin.com/feed/).
  3. Verify final URL contains /feed/ and not /login.
  4. evaluate_script: find <main>, step scrollTop += 1200 with 800ms waits until enough "Feed post" h2s have appeared.
  5. evaluate_script: extract {actor, text} for the first N posts (slice text to ~2500 chars).
  6. Summarize in Russian to Oleg.

Example: "what fields does this Greenhouse form want?"

  1. new_page(<job-application-url>).
  2. take_snapshot — read the accessibility tree, list every textbox, combobox, radio, file upload, checkbox with its label.
  3. Report the fields grouped: required vs optional (snapshot exposes required state), grouped by section if the form has fieldsets.
  4. Do not fill or submit. Wait for Oleg's instruction.