Product design skills

references/harnesses.md

A supporting file of the ultra11y skill.

Where this runs — Claude Code, Codex CLI, OpenCode, and everything else

The skills speak in actions — run the engine, invoke the review skill, adjudicate the findings. On each harness those resolve as below. Only the deltas are listed; anything not mentioned here is identical everywhere, because the engine is one zero-dependency bundle and the CLI it exposes is the same on all of them.

Two things are worth knowing before the table:

  • No harness has a git event. None of them can hook "about to commit". So the automatic review rides the shell tool on every one of them, recognising the commands that publish work — git commit, git push, gh pr create. That is a deliberate design constraint, not a Claude Code quirk.
  • A hook cannot force a skill to be invoked. It blocks the command and hands over the reason and the findings; the agent is what invokes review-a11y. In practice a motivated block is enough — it is not a guarantee, on any harness.

Claude Code

/plugin marketplace add maxgfr/ultra11y
/plugin install ultra11y@ultra11y

The plugin carries both skills and the hook. hooks/hooks.jsonhooks/pre-tool-use.mjsultra11y hook --claude-code. The engine resolves from ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs.

For a requested full audit, Claude Code follows references/claude-code-report.md: it acts as the auditor directly, then publishes the evidenced Markdown, HTML and per-page dossiers. The headless judge --runner claude transport is for CI or another unattended caller; nesting it inside the active Claude Code session wastes context and gives the report a weaker reader.

The review arrives as permissionDecision: "deny" on the pending command, with the findings in additionalContext.

Skills only, no hook: npx skills add maxgfr/ultra11y. Gate only, for an npm install: ultra11y install --claude-code.

OpenAI Codex CLI

codex plugin marketplace add maxgfr/ultra11y
codex plugin add ultra11y@ultra11y

Codex's hook engine is a near-clone of Claude Code's — same events, same payload fields, same hookSpecificOutput envelope — and it exports ${CLAUDE_PLUGIN_ROOT} alongside its own ${PLUGIN_ROOT}, so hooks/hooks.json is shared verbatim between the two.

The deltas that actually matter:

  • Its shell tool is shell, not Bash, and its command is an argv array (["bash","-lc","git commit -m x"]). commandOf in src/hook.ts extracts the script argument rather than joining the array — joining would put git off a command position and the gate would silently never fire.
  • On PreToolUse Codex accepts only permissionDecision: "deny", with a non-empty reason. That is exactly and only what this engine emits, so the envelope needs no branch.
  • .codex-plugin/plugin.json must not declare hooks — Codex's manifest validator rejects any key outside its allowlist. hooks/hooks.json is found by convention at the plugin root.
  • Hooks are behind a feature flag: [features] hooks = true in ~/.codex/config.toml.
  • Codex asks you to trust a hook command the first time it fires. Accept it, or review it with /hooks.

Gate only, without the marketplace: ultra11y install --codex (it sets the feature flag and copies both skills into ~/.codex/skills/).

The installed skill can also delegate an unattended local judgment pass to the same signed-in Codex account:

ultra11y judge --in audits/audit-latest.json --standard rgaa --runner codex --apply

That command uses codex exec with an ephemeral, read-only, offline session and ignores user config, repository rules and hooks. No API key is required; Codex reuses its ChatGPT login. For Claude CLI use --runner claude (--runner cli is the legacy alias).

OpenCode

ultra11y install --opencode

…or pin it in ~/.config/opencode/opencode.json: "plugin": ["ultra11y@latest"].

The delta: OpenCode has no permission-decision channel. Its plugin blocks a tool call by throwing, so the review arrives as a failed bash call whose error message carries the findings. Same content, different envelope — read the error, do not retry blindly.

Skills live under ~/.config/opencode/skills/; the plugin also registers the bundled skills/ directory via config.skills.paths.

Everything else (Cursor, Amp, Zed, Gemini CLI, Windsurf, aider…)

ultra11y install --agents-md

Writes a managed block into the repository's AGENTS.md. Be clear-eyed about what that is: there is no hook API, so nothing is automatic. The block makes the engine discoverable and hands over the adjudication protocol. What actually enforces anything on these harnesses is the repo's own gate — ultra11y init --hook (git pre-commit) and ultra11y init --ci.

There is also MCP, which carries the method where nothing else does — SKILL.md and every references/*.md are exposed as skill:// resources, plus three prompts (audit_wcag, adjudicate_criteria, review_diff_a11y):

claude mcp add ultra11y -- npx -y ultra11y mcp

# better: dedicate it to one project, so `cwd` is optional on every tool AND that
# project's own standards packs (.ultra11yrc.json) are loaded at startup
claude mcp add ultra11y -- npx -y ultra11y mcp --cwd /abs/path/to/project

It also carries the standards themselves — the reference block (ultra11y_standards, ultra11y_criteria, ultra11y_glossary, ultra11y_guidance, ultra11y_method) and the std:// resources. That is the half that turns the server into a rule engine rather than a linter: an agent can read a criterion's numbered tests and the terms it defines instead of recalling them. See references/mcp.md.

On a harness with no skill system at all, reading SKILL.md directly with the file-read tool is the blessed path, not a workaround. Discover what exists by listing skills/*/SKILL.md and reading the frontmatter.

The matrix

skillsautomatic reviewhow it blocksMCPrepo gate
Claude Codeplugin / skills addyespermissionDecision: "deny"yesyes
Codex CLIplugin / skills addyespermissionDecision: "deny"yesyes
OpenCodeplugin / skills addyesthrown tool erroryesyes
Cursor, Amp, Zed, Gemini…AGENTS.mdnoyesyes

ultra11y status reports which of these are actually wired on this machine — including the one silent failure worth naming, a Codex hook sitting behind a disabled feature flag.

Off-switches (identical everywhere)

They live in the engine, not in any harness, so one answer covers all of them: ULTRA11Y_HOOK_FAIL_ON=blocking|major|minor|off (env, wins), then "hook": { "failOn": … } in .ultra11yrc.json, else blocking. SKIP_A11Y=1 bypasses once; ULTRA11Y_HOOK=off disables the gate for the session.

On this page