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@ultra11yThe plugin carries both skills and the hook. hooks/hooks.json → hooks/pre-tool-use.mjs
→ ultra11y 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@ultra11yCodex'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, notBash, and itscommandis an argv array (["bash","-lc","git commit -m x"]).commandOfinsrc/hook.tsextracts the script argument rather than joining the array — joining would putgitoff a command position and the gate would silently never fire. - On
PreToolUseCodex accepts onlypermissionDecision: "deny", with a non-empty reason. That is exactly and only what this engine emits, so the envelope needs no branch. .codex-plugin/plugin.jsonmust not declarehooks— Codex's manifest validator rejects any key outside its allowlist.hooks/hooks.jsonis found by convention at the plugin root.- Hooks are behind a feature flag:
[features] hooks = truein~/.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 --applyThat 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-mdWrites 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/projectIt 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
| skills | automatic review | how it blocks | MCP | repo gate | |
|---|---|---|---|---|---|
| Claude Code | plugin / skills add | yes | permissionDecision: "deny" | yes | yes |
| Codex CLI | plugin / skills add | yes | permissionDecision: "deny" | yes | yes |
| OpenCode | plugin / skills add | yes | thrown tool error | yes | yes |
| Cursor, Amp, Zed, Gemini… | AGENTS.md | no | — | yes | yes |
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.