QA harness built for Claude Code | E2E testing with screen recordings, console logs, network HARs, and Playwright traces
> /plugin marketplace add 0xnyn/canary> /plugin install canary@canary-marketplace
Repo: 0xnyn/canary
What's inside
https://github.com/user-attachments/assets/53d10b52-35cf-496a-a342-e8719574a000
Canary is a QA harness purpose built for coding agents like Claude Code. It reads your code diffs, identifies the affected UI flows, and tests them in real browser instances using Claude Code.
Under the hood, it ships with a QuickJS WASM sandbox exposing the full Playwright API, letting Claude automate any long-running UI task β from handling logins to navigating complicated UIs.
Instead of clicking through flows by hand to reproduce and verify issues, Canary provides full session recordings. You get screen recordings with console logs, network requests, HARs, and Playwright traces so you can inspect exactly what the agent did.
Every Canary run captures a reusable Playwright script. Letting you re-run it in CI with zero inference cost on replay.
Most testing tools force you to choose between two extremes:
Canary doesn't make you choose: the agent does the QA and hands you a reproducible script.
report.html β open it, commit it, send it. No server, no build.Page API β no Node, no host access.https://github.com/user-attachments/assets/8459994a-b43c-4483-bb4a-00522d1d03fe
You describe the flow in plain language; your agent drives a real browser and hands back both a report you can just read and the exact Playwright script β plus the full trace β behind it. Most tools make you pick one: an opaque agent run you can't reproduce, or raw Playwright you write and maintain by hand. Canary gives you both.
| You are a⦠| Instead of⦠| Canary gives you⦠|
|---|---|---|
| Developer | Writing and maintaining Playwright/E2E scripts by hand | A reusable script captured from every run β re-run it in CI, no agent cost on replay |
| QA engineer | Clicking through flows manually to repro and verify | Evidence by default β trace, video, network, console, and a screenshot of every step |
| PM / reviewer | Waiting on a build or trusting "works on my machine" | A self-contained report.html you open and read β every step, replayable and shareable |
npm i -g @usecanary/cli @usecanary/ui # puts `canary` + `canary-viewer` on your PATH
canary install # one-time: Chromium + the runtime into ~/.canary (~150 MB)
β¦or run the guided wizard, which offers to install all of the above for you:
npm create canary@latest # guided setup (Ink wizard)
Record a session and open the report:
id=$(canary session start --name "checkout")
canary run ./open.js --session "$id" --step open
canary run ./submit.js --session "$id" --step submit
canary session end "$id" # -> ~/.canary/sessions/<id>/report.html
canary-viewer # browse every recorded session
canary stop # shut the background daemon down when you're done
Just need a quick one-off with no recording? Drive the browser engine directly:
echo 'const p = await browser.getPage("main");
await p.goto("https://example.com");
console.log(await p.title());' | canary-browser
Or attach to a Chrome you already have open β launch it with --remote-debugging-port=9222, then
canary-browser --connect (it auto-discovers the port, or pass the URL explicitly). Handy for driving
a browser that's already logged in:
canary-browser --connect http://localhost:9222 <<'EOF'
const page = await browser.getPage("main");
console.log(await page.title());
EOF
Prefer not to install? Every command also runs one-off via npx, e.g.
npx @usecanary/cli session start β¦andnpx @usecanary/ui.
Open any session and Canary replays the whole thing β the page, the script, every Playwright call, the
console, the network, the full trace. Nothing summarized, nothing reconstructed: it's the actual run.
(Every screenshot below is real output.) Capture is on by default; switch any stream off with
--no-trace / --no-video / --no-har / --no-console.
Status, a per-step timeline, the exact environment, and a full video replay of the run with a filmstrip of per-step screenshots β scrub straight to the moment something happened.
Each step, pass or fail, with its exit code, duration, and how many Playwright actions it ran.
This is the one that matters. Let your agent figure a flow out once β Canary keeps the script
behind every step and decodes the full Playwright trace into the exact calls it made (goto,
waitForSelector, evaluate, screenshot), with params and timing. What you get back is a real,
reusable script. Next time you don't pay an agent to rediscover the page β you just re-run it.
Every console message and uncaught page error, filterable by level β errors, warnings, info, logs β with the source URL. Errors flagged in red.
Every request with status, type, size, and timing. Filter by kind, then click any row to inspect its headers, payload, and response β like a devtools network panel, frozen at the moment it ran.
The raw Playwright trace.zip, the network HAR, the console log, the machine-readable results.json,
and the self-contained report.html β all under ~/.canary/sessions/<id>/, all one click away. Open
the trace in Playwright's own viewer with npx playwright show-trace.
In Claude Code, Canary is a first-class plugin β skills, subagents,
and /canary:* slash commands. Tell Claude what you changed or what to check; it plans the QA, drives
a real browser, and hands back the report.
/canary:verify # what changed? β a prioritized QA plan, then record it
/canary:session # record a flow end to end and render report.html
/canary:run # drive the browser once, nothing recorded
/canary:review # open the viewer and triage a recorded session
Or skip the slash and just say "QA the checkout flow and give me a report" β Canary's subagents pick it up. Install the plugin below.
Canary is built for agents β and it explains itself to them. Install it, then tell your agent to run
canary --help (or canary-browser --help for one-offs): each output is a complete, self-contained
usage guide β sandbox API, worked examples, a Playwright cheat sheet β written for an LLM to read.
No plugin required.
For deeper integration (slash commands, subagents, and skills), install the plugin pack. Canary ships
as a Claude Code plugin, a Cursor plugin, and a Codex plugin β all pointing at the same skills/ +
agents/ + commands/. There's no bespoke installer; each agent's own mechanism does the work.
# Claude Code
/plugin marketplace add wizenheimer/canary
/plugin install canary@canary-marketplace
# Cursor β install "canary" from the Marketplace, or symlink for local dev:
ln -sfn "$(pwd)" ~/.cursor/plugins/local/canary
# Codex
codex marketplace add wizenheimer/canary # then /plugins β install "canary"
You get canary-scripting (the sandbox API, with references/REFERENCE.md) plus the workflow
skills canary-verify, canary-automate, canary-session, and canary-review β
each paired with a subagent and a slash command: /canary:verify, /canary:run, /canary:session,
/canary:review.
| Tool | Command | Use it to |
|---|---|---|
CLI @usecanary/cli | canary | Record capture-enabled QA sessions and render reports. The main, user-facing tool. |
Engine @usecanary/browser | canary-browser | Drive a browser for quick, one-off automation β no recording, no report. |
Viewer @usecanary/ui | canary-viewer Β· npx @usecanary/ui | Browse, search, organize, and replay every recorded session locally. |
Both CLIs share one background daemon (Playwright + a QuickJS sandbox) that starts automatically when
needed. Stop it anytime with canary stop (alias: canary daemon stop, or canary-browser stop) β
it shuts down every browser and session it's running. You can also pass --stop-daemon to
canary session end to tear it down as soon as nothing else needs it.
Scripts are plain async JavaScript with top-level await.
Scripts execute inside a QuickJS WASM sandbox with no arbitrary access to the host system. This is NOT Node.js β there is no module system and no Node API:
require() / import() β no module loading; inline any helpers in the scriptprocess, fs / path / os β no process or direct filesystem access (use the file helpers)fetch / WebSocket β no direct network access (the page does the networking)__dirname / __filename β no path globalsMemory and CPU limits are enforced, and both CPU time and wall-clock time are bounded β infinite
loops or never-settling promises abort the script. Values crossing evaluate / $eval must be
JSON-serializable.
const page = await browser.getPage("main"); // named, persistent page
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
console.log(await page.title());
const headings = await page.evaluate(() =>
[...document.querySelectorAll("h1, h2")].map((h) => h.textContent.trim())
);
console.log(JSON.stringify(headings));
await page.locator("a.more").click();
const buf = await page.screenshot({ fullPage: false });
await saveScreenshot(buf, "page.png"); // saveScreenshot(buffer, name)
Browser
browser.getPage(nameOrId) β get-or-create a named page, or attach to an existing tab by the
id from listPages(). Named pages persist across steps in a session β call with the same
name to reuse the tab.browser.newPage() β an anonymous page, auto-closed when the script ends; does not persist.browser.listPages() β list every open tab: [{ id, url, title, name }] (name is null
for tabs you never named).browser.closePage(name) β close and forget a named page.Files
All file I/O is async (await it), sandboxed to ~/.canary/tmp/ (no filesystem escape), and
returns the full path to the file:
saveScreenshot(buffer, name) β persist a screenshot buffer; buffer first:
const path = await saveScreenshot(await page.screenshot(), "home.png");writeFile(name, data) β write a small file (e.g. JSON state):
await writeFile("results.json", JSON.stringify(data));readFile(name) β read it back (returns the contents as a string):
const data = JSON.parse(await readFile("results.json"));Output
console.log / console.info write to stdout; console.warn / console.error write to
stderr. Top-level console.log is your script's output channel.console.log inside page.evaluate(() => β¦) runs in the page and is captured into the
session's console artifact instead.Pages returned by browser.getPage() and browser.newPage() are full Playwright Page objects β
the same API (goto, click, fill, locator, evaluate, getByRole, waitForSelector, β¦):
https://playwright.dev/docs/api/class-page
For element discovery, await page.snapshotForAI() returns an LLM-friendly outline of the page β
the canary-scripting skill and its references/REFERENCE.md carry the full API.
Already installed? Grab the latest CLIs from npm, then refresh the runtime:
npm i -g @usecanary/cli@latest @usecanary/ui@latest # update canary + canary-viewer
canary install # refresh the runtime (Chromium + Playwright)
canary install is safe to re-run β it pulls the browser/runtime versions the new CLI pins. Running
via npx instead of a global install? npx @usecanary/cli@latest β¦ always fetches the newest release.
Agent integrations update through each agent's own mechanism:
# Claude Code β refresh the marketplace catalog, then update from /plugin:
/plugin marketplace update canary-marketplace
# or turn on auto-update: /plugin β Marketplaces β canary-marketplace β Enable auto-update
# (third-party marketplaces ship with auto-update OFF)
# Cursor / Codex β update "canary" from each marketplace UI.
Claude Code detects plugin updates by comparing manifest versions (bumped every release); Cursor
and Codex do the same against their plugin manifests, so every release makes the latest skills/
update-visible.
Canary is a pnpm + Turborepo monorepo: five apps and five packages cooperate to make agent-driven browser automation reproducible.
canary/
βββ apps/
β βββ canary/ # @usecanary/cli bin: canary β session orchestrator (record QA sessions, render reports)
β βββ canary-browser/ # @usecanary/browser bin: canary-browser β browser-automation engine (one-off runs)
β βββ canary-daemon/ # @usecanary/daemon no bin β Playwright + QuickJS runtime (embedded into the CLIs)
β βββ canary-ui/ # @usecanary/ui bin: canary-viewer β local session viewer (Astro); `canary-viewer`
β βββ create-canary/ # create-canary bin: create-canary β `npm create canary` setup wizard (Ink)
βββ packages/
β βββ protocol/ # @usecanary/protocol IPC schemas (Zod), single source of truth
β βββ config/ # @usecanary/config shared tsconfig bases
β βββ logger/ # @usecanary/logger pino-backed structured logger
β βββ cli-kit/ # @usecanary/cli-kit shared CLI helpers
β βββ daemon-client/ # @usecanary/daemon-client daemon transport + lifecycle; embeds the daemon bundle
βββ skills/ # agent skills: canary-scripting (+references), -verify, -automate, -session, -review
βββ agents/ # JTBD subagents: verify-agent, automate-agent, session-agent, review-agent
βββ commands/ # slash commands: /canary:verify, :run, :session, :review
βββ .claude-plugin/ # Claude Code plugin + marketplace manifests
βββ .cursor-plugin/ # Cursor plugin manifest (pairs with rules/)
βββ plugins/canary/ # Codex plugin wrapper (.codex-plugin β canonical skills/)
βββ .agents/ # Codex / agents marketplace manifest
βββ rules/ # Cursor rules (canary-workflows.mdc)
βββ examples/ # dev-only demo scripts (Hacker News, Product Hunt, GitHub Trending, Wikipedia)
βββ .github/ # CI
canary (the orchestrator) and canary-browser (the engine) both embed and supervise
canary-daemon (the long-running Playwright host). The viewer ships standalone β canary-viewer
(or one-off via npx @usecanary/ui).
make install # pnpm install across the workspace
make build # build everything in topo order
make test # run all tests
make check # compile + lint + test (what CI runs)
Run make with no args to see all targets.
commitlint + a husky commit-msg hook.pnpm lint checks, pnpm format autofixes; pre-commit runs lint-staged β ultracite fix on staged files.@usecanary/logger (pino, structured). Set CANARY_LOG_LEVEL (trace|debug|info|warn|error|silent); the CLI also accepts --verbose/-v..nvmrc and packageManager).turbo run build, dev, test, compile); lint/format run via Ultracite at the root.See AGENTS.md for architecture and orientation, CONTRIBUTING.md
for the contribution flow, and RELEASING.md for the publish pipeline.
MIT. Canary's daemon and CLIs are derived in part from MIT-licensed work by
Sawyer Hood β see LICENSE.
.agents/
plugins/
marketplace.json
.claude-plugin/
marketplace.json
plugin.json
.cursor-plugin/
plugin.json
.editorconfig
.github/
workflows/
ci.yml
release.yml
.gitignore
.husky/
commit-msg
pre-commit
.nvmrc
.vscode/
extensions.json
settings.json
agents/
AGENTS.md
automate-agent.md
review-agent.md
session-agent.md
verify-agent.md
apps/
.gitkeep
canary/
canary-browser/
package.json
README.md
scripts/
build.mjs
src/
cli.ts
commands/
browsers.ts
flags.ts
help-text.ts
install.ts
preprocess.ts
render.ts
run.ts
status.ts
stop.ts
inject-scripts.ts
logger.ts
util/
format.ts
request-id.ts
test/
helpers/
run-cli.ts
snapshots/
bin-entry.test.ts
browsers-status.test.ts
help.test.ts
unit/
format.test.ts
inject-scripts.test.ts
preprocess.test.ts
render.test.ts
request-id.test.ts
tsconfig.json
vitest.config.ts
canary-daemon/
package.json
scripts/
bundle-sandbox-client.ts
src/
browser-manager-init-script-integration.test.ts
browser-manager-init-scripts.test.ts
browser-manager-pages.test.ts
browser-manager-title-timeout.test.ts
browser-manager.ts
daemon.ts
local-endpoint.test.ts
local-endpoint.ts
lock.test.ts
lock.ts
sandbox/
__tests__/
auto-connect.test.ts
bundle-test-helpers.ts
forked-client-bundle.test.ts
named-pages.test.ts
playwright-api.test.ts
protocol-bridge.test.ts
quickjs-host.test.ts
sandbox-file-io.test.ts
sandbox-integration.test.ts
sandbox-security.test.ts
forked-client/
bundle-entry.ts
package.json
quickjs-platform.ts
README.md
src/
client/
android.ts
api.ts
artifact.ts
browser.ts
browserContext.ts
browserType.ts
cdpSession.ts
channelOwner.ts
clientHelper.ts
clientInstrumentation.ts
clientStackTrace.ts
clock.ts
connect.ts
connection.ts
consoleMessage.ts
coverage.ts
debugger.ts
dialog.ts
disposable.ts
download.ts
electron.ts
elementHandle.ts
errors.ts
eventEmitter.ts
events.ts
fetch.ts
fileChooser.ts
fileUtils.ts
frame.ts
harRouter.ts
input.ts
jsHandle.ts
jsonPipe.ts
localUtils.ts
locator.ts
network.ts
page.ts
platform.ts
playwright.ts
screencast.ts
selectors.ts
stream.ts
timeoutSettings.ts
tracing.ts
types.ts
video.ts
waiter.ts
webError.ts
worker.ts
writableStream.ts
protocol/
channels.d.ts
serializers.ts
validator.ts
validatorPrimitives.ts
utils/
isomorphic/
ariaSnapshot.ts
assert.ts
colors.ts
cssParser.ts
cssTokenizer.ts
headers.ts
imageUtils.ts
locatorGenerators.ts
locatorParser.ts
locatorUtils.ts
lruCache.ts
manualPromise.ts
mimeType.ts
multimap.ts
protocolFormatter.ts
protocolMetainfo.ts
rtti.ts
selectorParser.ts
semaphore.ts
stackTrace.ts
stringUtils.ts
time.ts
timeoutRunner.ts
types.ts
urlMatch.ts
utilityScriptSerializers.ts
yaml.ts
types/
protocol.d.ts
recorder-actions.d.ts
structs.d.ts
types.d.ts
host-bridge.ts
playwright-internals.ts
protocol-bridge.ts
quickjs-host.ts
quickjs-sandbox.ts
sandbox-transport.ts
script-runner-quickjs.ts
session-manager.test.ts
session-manager.ts
temp-files.ts
test-cleanup.test.ts
test-cleanup.ts
test-fixtures/
rrweb/
rrweb-record.min.js
update.sh
VERSION
tsconfig.json
vitest.config.ts
canary-ui/
astro.config.mjs
bin/
viewer.mjs
components.json
package.json
public/
apple-icon.png
icon.svg
manifest.webmanifest
README.md
src/
components/
app-sidebar.tsx
code-block.tsx
console-tab.tsx
copy-button.tsx
empty-state-illustration.tsx
library-island.tsx
library.tsx
logo.tsx
multi-select.tsx
network-tab.tsx
not-found.tsx
pager.tsx
session-view-island.tsx
session-view.tsx
top-bar.tsx
ui/
ui.tsx
alert.tsx
badge.tsx
breadcrumb.tsx
button.tsx
card.tsx
collapsible.tsx
dialog.tsx
dropdown-menu.tsx
input.tsx
popover.tsx
scroll-area.tsx
select.tsx
separator.tsx
sheet.tsx
sidebar.tsx
skeleton.tsx
table.tsx
tabs.tsx
textarea.tsx
tooltip.tsx
video-player.tsx
hooks/
use-mobile.ts
layouts/
layout.astro
lib/
api.ts
artifacts.ts
format.ts
fs-json.ts
manifest.ts
network.ts
overlay.ts
parse-console.ts
parse-har.ts
paths.ts
roots.ts
sessions.ts
trash.ts
utils.ts
middleware.ts
pages/
404.astro
api/
artifact.ts
health.ts
overlay.ts
roots.ts
session.ts
sessions.ts
trash.ts
index.astro
s/
[root]/
[id].astro
styles/
global.css
test/
artifacts.test.ts
manifest.test.ts
overlay.test.ts
parser-drift.test.ts
roots.test.ts
sessions.test.ts
tsconfig.json
turbo.json
vitest.config.ts
package.json
README.md
scripts/
build.mjs
src/
cli.ts
commands/
daemon-stop.test.ts
daemon-stop.ts
help-text.ts
init.ts
install.ts
render.ts
run.ts
session-abort.ts
session-end.ts
session-list.ts
session-start.ts
status.ts
ui/
ui.ts
open-browser.ts
resolve-server.ts
logger.ts
report/
load-and-render.ts
manifest.test.ts
manifest.ts
parse-console.test.ts
parse-console.ts
parse-har.test.ts
parse-har.ts
parse-trace.test.ts
parse-trace.ts
render-report.test.ts
render-report.ts
step-slug.test.ts
session/
artifacts.ts
lock.ts
reconcile.ts
registry.ts
util/
session-id.test.ts
session-id.ts
test/
helpers/
run-cli.ts
snapshots/
help.test.ts
tsconfig.json
vitest.config.ts
create-canary/
package.json
README.md
scripts/
build.mjs
src/
cli.tsx
run.ts
test/
run.test.ts
tsconfig.json
vitest.config.ts
biome.jsonc
CHANGELOG.md
CLAUDE.md
commands/
review.md
run.md
session.md
verify.md
commitlint.config.js
CONTRIBUTING.md
docs/
media/
preview.mp4
session-artifacts.png
session-commands.png
session-console.png
session-network.png
session-script.png
session-steps.png
session-summary.png
sessions-list.png
snippets/
api-browser.md
api-console.md
api-file-helpers.md
api-globals.md
api-playwright-methods.md
api-playwright-note.md
api-sandbox-env.md
api-snapshot.md
... 101 moreFAQ
canary is a Claude Code plugin with 5 hand-picked skills for testing work, indexed on Flowy. Install it with the command on its page. It includes canary-automate, canary-review, canary-scripting. Its skills do not fire on their own yet. Request auto-invocation to have Flowy route them as you prompt. Free and open source.