CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/Claude Code
Claude Code/2026-04-30Intermediate

Automating Frontend Accessibility Audits with Claude Code — axe-core, Hooks, and PR Comments That Actually Get Read

A practical way to make accessibility audits actually stick in a real codebase. Combine axe-core with Claude Code skills, pre-commit hooks, and a GitHub Actions PR comment, with code you can paste in today.

claude-code129accessibilitya11yaxe-coreautomation95

We all know accessibility matters, and yet a11y checks are still the first thing to slip when a release deadline looms. Running four small projects in parallel, I have personally pushed "just one more sprint" enough times that the regressions piled up and forced an embarrassing catch-up week.

This piece walks through a concrete way to keep the audit in the loop without relying on willpower. We use axe-core as the runtime, and Claude Code as the connective tissue that turns raw output into something a developer actually sees and acts on.

Why "we installed an audit tool" usually fails

Most teams already have a linter, a test runner, and probably a Lighthouse step somewhere. The reason a11y still slides is not that the tooling is missing. It is that the results live in the wrong place.

A warning buried in a CI log gets ignored. A Slack notification gets scrolled past. The only spot a developer reliably reads is the diff view of the pull request they just opened. Once I started piping audit results into PR comments instead of CI logs, the same warnings that used to be ignored for weeks were fixed within hours.

Claude Code earns its keep here in two ways. First, it converts noisy axe-core output into a focused summary that says what to fix and why. Second, hooks and skills let the same check run during local commits, so the issue is caught before the PR even exists.

Step 1: Run axe-core headlessly with Playwright

Start with the smallest possible runtime. Playwright + @axe-core/playwright is the most stable combination I have used.

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

Now write one small script with a single responsibility — visit a URL, run the audit, write the result to disk. Keep it boring; clever scripts are the ones that rot.

// scripts/a11y-audit.mjs
import { chromium } from 'playwright';
import AxeBuilder from '@axe-core/playwright';
import { writeFileSync } from 'node:fs';
 
const targets = process.argv.slice(2);
if (targets.length === 0) {
  console.error('Usage: node scripts/a11y-audit.mjs <url> [<url> ...]');
  process.exit(1);
}
 
const browser = await chromium.launch();
const page = await browser.newPage();
const results = [];
 
for (const url of targets) {
  await page.goto(url, { waitUntil: 'networkidle' });
  const r = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa']) // start at AA
    .analyze();
  results.push({ url, violations: r.violations });
}
 
await browser.close();
writeFileSync('a11y-report.json', JSON.stringify(results, null, 2));
console.log(`Found ${results.reduce((n, r) => n + r.violations.length, 0)} violations across ${results.length} pages.`);

The expected outputs are an a11y-report.json file in the repo root and a one-line summary on stdout. Both local runs and CI use exactly the same command, which keeps the operating model dead simple.

Step 2: Wrap the script as a Claude Code skill

A Claude Code skill is a tiny manual the model can invoke autonomously. Full mechanics are covered in the custom skills development guide; here is the minimum we need.

# .claude/skills/a11y-audit/SKILL.md
---
name: a11y-audit
description: "Run axe-core against a URL and summarize violations by impact. Triggers: accessibility, a11y, audit"
---
 
# How to use
 
1. Make sure `npm run dev` is running.
2. Run `node scripts/a11y-audit.mjs http://localhost:3000`.
3. Read `a11y-report.json` and bucket violations into critical / serious / moderate / minor.
4. For each critical and serious item, return the affected selector, the WCAG criterion, and a one-line fix suggestion.
5. Format the output as a Markdown block that can be pasted into a PR description.

Two things matter here. First, the trigger keywords in the description — without them, Claude Code cannot decide when to invoke the skill. Second, leave the judgment to the model. Don't try to encode "what counts as critical" in the skill itself; teams disagree, and hardcoded rules age badly.

Step 3: Add a pre-commit hook so nothing slips

Local audits only help if you remember to run them, and you won't. A Claude Code hook fixes that. The full hook system is documented in the hooks automation guide; here is the relevant snippet.

// .claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(git commit*)",
        "command": "node scripts/a11y-audit.mjs http://localhost:3000 || true",
        "description": "Run a11y audit before commit (does not block the commit)"
      }
    ]
  }
}

The || true is intentional. A hook that blocks commits will be disabled within a week. A hook that surfaces information without punishing the developer will quietly do its job for years. Claude Code summarizes the result in the conversation, the developer sees it, and that is enough to change behavior over time.

Step 4: Surface the result on every pull request

The last piece is a GitHub Actions job that runs the same script and posts a comment. Broader Claude Code + Actions patterns are in this workflow guide; the audit-only version is short.

# .github/workflows/a11y.yml
name: A11y Audit
on:
  pull_request:
    branches: [main]
 
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npm run build && npm run start &
      - run: sleep 5 && node scripts/a11y-audit.mjs http://localhost:3000
      - name: Comment summary on PR
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const data = JSON.parse(fs.readFileSync('a11y-report.json', 'utf8'));
            const counts = data.flatMap(p => p.violations).reduce((acc, v) => {
              acc[v.impact] = (acc[v.impact] || 0) + 1;
              return acc;
            }, {});
            const body = [
              '## Accessibility audit',
              `- critical: ${counts.critical || 0}`,
              `- serious: ${counts.serious || 0}`,
              `- moderate: ${counts.moderate || 0}`,
              `- minor: ${counts.minor || 0}`,
              '',
              counts.critical || counts.serious
                ? '⚠️ Critical or serious issues remain. Please address before merging.'
                : '✅ No critical or serious violations.',
            ].join('\n');
            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body
            });

Because the raw a11y-report.json ships as a workflow artifact, you can also feed it back into Claude Code later to prioritize a refactor sprint. That dual use — fresh signal in the PR, persisted data for planning — is what makes the setup pay off.

Three traps that will eat 30 minutes each

I tripped on all of these. None are obvious from the docs.

SPA hydration timing. Calling axe-core immediately after page.goto() on a client-rendered app produces phantom violations because the audit runs before React mounts. waitUntil: 'networkidle' solves it; add it before you debug anything else.

Playwright binary in CI. The Chromium download is roughly 200 MB. Without caching it, every CI run pays the tax. Add actions/cache@v4 keyed on the Playwright version and the second run drops by minutes.

Too much signal at once. Reporting every minor violation from day one is overwhelming, and overwhelmed developers ignore the comment entirely. Start with critical and serious only. Lower the bar after the team has cleared the top tier — the same staged approach we use for ESLint cleanup with Claude Code.

A note on what to skip in the early days

When I first set this up, I tried to be thorough. I instrumented every page in the app, ran the audit on multiple viewport widths, and asked Claude Code to summarize all four impact levels. The PR comments became 400 lines long, and the team's reaction was exactly what you'd expect — they collapsed the comment and stopped looking.

The version that actually changed behavior is much smaller. One target URL (the most common landing page), one viewport, only critical and serious violations, and a four-line summary at the top of the comment. Everything else can be added later, after the team has built the habit of opening the section.

This is the same dynamic I see with most "quality gate" tooling. The configuration that survives contact with reality is always less ambitious than the one we want to set up on day one. The audit you can sustain for six months beats the audit you abandon in three weeks.

Where Claude Code earns its place

A reasonable question is whether you need Claude Code at all. axe-core has its own reporters; GitHub Actions can post comments without any LLM in the loop. So why route the result through a model?

The honest answer is that the value shows up over time, not on day one. After two months of running this, the most useful output isn't the per-PR comment — it's being able to ask "show me all the color-contrast violations we've flagged this quarter" or "which routes have regressed since the redesign?" Claude Code reads the accumulated report artifacts, cross-references the affected selectors with the current codebase, and produces a prioritized refactor list. That kind of analysis is awkward to script and natural to ask.

If you're starting fresh, I'd suggest installing the script and the GitHub Action first, and adding the skill once you have a few weeks of reports to look back on. That way the skill is doing work that would otherwise be tedious, instead of duplicating what the JSON already shows.

Stop relying on memory; start relying on the workflow

Accessibility lives or dies on whether the feedback shows up where developers already look. Tools alone don't fix it. A small skill, a forgiving hook, and a PR comment together do.

If you want a single concrete next step, drop scripts/a11y-audit.mjs into your project and run it once against localhost:3000. The first violations you see will tell you whether you want the hook next, the Action next, or both at once. The order does not matter as much as starting today instead of next quarter.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-07-17
The Morning My Table Ended in "… 2,847 more rows" — Separating Render Caps from Token Cost in Tool Output
Claude Code 2.1.209 caps markdown tables at 200 rows plus a remainder count. Only the rendering is capped — the model still receives every row. Here is how to measure the gap and redesign tool output around aggregates.
Claude Code2026-07-15
Answering auto mode's confirmation prompts in headless runs — a deny-by-default permission-prompt-tool
auto mode's confirmation step is a friend when you're at the keyboard, but in an unattended midnight run it becomes the reason a job sits waiting until morning. Here is how I catch those prompts with permission-prompt-tool, decide deny-by-default, and log every ruling — with working code.
Claude Code2026-07-14
One Day My Push Had an Extra Destination — Guarding Against /commit-push-pr Pushing to Remotes Beyond origin
The July 14 update made /commit-push-pr push to configured push remotes in addition to origin. Convenient, but if you keep a mirror or backup as a second remote, unintended pushes quietly multiply. Here is how to inventory which remotes you can push to, block anything off the allowlist with a pre-push hook, and keep unattended runs safe — with working code.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →