bkit
A Claude Code plugin that checks AI-generated code against its design specs, with audit logs, auth, and database skills for bkend.ai projects. Useful for developers reviewing code that an AI assistant wrote.
What accessibility-agents runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Claude Code
> /plugin marketplace add Community-Access/accessibility-agents> /plugin install accessibility-agents@community-access
Cursor
Cursor also runs Claude Code hooks from .claude/settings.json, so copy these hooks there to use them in Cursor.
What fires, and when
UserPromptSubmit
Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.
node "${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs" --behaviour detect --client pluginPreToolUse
Edit|Write|NotebookEdit|MultiEdit|apply_patch|Bashnode "${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs" --behaviour gate --client pluginPostToolUse
Skillnode "${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs" --behaviour mark --client pluginSubagentStop
node "${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs" --behaviour mark --client pluginPreCompact
node "${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs" --behaviour persist --client pluginSessionStart
Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.
node "${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs" --behaviour persist --client pluginStop
node "${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs" --behaviour finalize --client pluginWhere it lives
#!/usr/bin/env node
/**
* guard.mjs - the accessibility enforcement gate, for every client.
*
* One script, four behaviours, four client manifests that map their own event
* names onto it. Previously each client had its own script in its own language
* with its own subtly different marker logic, which is why a lead dispatch
* recognised on one client was missed on another.
*
* ## The behaviours
*
* detect Inject the delegation reminder. Once per session, not per prompt.
* gate Refuse an edit to a user-facing file until the lead has completed.
* mark Record that the lead finished, which opens the gate.
* persist Save phase and findings path before compaction; restore after.
* finalize Refuse to end a turn that edited UI without a completed review.
*
* ## Why once per session
*
* The old hook injected roughly 570 tokens of reminder on every prompt. Over a
* forty-turn session that is 22,000 tokens spent repeating something the model
* read the first time. It now injects once, capped at sixty words, and the
* marker file records that it did.
*
* ## Why mark on completion, not on launch
*
* The gate used to open when the lead was dispatched. A cancelled or failed
* lead therefore unlocked every UI edit for the rest of the session. It now
* opens when the lead reports completion: a subagent stop event that names
* it (`agent_type` on Claude Code and Codex).
*
* One exception. On Claude Code the lead is usually invoked as a skill, which
* runs inline in the same turn and has no completion event at all, so waiting
* for SubagentStop locked UI edits for the whole session: verified live, the
* gate refused five edits after the lead had run three times. There, the
* manifests also mark on PostToolUse for the Skill tool, which fires once the
* lead's instructions are loaded into the turn. That is launch rather than
* completion, so the finalize behaviour stays the backstop.
*
* Antigravity has neither a skill tool nor a subagent stop event. It loads a
* skill by reading its SKILL.md with view_file (IsSkillFile set), so its
* manifest marks on PostToolUse for view_file when the file read is the
* lead's SKILL.md: the same launch-time signal as on Claude Code.
*
* Codex is the same: its subagent events fire only for subagents, and the
* model loads a skill by reading its SKILL.md with a shell command. Its
* manifest marks on PostToolUse for Bash when the command reads the lead's
* SKILL.md, and on UserPromptSubmit when the prompt names the lead with $,
* which is how a Codex user invokes a skill explicitly.
*
* The Copilot CLI sends toolName and toolArgs on its camelCase events, which
* the guard did not read, so every Copilot edit passed (#254). Copilot does
* not document how it loads a skill, only that SKILL.md is injected, so its
* manifests mark on every route it can take: a skill tool call, a view or
* shell read of the lead's SKILL.md, subagentStop, and /name in the prompt.
*
* ## Shell writes
*
* The gate also reads shell commands (Bash on Claude Code and Codex,
* run_shell_command on Gemini CLI), because sed -i, a redirect or a heredoc
* changes a .tsx file as surely as Edit does, and Claude Code's auto mode
* tells the model to prefer the shell for edits (#199). It finds the files a
* command writes by reading it: redirects, tee, sed/perl -i, cp/mv, dd,
* PowerShell's Set-Content and Out-File, and an inline interpreter script
* that opens a path for writing. It cannot see a path built in a variable or
* a script run by name. That is a guardrail against the ordinary edit, not a
* sandbox; a bypass now has to be deliberate.
*
* Exit codes follow the common hook convention: 0 proceed, 2 block.
* Structured decisions are emitted as JSON on stdout where the client reads it.
*
* Usage (from a client manifest):
* node hooks/guard.mjs --behaviour detect|gate|mark|persist|finalize --client claude|codex|copilot|gemini|antigravity
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
// --------------------------------------------------------------- parameters
/** Files whose change a person sees and operates. */
const UI_PATTERN = /\.(html?|jsx|tsx|vue|svelte|astro|css|scss|sass|less|ejs|hbs|handlebars|erb|leaf|jinja2?|twig|blade\.php|razor|cshtml)$/i;
/**
* Paths that look like UI but are not a user's interface: dependencies, build
* output, coverage, tests and stories.
*
* Each directory pattern matches at the start of the path as well as after a
* separator. Requiring a leading separator let `dist/main.css` through, which
* is the common shape of a relative path from a repository root.
*/
const SEP = '(?:^|[\\\\/])';
const EXEMPT = [
new RegExp(`${SEP}node_modules[\\\\/]`),
new RegExp(`${SEP}dist[\\\\/]`),
new RegExp(`${SEP}build[\\\\/]`),
new RegExp(`${SEP}out[\\\\/]`),
new RegExp(`${SEP}coverage[\\\\/]`),
new RegExp(`${SEP}vendor[\\\\/]`),
new RegExp(`${SEP}\\.git[\\\\/]`),
new RegExp(`${SEP}__(?:tests?|mocks?|snapshots?)__[\\\\/]`),
/\.(test|spec|stories)\.[jt]sx?$/i,
/\.min\.(css|js)$/i,
];
/**
* Sixty words. Anything longer is repeating what AGENTS.md already says.
* It names no file: a plugin install ships no AGENTS.md and the user's
* project has none, so a pointer at one sends the model looking for nothing
* (#219). The lead skill it names is in every install.
*/
export const REMINDER =
'This project enforces WCAG 2.2 AA. Before editing HTML, JSX, TSX, Vue, Svelte, CSS or a template, ' +
'dispatch the accessibility-lead skill and let it finish. Office and PDF files go to ' +
'document-accessibility-wizard, markdown to markdown-a11y-assistant. Edits to user-facing files are ' +
'blocked until the lead completes, including writes made through a shell command.';
/** Forty words. A refusal has to say what to do next, and nothing else. */
export const REFUSAL =
'Accessibility review/**
* guard.test.mjs - the enforcement gate, tested.
*
* This script is the only thing standing between a model and an unreviewed
* edit to a user's interface. Every test below is a way it could fail open,
* fail closed on the wrong thing, or cost tokens it should not.
*
* Run: node --test hooks/guard.test.mjs
*/
import { test, describe, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const GUARD = path.join(HERE, 'guard.mjs');
let scratch;
function run(behaviour, payload) {
const result = spawnSync(process.execPath, [GUARD, '--behaviour', behaviour, '--client', 'claude'], {
input: JSON.stringify(payload),
encoding: 'utf8',
env: { ...process.env, A11Y_GUARD_DIR: scratch },
});
let json = null;
try {
json = result.stdout.trim() ? JSON.parse(result.stdout) : null;
} catch {
json = null;
}
return { code: result.status, stdout: result.stdout, json };
}
const SESSION = { session_id: 'test-session' };
const edit = (file, extra = {}) => ({ ...SESSION, ...extra, tool_input: { file_path: file } });
beforeEach(() => {
scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
});
describe('detect', () => {
test('injects the contract once, then stays quiet', () => {
const first = run('detect', SESSION);
assert.equal(first.code, 0);
assert.ok(first.json, 'first prompt should inject the contract');
assert.match(first.json.additionalContext, /WCAG 2\.2 AA/);
const second = run('detect', SESSION);
assert.equal(second.stdout.trim(), '', 'a second prompt must not repeat the contract');
});
test('the reminder stays short enough to be worth repeating never', () => {
const { json } = run('detect', SESSION);
const words = json.additionalContext.trim().split(/\s+/).length;
assert.ok(words <= 70, `reminder is ${words} words; it is injected into every session and must stay brief`);
});
});
describe('gate', () => {
test('blocks an edit to a user-facing file before review', () => {
const result = run('gate', edit('src/components/Modal.tsx'));
assert.equal(result.code, 2, 'an unreviewed UI edit must be refused');
assert.equal(result.json.hookSpecificOutput.permissionDecision, 'deny');
assert.match(result.json.hookSpecificOutput.permissionDecisionReason, /accessibility-lead/i);
});
test('the refusal says what to do next and nothing else', () => {
const { json } = run('gate', edit('src/App.vue'));
const words = json.hookSpecificOutput.permissionDecisionReason.trim().split(/\s+/).length;
assert.ok(words <= 45, `refusal is ${words} words; it must name the next action and stop`);
});
test('opens when Claude Code reports the lead subagent stopping, by agent_type', () => {
// Claude Code's SubagentStop payload names the subagent in agent_type.
run('mark', { ...SESSION, hook_event_name: 'SubagentStop', agent_type: 'accessibility-agents:accessibility-lead' });
assert.equal(run('gate', edit('src/components/Modal.tsx')).code, 0);
});
test('opens when the lead skill is invoked through Claude Code\'s Skill tool', () => {
// An inline skill has no stop event. Without this, the gate stayed shut
// for the whole session: seen live, five edits refused after three leads.
run('mark', {
...SESSION,
hook_event_name: 'PostToolUse',
tool_name: 'Skill',
tool_input: { skill: 'accessibility-agents:accessibility-lead' },
});
assert.equal(run('gate', edit('src/components/Modal.tsx')).code, 0);
});
test('a general-purpose subagent stopping does not open the gate', () => {
run('mark', { ...SESSION, hook_event_name: 'SubagentStop', agent_type: 'general-purpose' });
assert.equal(run('gate', edit('src/components/Modal.tsx')).code, 2);
});
test('another skill through the Skill tool does not open the gate', () => {
run('mark', { ...SESSION, hook_event_name: 'PostToolUse', tool_name: 'Skill', tool_input: { skill: 'accessibility-agents:contrast-master' } });
assert.equal(run('gate', edit('src/components/Modal.tsx')).code, 2);
});
test('allows the edit once the lead has completed', () => {
run('mark', { ...SESSION, subagent_type: 'accessibility-lead' });
const result = run('gate', edit('src/components/Modal.tsx'));
assert.equal(result.code, 0, 'the gate must open after a completed review');
});
test('ignores files that are not user-facing', () => {
for (const file of ['server/db.ts', 'scripts/build.mjs', 'README.md', 'package.json']) {
assert.equal(run('gate', edit(file)).code, 0, `${file} is not an interface and must not be gated`);
}
});
test('ignores build output, tests and dependencies that merely look like UI', () => {
for (const file of [
'node_modules/react/index.css',
'dist/main.css',
'build/app.html',
'src/Modal.test.tsx',
'src/Button.stories.tsx',
'coverage/index.html',
]) {
assert.equal(run('gate', edit(file)).code, 0, `${file} is not a user's interface and must not be gated`);
}
});
test('gates every user-facing extension, not only the common ones', () => {
for (const file of [
'a.html', 'a.jsx', 'a.tsx', 'a.vue', 'a.svelte', 'a.astro',
'a.css', 'a.scss', 'a.ejs', 'a.hbs', 'a.erb', 'a.leaf', 'a.twig', 'a.cshtml',
]) {
assert.equal(run('gate', edit(file)).code, 2, `${file} is user-facing and must be gated`);
}
});
test('a client that names the path differently is still gated', () => {
for (const key of ['file_path', 'filePath', 'path', 'target_file']) {
const payload = { ...SESSION, tool_input: { [key]: 'src/Page.tsx' } };
assert.equal(run('gate', payload).code, 2, `a payload using ${key} must still be gated`);
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
WCAG 2.2 AA enforcement for agentic coding, as a set of Agent Skills. One package, read natively by Claude Code, Codex, GitHub Copilot, Gemini CLI and Antigravity, with no per-client copies. Models forget accessibility while generating code.
A Claude Code plugin that checks AI-generated code against its design specs, with audit logs, auth, and database skills for bkend.ai projects. Useful for developers reviewing code that an AI assistant wrote.
A methodology kit for Claude Code that drives feature work through acceptance test driven development, with spec-first artifacts, architecture checks, and a CRAP analyzer for risk-based refactor and test plans on changed code.
Next-generation UI automation harness powered by Python & Playwright. Chat with AI Agents to seamlessly generate, architect, and execute enterprise-grade UI test code.
Runs a disciplined, test-first workflow for building features and fixing bugs in Claude Code. It brainstorms and plans before coding, reviews finished work against the plan, and guides Playwright end-to-end tests for Next.js and React apps, so you stop skipping tests and debugging by guesswork.