Skip to content
Back to skills

Chrome

ASecurity

How to programmatically set up, control, and verify Google Chrome browser state using Python (Playwright CDP, SQLite, JSON configs). For setup-gen and reward-gen agents.

  • 227 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
ai-agentsjavascriptpythongojavasqldebugginggitapidatabase

Works with

  • cursor
  • api

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned November 7, 2026

npx -y skills add xlang-ai/CUA-Gym --skill chrome --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Chrome?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Chrome
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/xlang-ai-chrome/badge)](https://www.skillsdirectory.com/skills/xlang-ai-chrome)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: chrome
description: "How to programmatically set up, control, and verify Google Chrome browser state using Python (Playwright CDP, SQLite, JSON configs). For setup-gen and reward-gen agents."
user-invocable: false
---

# Chrome — Python Manipulation Guide

This skill teaches **setup-gen** (create initial browser state) and **reward-gen** (verify browser state after agent actions) how to work with Chrome programmatically. Unlike file-based domains, Chrome tasks involve **three data layers**: config files (JSON/SQLite), live browser via CDP, and accessibility tree.

- Libraries: `playwright`, `sqlite3`, `json`, `beautifulsoup4`
- Install: `pip3 install playwright beautifulsoup4 lxml rapidfuzz tldextract`

---

## 0. GUI Startup on VM (for setup-gen)

After preparing browser state, setup-gen should leave Chrome visibly opened for the GUI agent.

CRITICAL VM LIMIT: GUI launches must set `DISPLAY=:0`.

```python
import os
import shlex
import subprocess
import time

def launch_gui(command: str, delay_sec: float = 1.0):
    env = os.environ.copy()
    env["DISPLAY"] = ":0"
    subprocess.Popen(
        shlex.split(command),
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        env=env,
    )
    time.sleep(delay_sec)

# Open Chrome with default profile
launch_gui("google-chrome", delay_sec=2.0)

# Optional URL preload
# launch_gui('google-chrome "https://example.com"', delay_sec=2.0)
```

Guidelines:
- Use non-blocking launch (`Popen`) so `initial_setup.py` exits cleanly.
- If the task needs multiple apps, launch all required windows in order with small sleeps.

---

## 1. Setting Up Browser State (setup-gen)

### Chrome Config File Paths (Linux VM)

```python
import os, json, sqlite3, shutil, time

# Standard paths on the OSWorld Linux VM
CHROME_USER_DATA = os.path.expanduser("~/.config/google-chrome")
CHROME_DEFAULT = os.path.join(CHROME_USER_DATA, "Default")

PREFS_FILE = os.path.join(CHROME_DEFAULT, "Preferences")
LOCAL_STATE = os.path.join(CHROME_USER_DATA, "Local State")
BOOKMARKS_FILE = os.path.join(CHROME_DEFAULT, "Bookmarks")
HISTORY_DB = os.path.join(CHROME_DEFAULT, "History")
COOKIES_DB = os.path.join(CHROME_DEFAULT, "Cookies")
EXTENSIONS_DIR = os.path.join(CHROME_DEFAULT, "Extensions")

# ARM/Chromium alternative
CHROMIUM_DEFAULT = os.path.expanduser("~/snap/chromium/common/chromium/Default")
```

### Modifying Preferences (JSON)

```python
def read_prefs():
    with open(PREFS_FILE, 'r') as f:
        return json.load(f)

def write_prefs(prefs):
    with open(PREFS_FILE, 'w') as f:
        json.dump(prefs, f, indent=2)

prefs = read_prefs()

# Do Not Track
prefs['enable_do_not_track'] = True

# Default font size
prefs.setdefault('webkit', {}).setdefault('webprefs', {})
prefs['webkit']['webprefs']['default_font_size'] = 16
prefs['webkit']['webprefs']['default_fixed_font_size'] = 13
prefs['webkit']['webprefs']['minimum_font_size'] = 6

# Safe browsing
prefs.setdefault('safebrowsing', {})['enhanced'] = True
prefs['safebrowsing']['enabled'] = True

# Color scheme: 0=system, 1=light, 2=dark
prefs.setdefault('browser', {}).setdefault('theme', {})['color_scheme2'] = 2

# Profile name
prefs['profile'] = prefs.get('profile', {})
prefs['profile']['name'] = "Custom Profile"

# Startup: 5 = open new tab page (fresh start)
prefs.setdefault('session', {})['restore_on_startup'] = 5

# Default search engine
prefs.setdefault('default_search_provider_data', {}).setdefault('template_url_data', {})
prefs['default_search_provider_data']['template_url_data']['short_name'] = "DuckDuckGo"

write_prefs(prefs)
```

### Modifying Local State (JSON)

```python
def read_local_state():
    with open(LOCAL_STATE, 'r') as f:
        return json.load(f)

def write_local_state(state):
    with open(LOCAL_STATE, 'w') as f:
        json.dump(state, f, indent=2)

state = read_local_state()

# Chrome language
state['intl'] = state.get('intl', {})
state['intl']['app_locale'] = 'en-US'

# Chrome Labs experiments
state.setdefault('browser', {})['enabled_labs_experiments'] = [
    'smooth-scrolling@1', 'tab-hover-card-images@2'
]

write_local_state(state)
```

### Setting Up Bookmarks (JSON)

```python
import time as _time

def create_bookmark_entry(name, url):
    return {
        "date_added": str(int(_time.time() * 1e6)),  # Chrome microsecond timestamp
        "date_last_used": "0",
        "guid": "",  # Chrome generates this
        "id": str(int(_time.time() * 1e3)),
        "name": name,
        "type": "url",
        "url": url
    }

def create_bookmark_folder(name, children):
    return {
        "children": children,
        "date_added": str(int(_time.time() * 1e6)),
        "date_modified": str(int(_time.time() * 1e6)),
        "guid": "",
        "id": str(int(_time.time() * 1e3)),
        "name": name,
        "type": "folder"
    }

bookmarks = {
    "checksum": "",
    "roots": {
        "bookmark_bar": {
            "children": [
                create_bookmark_entry("Google", "https://www.google.com"),
                create_bookmark_folder("Liked Authors", [
                    create_bookmark_entry("Author1", "https://example.com/author1"),
                    create_bookmark_entry("Author2", "https://example.com/author2"),
                ]),
            ],
            "date_added": str(int(_time.time() * 1e6)),
            "date_modified": str(int(_time.time() * 1e6)),
            "guid": "",
            "id": "1",
            "name": "Bookmarks bar",
            "type": "folder"
        },
        "other": {"children": [], "type": "folder", "name": "Other bookmarks"},
        "synced": {"children": [], "type": "folder", "name": "Mobile bookmarks"}
    },
    "version": 1
}

with open(BOOKMARKS_FILE, 'w') as f:
    json.dump(bookmarks, f, indent=2)
```

### Setting Up Browsing History (SQLite)

```python
import datetime

def chrome_timestamp(dt=None, seconds_ago=0):
    """Chrome timestamps: microseconds since 1601-01-01 (Windows FILETIME)."""
    epoch_1601 = datetime.datetime(1601, 1, 1)
    if dt is None:
        dt = datetime.datetime.utcnow() - datetime.timedelta(seconds=seconds_ago)
    delta = dt - epoch_1601
    return int(delta.total_seconds() * 1_000_000)

def setup_history(history_items):
    """history_items: list of {url, title, visit_time_from_now_in_seconds}"""
    # MUST close Chrome before modifying History DB
    conn = sqlite3.connect(HISTORY_DB)
    c = conn.cursor()
    # Ensure tables exist (normally they do in an existing profile)
    c.execute("""CREATE TABLE IF NOT EXISTS urls (
        id INTEGER PRIMARY KEY, url TEXT, title TEXT, visit_count INTEGER DEFAULT 1,
        typed_count INTEGER DEFAULT 0, last_visit_time INTEGER, hidden INTEGER DEFAULT 0
    )""")
    c.execute("""CREATE TABLE IF NOT EXISTS visits (
        id INTEGER PRIMARY KEY, url INTEGER, visit_time INTEGER, from_visit INTEGER DEFAULT 0,
        transition INTEGER DEFAULT 805306368, segment_id INTEGER DEFAULT 0,
        visit_duration INTEGER DEFAULT 0
    )""")
    for item in history_items:
        ts = chrome_timestamp(seconds_ago=item['visit_time_from_now_in_seconds'])
        c.execute("INSERT INTO urls (url, title, last_visit_time) VALUES (?, ?, ?)",
                  (item['url'], item['title'], ts))
        url_id = c.lastrowid
        c.execute("INSERT INTO visits (url, visit_time) VALUES (?, ?)", (url_id, ts))
    conn.commit()
    conn.close()

setup_history([
    {"url": "https://www.youtube.com/watch?v=abc", "title": "Video", "visit_time_from_now_in_seconds": 3600},
    {"url": "https://news.example.com", "title": "News", "visit_time_from_now_in_seconds": 7200},
])
```

### Opening Tabs via Playwright CDP

```python
from playwright.sync_api import sync_playwright

def open_chrome_tabs(urls, host="localhost", port=9222):
    """Open URLs in existing Chrome (must be running with --remote-debugging-port)."""
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(f"http://{host}:{port}")
        context = browser.contexts[0]  # use existing default profile context
        for url in urls:
            page = context.new_page()
            page.goto(url, timeout=60000, wait_until='networkidle')
        # Close default blank tab if present
        for page in context.pages:
            if page.url == "about:blank" or page.url == "chrome://newtab/":
                page.close()
                break
        # Do NOT call browser.close() — keep Chrome open for agent

open_chrome_tabs(["https://www.google.com", "https://www.github.com"])
```

---

## 2. Reading & Verifying Browser State (reward-gen)

### Reading Preferences

```python
prefs = read_prefs()  # reuse from above

# Do Not Track
do_not_track = prefs.get('enable_do_not_track', False)

# Font size
font_size = prefs.get('webkit', {}).get('webprefs', {}).get('default_font_size', 16)

# Safe browsing
safe_browsing = prefs.get('safebrowsing', {}).get('enabled', False)
enhanced_safe = prefs.get('safebrowsing', {}).get('enhanced', False)

# Color scheme
scheme = prefs.get('browser', {}).get('theme', {}).get('color_scheme2',
         prefs.get('browser', {}).get('theme', {}).get('color_scheme', 0))
# 0=system, 1=light, 2=dark

# Startup page
startup = prefs.get('session', {}).get('restore_on_startup', None)
# 5 or missing = open new tab page

# Search engine
search = prefs.get('default_search_provider_data', {}).get('template_url_data', {}).get('short_name', '')
```

### Reading Local State

```python
state = read_local_state()
language = state.get('intl', {}).get('app_locale', '')
experiments = state.get('browser', {}).get('enabled_labs_experiments', [])
exp_names = [e.split("@")[0] for e in experiments]
```

### Verifying Bookmarks

```python
with open(BOOKMARKS_FILE, 'r') as f:
    bm = json.load(f)

bar = bm['roots']['bookmark_bar']['children']
folder_names = [b['name'] for b in bar if b['type'] == 'folder']
bookmark_urls = [b['url'] for b in bar if b['type'] == 'url']

# Check specific folder contents
def get_folder_urls(bar_children, folder_name):
    for b in bar_children:
        if b['type'] == 'folder' and b['name'] == folder_name:
            return [c['url'] for c in b['children'] if c['type'] == 'url']
    return []
```

### Verifying History (SQLite)

```python
def read_history():
    conn = sqlite3.connect(HISTORY_DB)
    c = conn.cursor()
    c.execute("SELECT url, title, last_visit_time FROM urls")
    rows = c.fetchall()
    conn.close()
    return rows  # list of (url, title, timestamp)

def check_history_keyword_deleted(keyword):
    """True if no history URL contains the keyword."""
    return all(keyword not in url for url, _, _ in read_history())
```

### Verifying Cookies (SQLite)

```python
def read_cookies():
    conn = sqlite3.connect(COOKIES_DB)
    c = conn.cursor()
    c.execute("SELECT * FROM cookies")
    rows = c.fetchall()
    conn.close()
    return rows

def check_cookie_domain_deleted(domain):
    """True if no cookies from this domain exist."""
    cookies = read_cookies()
    return all(domain not in str(row[1]) for row in cookies)
```

### Verifying Extensions

```python
def get_installed_extensions():
    """Read extension names from manifest.json files."""
    names = []
    if not os.path.isdir(EXTENSIONS_DIR):
        return names
    for ext_id in os.listdir(EXTENSIONS_DIR):
        ext_path = os.path.join(EXTENSIONS_DIR, ext_id)
        if not os.path.isdir(ext_path):
            continue
        for version in os.listdir(ext_path):
            manifest = os.path.join(ext_path, version, "manifest.json")
            if os.path.exists(manifest):
                with open(manifest, 'r') as f:
                    data = json.load(f)
                    names.append(data.get('name', ''))
    return names

# Extension name aliases (same extension, different names across versions)
EXTENSION_ALIASES = [{"Zoom Chrome Extension", "Zoom for Google Chrome"}]

def canonicalize_ext(name):
    for group in EXTENSION_ALIASES:
        if name in group:
            return sorted(group)[0]
    return name
```

### Verifying Active Tab URL via CDP

```python
def get_active_tab_url(host="localhost", port=9222):
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(f"http://{host}:{port}")
        context = browser.contexts[0]
        # Last page is usually the active one
        if context.pages:
            page = context.pages[-1]
            return page.url
    return None
```

### Verifying Page Content via CDP

```python
def get_page_info(url, host="localhost", port=9222):
    """Navigate to URL and return {title, url, content}."""
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(f"http://{host}:{port}")
        context = browser.contexts[0]
        page = context.new_page()
        page.goto(url, timeout=60000, wait_until='networkidle')
        return {"title": page.title(), "url": page.url, "content": page.content()}

def get_all_open_tabs(host="localhost", port=9222):
    """Return list of {title, url} for all open tabs."""
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(f"http://{host}:{port}")
        tabs = []
        for ctx in browser.contexts:
            for page in ctx.pages:
                try:
                    page.wait_for_load_state('networkidle', timeout=30000)
                    tabs.append({"title": page.title(), "url": page.url})
                except:
                    tabs.append({"title": "", "url": page.url})
        return tabs
```

### URL Comparison Helper

```python
from urllib.parse import urlparse, urlunparse
import tldextract

def normalize_url(url):
    """Normalize URL: drop www, ignore TLD suffix differences, strip trailing slash."""
    if not url:
        return ""
    import re
    if not re.match(r'^[a-zA-Z][a-zA-Z0-9+\-.]*://', url):
        url = f"http://{url}"
    parsed = urlparse(url)
    extracted = tldextract.extract(parsed.netloc.lower())
    subdomain = '' if extracted.subdomain == 'www' else extracted.subdomain
    netloc = f"{subdomain}.{extracted.domain}" if subdomain else extracted.domain
    path = parsed.path if parsed.path != '/' else ''
    return urlunparse((parsed.scheme.lower(), netloc, path, parsed.params, parsed.query, parsed.fragment))

def compare_urls(url1, url2):
    return normalize_url(url1) == normalize_url(url2)
```

### Verifying Desktop Shortcuts (.desktop files)

```python
def check_desktop_shortcut(expected_name):
    """Check if a .desktop shortcut exists on Desktop."""
    desktop_dir = os.path.expanduser("~/Desktop")
    for fname in os.listdir(desktop_dir):
        if fname.endswith('.desktop'):
            with open(os.path.join(desktop_dir, fname), 'r') as f:
                content = f.read()
            for line in content.split('\n'):
                if line.startswith('Name='):
                    actual = line[5:]
                    if expected_name.lower() in actual.lower() or actual.lower() in expected_name.lower():
                        return True
    return False
```

---

## 3. Bitter Lessons

1. **Chrome must be killed before modifying SQLite databases.** History, Cookies, and other SQLite files are locked while Chrome is running. Setup scripts must `pkill chrome`, modify files, then relaunch with `--remote-debugging-port=1337`. Forgetting this causes "database is locked" errors.

2. **Chrome timestamps are NOT Unix timestamps.** Chrome uses microseconds since 1601-01-01 (Windows FILETIME epoch). `datetime(1601,1,1) + timedelta(microseconds=chrome_ts)` gives the real time. Writing Unix timestamps will produce dates in the 1600s.

3. **`connect_over_cdp` reuses existing contexts.** `browser.contexts[0]` is the real Chrome profile with cookies, history, and extensions. Do NOT create `browser.new_context()` — it creates an incognito-like context that shares nothing with the user's profile.

4. **URL comparison must normalize aggressively.** `www.google.com` and `google.com` are the same. `airbnb.com` and `airbnb.com.sg` may be the same. Always strip `www`, ignore TLD suffix, and normalize trailing slashes before comparing.

5. **Preferences file has async writes.** Chrome may not flush settings to Preferences immediately. After changing settings via the Chrome UI, poll the file up to 5 times with 1-second delays. Similarly, after writing Preferences, Chrome may overwrite your changes on next save. Write BEFORE Chrome starts.

6. **Accessibility tree URL lacks protocol.** The URL from AT-SPI's "Address and search bar" entry has no `https://` prefix. Always prepend a `goto_prefix` (typically `"https://"`) before using it.

7. **Extension names have aliases.** The same Chrome extension can have different `manifest.json` names across versions (e.g., "Zoom Chrome Extension" vs "Zoom for Google Chrome"). Canonicalize names before comparison using alias groups.

8. **`page.goto()` with `networkidle` can hang on streaming pages.** Some pages (YouTube live, infinite feeds) never reach `networkidle`. Use `timeout=60000` and catch `TimeoutError` gracefully — the page may still be usable.

9. **socat bridges port 9222 to 1337 inside the VM.** Chrome runs with `--remote-debugging-port=1337` internally. An `socat tcp-listen:9222,fork tcp:localhost:1337` bridge exposes it on port 9222 for external access. Setup scripts must launch both processes.

10. **ARM machines use `chromium`, x86 uses `google-chrome`.** Binary name, user data path, and accessibility tree application name all differ. Check `platform.machine()` to determine which binary to use. ARM: `~/snap/chromium/common/chromium/Default/`, x86: `~/.config/google-chrome/Default/`.

11. **HTML parsing needs shadow DOM traversal for Chrome settings.** Chrome settings pages (chrome://settings/*) use nested shadow DOMs. Regular CSS selectors don't penetrate shadow roots. Use `page.evaluate()` with JavaScript that calls `.shadowRoot.querySelector()` chains.

12. **`compare_urls` drops TLD suffixes entirely.** The URL normalizer uses `tldextract` and ignores the suffix, so `airbnb.com` and `airbnb.co.uk` normalize to the same domain. This is intentional for cross-region tolerance but can cause false matches.

13. **Cookie values are encrypted on disk.** Chrome encrypts cookie values using platform-specific keys (DPAPI on Windows, keychain on macOS, gnome-keyring on Linux). The `cookies` table has encrypted `value` and `encrypted_value` columns. For verification, check domain/name presence rather than decrypting values.

14. **Post-config must kill and restart Chrome before reading settings.** Many evaluators use a `postconfig` sequence: `pkill chrome` → sleep 3s → relaunch Chrome → sleep 3s → then read. This ensures settings are flushed to disk and Chrome re-reads the state cleanly.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.