Docs / Harnesses
Three targets today. They do not support the same things, and the differences decide what your contract can actually promise.
| harness | backend | install | tested against |
|---|---|---|---|
| claude-code | hooks-json | its own plugin marketplace | 2.1.0 and up |
| codex | hooks-json | its own plugin marketplace | 0.144.0 and up |
| opencode | ts-plugin | Whippletree writes the shim | 1.18.10 and up |
A dash means the harness has no equivalent, so a requirement bound to that event lands ABSENT there, and REFUSES if you marked it hard-required.
| event | claude-code | codex | opencode |
|---|---|---|---|
| session-start | SessionStart | SessionStart | session.created |
| session-end | SessionEnd | SessionEnd | none |
| turn-end | Stop, blocking | Stop, blocking | none |
| tool-pre | PreToolUse, blocking | PreToolUse, blocking | tool.execute.before, blocking |
| tool-post | PostToolUse | PostToolUse | tool.execute.after |
This is what the file-read, file-write and
shell-exec aliases resolve to.
| class | claude-code | codex | opencode |
|---|---|---|---|
| read | Read | no such tool | read |
| write | Write | apply_patch | write |
| shell | Bash | Bash | bash |
The most complete target. Every event maps to a native hook, all three tool classes have a dedicated tool, and skills ride inside the plugin. Nothing degrades.
Identical on events, but it has no file-read tool. Rather than drop
file-read, the target definition declares a degradation to T2:
file-read want ≥T4 got T2 SATISFY matcher Bash|Edit|Write|apply_patch;
misses reads in pipelines/heredocs/scripts;
may double-count
The lossage is written into the target definition, not discovered at runtime, so
preflight prints it before you install rather than after something goes wrong. If T2 is
not good enough, declare minTier: T1 and it will REFUSE here instead.
The one that genuinely cannot do something. It has no blocking stop event. A
thrown error fails a single tool call and the agent loop carries on, so there is nothing
to bind turn-end to.
blocking-gate on turn-end
REFUSES on opencode. That is the correct answer, not a limitation to work around:
a gate that silently becomes advice is worse than one that will not install, because you
would believe it was running.If advice is acceptable, pair the gate with a skill through
fallbackSkill and the step compiles into instructions at T3 instead of
refusing. It also uses a different backend: opencode has no plugin manifest to extend, so
install writes a TypeScript shim into .opencode/plugin/ and
copies skills into .opencode/skills rather than shipping them in the bundle.
Every row above comes from a target definition in the repository, and each one records the harness versions it was probed against. Those definitions are established by running the harness in a sandbox, not by reading its documentation, and the probe notes are kept with the code. A nightly job reinstalls all three and reruns the end-to-end suite, so a harness changing under a definition shows up as a failure rather than as a wrong answer in your terminal.