---
name: chrome-cookie-extraction-cdp
title: Chrome Cookie Extraction via DevTools Protocol
description: Extract cookies from Chrome (including httpOnly) using Chrome DevTools Protocol via remote debugging
triggers:
  - need chrome cookies for automation
  - extract cookies from browser
  - httpOnly cookies access
  - bypass cookie encryption
  - chrome devtools protocol
---

# Chrome Cookie Extraction via DevTools Protocol

Extract cookies from Chrome (including httpOnly cookies) using the Chrome DevTools Protocol (CDP) via remote debugging.

## When to Use

- Need to extract cookies from Chrome for automation/scripts
- Cookies are encrypted in SQLite (macOS Keychain, Windows DPAPI)
- httpOnly cookies needed (not accessible via `document.cookie`)
- Want to avoid manual cookie export from browser

## Prerequisites

- Chrome must be closed or restarted with remote debugging enabled
- Port 9222 must be available

## macOS-Specific Quirks (High Sierra / Old Chrome)

### If Chrome is already running and you CAN'T restart it

Launch a **second Chrome instance** with a temp profile:

```bash
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  --remote-debugging-port=9222 \
  --no-first-run \
  --new-window \
  --user-data-dir=/tmp/chrome-cdp-profile
```

⚠️ **Limitation:** This second instance starts with a **clean profile** — no cookies, no saved sessions. The user must log in again on that window, or you'll only get basic cookies.

### If you NEED the user's session/cookies

You MUST use the user's main profile. Either:
1. Kill existing Chrome: `killall "Google Chrome"; sleep 2`
2. Restart with debug: same command but WITHOUT `--user-data-dir` (uses default profile)

### Chrome v116+ behavior changes

- `/json/new` endpoint requires **PUT** method (GET returns error)
  - `curl -s -X PUT http://127.0.0.1:9222/json/new` ✅
  - `curl -s http://127.0.0.1:9222/json/new` ❌ "Using unsafe HTTP verb"
- Empty `[]` from `/json` means no tabs — create one with PUT `/json/new`

## Steps

### 1. Start Chrome with Remote Debugging

```bash
# Kill existing Chrome first
killall "Google Chrome" 2>/dev/null; sleep 2

# Start with remote debugging
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  --remote-debugging-port=9222 \
  --restore-last-session
```

### 2. Verify CDP is Running

```bash
curl -s http://localhost:9222/json/version
```

Should return JSON with browser info.

### 3. Find Target Page

```python
import requests

pages = requests.get('http://localhost:9222/json/list', timeout=10).json()
target_page = None
for p in pages:
    if 'example.com' in p.get('url', ''):
        target_page = p
        break

ws_url = target_page.get('webSocketDebuggerUrl')  # WebSocket URL for the page
```

### 4. Connect via WebSocket (Custom Implementation)

Chrome requires masked frames from client. Here's a minimal implementation:

```python
import socket
import json
import struct
import os

class ChromeWebSocket:
    def __init__(self, ws_url):
        # Parse ws://host:port/path
        parts = ws_url.replace('ws://', '').split('/')
        host_port = parts[0].split(':')
        self.host = host_port[0]
        self.port = int(host_port[1]) if len(host_port) > 1 else 80
        self.path = '/' + '/'.join(parts[1:])
        self.sock = None
        
    def connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.connect((self.host, self.port))
        
        # WebSocket handshake
        import base64
        key = base64.b64encode(os.urandom(16)).decode()
        
        handshake = (
            f"GET {self.path} HTTP/1.1\r\n"
            f"Host: {self.host}:{self.port}\r\n"
            f"Upgrade: websocket\r\n"
            f"Connection: Upgrade\r\n"
            f"Sec-WebSocket-Key: {key}\r\n"
            f"Sec-WebSocket-Version: 13\r\n"
            f"\r\n"
        )
        self.sock.send(handshake.encode())
        
        # Wait for 101 response
        response = b''
        while b'\r\n\r\n' not in response:
            response += self.sock.recv(1024)
        
        if b"101" not in response:
            raise Exception("WebSocket handshake failed")
            
    def send(self, data):
        """Send masked text frame (REQUIRED for client→server)"""
        message = json.dumps(data)
        payload = message.encode('utf-8')
        mask = os.urandom(4)
        
        frame = bytearray()
        frame.append(0x81)  # FIN=1, opcode=text
        frame.append(0x80 | len(payload))  # MASK=1, length
        frame.extend(mask)
        
        # XOR payload with mask
        for i, byte in enumerate(payload):
            frame.append(byte ^ mask[i % 4])
        
        self.sock.send(frame)
        
    def recv(self, timeout=5):
        self.sock.settimeout(timeout)
        try:
            header = self.sock.recv(2)
            if not header or len(header) < 2:
                return None
            
            opcode = header[0] & 0x0f
            length = header[1] & 0x7f
            masked = (header[1] & 0x80) != 0
            
            if length == 126:
                length = struct.unpack('>H', self.sock.recv(2))[0]
            elif length == 127:
                length = struct.unpack('>Q', self.sock.recv(8))[0]
            
            if masked:
                mask = self.sock.recv(4)
            
            payload = self.sock.recv(length)
            
            if masked and mask:
                payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
            
            if opcode == 0x1:  # Text frame
                return json.loads(payload.decode('utf-8'))
            return payload
        except socket.timeout:
            return None
        finally:
            self.sock.settimeout(None)
            
    def close(self):
        if self.sock:
            self.sock.close()
```

### 5. Extract Cookies

```python
import time

ws = ChromeWebSocket(ws_url)
ws.connect()

# Enable Network domain
ws.send({"id": 1, "method": "Network.enable"})

# Wait for confirmation
while True:
    msg = ws.recv(timeout=2)
    if msg and msg.get('id') == 1:
        break

# Get all cookies for URLs
ws.send({
    "id": 2,
    "method": "Network.getCookies",
    "params": {
        "urls": ["https://example.com/"]
    }
})

# Receive response
while True:
    msg = ws.recv(timeout=3)
    if msg and msg.get('id') == 2:
        cookies = msg.get('result', {}).get('cookies', [])
        for c in cookies:
            print(f"{c['name']}: {c['value'][:50]}... (httpOnly={c.get('httpOnly')})")
        break

ws.close()
```

### 6. Use Cookies in Requests

```python
import requests

# Build cookie dict
cookie_dict = {c['name']: c['value'] for c in cookies}

# Use in requests
response = requests.get(
    'https://example.com/protected-page',
    cookies=cookie_dict,
    headers={'User-Agent': 'Mozilla/5.0 ...'}
)
```

## Common Issues

| Issue | Solution |
|-------|----------|
| `Connection reset by peer` | Ensure proper WebSocket masking (client MUST mask) |
| No httpOnly cookies | Use `Network.getCookies`, not `document.cookie` |
| Chrome won't start | Kill all Chrome processes first |
| Handshake fails | Check 101 status in response |

## CDP Methods Reference

- `Network.enable` - Enable network domain
- `Network.getCookies` - Get all cookies (including httpOnly)
- `Runtime.enable` - Enable runtime domain
- `Runtime.evaluate` - Execute JavaScript

## Security Note

Remote debugging allows full control over Chrome. Only use on trusted networks and disable when done:
```bash
killall "Google Chrome"
```

## Alternative: WebSocket via Python `websockets` library (simpler)

If `websockets` is available (`pip install websockets`), this is much cleaner:

```python
import json, asyncio, websockets

WS_URL = "ws://127.0.0.1:9222/devtools/page/..."  # from /json or /json/new

async def get_cookies():
    async with websockets.connect(WS_URL) as ws:
        # Navigate
        await ws.send(json.dumps({"id": 1, "method": "Page.navigate", "params": {"url": "https://target.site.com"}}))
        await ws.recv()
        await asyncio.sleep(3)
        
        # Drain pending events
        try:
            while True:
                await asyncio.wait_for(ws.recv(), timeout=0.3)
        except asyncio.TimeoutError:
            pass
        
        # Get cookies
        await ws.send(json.dumps({"id": 2, "method": "Network.getAllCookies"}))
        for _ in range(3):
            resp = await ws.recv()
            d = json.loads(resp)
            if "result" in d:
                cookies = d["result"]["cookies"]
                with open("/tmp/cookies.json", "w") as f:
                    json.dump(cookies, f, indent=2)
                break
        
        # Check login state
        await ws.send(json.dumps({"id": 3, "method": "Runtime.evaluate", "params": {
            "expression": "document.querySelector('a[href*=\"login\"]') ? 'NOT_LOGGED_IN' : 'LOGGED_IN'"
        }}))
        for _ in range(3):
            resp = await ws.recv()
            d = json.loads(resp)
            if "result" in d:
                print("Status:", d["result"]["result"]["value"])
                break

asyncio.run(get_cookies())
```

**Tip:** When draining, catch `asyncio.TimeoutError` specifically to avoid swallowing real errors.

## Alternative: SQLite Decryption (Limited)

On macOS, cookies are encrypted with Keychain. Direct SQLite access only shows encrypted values:
```bash
sqlite3 ~/Library/Application\ Support/Google/Chrome/Default/Cookies \
  "SELECT host_key, name FROM cookies WHERE host_key LIKE '%example.com%'"
```

CDP method above is the reliable approach.