Fanning out agents to read every component file is a tempting shape for accessibility review. It demos well in a small project. In a real codebase with hundreds of components, multiple routes, and several active branches, it's a budget problem. You're paying LLM rates for work a deterministic audit engine does in JavaScript.
The asymmetry is the point. Rule checks against a rendered DOM are deterministic JavaScript, not LLM tokens. The agent's leverage is in the judgment: choosing the right alt text, the right label, applying the edit at the right place in source. Spend tokens on revisions, not discovery.
This morning I ran that loop against a deployed React app. A browser-side audit returned 29 violation types from the live DOM. The agent opened only the two source files those violations pointed to, applied fixes, and that was the work. No file walk, no codebase scan.
The cost shape
A naive agent scan walks every component file: read, reason, report. Cost grows with the size of the repo. Run it on every PR plus a nightly sweep across long-lived branches, and you're paying for everything that exists, every cycle.
A browser audit is bounded by what's actually rendered. One injection runs against the page, returns a structured violation list, and the agent only opens the files implicated by what came back. Cost scales with what's broken, not with what exists.
That's the difference between "we'd love to run this nightly" and "we are running this nightly."
What the loop looks like with MCP
The MCP version is concrete. With @accesslint/mcp paired with a browser MCP (chrome-devtools-mcp recommended), the agent does roughly this:
- Calls
audit_browser_scriptand gets back a short JS snippet. - Asks the browser MCP to
evaluate_scriptthat snippet on whatever URL is loaded. - The snippet bootstraps
@accesslint/corefrom a CDN, runs the audit, returns JSON. - Calls
audit_browser_collectand gets back[{ ruleId, selector, html, impact, message, ... }, ...]. - For each violation, greps source for stable hooks in the snippet (
data-testid,aria-label, visible text, class names) to locate the file. - Applies the fix at the source location.
Steps 1 to 4 are deterministic JavaScript. Steps 5 and 6 are where agent tokens get spent, and they scale with the number of violations, not the size of the codebase.
Production to PR
The same loop runs against production URLs. You don't need a dev server, a preview deploy, or a Docker image. A URL is enough to find the issues; pair it with the standard GitHub permissions a code-writing agent already needs (contents and pull-requests write), and a scheduled background agent can open overnight PRs against the issues an a11y engine flags: alt attributes, ARIA values, contrast bumps, missing labels, structural fixes. PR review is where overreach gets caught.
A minimal nightly GitHub Action:
name: AccessLint nightly scan
on:
schedule:
- cron: '0 6 * * *'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Claude Code and MCPs
run: |
npm i -g @anthropic-ai/claude-code
npx playwright install --with-deps chromium
claude mcp add accesslint -- npx -y @accesslint/mcp
claude mcp add playwright -- npx -y @playwright/mcp@latest
- name: Audit and apply fixes
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude -p \
--allowedTools "mcp__accesslint__*,mcp__playwright__*,Read,Write,Edit,Glob,Grep" \
"Audit https://your-app.example.com using the accesslint and playwright MCPs. For each violation, find the source file in this repo and apply the fix. Use your judgment on content (alt text, label wording, heading copy); preserve component intent and avoid invasive refactors. Do not modify tests."
- name: Open PR if there are changes
env:
GH_TOKEN: ${{ github.token }}
run: |
[ -z "$(git status --porcelain)" ] && exit 0
git config user.name "accesslint"
git config user.email "accesslint@users.noreply.github.com"
git checkout -B a11y/nightly
git add -A && git commit -m "a11y: mechanical fixes from rendered DOM audit"
git push --force-with-lease origin a11y/nightly
gh pr view a11y/nightly >/dev/null 2>&1 || gh pr create \
--title "a11y: mechanical fixes from nightly audit" \
--body "Automated fixes from a live audit." \
--base main --head a11y/nightly
Swap in your URL and store ANTHROPIC_API_KEY as a repo secret. If nothing changed, the step exits cleanly with no PR. Subsequent runs force-push to the same branch, so reviewers see one rolling PR rather than a new one each night. The same shape works against a preview-deploy URL if you'd rather audit branch builds than production: add a pull_request trigger and point at the preview environment.
And the same shape locally
When you're refactoring with the dev server up, the same logic applies. The audit runs in the browser against localhost, not against your JSX. Static analysis of source can't see computed colors, conditional ARIA state, or labels on children that haven't mounted. Pseudo-transpiling components into jsdom doesn't help. jsdom doesn't lay out, doesn't measure fonts, doesn't compute contrast. The DOM the user sees is the most honest input, regardless of where the page is hosted.
Try it
The fastest path is the Claude Code plugin from the AccessLint marketplace:
/plugin marketplace add AccessLint/claude-marketplace
/plugin install accesslint@accesslint
That installs the MCP server and the slash commands together: /accesslint:audit-and-fix <url>, /accesslint:audit-react-component, and the live-page review prompts. Pair it with a browser MCP (chrome-devtools-mcp recommended) and you have the full loop.
If you'd rather skip the marketplace, @accesslint/mcp drops into any MCP-compatible host as a standalone server (no slash commands, just the tools).
Browser does the finding. Agent does the fix.
Top comments (0)