.claude/skills/pinchtab-cdp-token-capture/SKILL.md
Capture authentication tokens from reCAPTCHA-protected websites using pinchtab + Chrome DevTools Protocol (CDP). Use when: (1) need to automate token refresh for sites with reCAPTCHA login, (2) need to find Chrome's CDP port when pinchtab uses --remote-debugging-port=0, (3) lsof not working in LaunchAgent context on macOS, (4) need to click inside cross-origin iframes via CDP Input.dispatchMouseEvent, (5) Chrome cookies not persisting between pinchtab restarts, (6) Network.getResponseBody returns empty for fetch() API responses (use Fetch.enable + Fetch.getResponseBody instead), (7) need to capture tokens from login responses by monitoring CDP network traffic passively during browser form submission, (8) API response body structure nests tokens in unexpected locations (e.g., data.user.X not data.X).
npx skillsauth add Dbochman/dotfiles pinchtab-cdp-token-captureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Automating login to websites with reCAPTCHA v2 protection to capture authentication tokens for API use. reCAPTCHA can't be reliably solved programmatically, so you need a hybrid manual+automated approach.
lsof commands fail or return wrong data in LaunchAgent contextCookies persist in ~/.pinchtab/chrome-profile/. Even after killing and restarting pinchtab,
Chrome loads the same profile. Authenticated sessions survive restarts indefinitely (until
the site's session cookies expire, typically weeks/months).
# Start pinchtab with VISIBLE browser window
BRIDGE_HEADLESS=false pinchtab &
sleep 5
pinchtab nav "https://example.com/login"
pinchtab fill 'input[name=email]' '[email protected]'
# User solves CAPTCHA manually, clicks Sign In
Pinchtab uses --remote-debugging-port=0 (random port). To find it:
import subprocess, re
# Get Chrome main process PID (exclude --type= worker processes)
ps = subprocess.check_output(['ps', 'aux'], text=True)
for line in ps.splitlines():
if 'chrome-profile' in line and 'remote-debugging' in line and '--type=' not in line:
pid = line.split()[1]
# CRITICAL: use /usr/sbin/lsof with -anP flags on macOS
lsof = subprocess.check_output(
['/usr/sbin/lsof', '-anP', '-p', pid, '-i', 'TCP', '-sTCP:LISTEN'],
text=True, stderr=subprocess.DEVNULL
)
for l in lsof.splitlines():
m = re.search(r':(\d+)\s+\(LISTEN\)', l)
if m:
print(m.group(1)) # This is the CDP port
Critical macOS lsof flags:
-a = AND mode (without this, returns ALL system sockets, not just the target PID's)-n = numeric IPs (skip DNS)-P = numeric ports (skip service name resolution)-sTCP:LISTEN = only LISTEN state sockets/usr/sbin/lsof (not in default LaunchAgent PATH!)import json, asyncio, websockets
async def capture_token(cdp_port):
tabs = json.loads(subprocess.check_output(
["curl", "-s", f"http://localhost:{cdp_port}/json"], text=True))
tab = next(t for t in tabs if "example.com" in t.get("url", ""))
async with websockets.connect(tab["webSocketDebuggerUrl"]) as ws:
await ws.send(json.dumps({"id": 1, "method": "Network.enable"}))
await ws.recv()
await ws.send(json.dumps({"id": 2, "method": "Page.reload"}))
while True:
msg = json.loads(await ws.recv())
if msg.get("method") == "Network.requestWillBeSent":
headers = msg["params"]["request"].get("headers", {})
auth = headers.get("authorization", "")
if auth and len(auth) > 50:
return auth # Got the token!
Regular JS dispatchEvent can't cross iframe origin boundaries. Use CDP:
# Input.dispatchMouseEvent works at absolute page coordinates
await ws.send(json.dumps({
"id": 1, "method": "Input.dispatchMouseEvent",
"params": {"type": "mousePressed", "x": 681, "y": 540, "button": "left", "clickCount": 1}
}))
await ws.send(json.dumps({
"id": 2, "method": "Input.dispatchMouseEvent",
"params": {"type": "mouseReleased", "x": 681, "y": 540, "button": "left", "clickCount": 1}
}))
Note: This triggers the CAPTCHA but doesn't solve it — it escalates to image challenges.
# Test token works
curl -s "https://api.example.com/endpoint" \
-H "authorization: $TOKEN" | python3 -c "
import json,sys; d=json.loads(sys.stdin.read()); print('ok' if d.get('status')==200 else 'fail')"
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<!-- MUST include /usr/sbin for lsof -->
<string>/opt/homebrew/bin:/usr/local/bin:/usr/sbin:/usr/bin:/bin</string>
<key>HOME</key>
<string>/Users/username</string>
</dict>
<key>StartInterval</key>
<integer>1800</integer>
Problem: Network.getResponseBody returns empty body for responses consumed by the page's
fetch() API. Chrome doesn't cache the response body for CDP when JavaScript reads it as a stream.
Even waiting for Network.loadingFinished doesn't help — the body is simply not available.
Fix: Use CDP's Fetch domain instead, which intercepts responses at the network layer before the page consumes them:
# Enable Fetch to intercept responses before page consumes them
await ws.send(json.dumps({
"id": 2,
"method": "Fetch.enable",
"params": {
"patterns": [
{"urlPattern": "*api.example.com*", "requestStage": "Response"}
]
}
}))
# On Fetch.requestPaused (response ready but paused):
if method == "Fetch.requestPaused":
fetch_rid = params["requestId"]
# Read body while paused — this ALWAYS works
await ws.send(json.dumps({
"id": next_id, "method": "Fetch.getResponseBody",
"params": {"requestId": fetch_rid}
}))
# ... handle response, decode base64 if base64Encoded ...
# MUST resume the request or page hangs:
await ws.send(json.dumps({
"id": next_id, "method": "Fetch.continueRequest",
"params": {"requestId": fetch_rid}
}))
Important: Always call Fetch.continueRequest after reading the body, or the page will hang.
Disable Fetch when done: Fetch.disable.
When you need to capture tokens from a login API response (e.g., refreshToken), you can't
just reload the dashboard — authenticated sessions only hit data endpoints, not auth endpoints.
Pattern: Start CDP Fetch interception BEFORE the login form is submitted, then capture the auth response as it flows through.
# 1. Start passive CDP listener in background
python3 grab-tokens.py $CDP_PORT --passive &
GRAB_PID=$!
# 2. Submit login form (via pinchtab eval or other method)
pinchtab eval "document.querySelector('form').submit()"
# 3. Wait for grabber to capture auth response
sleep 5
wait $GRAB_PID 2>/dev/null || kill $GRAB_PID 2>/dev/null
Gotcha: Check nested response structures. Cielo's /auth/login returns tokens at
data.user.accessToken and data.user.refreshToken, NOT directly under data.
When an Angular/React SPA has obfuscated JS, you can still find API paths:
# Download the compiled bundle
curl -s 'https://example.com/main.HASH.js' > /tmp/bundle.js
# Extract all API paths (works even on obfuscated code — paths are string literals)
grep -oE '/web/[a-zA-Z/]+' /tmp/bundle.js | sort -u
# Find auth-related URLs
grep -oE '(https?://[a-zA-Z0-9./-]+|/[a-zA-Z]+/[a-zA-Z/]+)' /tmp/bundle.js | grep -iE '(auth|login|password|token)' | sort -u
pinchtab health shows "cdp":"" — it doesn't expose the CDP port, must use lsofpgrep -f "Google Chrome.*chrome-profile.*remote-debugging" may match worker processes;
filter with --type= not in line to get main process onlyNetwork.webSocketCreated events and parse URL paramsuIn, dtd, ad) — can't extract plaintext tokens from storage/authenticate) is CORS-blocked — must capture via browser CDP during actual login flow/web/devices, /web/default-threshold), never auth endpointsdevelopment
Search the web for current information, news, facts, and answers. Use when asked questions about current events, needing to look something up, finding websites, researching topics, or when you need up-to-date information beyond your training data.
development
Summarize any URL, YouTube video, podcast, PDF, or file into concise text. Use when asked to read an article, summarize a link, get the gist of a video or podcast, extract content from a URL, or when you need to understand what a web page or document contains.
development
Play music via Spotify and control Google Home speakers. Use when asked to play music, songs, artists, playlists, podcasts, or control speakers/volume/audio.
testing
Create new OpenClaw skills, modify and improve existing skills, and measure skill performance with evals. Use when users want to create a skill from scratch, update or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy. Also use when asked to "make a skill", "turn this into a skill", "improve this skill", or "test this skill".