惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
WordPress大学
WordPress大学
S
SegmentFault 最新的问题
小众软件
小众软件
T
Tailwind CSS Blog
博客园 - 聂微东
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
人人都是产品经理
人人都是产品经理
V
Visual Studio Blog
罗磊的独立博客
有赞技术团队
有赞技术团队
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Jina AI
Jina AI
量子位
云风的 BLOG
云风的 BLOG
Recent Announcements
Recent Announcements
Hugging Face - Blog
Hugging Face - Blog
P
Proofpoint News Feed
N
Netflix TechBlog - Medium
GbyAI
GbyAI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
腾讯CDC
美团技术团队

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
"Persisting your real Chrome login across Playwright rest...
Takayuki Kaw · 2026-05-10 · via DEV Community

Every macOS reboot, the same ritual. Open the Playwright-controlled Chrome window, see seven publishing tabs all logged out, and spend the next ten minutes typing passwords and tapping the Google account picker. Zenn, dev.to, note, Substack, X, LinkedIn, the Google Search Console dashboard. All gone, all needing the same Google SSO dance through my corevice.com workspace account.

I run a one-person GTM operation for Codens and the publishing pipeline is entirely Playwright-driven. npx @playwright/cli@latest opens a real Chrome with a persistent profile, and a stack of small scripts paste titles and bodies into each editor. It works beautifully until the host reboots and the user-data-dir at /tmp/chrome-pw-corevice evaporates with the rest of /tmp.

I finally sat down and fixed it. The result is a thirty-line shell script that clones my daily-driver Chrome profile into the Playwright tmpdir on every launch, with two non-obvious tricks that make the cookies actually decrypt. This post is about those two tricks.

Why the obvious copy doesn't work

The first thing anyone tries is the obvious thing.

cp -r ~/Library/Application\ Support/Google/Chrome/Default \
      /tmp/chrome-pw-corevice/Default

Enter fullscreen mode Exit fullscreen mode

Run it, fire up Playwright, and Chrome opens looking like it has my profile. History is there. Bookmarks are there. Extensions are there. But every site is logged out, and the cookie jar in DevTools is either empty or full of cookies that don't authenticate anything.

The reason is that Playwright launches Chrome with two flags I didn't know about until I started digging:

--use-mock-keychain
--password-store=basic

Enter fullscreen mode Exit fullscreen mode

Those flags tell Chrome to bypass the macOS Keychain entirely and use a hardcoded mock encryption key for cookies and the password store. From Playwright's point of view this is the right default. CI runners don't have a real keychain. Headless containers don't have a real keychain. The mock makes Chrome boot reliably in places where Keychain Access doesn't exist.

But for me, this is exactly wrong. The cookies my daily-driver Chrome wrote to disk were encrypted with the real keychain key, the one Chrome stored under "Chrome Safe Storage" in my login keychain on first install. The cookies that just got copied over are still encrypted with that real key. Playwright's Chrome boots with the mock key, tries to decrypt them, gets garbage, and silently treats every cookie as invalid.

I tried storageState first, which is the documented Playwright path for this. Export cookies and localStorage from one context, inject into another. It works for some sites and dies for others. Substack stalled at the Google SSO redirect and never finished the auth handshake. Note's editor wanted a CSRF token tied to a session cookie that storageState had captured but which the server no longer accepted, presumably because the session was bound to the original UA fingerprint. After the third site failed in a different way I gave up on storageState and went back to cloning the whole profile.

So: two real fixes are needed. Make Playwright's Chrome speak the same encryption language as my daily Chrome, and copy the cookie database in a way that doesn't corrupt it.

Fix one, patch the keychain flag

Playwright's CLI bundles its Chrome launch arguments inside playwright-core/lib/coreBundle.js. When you run npx @playwright/cli@latest, npm caches that file under ~/.npm/_npx/<hash>/node_modules/playwright-core/lib/coreBundle.js. The file is huge and minified, but the two strings I care about appear verbatim:

"--use-mock-keychain",
"--password-store=basic"

Enter fullscreen mode Exit fullscreen mode

A sed rewrite is enough. Swap the first to --use-real-keychain and the second to --password-store=keychain. Chrome on macOS recognizes both, and once they're in place the launched Chrome reads its encryption key from the same login keychain entry as my daily-driver Chrome. The cookies decrypt. SSO holds.

The patch wants to be idempotent because npx happily re-extracts the package if it gets purged from the cache, and I don't want to re-edit the file by hand each time. So the script does three things. It locates the bundle with find. It checks whether the bundle still contains --use-mock-keychain, which means it hasn't been patched yet. If so, it makes a .bak copy on first patch and runs sed -i '' in place.

The .bak is the escape hatch. If a future Playwright update changes those flags or relies on the mock keychain elsewhere and my patch breaks something, I can mv coreBundle.js.bak coreBundle.js and be back to stock in one command.

The first time you launch the patched Chrome, macOS will pop a Keychain Access dialog asking you to allow access to "Chrome Safe Storage." Click Always Allow. After that, no prompts.

Fix two, SQLite backup for the cookie file

With the keychain flag patched, the next failure mode is more subtle. Sometimes the cookies decrypt, sometimes they don't, and when they don't, the SQLite file looks corrupt. Chrome refuses to read it and silently starts a fresh empty cookie jar.

Chrome's Cookies file is a SQLite database. My daily-driver Chrome is almost always running, which means it's holding write locks on that database, and depending on timing it may have a partial write in progress when cp reads the file. The result is a torn copy: the bytes are physically there, but the SQLite page checksums don't match the WAL log, and SQLite refuses to open it.

The right tool for snapshotting a live SQLite database is the .backup command:

sqlite3 "${SOURCE_PROFILE}/Default/Cookies" \
  ".backup ${PW_PROFILE_DIR}/Default/Cookies"

Enter fullscreen mode Exit fullscreen mode

This isn't just a smarter copy. It uses SQLite's online backup API, which acquires a read lock, copies pages in a way that's transactionally consistent with the source database's current state, and produces a target file that opens cleanly. You can run it while Chrome is actively writing to the source. The output is always a valid database.

The script removes the stale Cookies and Cookies-journal files first, then runs .backup on every launch. That way the cookie jar is always fresh, even if I haven't rebooted but I have used my daily Chrome to log into a new site since the last Playwright session.

The script

The whole thing is at runbooks/launch/playwright-launch.sh in my GTM repo. Roughly thirty lines if you don't count comments.

#!/usr/bin/env bash
# Launch playwright-cli against the corevice.com Chrome profile copy.
# Idempotent: if profile copy missing, re-creates it; if patch missing, re-applies.
# Usage: ./playwright-launch.sh open <url>
#        ./playwright-launch.sh <command> [args...]

set -euo pipefail

PW_CACHE_BASE="${HOME}/.npm/_npx"
PW_PROFILE_DIR="/tmp/chrome-pw-corevice"
SOURCE_PROFILE="${HOME}/Library/Application Support/Google/Chrome"

ensure_profile_copy() {
  if [ ! -d "${PW_PROFILE_DIR}/Default" ] || [ ! -f "${PW_PROFILE_DIR}/Default/Cookies" ]; then
    echo "[setup] copying profile..."
    mkdir -p "${PW_PROFILE_DIR}/Default"
    rsync -a \
      --exclude='Cache' \
      --exclude='Code Cache' \
      --exclude='GPUCache' \
      --exclude='Service Worker' \
      --exclude='ShaderCache' \
      --exclude='GraphiteDawnCache' \
      --exclude='component_crx_cache' \
      --exclude='extensions_crx_cache' \
      --exclude='Sessions' \
      --exclude='File System' \
      --exclude='blob_storage' \
      --exclude='Cookies' \
      --exclude='Cookies-journal' \
      "${SOURCE_PROFILE}/Default/" "${PW_PROFILE_DIR}/Default/"
    cp "${SOURCE_PROFILE}/Local State" "${PW_PROFILE_DIR}/" 2>/dev/null || true
    cp "${SOURCE_PROFILE}/First Run" "${PW_PROFILE_DIR}/" 2>/dev/null || true
  fi

  # Always refresh cookies via SQLite .backup (safe with Chrome running)
  rm -f "${PW_PROFILE_DIR}/Default/Cookies" "${PW_PROFILE_DIR}/Default/Cookies-journal"
  sqlite3 "${SOURCE_PROFILE}/Default/Cookies" \
    ".backup ${PW_PROFILE_DIR}/Default/Cookies" 2>/dev/null
}

ensure_patch() {
  local cb
  cb=$(find "${PW_CACHE_BASE}" -path '*playwright-core/lib/coreBundle.js' 2>/dev/null | head -1)
  if [ -z "$cb" ]; then
    echo "[setup] @playwright/cli not yet installed; npx -y will install it"
    return
  fi
  if grep -q -- '--use-mock-keychain' "$cb"; then
    echo "[setup] patching playwright to use real keychain..."
    [ ! -f "${cb}.bak" ] && cp "$cb" "${cb}.bak"
    sed -i '' \
      -e 's|"--password-store=basic"|"--password-store=keychain"|' \
      -e 's|"--use-mock-keychain",|"--use-real-keychain",|' \
      "$cb"
  fi
}

ensure_profile_copy
ensure_patch

export PATH="${HOME}/.asdf/shims:${PATH}"

if [ "${1:-}" = "open" ]; then
  shift
  exec npx -y @playwright/cli@latest open --headed "$@" \
    --browser chrome \
    --profile "${PW_PROFILE_DIR}"
fi

exec npx -y @playwright/cli@latest \
  "$@" \
  --browser chrome \
  --profile "${PW_PROFILE_DIR}"

Enter fullscreen mode Exit fullscreen mode

A few notes on the rsync exclude list. All the cache directories are excluded because they're large, regenerable, and sometimes hold OS-specific binary blobs that Chrome will rebuild on first launch. Sessions is excluded so Playwright's Chrome doesn't try to restore tabs from my daily browsing. File System and blob_storage are excluded for size. Cookies and Cookies-journal are excluded specifically because we handle them via .backup immediately after the rsync, and we want that to be the authoritative copy.

Local State and First Run are copied separately. Local State is where Chrome stores the encrypted master key reference and a few profile-level settings. First Run is a sentinel file that suppresses the first-run wizard.

The patched diff itself is two lines:

- "--use-mock-keychain",
+ "--use-real-keychain",
- "--password-store=basic"
+ "--password-store=keychain"

Enter fullscreen mode Exit fullscreen mode

That's the whole keychain fix. Two strings.

A side issue, headless mode breaks the clipboard

The script forces --headed for the open subcommand, and there's a story behind that. My publish scripts work by pbcopy-ing the title and body into the clipboard, focusing the editor field via Playwright, and then sending Cmd+V. CodeMirror, Substack's editor, dev.to's editor — they all behave better with a real paste than with type() calls that fire individual keypress events. Markdown formatting survives. Code blocks stay intact. Smart-quote autocorrect doesn't fire.

But headless Chromium doesn't have a system clipboard. navigator.clipboard.readText() returns empty, the paste handler sees no data, and the form silently stays empty. I lost an hour to that one before realizing the open command was defaulting to headless mode in the version of @playwright/cli I was on. Forcing --headed makes the daemon run as a real Chrome window with full clipboard access, which is what I want anyway because I sometimes want to glance at the publish flow while it's running.

The non-open commands pass through unchanged, so anything else that wants headless behavior still gets it.

What it's worth

Five to ten minutes of manual relogins per reboot, multiplied by however often macOS decides to update overnight. Across a year that's hours I get back, and more importantly the publish scripts now run unattended. I push a draft, the script opens the right tab, pastes the right content, and I review the rendered preview before clicking publish.

If you're running a similar setup, the script is generic. Change SOURCE_PROFILE if you use a non-Default Chrome profile, change PW_PROFILE_DIR if you don't trust /tmp to survive your reboot policy, and the rest should work.

This is the kind of small infrastructure work that makes solo operations possible. We build a lot of these at Codens, where the day job is wiring AI agents into the same kind of publishing and dev pipelines.