In February 2026, WebAIM scanned the home pages of a million websites. 82.7% of them used ARIA, with over 133 ARIA attributes per page on average. Pages with ARIA averaged 59.1 detected errors. Pages without it averaged 42. WebAIM is careful to say the ARIA pages were also more complex, so ARIA may not be the cause. Still, more ARIA has not meant fewer errors.

Now type "make this page accessible" into a coding agent. The fastest-looking change it can make is an aria-label on every element that looks unlabelled. The diff is big and it looks busy, and you have nothing to check it against. The W3C's own guide puts the risk plainly: "Authors can inadvertently override accessibility semantics."

The fix is to stop handing the agent a wish and hand it a list. axe-core gives you violations by rule id, with a CSS selector for every failing element. That list is the prompt. A Playwright test that runs the same rules is the proof. Below, every step runs against a small sample page with four real violations.

Step 1: a sample page and its violations as JSON

Save this as site/signup.html. It has four problems on purpose: grey hint text on white, an email field with no label, a submit button that is only an icon, and no lang on <html>.

<!doctype html> <html> <head> <meta charset="utf-8"> <title>Sign up</title> <style> .hint { color: #aaaaaa; background: #ffffff; } </style> </head> <body> <main> <h1>Create your account</h1> <p class="hint">We never share your email.</p> <form> <input type="email" name="email"> <button type="submit"> <svg width="16" height="16" viewBox="0 0 16 16"> <path d="M2 8h12M9 3l5 5-5 5" stroke="currentColor" fill="none"/> </svg> </button> </form> </main> </body> </html> 

Serve it with http-server and scan it with the axe CLI. The CLI drives Chrome through ChromeDriver, and its README suggests browser-driver-manager to install a matching driver:

npm i -D @axe-core/cli http-server npx browser-driver-manager install chrome npx http-server site -p 8080 & mkdir -p axe-results npx @axe-core/cli http://localhost:8080/signup.html \ --tags wcag2a,wcag2aa,wcag21a,wcag21aa \ --stdout > axe-results/signup.json echo "axe-results/" >> .gitignore 

--tags runs only the rules tagged for WCAG 2.0 and 2.1 A and AA, the same four tags the test in Step 2 uses. --stdout prints the full result and keeps the other logs quiet. The output is a JSON array (the README's own example reads .[0].violations), and each entry has a violations array. Every violation carries an id, an impact, a helpUrl and a nodes list. Each node has the element's html, its target selectors and a failureSummary that says how to fix it (results format). Read the short version with jq:

jq -r '.[0].violations[] | "\(.id)\t\(.impact)\t\(.nodes | length) node(s)"' \ axe-results/signup.json 

From the rule descriptions, this page should trip four rules. color-contrast (serious, WCAG 1.4.3): #aaaaaa on white works out to about 2.3:1, and normal text needs at least 4.5:1. label (critical): every form element needs a label. button-name (critical): buttons need discernible text. html-has-lang (serious): the document needs a lang attribute. If your run shows something else, trust the JSON over this paragraph. The JSON is what the agent gets.

A vague speech bubble on one side and a neat stack of rule-id cards with element pointers on the other, each card being handed to a robot arm
A wish gives the agent nothing to aim at. A rule id with a selector is a job it can finish.

Step 2: write the test before the agent touches anything

The agent should not write the check that grades it. Write it yourself, commit it, and only then start the agent. Install @axe-core/playwright next to Playwright Test:

npm i -D @playwright/test @axe-core/playwright npx playwright install chromium 

Point Playwright at the same static server:

// playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ webServer: { command: 'npx http-server site -p 8080', url: 'http://localhost:8080/signup.html', reuseExistingServer: !process.env.CI, }, use: { baseURL: 'http://localhost:8080' }, }); 

Then the test. It uses the four WCAG A and AA tags that Playwright's accessibility guide lists, attaches the full axe result to the report, and fails with a readable list of rule ids and selectors:

// tests/a11y.spec.ts import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; const WCAG_AA = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']; test('signup page has no WCAG A/AA violations', async ( { page }, testInfo, ) => { await page.goto('/signup.html'); const results = await new AxeBuilder({ page }) .withTags(WCAG_AA) .analyze(); await testInfo.attach('axe-results', { body: JSON.stringify(results, null, 2), contentType: 'application/json', }); const found = results.violations.map((v) => ({ id: v.id, targets: v.nodes.map((n) => n.target.join(' ')), })); expect(found).toEqual([]); }); 

Run npx playwright test now and it should fail, listing the rule ids the CLI found. That red run is the starting point. In CI it stays the gate. Playwright's CI guide gives a GitHub Actions workflow with actions/checkout@v6, actions/setup-node@v6 on lts/*, npm ci, npx playwright install --with-deps and npx playwright test. Any new violation turns the pull request red.

If a real page already carries debt you can't fix today, disableRules(['color-contrast']) turns off one rule by id, so everything else still gates. Be careful with .exclude(): Playwright's guide warns it skips every rule on that element and all its descendants, not just the one you meant.

Commit the page, package.json, package-lock.json, the config and the test on a branch, say a11y/signup, and keep that branch checked out in the project.

Step 3: hand the agent the JSON in its own worktree

I build Hermes IDE, so weigh this section with that in mind. Everything above is plain npm and git and works with Claude Code in any terminal. Hermes adds a separate worktree per task and a review screen.

One snag first. A new worktree holds only tracked files, and the JSON is ignored on purpose. A Hermes worktree recipe copies ignored files into each new worktree, and it refuses any pattern that matches a tracked file. It also installs dependencies:

# .hermes/worktree.toml setup = ["npm ci", "npx playwright install chromium"] copy = ["axe-results/*.json"] done_when = ["npx playwright test"] 

done_when sets the default checks for the task. The first time Hermes sees the recipe, and whenever it changes, it shows you the commands. Nothing runs until you choose Run setup. Commit the recipe on a11y/signup too.

Press ⌘N (Ctrl+Shift+N on Windows and Linux) and pick Claude Code. The task gets its own hermes/ branch and worktree, and the text you type becomes the agent's first prompt. Paste the list:

Fix the accessibility violations in axe-results/signup.json. - Work through them by rule id. For each one, change only the elements in nodes[].target. - Prefer native HTML: a visible <label for> over aria-label, visible button text over aria-label. - color-contrast: pick a colour that reaches 4.5:1 on its background and state the ratio. - Do not edit tests/, playwright.config.ts or the axe tags. Do not disable or exclude rules. - You are done when `npx playwright test` passes. - Reply with one line per rule id: what you changed. 

Hermes IDE's New task sheet with a task typed in and chips for Claude Code, the project, a new hermes/ worktree, Accept edits, model and effort
Hermes 2.3.4's ⌘N New task sheet on a demo project: chips for Claude Code, the project, a new hermes/ worktree, Accept edits, model and effort, and a preview of the claude command Hermes will run.

Every line in that prompt maps to something you can check. The agent knows which elements, which rules and which command decides "done". The "prefer native HTML" line is there to head off the aria-label sprinkle.

Step 4: review the fix rule by rule

A good fix for this page is small. Something close to this:

<html lang="en"> <!-- … --> <style> .hint { color: #767676; background: #ffffff; } </style> <!-- … --> <label for="email">Email</label> <input id="email" type="email" name="email"> <button type="submit"> Create account <svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><!-- same path --></svg> </button> 

#767676 on white is about 4.54:1, just over the line. The label is visible to everyone, not only to screen readers. The button has real text, and the arrow is hidden from assistive tech because it adds nothing.

Press ⌘G (Ctrl+Shift+G) for the Review Desk. It shows the agent's changes by turn or by file, with risk flags, and a line comment there goes back to the agent that wrote the line. Read it by file, in this order:

  1. tests/a11y.spec.ts and playwright.config.ts. They should be untouched. If either changed, the green run means nothing. Stop there.
  2. The HTML and CSS. Match each hunk to a rule id from the agent's reply. A hunk with no rule id behind it is scope creep.
  3. Any aria-* the agent added. Ask if a native element would do the same job. If it would, leave a line comment asking for the native version.

The Hermes Review Desk in By turn view, listing two turns with their file and line counts, one turn's diff, the changed-files list and a drafted commit message
The Review Desk (⌘G) in By turn view on a demo task: each turn with files and lines changed, a turn's diff, the changed-files list, and a commit message drafted from the turns.

Hermes saves every agent turn, so you can diff or restore a single turn, with undo. Land then commits, opens a pull request or merges into the branch the task started from, and that can be undone too. Run the CLI scan once more on the worktree before you land. An empty violations array is your before-and-after.

What axe can't tell you

A green axe run is a floor. Deque, which makes axe, reported in 2021 that automated testing covered 57% of issues across 2,000+ audits. That is Deque's own number, counted by issue volume. Playwright's guide puts it plainly: "automated testing cannot detect all types of WCAG violations."

axe checks that an image has alt text, not that the alt text is right. It doesn't check that tab order makes sense, or what a screen reader says when the form fails. Check the result's incomplete array too. The API docs call those results "needs review": axe couldn't decide, so a person has to. Don't put those in the agent's prompt as if they were bugs. Tab through the page yourself, and run a screen reader over the form once.

Keep the agent on the list axe can prove, and keep yourself on everything else.

If this was useful

The trick in this post is a prompt trick: swap an open-ended wish for a machine-made list with ids, targets and a done-check, and the agent's output becomes something you can grade. The Prompt Engineering Pocket Guide is a short book about building prompts that way, with clear inputs, constraints and outputs you can verify. If you want to try the worktree-and-review flow, Hermes IDE is free, source-available (BSL 1.1), and runs on macOS, Windows and Linux; issues go to GitHub.

Prompt Engineering Pocket Guide: Techniques for Getting the Most from LLMs