cd /news/developer-tools/mincdp-give-claude-eyes-and-hands-in… · home topics developer-tools article
[ARTICLE · art-63850] src=github.com ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

Mincdp – Give Claude eyes and hands in a browser, in ~410 lines of C

Mincdp, a minimal C and Java client library (~410 lines), allows direct control of headless Chrome via the Chrome DevTools Protocol without Selenium or chromedriver, enabling browser automation for testing and AI agent tasks. The library focuses on simplicity and zero dependencies, supporting navigation, JavaScript evaluation, typing, and screenshots.

read7 min views1 publishedJul 17, 2026
Mincdp – Give Claude eyes and hands in a browser, in ~410 lines of C
Image: source

Drive a real headless Chrome for tests without Selenium or a version-matched chromedriver. Two tiny, dependency-free clients that speak the Chrome DevTools Protocol (CDP) straight to the browser over a WebSocket, in the language of your stack:

C:c/cdp.h

, a single header-only library (~410 lines, libc + POSIX sockets only).Java:java/Cdp.java

, a single file (JDKjava.net.http

only, nothing to vendor).

Same idea, same small command set, one demonstration each against the same page.

The image above was produced by mincdp itself: the demo typed into the page, pressed Enter, then captured this driven state with cdp_screenshot (Page.captureScreenshot).

CDP is the wire protocol Puppeteer and Playwright use under the hood. Talking it directly means no chromedriver middleman and no external dependency to vendor and keep version-matched to Chrome. For a fixed set of internal pages, headless, one browser, that is a good trade: the client is small enough to read in a sitting, and if Chrome ever ships a breaking CDP change you fix the one file rather than wait for an upstream driver release.

It is deliberately not a general framework. It knows only the commands a click-and-assert test needs: navigate, evaluate JS, type text, press a key. If you need cross-browser (Firefox/Safari) or a rich interaction API, use Playwright; this earns its place only where minimalism and zero dependencies do.

Needs a headless Chrome/Chromium on PATH

(and curl

, and cc

/ javac

).

make ut          # the whole regression suite (see Tests)
make demo-c      # build c/demo and run it against page.html
make demo-java   # build java/Demo and run it against page.html
make demo        # both
make shot        # screenshot page.html so you can SEE it
make agent GOAL="..."   # let Claude drive the page toward a goal (needs an API key)

Each demo prints a series of assertions and a demo: N passed, 0 failed

line. It first inspects the page's internals (title, attribute, text, computed style, geometry), then drives the real input path: type "mincdp works", press Enter, and assert the page echoes it. Set CDP_DEBUG=1

to see every CDP frame.

make ut

runs the full regression, in two kinds, so a test set has both hands and eyes:

codeut-- "regular" browser-free unit tests of the client's pure helpers (base64, JSON escaping), several examples each. No Chrome needed, so it always runs (tests/codeut.c

).uiut-- the client driving real headless Chrome, in two kinds:page internals + hands-- the demos inspect the rendered DOM (presence, attribute, text, computed style, geometry) throughRuntime.evaluate

, then interact (type, press Enter, assert the DOM changed), then screenshot thedrivenstate over the protocol (cdp_screenshot

->Page.captureScreenshot

) and assert a valid PNG (c/demo.c

,java/Demo.java

).eyes, for agents--tests/shot.sh

is the standalone shell version: screenshots a fresh page load with Chrome's--screenshot

, no client code needed. It complements the demos' protocol screenshot (which captures a driven state). Either PNG is there to be looked at: open it, or have an agent Read it.

Each layer reports PASS

/ FAIL

/ SKIP

(a missing toolchain SKIPs, exit 77); the suite fails only if a non-skipped layer fails:

mincdp regression:
  codeut (units)       PASS
  uiut hands: C        PASS
  uiut hands: Java     PASS
  uiut eyes: shot      PASS

The demos double as the interaction tests; make demo-c

/ demo-java

run them on their own.

You start Chrome; the client attaches. demo.sh

does that wiring (launch a headless Chrome with --remote-debugging-port

, run the demo, tear down), and it invokes both demos with the same contract, so one launcher and one page.html

serve both languages:

PROG... 127.0.0.1 <chrome-port> file://<abs>/page.html

The same two primitives that make a smoke test, a screenshot (eyes) and the input calls (hands), are the entire surface an LLM needs to drive a browser. c/agent.c

is that loop and nothing else. See, think, act, repeated.

See.cdp_screenshot

captures the live page, whatever state prior actions drove it to.Think. The PNG plus a one-line goal go to the Claude API overcurl

(the one hop raw sockets can't do here is the TLS toapi.anthropic.com

). The model replies with exactly one action.Act. The client replays that one action against the page, then loops.

The model gets a screenshot and answers with a single line, one of:

Reply Hand
NAV <url>
cdp_navigate
TYPE <text>
cdp_insert_text
KEY <name>
cdp_key (e.g. KEY Enter )
JS <expr>
cdp_eval_bool , a `querySelector(...).click()
DONE <note>
stop

Same substring trick as the client: the reply is one short line, so pulling the action out of the response is a single "text":"..."

find, not a JSON parser.

export ANTHROPIC_API_KEY=sk-ant-...
make agent GOAL="type 'mincdp works' into the box and press Enter"

A run against page.html

looks like:

step  1: JS document.getElementById('q').focus()||true
step  2: TYPE mincdp works
step  3: KEY Enter
step  4: DONE the box shows "echo: mincdp works"

Each step is a full loop: the model looked at a fresh screenshot, chose the next keystroke, and mincdp replayed it, closing on DONE

when the screenshot showed the goal met.

It is deliberately a sketch: one action per turn, substring parsing, no retries, thinking off for speed. That minimalism is the point. It is small enough to read end to end before you let a model drive a browser. Like the demos, it SKIPs (exit 77) when Chrome, curl

, or ANTHROPIC_API_KEY

is missing. See c/agent.c

and agent.sh

.

Both clients expose the same shape (C names shown; Java is the camelCase twin):

Call Does
cdp_open(host, port) / Cdp.attach
attach to the first page target of a running Chrome
cdp_navigate(url)
Page.navigate (does not wait for load)
cdp_eval_bool(js, &out) / evalBool
Runtime.evaluate a boolean expression
cdp_wait_bool(js, ms) / waitBool
poll a boolean expression until true or timeout
cdp_insert_text(text) / insertText
Input.insertText into the focused element
cdp_key("Enter") / key
Input.dispatchKeyEvent keyDown+keyUp
cdp_screenshot(path) / screenshot
Page.captureScreenshot -> base64 -> write a PNG
cdp_close() / close
detach

Responses are matched by id and read with targeted substring checks, not a JSON parser. That is the trick that keeps both clients dependency-free: every command returns a boolean, a short ack, or one flat field (the screenshot's base64), so looking for "value":true

/ exceptionDetails

/ "data":"..."

is enough. A command that returned deeply nested JSON would need a real parser; none does.

c/cdp.h

is a single-header library. In exactly one translation unit:

#define CDP_IMPLEMENTATION
#include "cdp.h"

and just #include "cdp.h"

elsewhere for the declarations. See c/demo.c

.

Same category as a Selenium test (out-of-process automation: your code in one process, a real browser in another, talking over a wire protocol), minus the chromedriver middleman and the external dependency. Closer in spirit to a minimal, hand-rolled Puppeteer.

Selenium mincdp
WebDriver driver
cdp handle
driver.get(url)
cdp_navigate(url)
findElement / waits
cdp_wait_bool(sel-exists)
((JavascriptExecutor)driver).executeScript
cdp_eval_bool(js)
element.sendKeys
cdp_insert_text
key press cdp_key("Enter")

What differs:

Selenium talks the WebDriver protocol to a separatechromedriver

binary that then drives Chrome: three processes, and you must keep chromedriver's version matched to Chrome's. It is a jar plus a versioned native binary to vendor and update.mincdp talks CDP directly to Chrome over a WebSocket, no middleman. Each client is one file using only its language's standard library.

The trade, honestly: Selenium and Playwright give a big, polished, cross-browser API (rich waits, action chains, a grid). mincdp gives the handful of commands a headless smoke test needs, in code you can read end to end. Pick accordingly.

page.html          self-contained demo target (no server, no network)
demo.sh            launch Chrome, run a demo, tear down (shared by both)
Makefile           make ut / demo-c / demo-java / demo / shot / clean
c/cdp.h            the C client (single-header library)
c/agent.c          the agent loop: screenshot, Claude, replay one action
agent.sh           launch Chrome, run the agent toward a GOAL, tear down
c/demo.c           the C demonstration = the uiut interaction test (hands)
java/Cdp.java      the Java client (single file)
java/Demo.java     the Java demonstration = the uiut interaction test (hands)
tests/run.sh       the regression runner (make ut): codeut + uiut, PASS/FAIL/SKIP
tests/codeut.c     browser-free unit tests of the client's pure helpers
tests/shot.sh      the eyes: screenshot page.html to a PNG (Chrome --screenshot)

legacy/

holds the 2005 ancestor of this project: an Xvfb-based screenshot capture proof of concept, from before browsers had a headless mode or a remote protocol. It is where mincdp started. See legacy/README.md

.

The dated record of how the "eyes" came about (the origin dialog, the timeline, and the attribution ask) is in legacy/PROVENANCE.md.

MIT. See LICENSE

.

All product names, logos, and brands are property of their respective owners. Chrome is a trademark of Google; Selenium, Puppeteer, and Playwright are the names of their respective projects. They are used here only for identification and comparison, and no affiliation or endorsement is implied.

── more in #developer-tools 4 stories · sorted by recency
── more on @chrome 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/mincdp-give-claude-e…] indexed:0 read:7min 2026-07-17 ·