Your agent codes in a browser it cannot see. cdpx gives it eyes — and gives you proof.

cdpx exposes Chrome DevTools Protocol primitives as a command line. Console, network, Symfony profiler, Core Vitals, SEO of the rendered DOM, user journeys: it all becomes assertable JSON, replayable by a human and version-controlled — during agentic dev, in review, in CI and in anti-regression monitoring.

MIT Python ≥ 3.11 32 commands stdout = JSON · exit 0/1/2 12 tutorials recorded on real Chrome 0.3.1 · pre-1.0 beta
supervised session — disposable Chrome profile, loopback only real output · reference site
$ cdpx goto http://127.0.0.1:8899/form.html
{"url":"http://127.0.0.1:8899/form.html","errorText":null,"waited":"load","ok":true,"elapsed_ms":23.0,"_cdpx":{…}}
$ cdpx type "#name" --secret-env FORM_NAME --clear
{"typed":true,"value_masked":true,"selector":"#name","cleared":true,"_cdpx":{…}}
$ cdpx click "#submit-btn"
{"clicked":"#submit-btn","x":531.7,"y":90.4,"_cdpx":{…}}
$ cdpx text "#result"
{"selector":"#result","text":"OK:Ada","_cdpx":{…}}
Six families · one catalog

One intent per family, its commands sorted below

cdpx gives the agent controlled action, senses and measuring instruments, then connects every result to proof and guardrails (product rationale and design). Each family includes a tutorial recorded on real Chrome and practical recipes. The shared foundation — supervised sessions, navigation, reading and trusted interaction — lives in the grid of 32 commands.

You are:
Family 01 · See

Give the agent senses — and the dev steering it

front-end devagenticdaily dev

An agent that reads neither the JS console nor the failed requests debugs a front end blind. Console and network are the two feedback loops of front-end dev: they are the most cost-effective primitives in the whole tool, complemented by pixel capture and the semantic view of the accessibility tree.

consolenetworkmetricsscreenshotpdfa11y

Recorded demo — real Chrome, repository reference site (cast source): console, network, runtime metrics.

"It's fixed" — except the page is broken · the exception, the 500 XHR and the dead click become three JSON objects

The agent announces it's fixed; you discover otherwise by opening the browser. So you become a scribe: a screenshot pasted into the chat, a stack trace transcribed by hand, the Network tab described in words.

the agent sees it for itselfsheet example
$ cdpx console --duration 3
{"entries":[{"kind":"console","type":"error","text":"TypeError: cart is undefined","ts":1751700000123.4}],"count":1,"errors":1,"duration":3.0}
$ cdpx network http://demo.test/checkout
{"url":"http://demo.test/checkout","requests":[{"url":"http://demo.test/api/cart","method":"GET","resourceType":"XHR","status":500,…}],"summary":{"total":14,"errors_4xx_5xx":1,"bytes":184320}}
$ cdpx dom-diff -- click "#offcanvas-cart"
{"action":["click","#offcanvas-cart"],"changed":false,"diff":[],"lines":0}

What changes: the exception, the 500 XHR and the dead click are three JSON objects that the agent reads, correlates and re-checks after its fix. The "fix, then prove" loop no longer runs through your eyes.

Reading the page like a screen reader · roles and names, without the HTML noise

Full HTML is expensive to read — for an agent as much as for a human. The accessibility tree is the semantic view of the page: roles and names, without the noise. A button with no accessible name disappears from it — which is exactly the signal being sought.

the page as roles and namesreal output · reference site
$ cdpx a11y
{"nodes":[{"role":"RootWebArea","name":"Fixture: form","ignored":false},{"role":"heading","name":"Form","ignored":false},…,{"role":"textbox","name":"Name","ignored":false},…,{"role":"textbox","name":"Email","ignored":false},…,{"role":"button","name":"Submit","ignored":false},…]}

What changes: the agent checks labels, heading structure and landmark regions without parsing the HTML — and accessibility gaps become visible at the same cost.

Recorded demo — real Chrome, repository reference site (cast source): targeted screenshot, full-page JPEG, PDF, then the whole page as accessible roles and names.

observability & capture sheet →

Family 02 · Measure

Objectify instead of assume

Symfony backend devfront-end devperf

"It's slow", "it looks better": as long as performance is an impression, it's up for debate. cdpx turns the Symfony profiler, Core Vitals and code coverage into numbers — and a number can be compared, tracked, refused.

profilervitalscoverage

Recorded demo — real repository reference Symfony app (cast source): the same page in a healthy variant (3 queries, 0 duplicates) then N+1 (6 queries, 4 duplicates, SQL included), and the jq -e one-liner that refuses the merge.

The Doctrine N+1 caught on the product page, not in prod · 6 queries including 4 duplicates: a number, not a hunch

The WebProfiler has had the answer all along, but it lives in a browser: nobody clicks through nine panels on every iteration, and an agent can't read it at all.

the profiler becomes datasheet example
$ cdpx profiler http://127.0.0.1:8000/product/42 --panels db,cache
{"token_present":true,"status":200,"panels":{"db":{"available":true,"queries":6,"statements":2,"duplicates":4,"time_ms":1.76,…},"cache":{"available":true,"calls":4,"hits":3,"misses":1,…}}}
$ cdpx profiler http://127.0.0.1:8000/product/42 --panels db | jq -e '.panels.db.duplicates == 0' || echo "N+1 detected"
N+1 detected

What changes: 6 queries including 4 duplicates — the N+1 is a number, not a hunch. The same one-liner, dropped into CI, prevents the next one (see family 06).

Core Vitals on a real interaction, dead code included · the INP comes from a real dispatched click, not an estimate

Lab tools measure a cold URL's load; your INP plays out on the "Add to cart" click. And the LCP regression turns up in Search Console, three weeks too late.

LCP, CLS, INP — then the weight of code that never runsreal output · reference site
$ cdpx vitals http://127.0.0.1:8899/vitals.html --click "#inp-button" --settle 1.0
{"url":"http://127.0.0.1:8899/vitals.html","lcp":16,"cls":0,"inp":24,"_cdpx":{…}}
$ cdpx coverage http://127.0.0.1:8899/coverage.html
{"url":"http://127.0.0.1:8899/coverage.html","files":[{"url":"http://127.0.0.1:8899/coverage.js","functions":3,"used_ranges":2,"total_bytes":163,"used_bytes":107,"unused_bytes":56,"coverage_percent":65.6}],"count":1,"js":{"total_bytes":163,"used_bytes":107,"unused_bytes":56},"css":{"rules":1,"used":1,"unused":0},"_cdpx":{…}}

What changes: the 24 ms INP comes from a real click dispatched via the Input domain, not an estimate. LCP, CLS, INP and dead bytes get measured on your machine, on every branch.

Recorded demo — real Chrome, repository reference site (cast source): vitals on a real click, JS/CSS coverage, perf budget in one line.

performance sheet → · profiler sheet →

Family 03 · Audit

Auditing the render — the one Googlebot indexes

product / SEOe-commercesite changes

For an SEO audit, what matters is the final DOM as seen by Googlebot's rendering, not the served HTML. curl and grep stop at the source; cdpx extracts the SEO contract from the rendered DOM in one call: title, metas, canonical, hreflang, JSON-LD, h1, alt, links — plus findings ready to assert.

seorgaa

Recorded demo — real Chrome, repository reference site (cast source): full SEO contract in one call, empty findings on the compliant page, named findings on the broken one.

The SEO contract, diffable between staging and prod · a vanishing canonical shows up in the PR, not in Search Console

Canonicals and hreflang break silently during a redesign, and the agency audit arrives after the traffic drop.

SEO becomes a comparable objectsheet example
$ cdpx seo https://staging.example.test/product-42 > staging.json
$ cdpx seo https://www.example.test/product-42 > prod.json
$ diff <(jq -S '{title,canonical,h1,robots}' staging.json) <(jq -S '{title,canonical,h1,robots}' prod.json)
<   "canonical": null,
>   "canonical": "https://www.example.test/product-42",
$ jq .findings staging.json
["2 image(s) without alt"]

What changes: the SEO contract is a version-controlled JSON, compared before and after release — a vanishing canonical shows up in the PR, not in Search Console.

Auditing state after interaction, not just the URL · the intermediate states of a journey get audited like pages

An SPA, a selected variant, an open cart: the state that matters sometimes has no URL. cdpx seo with no argument audits the page currently displayed, whatever the path that led there.

the audit follows the journeysheet example
$ cdpx goto http://shop.localhost/collection/robes
$ cdpx click "#size-variant-38"
$ cdpx seo
# audits the rendered DOM of the current state — title, canonical, JSON-LD after client-side routing

What changes: the intermediate states of an e-commerce journey get audited like pages — that's where SPA SEO bugs live.

SEO sheet →

Family 04 · Reproduce

Reproducing the irreproducible: outages, mobile, state

QAsupportfront-end dev

"It only happens for the customer, on mobile, when the API lags." The most expensive bug is the one you can't reproduce. cdpx controls the conditions: network interception, device emulation, prepared session state — all scripted, so it replays identically.

interceptemulatecookiesstorage

Recorded demo — real Chrome, repository reference site (cast source): simulated API outage, fallback read from the DOM, mobile emulation and slow 3G network.

What happens to the page when the payment API goes down? · outage behavior reads straight from the DOM, without touching the infra

The error fallback is never tested because "we're not going to turn off the API just to check." An interception rule simulates the outage, and the page reveals its real behavior.

the outage becomes a test casereal output · reference site
$ cdpx intercept --rule "*api* => 503" --settle 1 -- goto http://127.0.0.1:8899/intercept.html
{"url":"http://127.0.0.1:8899/intercept.html","rules":["*api* => 503"],"hits":[{"url":"http://127.0.0.1:8899/intercept.html","action":"continue"},{"url":"http://127.0.0.1:8899/api/json","action":"503"},{"url":"http://127.0.0.1:8899/api/status/500","action":"503"},…],"count":5,"settle":1.0,"_cdpx":{…}}
$ cdpx text "#intercept-result"
{"selector":"#intercept-result","text":"/api/json:503|/api/status/500:503|/api/slow?ms=120:503|/api/echo:503","_cdpx":{…}}

What changes: outage behavior reads from the DOM, in one command, without touching the infra. The rule accepts continue, block or a status — enough to script degradations.

"It breaks on mobile" — okay, which one, and prove it · a named, replayable preset instead of an anecdote

The mobile viewport, the slow network and the throttled CPU change the page's real behavior. emulate applies a profile for the duration of one action, then everything returns to normal.

mobile profile applied for one actionreal output · reference site
$ cdpx emulate mobile -- goto http://127.0.0.1:8899/index.html
{"preset":"mobile","applied":true,"action":{"argv":["goto","http://127.0.0.1:8899/index.html"],"result":{"url":"http://127.0.0.1:8899/index.html","errorText":null,"waited":"load","ok":true,"elapsed_ms":11.8}},"_cdpx":{…}}

What changes: "on mobile" becomes a named, replayable preset — combine it with screenshot, vitals or dom-diff to turn the anecdote into a finding.

Preparing a scenario's state without leaking a session · cookies and storage set from a script, values redacted by default

Consent given, cart filled, session logged in: realistic scenarios require state. cdpx reads and prepares it — cookies and storage — with redaction by default, because an agent copies its outputs around.

the state, without the valuesreal output · reference site
$ cdpx storage
{"kind":"local","entries":{"cdpx-key":"***"},"count":1,"values_masked":true,"_cdpx":{…}}
$ cdpx cookies set --name consent --value-env CONSENT_VALUE --url http://127.0.0.1:8899/
# the value comes from the environment, never from argv or a log

What changes: state is prepared in a script instead of recreated by hand — and values stay redacted by default, --show-values being an explicit choice.

Recorded demo — real Chrome, repository reference site (cast source): storage and cookies redacted by default, cookie set from the environment, cleanup.

resilience sheet → · state & session sheet →

Family 05 · Prove

Trust isn't declared, it's replayed

reviewer / leadQAagentic

Reviewing an agent's work — or a colleague's — on the faith of an "it works" doesn't hold up. Every cdpx action can be logged, replayed and verified: the NDJSON from record, the divergences from replay, the verdicts from scenario form the run's admissible record.

recordreplayscenario

Recorded demo — real Chrome, repository reference site (cast source): journey logged, replayed, then the pass verdict of the YAML scenario.

The journey clicked once becomes a test · the first divergence stops everything with exit 1

After every change, someone re-clicks through checkout by hand — or nobody does. The log of a recorded journey gets replayed, and the first divergence stops everything with exit 1.

record → replay → divergencesheet example
$ cdpx record -o journey.ndjson -- goto http://demo.test/product-42
$ cdpx record -o journey.ndjson -- click "#buy"
$ cdpx --max-actions 20 replay journey.ndjson
{"path":"journey.ndjson","events":3,"played":3,"ok":true}
# … cart template redesign …
$ cdpx --max-actions 20 replay journey.ndjson
{"path":"journey.ndjson","events":3,"played":1,"ok":false,"divergence":"event 1: selector not found after 10.0s: #buy"}

What changes: exploration leaves behind a replayable log, validated in full before the first action; secrets live in it by reference (@env:NAME), never by value.

The business scenario with verdict, assertions and attachments · a structured verdict, not an impression

A critical journey deserves more than a sequence of commands: a versioned YAML scenario composes reusable local step fragments, sets its assertions and archives its proof — a capture per step, console errors counted.

a verdict, not an impressionsheet example
$ cdpx scenario run checkout_guest_add_to_cart.yml
{"name":"checkout_guest_add_to_cart","verdict":"pass","findings":[],"steps":[{"index":0,"label":"product_page","verb":"goto","ok":true},…],"assertions":[{"name":"no_console_errors","expected":true,"ok":true,"actual":0}],"artifacts":[{"type":"screenshot","label":"product_page",…}],…}

What changes: use cdpx scenario validate FILE without Chrome to inspect expanded labels, authority and dependency hashes. Reviewing an agent's PR means reading a structured verdict and its proof — the cdpx repository applies the rule to itself, too, with a proof cockpit generated by ./dev proof where every test attaches captures, logs and casts.

orchestration sheet →

Family 06 · Lock down & watch

From merge gates to night watches

devopsleadCImonitoring

Everything the previous families measure fits in one shell line with an exit code — which is exactly what a CI and a monitor are made of. The same commands used to build the app lock down the merge, then watch in production. No dedicated command: just composition.

jq -ediffexit 0/1/2session --ttlcron
four gates, four one-linerspipeline example
$ cdpx vitals "$APP_URL/product/42" --click "#add-to-cart" | jq -e '.lcp < 2500 and .cls < 0.1'
$ cdpx profiler "$APP_URL/product/42" --panels db | jq -e '.panels.db.duplicates == 0'
$ diff <(cdpx seo "$APP_URL/product/42" | jq -S '{title,canonical,robots}') seo-contract.json
$ cdpx --max-actions 20 replay journey-checkout.ndjson
Budgets become merge gates · the refusal arrives with its own JSON evidence, not with "it seems slower"

A perf budget on a wiki page has never blocked a regression. A budget in jq -e inside the Makefile has: exit 1, the PR waits.

flowchart LR
  PR["Pull request"] --> S["disposable cdpx session
+ branch app"] S --> G1["vitals ≤ budget"] S --> G2["db.duplicates == 0"] S --> G3["SEO contract diff"] S --> G4["journey replay"] G1 --> V{"all exit 0?"} G2 --> V G3 --> V G4 --> V V -->|yes| M["merge"] V -->|no| B["blocked
+ JSON evidence"]

What changes: the regression is paid for at its cheapest moment — before the merge — and the refusal arrives with its own JSON evidence, not with "it seems slower".

Anti-regression monitoring: the automatic round · divergence or budget overrun = alert, within minutes

Between two deploys, nobody re-tests checkout or watches the canonical. An ephemeral cron session replays critical scenarios and measures key pages — divergence or budget overrun = alert.

flowchart LR
  C["cron / scheduler"] --> S["cdpx session start
disposable profile, short TTL"] S --> R["scenario run
critical journeys"] S --> V["vitals + seo
key pages"] R --> J["JSON + exit codes"] V --> J J -->|"exit 1"| A["alert / issue
with the JSON evidence"] J -->|"exit 0"| Z["session stop
nothing to report"]
the round, in shellscript example
# every 30 minutes, on a monitoring machine
$ eval "$(cdpx session start --run-id monitor --authority interaction --origins "https://www.example.test" --ttl 300 --export)"
$ trap 'cdpx session stop' EXIT
$ cdpx scenario run checkout_guest.yml | jq -e '.verdict == "pass"' || alert "checkout broken"
$ cdpx vitals https://www.example.test/ | jq -e '.lcp < 2500' || alert "LCP degraded"

What changes: functional or perf regressions get detected within minutes, with the exact JSON evidence in the alert — and the disposable session leaves neither profile nor cookie behind.

orchestration sheet →

Catalog

32 commands, one output contract

stdout = a JSON object, stderr = diagnostics, exit 0/1/2. The foundation serves every family; each group ships its own recorded tutorial — demos produced against real Chrome and the repository's reference sites, genuine JSON outputs and timings, only the keystrokes are synthesized (protocol). The reference catalog lives in docs/PRIMITIVES.md.

Foundation · session

session start|stopassign a disposable, exclusive browser session to a run; --export installs the identity with one eval
tabs listinspect the single target assigned to the session
versioncheck the targeted Chrome before acting
recorded tutorial — the full lifecycle

Real Chrome (cast source): session start --export, status, version of the assigned Chrome, single target, teardown.

supervised session reference →

Foundation · navigation

gotomove and know when the page is ready (load, domcontentloaded, none)
waitwait for an element injected later (SPA, deferred content)
recorded tutorial — waiting for an SPA's real state

Real Chrome (cast source): navigation, counting, then the SPA content that only arrives 300 ms after the load event, waited for cleanly.

navigation sheet →

Foundation · reading

textinnerText — low-cost semantic view, far cheaper than a screenshot
htmlouterHTML — check attributes, classes, data-*
countcheap assertion ("there really are 12 products")
evalroot primitive, universal escape hatch — last resort
frameread inside a same-origin iframe (payment, consent)
recorded tutorial — five levels of reading

Real Chrome (cast source): five levels of reading on the reference form, iframe included.

DOM sheet →

Foundation · interaction

clicktrusted click — requires attached, visible, enabled, stable, hit-test at center
typefill a field without ever putting the secret in argv (--secret-env)
keyEnter, Tab, arrows… a user's keyboard validation
dom-diffbefore/after snapshot of an action → stable structural diff
recorded tutorial — act and prove the effect

Real Chrome (cast source): input redacted from the environment, click, keyboard validation, and the exact DOM diff of a click.

interactions sheet →

01 · See

consoleJS logs + exceptions — THE missing feedback, streamable as NDJSON
networknavigate while capturing XHR, statuses, weight — summary + per-request detail
metricsheap, nodes, layouts — objectify a leak or a drift
screenshotpixel view — CSS bugs, renders; JPEG to keep it light
pdffreeze a page as printable proof of state
a11ycompacted accessibility tree — structured semantic view

demos in the See family →

02 · Measure

vitalsLCP/CLS/INP — the interaction for the INP, measured locally on every branch
coveragedead JS/CSS by file — front-end debt measured, not guessed
profilerparse the WebProfiler panels (Doctrine, Twig, cache, exceptions…) — the N+1 quantified by the agent

demos in the Measure family →

03 · Audit

seoon-page diagnostic of the rendered DOM — diffable between staging and prod, assertable with jq -e
rgaapinned RGAA 4.1.2 catalog, prudent page evidence and declared sample audits — never an automatic certification

demos in the Audit family →

04 · Reproduce

interceptmock/block requests during a navigation ("PAT => 503|block|continue")
emulatedevice, network/CPU throttling — applied for the duration of one action
cookiesinspect (redacted by default) and prepare state via --value-env
storagelocalStorage / sessionStorage — guest cart, consent, front-end caches

demos in the Reproduce family →

05 · Prove

recordredacted cdpx.record/v2 log — secrets by reference @env:
replaypre-validate then replay, stop at the first divergence, --max-actions budget
scenariodeclarative business journey — single verdict, findings, evidence folder

demos in the Prove family →

Positioning

Why not just an MCP DevTools server?

Wiring DevTools to an agent through MCP gives eyes to a conversation. cdpx makes a different bet: giving primitives to the project.

  • Replayable by a human. An MCP tool call lives and dies inside the agent's context. Every cdpx action is a shell line: you copy it, it does the same thing. Yesterday's debugging session replays today, identically.
  • Assertable in CI. stdout JSON + exit 0/1/2: jq -e, diff, a threshold in the Makefile. Perf budgets, SQL query caps, SEO contracts become CI gates. A tool call doesn't fit in a pipeline.
  • Traceable and replayable. record logs to NDJSON, replay detects divergences, scenario runs YAML journeys with assertions. What the agent did is written down, provable, re-runnable.
  • Nothing to wire up. An agent needs only a shell — no tool schema to maintain. 32 commands under a single contract; the full catalog lives in docs/PRIMITIVES.md.
Security

Safe by default, built for agents

An agent driving a browser handles untrusted data. cdpx guards every action; the full rules live in HARNESS.md.

  • Supervised sessions, never your own Chrome. Every session has its own disposable profile, its own dynamic loopback port and a single target; the supervisor destroys the profile and artifacts on stop. A raw port 9222 exposed to an agent has none of these safety belts.
  • Mandatory, fail-closed origin allowlist. Any unauthorized destination is refused before acting. Three authority levels — observation, interaction, privileged — bound what a command is allowed to do.
  • Secrets out of argv, values redacted. --secret-env and secret_ref resolve sensitive values in memory; cookies and storage are redacted by default in every output.
  • Page content is untrusted input. Outputs carry "_cdpx":{"content_trust":"untrusted"}: an instruction read from the page can never alter the run, its grants or the harness rules.
Get started

Installation

Docker is the only runtime dependency. One command installs the digest-pinned launcher; the image supplies Python 3.14, Chromium and cdpx.

Version 0.3.1 · pre-1.0 beta. The surface is tested against a mock CDP, a real Chrome and a Dockerized Symfony application. Contract changes remain possible before 1.0 and are announced in the changelog.

This installs cdpx for use in your projects. Contributing to cdpx itself is a separate path: clone the repository and use ./dev — see the development portal and the contribution guide.

installation
$ curl -fsSL https://inem0o.github.io/cdpx/install | sh
$ cdpx --version
Agent-assisted onboarding

Or let your agent introduce you

Already running an AI coding agent? Give it the canonical guide so it can explain cdpx, inspect your project and walk you through a bounded local setup instead of improvising commands.

The guide covers installation, cdpx.yaml, the first supervised session, authority choices, cleanup and common fixes. Read the agent guide →

paste into your coding agent
Help me understand and set up cdpx for this project. Read https://inem0o.github.io/cdpx/agent-guide.md first, then walk me through installation, project configuration, and a safe local smoke test step by step.