A Windows-focused CLI for driving a persistent Chrome profile over CDP and Playwright.
This repo is the reusable subset of the custom browser tooling:
- launch Chrome with a persistent profile
- reuse or navigate existing tabs
- inspect pages, frames, form controls, and interact with tabs over CDP
- close duplicate tabs
- back up the persistent profile
- shut the browser down cleanly with
Browser.close - forward normal Playwright CLI commands when you intentionally want a separate Playwright CLI session
- check GitHub Releases for updates and pull the latest tagged release into a git checkout
This repo intentionally does not include personal automation, job-search scripts, private candidate data, resumes, account exports, or site-specific application flows.
- Windows
- PowerShell
- Node.js / npm
- Google Chrome
git clone <your-repo-url>
cd persistent-browser-cli
npm installOptional global install from the repo folder:
npm install -g .That gives you:
pbc --help
pbc-cli --help
persistent-browser-cli --help
cli --helpFor local development without a full global publish, you can also use:
npm linkThat creates the command shims from this working copy so you can run:
cli open https://example.com
cli cdp
cli saccli is convenient, but on PowerShell it collides with the built-in Clear-Item alias. In practice you should use pbc or pbc-cli.
For a user-local install that also verifies the environment:
.\install.ps1 -LinkGlobalOr from the CLI:
pbc install --link-globalOr with the bootstrapper:
.\dist\setup.exe --link-globalOptional stable-profile clone flow:
.\install.ps1 -LinkGlobal -CloneStableChromeProfileOr from the CLI:
pbc install --link-global --clone-stable-chrome-profileOr with the bootstrapper:
.\dist\setup.exe --link-global --clone-stable-chrome-profileThat script clones the repo into %LOCALAPPDATA%\persistent-browser-cli, runs npm install, links the command globally if requested, and checks pbc doctor.
You can also build a Windows executable:
npm install
npm run build:exeThat writes:
dist\pbc.exe
dist\setup.exeExample:
.\dist\pbc.exe --help
.\dist\pbc.exe doctor
.\dist\setup.exe --helpImportant constraints:
pbc.exeis the runtime CLI.setup.exeis the installer/bootstrapper.- Neither EXE is a fully standalone packaged app.
- It still expects the repo's installed dependencies to exist, so run
npm installfirst. - In practice,
dist\pbc.exeresolvesplaywright-corefrom the repo'snode_modulesdirectory. - The build process uses Node's single-executable application workflow plus
postjecton Node versions that do not yet support--build-seadirectly. - On Windows, the build may print a signature warning after injection. That is expected for a local unsigned build.
setup.exedownloads a portable Node.js LTS build into the user profile if Node is missing, refreshes PATH, and then runs the repo bootstrap flow.
By default the CLI uses:
- Chrome executable:
C:\Program Files\Google\Chrome\Application\chrome.exe - persistent profile dir:
%LOCALAPPDATA%\persistent-browser-cli\profiles\default - backup dir:
%LOCALAPPDATA%\persistent-browser-cli\backups - CDP port:
9222
You can override any of these with environment variables:
$env:PBC_CHROME_EXE = 'C:\Program Files\Google\Chrome\Application\chrome.exe'
$env:PBC_USER_DATA_DIR = 'D:\browser-profiles\my-profile'
$env:PBC_BACKUP_ROOT = 'D:\browser-profiles\backups'
$env:PBC_CDP_PORT = '9333'
$env:PBC_OPEN_TIMEOUT_MS = '120000'
$env:PBC_PWCLI_SESSION = 'my-browser-session'For machine-specific defaults that should not be committed, create config.local.json in the repo root. Example:
{
"USER_DATA_DIR": "C:\\Users\\yourname\\playwright-persistent-contexts\\chrome-jobhunt",
"DEFAULT_CDP_PORT": 9223
}config.local.json is ignored by git and is applied after built-in defaults but before environment variables.
Open Chrome with the persistent profile:
cd persistent-browser-cli
node cli.js open https://mail.google.comLog in normally in that Chrome window. When Chrome is closed cleanly, the login state remains in the profile directory.
Open or reuse a tab:
pbc open https://mail.google.com
pbc open https://example.com --reusepbc open polls every 500 ms until CDP exposes a usable page target before returning, so follow-up commands can be chained in the same one-liner. Set PBC_OPEN_TIMEOUT_MS if a very slow machine needs more than the default 120 seconds.
Check whether CDP is up:
pbc cdpRun a quick environment check:
pbc doctorThis verifies the effective Chrome path, profile path, backup path, command shims, and whether the configured CDP endpoint is reachable.
Check for updates:
pbc update --check-onlyPull the latest tagged release into this git checkout:
pbc updateOn startup, pbc prints a non-blocking update notice if GitHub Releases reports a newer tag. Set PBC_SKIP_UPDATE_CHECK=1 to disable that check for scripts or offline use.
List and reuse tabs:
pbc tab list
pbc tab list --all
pbc tab activate 2
pbc tab goto active https://mail.google.com
pbc tab close 2
pbc tab pruneBy default, pbc tab list hides Chrome internal/system pages like omnibox popups and extension pages. Use --all when you want the raw full tab list.
Inspect frames and form controls:
pbc tab frames active
pbc tab inspect active
pbc tab inspect active --frame gmail--frame matches Playwright frame names and URLs first, then falls back to the parent DOM iframe element's id, name, src, title, and aria-label. This helps with cross-origin frames that appear blank in raw frame metadata.
Drive the already-open persistent Chrome tab directly over CDP:
pbc tab snapshot active
pbc tab text active
pbc tab click active e3
pbc tab fill active e7 "gaston@example.com"
pbc tab screenshot active .\output\page.png
pbc tab eval active "document.title"These commands attach to the Chrome instance started by pbc open, so they use the same logged-in persistent profile and the same tabs. They do not go through pbc pw.
When changing browser behavior, run a short smoke test and commit visual proof when the check produces a screenshot.
pbc open about:blank
pbc tab eval active 'document.body.innerHTML = `<label>Name <input aria-label="Name"></label><button id="go" onclick="document.querySelector(''#out'').textContent=document.querySelector(''input'').value">Go</button><div id="out"></div>`; "ready"'
pbc tab snapshot active
pbc tab fill active e0 Gaston
pbc tab click active e1
pbc tab text active
pbc tab screenshot active .\photos\pbc-smoke.png
pbc sacExpected visual proof:
If a smoke check is not visual, paste the terminal output instead. For example:
CDP: UP (http://127.0.0.1:9223)
[pbc] Filled ref "e0" using fill.
[pbc] Clicked ref "e1".
Name Go
Gaston
Useful variants:
pbc tab snapshot active --frame gmail
pbc tab snapshot active --json
pbc tab text active --json
pbc tab screenshot active --full-page
pbc tab eval active "Array.from(document.links).map(a => a.href)" --jsonsnapshot prints stable refs like e0, e1, e2. Use those refs immediately with click or fill. Re-run snapshot after navigation, reloads, or large DOM changes because refs can go stale.
Graceful shutdown:
pbc saveandcloseShort alias:
pbc sacThis closes tabs first and then sends DevTools Browser.close so Chrome can flush the persistent profile to disk without a forced kill.
Back up the profile:
pbc backupBack up after force-closing Chrome instances that are using the same profile:
pbc backup --killThe pw command forwards to playwright-cli, defaulting to:
- Chrome
- headed mode
- session name
persistent-browser-cli
It does not automatically force --persistent or --profile. That is intentional. The installed Playwright CLI version does not reliably hand off between a Playwright-managed browser and the separate CDP browser started by pbc open, and forcing the same profile directory causes browser-lock conflicts.
Examples:
pbc pw open https://example.com
pbc pw list --all
pbc pw close-allIf you want a persistent Playwright profile, pass it explicitly:
pbc pw open https://example.com --persistent --profile C:\path\to\profileDo not point that at the same profile directory that pbc open is already using.
The @playwright/cli session model is version-sensitive. On some machines, open keeps a live session you can continue to drive; on others, the session may already be closed by the time the next command runs. Use pbc pw list --all to confirm the browser is actually still open before assuming follow-up commands like snapshot or click will attach.
If a ref-based command fails, the wrapper automatically runs a fresh snapshot so you can keep going.
For day-to-day work against your logged-in persistent Chrome, prefer the CDP-native commands:
pbc open https://example.com
pbc tab snapshot active
pbc tab click active e0Use pbc pw only when you intentionally want the external Playwright CLI workflow.
pbc open <url>- log in or navigate manually
pbc tab listpbc tab snapshot activeorpbc tab inspect activeif you need to understand the pagepbc tab click/fill/text/evalfor automation inside the same persistent Chrome instancepbc pw ...only for Playwright CLI work in its own sessionpbc sacwhen you are done
- Do not run two separate Chrome processes against the same
PBC_USER_DATA_DIRat the same time. - Using multiple tabs in the same browser is fine.
- The PowerShell launch/backup scripts are Windows-specific by design.
- For PATH-based everyday use,
npm linkpluspbc/pbc-cliis still the cleaner workflow than calling the built EXE directly. pbc updateexpects a git checkout. If you installed only from a binary asset, use the installer again or replace the binary with the newest release asset.pbc installis the preferred first-run bootstrap command when you want one command that clones the repo, installs dependencies, and linkspbconto PATH.
