Skip to content

Instantly share code, notes, and snippets.

@jmanhype
Created February 12, 2026 16:32
Show Gist options
  • Select an option

  • Save jmanhype/a36b289dfef01f54f71cebd4d001a25d to your computer and use it in GitHub Desktop.

Select an option

Save jmanhype/a36b289dfef01f54f71cebd4d001a25d to your computer and use it in GitHub Desktop.
Extract X (Twitter) cookies from Chrome via CDP for use with Bird CLI
#!/usr/bin/env python3
"""
Extract X (Twitter) cookies from Chrome via CDP (Chrome DevTools Protocol).
Requirements:
- Chrome running with --remote-debugging-port=9222
- pip install websockets
- Must be logged into x.com in Chrome
Usage:
python3 extract_x_cookies_cdp.py
python3 extract_x_cookies_cdp.py --port 9222
python3 extract_x_cookies_cdp.py --export-env # prints export lines for shell
"""
import argparse
import asyncio
import json
import sys
import urllib.request
def get_x_tab(port: int) -> dict | None:
"""Find an x.com tab via CDP /json endpoint."""
url = f"http://localhost:{port}/json"
try:
with urllib.request.urlopen(url, timeout=3) as resp:
tabs = json.loads(resp.read())
except Exception as e:
print(f"ERROR: Cannot reach Chrome CDP on port {port}: {e}", file=sys.stderr)
print("Start Chrome with: open -a 'Google Chrome' --args --remote-debugging-port=9222", file=sys.stderr)
sys.exit(1)
for tab in tabs:
tab_url = tab.get("url", "")
if "x.com" in tab_url or "twitter.com" in tab_url:
if not tab_url.startswith("blob:"):
return tab
return None
async def extract_cookies(ws_url: str) -> tuple[str, str]:
"""Connect to a page's CDP WebSocket and extract auth_token + ct0."""
try:
import websockets
except ImportError:
print("ERROR: pip install websockets", file=sys.stderr)
sys.exit(1)
async with websockets.connect(ws_url) as ws:
# Enable Network domain
await ws.send(json.dumps({"id": 1, "method": "Network.enable"}))
# Get cookies for x.com
await ws.send(json.dumps({
"id": 2,
"method": "Network.getCookies",
"params": {"urls": ["https://x.com", "https://twitter.com"]}
}))
# Read responses, skip events
while True:
msg = json.loads(await ws.recv())
if msg.get("id") == 2:
break
cookies = msg.get("result", {}).get("cookies", [])
auth_token = next((c["value"] for c in cookies if c["name"] == "auth_token"), None)
ct0 = next((c["value"] for c in cookies if c["name"] == "ct0"), None)
if not auth_token or not ct0:
print("ERROR: Could not find auth_token/ct0 in x.com cookies.", file=sys.stderr)
print("Make sure you're logged into x.com in Chrome.", file=sys.stderr)
sys.exit(1)
return auth_token, ct0
def main():
parser = argparse.ArgumentParser(description="Extract X cookies from Chrome via CDP")
parser.add_argument("--port", type=int, default=9222, help="Chrome CDP port (default: 9222)")
parser.add_argument("--export-env", action="store_true", help="Print shell export lines")
parser.add_argument("--bird-config", action="store_true", help="Write to ~/.config/bird/config.json5")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
tab = get_x_tab(args.port)
if not tab:
print("ERROR: No x.com tab found in Chrome. Open x.com first.", file=sys.stderr)
sys.exit(1)
ws_url = tab["webSocketDebuggerUrl"]
auth_token, ct0 = asyncio.run(extract_cookies(ws_url))
if args.export_env:
print(f'export AUTH_TOKEN="{auth_token}"')
print(f'export CT0="{ct0}"')
elif args.bird_config:
import os
config_dir = os.path.expanduser("~/.config/bird")
os.makedirs(config_dir, exist_ok=True)
config_path = os.path.join(config_dir, "config.json5")
from datetime import datetime
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
with open(config_path, "w") as f:
f.write(f'{{\n // Auto-extracted from Chrome via CDP ({ts})\n')
f.write(f' "authToken": "{auth_token}",\n')
f.write(f' "ct0": "{ct0}"\n}}\n')
print(f"Wrote {config_path}")
elif args.json:
print(json.dumps({"auth_token": auth_token, "ct0": ct0}, indent=2))
else:
print(f"auth_token: {auth_token[:12]}...")
print(f"ct0: {ct0[:12]}...")
print()
print("Use with Bird CLI:")
print(f' bird whoami --auth-token "{auth_token}" --ct0 "{ct0}"')
print()
print("Or set env vars:")
print(f' export AUTH_TOKEN="{auth_token}"')
print(f' export CT0="{ct0}"')
if __name__ == "__main__":
main()

X (Twitter) Cookie Extraction via Chrome CDP

The Problem

Bird CLI (@steipete/bird) needs auth_token + ct0 cookies to authenticate with X's GraphQL API. The built-in sweet-cookie library reads Chrome's SQLite cookie DB, but often fails on macOS due to Keychain access permissions.

The Solution: Chrome DevTools Protocol (CDP)

Instead of reading the cookie SQLite file, connect to Chrome's DevTools Protocol and ask the running browser for its cookies directly. No Keychain access needed.

Prerequisites

  1. Chrome running with remote debugging:

    # Launch Chrome with CDP enabled (add to alias)
    open -a "Google Chrome" --args --remote-debugging-port=9222
    
    # Or if Chrome is already running, check if CDP is available:
    curl -s http://localhost:9222/json/version
  2. Logged into x.com in Chrome

  3. Python 3 + websockets:

    pip install websockets

How It Works

Chrome (CDP port 9222)
  ├── GET /json                    → List all open tabs
  ├── Find tab with x.com URL     → Get WebSocket debugger URL
  └── WebSocket connection
       ├── Network.enable          → Activate network domain
       └── Network.getCookies      → Extract auth_token + ct0
            └── {urls: ["https://x.com"]}
  1. Discovery: GET http://localhost:9222/json returns JSON array of all tabs
  2. Filter: Find any tab with x.com or twitter.com in the URL
  3. Connect: Open WebSocket to that tab's webSocketDebuggerUrl
  4. Extract: Call Network.getCookies with x.com URLs
  5. Parse: Pull auth_token and ct0 from the cookie array

Quick One-Liner (Python)

python3 extract_x_cookies_cdp.py
python3 extract_x_cookies_cdp.py --export-env    # shell export lines
python3 extract_x_cookies_cdp.py --json           # JSON output
python3 extract_x_cookies_cdp.py --bird-config    # write ~/.config/bird/config.json5

Using with Bird CLI

# Method 1: CLI flags
bird whoami --auth-token "TOKEN" --ct0 "CT0"

# Method 2: Env vars
export AUTH_TOKEN="TOKEN"
export CT0="CT0"
bird whoami

# Method 3: Inline
eval $(python3 extract_x_cookies_cdp.py --export-env) && bird whoami

Why Not sweet-cookie?

Method Pros Cons
sweet-cookie (SQLite) No Chrome needed running Keychain permission errors on macOS, browser must be closed
CDP (WebSocket) Works while Chrome is running, no Keychain issues Chrome must be running with --remote-debugging-port

Security Notes

  • CDP port is localhost only by default — no remote access
  • Tokens are session cookies — they rotate when you log out/in
  • Don't commit tokens to version control
  • The auth_token cookie is your full session — treat it like a password
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment