ClickMonkey is the same idea it always was: point it at a URL, let it drive the
UI, and keep a tape you can replay when something is wrong. v2 is that product
without the JRE, the local proxy, or a WebdriverIO intro callback.
The leash is one JSON file. The sitemap, ledgers, and run tapes live next to
it in clickmonkey/. The log is a line DSL. The view is what a brain
(human or model) sees — ids, values, actions, a scoped accessibility
snapshot, and a look block (font palette + hit-tested cover) — not HTML.
What it harvests vs what QA still owns: docs/issue-classes.md. Finding catalog (ClickMonkey vs AXE / WCAG / html-validate / HTML): docs/findings (GitHub Pages: https://morkeleb.github.io/clickmonkey/findings/). Leftover WCAG a person still tests: qa-left. Jobs, modes, and fog: docs/walkers.md. Fog ledger and hunger: docs/fog.md. How to read the dashboard map (haze, m/u/n pips, live letters): docs/map.md (GitHub Pages: https://morkeleb.github.io/clickmonkey/map/). Host-LLM MCP (explore, then freeze and prove a spec): docs/mcp.md.
0.0.7 stays tagged. Those configs will not run here.
npm install -g clickmonkey
# or from this repo
npm install
npm install --prefix web
npx playwright install chromium
npm run build
npm run linknpm run link puts clickmonkey on your PATH and installs zsh completions. Type click<TAB>, not cli<TAB> (cli is clippy-driver). After linking, run rehash or open a new terminal. The launcher is bin/clickmonkey.mjs: it runs src/ via tsx until you npm run build, then it uses dist/. clickmonkey ui needs web/dist from npm run build (CLI tsc plus the Vite app).
Node 22.22+ or 24.8+. No JRE.
Serve a fixture, then map it and run the playbook:
# from the repo, any static server on the fixture folder
npx --yes serve fixtures/sites/accepts-empty -l 4173
clickmonkey init --url http://127.0.0.1:4173/
clickmonkey inspect
clickmonkey view
clickmonkey playbook empty-required
clickmonkey report --all
clickmonkey replay clickmonkey/reports/<id>/findings.mdpage.ready prefers a unique data-testid when one exists, then a unique
landmark or heading, then document. Sites under test are not required to
add test ids — Google and example.com work — but a visible field or click
target with no id / data-testid (or data-cy / data-test) is a
missingStableId warn. That is the hook classical e2e tools use. It
does not block the walk.
This repo dogfoods the same layout: clickmonkey-testing
has one leash that maps the GitHub Pages catalog and one that maps clickmonkey ui.
fixtures/sites/validates is the same form with client-side validation. The
playbook should pass there and write no findings.
clickmonkey step 'click page.open_create' runs one line against the live
page and appends it to clickmonkey/runs/<id>/log.txt.
The map is fog of war. Walkers have jobs; on a tile they pick a mode. Jobs, modes, and fog hunger: docs/walkers.md, docs/fog.md. Dashboard haze and m/u/n pips: docs/map.md.
clickmonkey map --steps 80 # grow pages/surfaces, never fill
clickmonkey unleash --steps 200 # hunt mapped forms, fill, submit
clickmonkey unleash --form clients_new # pin one mapped form, fill, submit, stop
clickmonkey nasty --steps 200 # junk in those forms (site you own)
clickmonkey explore --charter "…" # exploratory testing, no MCP (needs brain)
clickmonkey mcp # host-LLM walk, then freeze/replay specs
clickmonkey fog --reset # drop fog clocks on the sitemap; rooms stayFive monkeys (working names): map, unleash, nasty, explore, mcp.
map clicks unseen doors (unvisited pages and unopened dialogs) and pathfinds toward rooms it has not stood on, instead of grinding the same sidebar. It never fills and never clicks submit/save/delete. After it, the others have legal ids. Stale rooms (last visit days/weeks ago) pull the same map run — fog drives hunger, not a fifth monkey.
unleash pathfinds toward forms (fields + submit) this job has not walked recently, fills them, and watches what happens. Forms already filled this run drop in priority but stay in the pool. On the tile it picks wizard (lock), or the least-recent of form / list / tab / dialog / empty, else nav. Wizard fills then Next — it does not hop to the sidebar mid-stepper.
nasty is the same hunt on its own fog clock (clickmonkey nasty, same as
unleash --nasty): XSS/SQLi/overlong junk and missed validation, only on a
site you own.
explore takes a charter (ticket, git log) for what “doing the job”
means; the model walks legal ids toward that, not toward random forms. It
sees the same Mode: as unleash. clickmonkey explore is exploratory
testing without MCP (needs brain). MCP is the same walk with the host LLM
as the brain, then spec_save / spec_run to freeze a replayable spec and
prove it — that is why you would use the MCP server instead of explore-only.
Explore pings brain before it opens a browser. If the model is unreachable
or the API key is missing, it exits 2 instead of walking blind. It will not
take a screenshot when the last step was already a screenshot.
clickmonkey.json # leash: url, fence, intro, writePolicy, screenshots, brain, vision, seo
clickmonkey/
map.json # living sitemap (rooms, doors, descriptions, fog clocks)
testability.json # legacy workspace ledger (inspect without a run)
quality.json # legacy workspace ledger (inspect without a run)
broken.json # legacy workspace ledger (inspect without a run)
runs/<id>/ # tape, shots, findings (idempotencyKey), nav.jsonl for one walk
runs/<id>/quality.json # HTML, a11y, JS, SEO, visual extras for that walk
runs/<id>/testability.json # locatability for that walk
runs/<id>/broken.json # 404s seen on that walk
runs/<id>/verbose/ # --verbose only: per-step HTML + view.txt (safe to delete)
reports/<id>/findings.md # shareable reports (one folder per report)
dismissed.json # ik_ keys prune wrote; later report skips the same bug
fixed.json # ik_ keys replay confirmed; later report moves them to Fixed
replays/<id>/comparison.md # before/after vs that report
explore-context.md # optional: app architecture for explore --skills
specs/*.md # replayable clickmonkey fences
lands.json # leftover sidecar; absorbed onto map pages then deleted
Commit the leash, the map, specs, explore context, and dismissed.json (false positives you already judged). Ignore generated walks (screenshots, verbose DOM, presence) — they are large and local.
# ClickMonkey
clickmonkey/runs/
clickmonkey/replays/
clickmonkey/bundle/
clickmonkey/reports/
clickmonkey/dev-origin
clickmonkey/ui.pid
clickmonkey/lands.json
clickmonkey/**/*.json.lock
clickmonkey/**/*.json.tmpLeave clickmonkey.json, clickmonkey/map.json, clickmonkey/specs/, and clickmonkey/explore-context.md tracked. Secrets in the leash are $CLICKMONKEY_* tokens, not values. Fog clocks live on each sitemap page (page.fog); they change as monkeys walk. clickmonkey fog --reset drops the clocks and keeps the rooms — use that in CI or before a commit if you do not want hunger in git.
reports/ is optional to ignore: markdown without runs/ has no screenshots. CI should keep reports as job artifacts (examples/gitlab-ci.yml). Commit a single findings.md only if you want a paper trail.
Do not gitignore the whole clickmonkey/ folder.
clickmonkey.json is the file you edit:
{
"url": "http://127.0.0.1:4173/",
"fence": { "path": "/", "blacklist": ["/logout"] },
"intro": [
"fill login.user $CLICKMONKEY_USER",
"fill login.password $CLICKMONKEY_PASSWORD",
"click login.submit"
],
"writePolicy": "validationOnly",
"seo": { "private": ["/app"] }
}The seo block is optional. Leave it off for an app-only site. Use private prefixes for a site that is half marketing, half signed-in app.
- url — where the run starts.
- fence — pathname prefix plus blacklist substrings. Crossing it bounces the walker back to seed; it is not a website finding.
-
intro — DSL lines run after
goto, before inspect/playbook/replay. Not a function. If a walker logs itself out, itgotos the leashurland runs intro again — it does not hunt from the login page. -
$VAR / $ {VAR} — fill values resolved from the environment. The log keeps the token. -
writePolicy —
validationOnly(default) refuses a submit when required fields are filled, so the monkey does not create/update data.allowfills empty fields then clicks submit in one burst (it does not click away mid-form). Useallowonly on a disposable test instance. - skip — extra widget id/label substrings the walker will not click. Sign out, log out, and close panel are skipped by default.
-
screenshots — per-step screenshots, default on.
"screenshots": falseturns auto shots off. -
seo — optional. Scan the live
<head>for title, meta description, Open Graph, and canonical problems. Off when omitted. See SEO / meta. -
vision — optional screenshot model (blurbs, Sight, pixel-only defects). Same connection shape as
brain(baseUrl,model,apiKeyEnv). Mix models (qwen text + qwen-vl on another host).modelis required and is never copied frombrain.baseUrlinherits when omitted.apiKeyEnvinherits only whenvision.baseUrlis also omitted;"apiKeyEnv": falsemeans no key. Per-step screenshots must stay on. Layout is a DOM pass on inspect and does not need this block (overflow at 1280/375/320, clip, overlap, hit targets, focus, text spacing, dead hashes, and the rest — docs/issue-classes.md).issues(default true) lets the model add pixel-only extras (contrast, align, empty-vs-broken, toasts, …). It is grounded with DOM hits and cannot overwrite them.high/mediumconfidence;lowis dropped. High-confidence extras are findings with the step screenshot (the walk does not stop). Medium stays on the quality ledger. A visual report that quotes a--nastycatalog payload is leftover test data, not a defect; overflow or clip of a product string still counts.assist(default true) adds explore sight notes. decide stays text-only.
v1 used fence.blacklist only for URLs (e.g. #/login after the monkey logged itself out). That is still the fence. skip is the widget denylist.
Duplicate accessible names (two Employees buttons) are a duplicateName warn. Collect keeps both: the first match stays the plain locator, the rest get nth so the walker can open the child list, not only the section expander.
clickmonkey/map.json is the page model inspect / map grow. Each page gets a one-line description (path, heading, fields, dialogs). With vision configured, a page-level screenshot upgrades that to what is actually on screen (dashboard, list, form, details) — not a modal shot. A page whose last land before this run is within ~2 days and whose PNG hash matches the last scan skips extras (a missing caption still asks). Explore may still polish with the text brain. Extra widgets never fail a replay. Several processes may share that file: each step takes a short lock, unions the trees, and writes back. That is cheap next to Playwright and the LLM.
As the monkey walks, each inspect updates that run's testability.json (can we locate the controls?) and quality.json (html-validate for HTML, axe-core for WCAG 2.0/2.1 A/AA plus a small extra allowlist, JS console / pageerror, and a DOM layout pass — overflow, clip, overlap, focus, text spacing, dead hashes, and the rest in docs/issue-classes.md). A report also flags when most walked pages share one document.title (tabs, screen readers, and search cannot tell routes apart). A 404 goes in that run's broken.json. The shared sitemap stays in map.json. Testability, HTML, axe, and layout issues keep a short where (accessible name, testid, id, or compacted CSS — not XPath). Per-step screenshots are on by default (shots/step-NNN.png). With vision configured, a second model may add pixel extras and a short Sight note for explore; it cannot overwrite DOM layout hits. High-confidence visual extras are also findings (with the screenshot); medium stays on the ledger. clickmonkey report combines quality/testability from the selected runs so two reports can be compared. Console warnings are ledger-only; the first uncaught pageerror per message is still a finding.
A page that lives on another host than the leash url (SSO, IdP) gets an origin. Pages seen during intro (login, callback) are marked entry. Walkers only hop to leash-origin pages that have widgets, are not entry, and would not cross the fence. open uses the page origin so a path like /u/login is not rewritten onto the app host. Intro waits until the browser has left the start URL and landed on a real app page before the walk starts.
clickmonkey init writes the leash and an empty map. If the leash already exists: monkey settings already exists: <path>. A one-file clickmonkey.json that still contains map still loads (an empty seeded map.json yields to a richer inline map); the next persist writes clickmonkey/map.json.
open home
click page.open_create
click page.projects nav
fill create.name ""
click create.submit
expect create.name invalid
expect create visible
expect path /
A log is those lines, plus optional headers:
# bug: empty name is accepted on create
# found: 2026-08-14T12:00:00.000Z
clickmonkey compact <log> drops everything before the last open or
nav-landmark click (click page.x nav) and keeps the bug header and comments.
A nav click is a state reset in typical apps, same as open.
clickmonkey init [--url <url>] [--config <path>]
clickmonkey inspect [--config] [--url] [--headed] [--timeout]
clickmonkey view [--config] [--url] [--headed]
clickmonkey step '<line>' [--config] [--url] [--out]
clickmonkey playbook empty-required [--config] [--url] [--out]
clickmonkey map [--config] [--url] [--out] [--steps] [--verbose]
clickmonkey unleash [--config] [--url] [--out] [--steps] [--nasty]
clickmonkey nasty [--config] [--url] [--out] [--steps]
clickmonkey explore [--config] [--url] [--out] [--steps] [--minutes] [--charter] [--skills]
clickmonkey mcp [--config]
clickmonkey fog [--config] [--reset] [--job map|unleash|nasty|spec]
clickmonkey pages [--config] [--drop id,id]
clickmonkey report [--config] [--runs id,id] [--all] [--out]
clickmonkey prune [reportId] [--config] [--ids id,id]
clickmonkey replay <log|report.md> [--config] [--url] [--out]
clickmonkey spec [file.md] [--check] [--fail-on-findings]
clickmonkey emit [--config]
clickmonkey compact <log> [--out <file>]
clickmonkey bundle [--config] [--out]
clickmonkey ui
clickmonkey ui --stop
clickmonkey emit writes clickmonkey/ts/generated.ts from the map and leash intro. That file is a typed page object: session() logs in via intro ($CLICKMONKEY_* tokens stay tokens), then you chain mapped ids (await app.home.openCreate().fill({ name: "Acme" }).submit()). Call app.close() when done, or await using app = await session(). The chain is a spec specialization — same executor, inspect, findings, and map presence (live letter t). visualIssue does not fail the chain; expectFailed, unknown ids, and a filled submit under default writePolicy: "validationOnly" do (set "allow" on a disposable instance to commit). Findings are on app.findings / app.ledger. Re-run emit when the map changes; do not edit the generated file. Write tests against it. Spec stays markdown fences (clickmonkey spec, live letter s).
clickmonkey pages is sitemap GC. List rooms (path, last land, live inbound doors, 404) and drop ones that are gone. Recommend drop is only a document 404 in broken.json plus no live inbound opens, and never for entry, another origin, or a parametric path. Hunger alone is not a recommend. A TTY checkbox pre-checks those rooms. --drop id,id skips the prompt for scripts. Doors that opens a dropped page lose that hint. Then clickmonkey emit if you use the typed page model. This does not touch reports or dismissed.json.
clickmonkey ui reads clickmonkey.json in the current directory (or --config) and serves a localhost-only dashboard on 127.0.0.1:4174. It never binds a public interface. --port and --no-open are optional. After a clone, npm install --prefix web && npm run build once so web/dist exists. If the banner says the UI is stale, use Restart UI in that banner, or clickmonkey ui --stop then clickmonkey ui. A report page has Copy (markdown + inlined screenshots for pasting into a model) next to Print.
clickmonkey bundle writes a static copy of that dashboard (default clickmonkey/bundle/). It does not need the CLI to view: serve the folder (python3 -m http.server 4174) or upload it to GitLab Pages. Do not open index.html as file:// — fetch is blocked. A GitLab job example is examples/gitlab-ci.yml.
clickmonkey explore is exploratory testing without the MCP server (needs brain in the leash). Use it in CI and when you want an unattended explore.
The MCP server is the host LLM walking, then freezing that walk as a replayable spec and proving the replay. How to wire Grok / Claude / Cursor, which prompts to read (explore_tester, spec_writer), and the explore → spec_save → spec_run loop: docs/mcp.md.
--nasty / nasty_* is for a site you own.
ClickMonkey is headless by default. A pipeline that already deploys a preview URL can explore that URL, write a report, and ship a zip/Pages folder:
- Deploy this push to staging / preview.
- Keep
clickmonkey/explore-context.md(copyexamples/explore-context.mdand describe how this site is put together: chrome vs main, which nav item is billing, where a “fix invoice rounding” commit should land). The charter isgit logfor the push — ticket titles, not test steps. The context file is how the model translates those commits into walks. -
clickmonkey explore --url "$PREVIEW_URL" --charter "$(git log …)" --skills clickmonkey/explore-context.md(needsbrain+$CLICKMONKEY_*CI variables). clickmonkey report --allclickmonkey bundle --out clickmonkey-bundle- Upload
clickmonkey-bundle/as a job artifact (and optionally GitLab Pages).
Explore exit 1 means findings, not a crash — mark the job allow_failure. Exit 2 means the model was unreachable or the API key is missing (not findings). Replay of a previous report is the cheap gate (no LLM). See examples/gitlab-ci.yml. Downloading the artifact: python3 -m http.server 4174 in clickmonkey-bundle/ and open http://127.0.0.1:4174/. You cannot replay from the zip unless the preview URL is still up.
--nasty fills fields from a catalog of XSS, SQLi, format, and overlong junk. It is for a site you own (your staging). Do not point it at anyone else's production.
clickmonkey report writes clickmonkey/reports/<id>/findings.md plus report.json (which runs it covers). A TTY asks which runs to combine (checkbox, none pre-selected), then Pages with issues? — default (top 8) or full (every page with its own issues). --runs id,id is explicit; --all takes every run that has findings. --quality-full skips the prompt (scripts). Chapters match the sitemap page sheet: Findings, Testability, Accessibility, Visual, Quality (HTML, SEO, Runtime). Pages groups list unique-to-a-route issues (default: top 8; full: every such page). The report also has a By page index: default lists those pages; --quality-full indexes every labeled ledger page (chrome as labels on those lines, not extra headings). Compact drops the leash intro (replay runs it from config) and keeps the path from the last open or nav-landmark click. Findings and ledger rows include a short Why it matters paragraph (copy-pasteable). With brain configured it adds titles and expected/actual. --out also copies the markdown to a path you name. The dashboard lists every report and has Print (browser Save as PDF) and Copy (text + screenshots).
Open cards are tagged new (first seen in the newest selected run) or still (seen before). Fixed means clickmonkey replay ran that finding’s tape with the same fills and it did not reproduce. A later unleash/nasty land is not a retest — those walks type different values.
clickmonkey prune is human review of a report, not the sitemap. After clickmonkey report you have clickmonkey/reports/<id>/findings.md. Some cards are real bugs; some are walker noise (clip on an icon rail, a 1×1 hidden native <select> under a custom combobox). Prune:
- Pick a report (TTY select, or pass the report id).
clickmonkey prune --helpprints the same story. - Checkbox the findings that are false positives. With
brainconfigured, likely noise is pre-checked (uncheck anything real). Scripts:clickmonkey prune <reportId> --ids ik_ab12cd34ef567890,fnd_10_visualIssue. The loc line’sik_is the bug;fnd_*is that run’s folder. - The markdown is rewritten without those cards. Finding folders under
runs/stay — the tape is the tape. Replay still has the original walk. - One dismissed item per card (the
ik_plus fingerprint) goes intoclickmonkey/dismissed.json. The nextclickmonkey reportskips that bug even when a later walk files it under a newfnd_*.
It never edits map.json. A 404 room still on the sitemap is clickmonkey pages.
| Command | Edits | When |
|---|---|---|
pages |
map.json |
that URL is gone |
prune |
findings.md + dismissed.json |
that finding is a false positive |
Each run writes nav.jsonl (and echoes timestamped lines on stderr): every DSL step (step / ok / fail) plus main-frame redirects, document loads, and in-page URL changes. Gaps between step and ok are waits. That is not the replay tape — log.txt stays click/fill/open only.
--verbose writes verbose/NNN.html (live DOM) and verbose/NNN.view.txt (what the walker extracted) plus verbose/index.jsonl. Compare those two to see naming/locator drift. Delete the folder anytime: rm -rf clickmonkey/runs/*/verbose.
A run is a walk (map / unleash / nasty / explore / playbook). A report (clickmonkey report) is the shareable markdown of those findings. A replay of a report is not a third walk — it is a comparison against that report: same tapes, new shots, comparison.md with before/after.
clickmonkey replay clickmonkey/reports/<id>/findings.md writes clickmonkey/replays/<id>/comparison.md and records FIXED ik_ keys in clickmonkey/fixed.json. Use the path that report printed. STILL = the bug came back (and clears a prior fixed mark). FIXED = the same tape did not reproduce. LOOK = a human has to compare the pictures (UI / screenshot ui). Exit 1 only on STILL or ERROR. The next clickmonkey report moves replay-confirmed bugs to Fixed unless a later walk files that ik_ again.
Defaults: config clickmonkey.json, out clickmonkey/runs/<id>/.
Exit codes: 0 success no findings, 1 run completed with findings, 2 usage /
schema / unknown command / legacy config, 3 live-validate of ready failed
before start.
Off until you add seo to the leash. Then each inspect reads the live <head> (what a renderer sees, not view-source) and writes a SEO group into that run's quality.json. Not findings — same ledger as HTML/a11y.
"seo": { "private": ["/app", "/login"] }private is pathname prefixes with a segment boundary (/app matches /app/x, not /application). Pages under those prefixes skip the scan. "/" matches every path, so "private": ["/"] turns the scan off. Omit seo for the same effect. "private": [] scans every path.
A page with meta name="robots" / googlebot noindex is skipped even when it is not in private.
Checks (public pages only):
- title missing, empty, a framework placeholder (
Create Next App,Vite App, …), or longer than ~60 characters - at report time: the same
document.titleon most walked pages (browser tabs and screen readers cannot tell routes apart; search sees one title too) - at report time: two records on a parametric path (
/customers/:id1) that still share one title (two customer tabs both say “Customer”) - meta description missing, a copy of the title, or outside ~20–160 characters
- Open Graph: missing
og:title,og:description,og:image,og:url;og:image/og:urlnot an absolutehttp(s)URL rel=canonicalmissing, not absolute, or a different origin than the live page
App shells should stay in private. Marketing / and /docs stay out of it.
fence.path is a pathname prefix with a segment boundary (/app matches
/app/x, not /application). fence.blacklist is a list of URL substrings.
Leaving the fence skips the post-step inspect and returns to the seed page. That is leash control, not a website finding.
After inspect, ClickMonkey clicks page actions to learn which dialogs they open, then for each required field on a reachable surface:
open <page>
click <opener> # if the field lives in a dialog
fill <surface>.<field> ""
click <submit>
expect <surface>.<field> invalid
A passing expect is not a finding. A failing expect writes replay.log, a
screenshot, and a finding JSON. Replay that log with no brain in the loop.
Format rules, cross-field checks, and “Next stays disabled” are specs (or a
human), not this playbook — see docs/issue-classes.md.
The 0.0.7 tag is the old tool. v2 will not load those files.
| 0.0.7 | v2 |
|---|---|
| JRE + Selenium | Playwright / Chromium only |
| local HTTP proxy | page + HTTP oracles on the browser |
clickmonkey.js (intro function, proxy_port) |
clickmonkey.json |
default command unleash |
no default; pick a command |
| WebdriverIO intro | DSL intro[] with $ENV secrets |
| random clicker | inspect + playbook + replay |
Run clickmonkey init and rewrite the intro as lines. Secrets stay out of the log.
MIT
Less is more. Small, readable changes. The monkey should keep a tape, not a framework. Pull requests are welcome; expect discussion.