Skip to content
Testing
Hook

Hooks

What accessibility-agents runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
accessibility-agents
431113 skills7 hooks

Claude Code

> /plugin marketplace add Community-Access/accessibility-agents
> /plugin install accessibility-agents@community-access

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 plugin

PreToolUse

  • MatchesEdit|Write|NotebookEdit|MultiEdit|apply_patch|Bashnode "${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs" --behaviour gate --client plugin

PostToolUse

  • MatchesSkillnode "${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs" --behaviour mark --client plugin

SubagentStop

  • node "${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs" --behaviour mark --client plugin

PreCompact

  • node "${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs" --behaviour persist --client plugin

SessionStart

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 plugin

Stop

  • node "${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs" --behaviour finalize --client plugin
Read hooks/hooks.json

Where it lives

  • hooks/guard.mjsRunsGitHub
    Read the script
    #!/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
  • hooks/guard.test.mjsGitHub
    Read the script
    /**
     * 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.

@community-access
Ships withaccessibility-agents

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.

Stars on GitHub

Momentum

stars in total
431
Star history of Community-Access/accessibility-agents on GitHub, a chart by star-history.com

More plugins that ship hooks.

bkit

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.

44 skills+3606
@popup-studio-ai@popup-studio-ai
Install Plugin
atdd

atdd

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.

28 skills+3163
@swingerman@swingerman
Install Plugin
velocitai

velocitai

Next-generation UI automation harness powered by Python & Playwright. Chat with AI Agents to seamlessly generate, architect, and execute enterprise-grade UI test code.

13 skills+2158
@danielsuo117@danielsuo117
Install Plugin
e2e-testing

e2e-testing

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.

9 skills2 hooks16
@burhankhatri@burhankhatri
Install Plugin

Questions about the hooks in accessibility-agents.

Which coding agents can use the hooks in accessibility-agents?
The hooks in accessibility-agents install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.