Skip to content

Adapters

Add a harness or an editor to omh with one TOML file. A harness is data, not a code change.

Status: built. Four adapters ship — claude, codex, opencode and omp. Only claude has been driven for real work; the other three pass omh doctor, which proves their paths and nothing about their behaviour.

  1. Write ~/.omh/adapters/<name>.toml.

    Declare only what the harness genuinely supports: an absent key means it cannot do that thing. Every field is in Adapter fields below, and the four bundled adapters are worked examples.

  2. Check it inside a real sandbox.

    Terminal window
    $ omh doctor --harness <name>
  3. Fix whatever it says, and run it again.

Step 2 is not optional, and it is not a formality. Adapter facts are unverified claims about external software that ships weekly, and they break silently — a wrong path means the harness starts fine and simply never sees your catalogue. A green unit suite proves omh mounted a path faithfully. It cannot prove anything read it. See Troubleshooting.

All four bundled adapters are verified by doctor against a real container, hooks included on every one. Any fifth adapter inherits the same bar.

  • Passing doctor is not being proven. It means the paths are right, not that the harness has been driven for real work — only claude has. That distinction is deliberate, is repeated wherever the claim appears, and applies to opencode, omp and codex in the same words.
  • The check count is not an adapter fact. It varies with the capabilities your catalogue declares and with whether a login has been captured for that harness.
How each adapter's hooks are checked, and omp's one weaker claim

claude’s and codex’s hooks are configuration files, which doctor expects to find and to find non-empty. opencode’s and omp’s are generated plugins rather than config files, so they are checked by node --check inside the sandbox rather than by existing.

omp carries one claim weaker than the others, and it is written here rather than left in a comment: its token-probe greps omp usage --json for accountId, and that field name was read out of oh-my-pi’s source rather than observed in the output of a logged-in run. doctor reports the probe green only against an account that actually logged in, so an adapter shipped without one has had its shape checked and not its answer. The other three adapters have no equivalent gap, because a token file either holds bytes or does not.

Same principle, smaller file. Write ~/.omh/editors/<name>.toml:

~/.omh/editors/zed.toml
name = "zed"
bin = "zed"
args = ["$URL"]
Field What it says Default
name the word omh sNN attach <editor> takes: letters, digits, - and _ required
bin the executable on your host — an editor is not installed in the sandbox required
args its arguments; $URL becomes ssh://<alias>/work and $ALIAS the session’s SSH alias required

Editors attach from outside over SSH rather than running inside the sandbox, so they have no capabilities to declare. See Editors.

The claude adapter, shortened:

name = "claude"
bin = "claude"
version = "2.1.316"
install = "npm install -g @anthropic-ai/claude-code@{{version}}"
creds = ["$HOME/.claude/", "$HOME/.claude.json"]
token = ["$HOME/.claude/.credentials.json"]
[capabilities.rules]
path = "/work/CLAUDE.md"
also = ["/work/AGENTS.md"]
render = "concat"
[capabilities.mcp]
path = "/work/.mcp.json" # project-scoped; this harness reads no other
render = "mcp-json"
import = "$REPO/.mcp.json" # host-side, for `omh settings mcp import`
verify = "claude mcp list" # what `omh doctor` asks the harness itself
ready = "Connected" # what that answer calls a server it loaded
Field What it says Default
name the harness’s name, as omh new, omh account login and omh doctor --harness take it. Letters, digits, - and _ only — it is joined into paths required
bin the executable to run inside the sandbox required
install the command that installs bin into the image; it may interpolate the pin as {{version}} required
version the harness version this adapter pins. install must interpolate it no pin
checksums SHA-256 sums keyed by uname -m (x86_64, aarch64), for a harness delivered as a release binary rather than an npm package. Each must appear in install, which checks it none
creds the paths holding credentials, captured by omh account login and remounted on every launch. A trailing / declares a directory none
token the file(s) whose contents prove a login — see below none
[token-probe] run and ready: ask the harness whether it is logged in, where no file can say none
login what you have to do once the harness opens, to log in none
mask paths strictly inside one of creds’ directories, with no .., that a session could write and the harness reads at its next start as the user’s own — instructions, rules, skills, code modules — so one session could leave them for every repo on the account. A file is hidden behind an empty read-only file; a directory, ending in /, behind an empty read-only directory holding only what omh mounts into it. Never a capability’s path, a [policy] mask, or inside a masked directory. A capability whose path is in the account is masked the same way when the catalogue carries nothing for it. Claude Code’s CLAUDE.md and rules/; Codex’s AGENTS.md, AGENTS.override.md and skills/; omp’s AGENTS.md, RULES.md, SYSTEM.md, hooks/pre/, hooks/post/, tools/ and extensions/ — each measured in tests/e2e none
transcripts where the harness writes its transcripts. omh mounts a per-session directory over it, so omh sNN can read what the agent did and what it cost none, and omh says not recorded rather than showing an empty session
[tools] how the harness names edit, read, shell and search — see Hooks none
[capabilities.<class>] one binding per capability the harness can express none
[policy] the harness’s own policy file, which omh fills on every launch — see below none

creds names a directory rather than the token file because a bind-mounted file cannot be replaced by rename, and every tool saves its token by writing a temporary file and renaming it over.

Field What it says Default
path the guest path the harness reads; $HOME expands per image required
also more guest paths pointed at the same content none
render how omh produces what the harness reads — see Renderers required
import the host-side path omh import and omh settings mcp import read from: where you keep it, not where the container reads none — omh import then needs --from
verify the harness’s own command for listing what it loaded, run by omh doctor inside the sandbox not asked
ready the word verify’s output uses for a server the harness took up not asked
flag the option the harness is handed path by, for a document it reads only when told to; path is then a file under /etc/. Only claude-settings — see Claude’s settings document none: the harness finds path itself
eject with flag, the file under $HOME the harness reads on its own, where omh eject writes the document required with flag, refused without
events, fields, fields-by-tool, inject, refuse hooks only — see Hooks none

Claude Code reads omh’s settings — its hooks, the approval of omh’s MCP document and of omh’s own servers’ tools, disableAllHooks: false and attribution: false — from --settings, never from ~/.claude/settings.json, which is each session’s own preferences file:

adapters/claude.toml
[capabilities.hooks]
path = "/etc/omh/claude/settings.json"
render = "claude-settings"
flag = "--settings"
eject = "$HOME/.claude/settings.json"

The image puts a wrapper where claude was on PATH, so every claude in the sandbox is handed the document — omh’s launches, omh doctor’s claude mcp list, one run by hand in an editor’s terminal, a nested claude -p — ahead of any subcommand, since claude mcp list --settings <f> is an unknown option. --settings sits above user, project and local settings, so a session’s disableAllHooks: true or attribution text in the worktree’s .claude/settings.local.json changes nothing (tests/e2e). Of two --settings, Claude keeps the last whole, so one you pass — a file, --settings=<f> or inline JSON — is merged into omh’s instead, with jq’s *: omh’s keys win, the rest of yours is kept (tests/e2e). One that does not parse stops the run: omh could not merge your --settings <value> into its own. Calling the real binary by its absolute path skips the wrapper. Where the document is not there — the image run by something other than omh, or on sbx, where omh’s entrypoint never runs (#199) — the wrapper runs Claude handed exactly what it was, as it ran there before.

attribution: false hides the attribution Claude adds to the commits and pull requests it writes — text it puts before the model at every start, which a file a session writes could otherwise set. It needs Claude Code 2.1.281 or later; an older one skips the whole file, hooks included, and a test holds the pin above it.

adapters/codex.toml
[policy]
path = "/etc/codex/requirements.toml"
format = "codex-requirements"
mask = ["$HOME/.codex/cloud-config-bundle-cache.json"]

The account directory is writable and shared by every session on it, so anything a session adds there — an MCP server through codex mcp add, a hook — would load in every later session (Claude Code’s ~/.claude.json is each session’s own copy; see copy). A harness’s policy layer is read from /etc, which omh fills from the launch with a read-only file, and nothing a session writes there outlives its container:

Field What it says Default
path the guest path the harness reads its policy from: a file under /etc/, absolute, with no .., not under a creds or transcripts path and not a capability’s own path — a literal, never expanded required
format codex-requirements or claude-managed required
mask guest paths strictly inside one of creds’ directories, with no .., never a capability’s own path, that a session could write and the harness would trust on its next start; each is hidden behind an empty read-only file none
copy creds file entries each session gets its own copy of, seeded from the account’s without what names, switches or grants a server, and never written back — see ~/.claude.json is each session’s own. Only a file its format knows how to seed: $HOME/.claude.json, for claude-managed none

The policy admits exactly the MCP servers the launch staged, by command and arguments compared exactly — and by name too, on Codex. Neither harness compares env: on Claude Code each session’s own ~/.claude.json (copy above), and on Codex each session’s own config.toml ([preferences]), keep such an entry from reaching another session. It is written even when nothing is staged, because a harness with no allowlist admits anything. On Codex, what the policy stops in your checkout or account — a repo’s own .codex/config.toml servers or hooks, an enterprise bundle cache — is named at launch and by omh doctor. omh eject writes the policy itself under system/.

  • codex — requirements.toml: the [mcp_servers] allowlist, an entry that holds every plugin’s servers to a list of their own — empty for any plugin not named — allow_managed_hooks_only (omh’s hooks are in the system layer and still run; a repo’s .codex/config.toml hooks do not, by Codex’s source), and a pinned chatgpt_base_url. An enterprise account’s cloud bundle cache is masked, so Codex fetches the bundle at every start — and, by Codex’s source, offline it does not start; no enterprise account has been run. The account’s model provider, provider.toml at the account’s root, is rendered into the system layer beside omh’s hooks, held to model_provider and the fields of [model_providers.*] — declared by [provider], not here; see A model provider of your own. Codex uses it from there (tests/e2e: every codex turn’s provider comes from it); a session’s own config.toml, which ranks above the system layer (Codex’s source), holds no provider to override it.
  • claude — a managed-settings.d drop-in: allowManagedMcpServersOnly, allowedMcpServers, and disableClaudeAiConnectors. claude.ai connectors are switched off in a session (read in the 2.1.300 binary; not measured): one is matched by its URL, and allowlisting that URL would let a session put its own server there. An organisation’s own managed list replaces omh’s (Claude Code’s docs; not measured). .config.json, in the account’s shared ~/.claude/, is masked: Claude reads it instead of ~/.claude.json when it is not empty. settings.json beside it, user settings, is each session’s own preferences file; omh’s own settings reach Claude another way: Claude’s settings document.
adapters/codex.toml
[preferences]
mount = "link"
[preferences.files]
"$HOME/.codex/config.toml" = ["model", "model_reasoning_effort", "tui.theme", …]
[preferences.quiet]
"$HOME/.codex/config.toml" = ["notice.hide_rate_limit_model_nudge", …]
adapters/claude.toml
[preferences]
mount = "file"
[preferences.files]
"$HOME/.claude/settings.json" = ["model", "advisorModel", "modelSettings", …]
"$HOME/.claude.json" = ["copyOnSelect", "respectGitignore", …]
adapters/omp.toml
[preferences]
mount = "link"
[preferences.files]
"$HOME/.omp/agent/config.yml" = ["modelRoles", "defaultThinkingLevel", "theme.dark", …]
[preferences.fallback]
"$HOME/.omp/agent/config.yml" = ["$HOME/.omp/agent/config.yaml"]
adapters/opencode.toml
[preferences]
mount = "dir"
root = "$HOME/.local/state/opencode"
[preferences.files]
"model.json" = ["recent", "favorite", "variant"]
"kv.json" = ["theme", "theme_mode", "thinking_mode", …]

A harness’s own config file sits in the account, and a session writes it. The keys named here are the user’s preferences: the account keeps them, at its root where no mount reaches, and each session starts from a copy of them. What a session changes among them reaches the account through the review; everything else it writes there lasts that session — see your preferences.

Field What it says Default
mount how a session’s copy reaches the harness. link: the account’s file is a link to the same name under /omh/prefs, which each container mounts from its session’s own copy — for a harness that saves by renaming over the file, which a mounted file refuses, and resolves the link first. file: the session’s copy is mounted on its own where the harness reads it — for a harness that, its rename refused, writes the file in place. dir: the session’s copy of a whole directory is mounted at root — for preferences kept in a directory of the harness’s own state that no account holds, opencode’s; what else the harness keeps there is the session’s required
root for dir, and only there: the directory the session’s copy is mounted at, under $HOME, with no .., and neither inside nor around one of creds’ paths none
files each file and its keys that are preferences, as dotted keys (tui.theme, notice."a.b"), a table declared whole by its name. A file is TOML, JSON or YAML, by its extension; has no ..; is named unlike every other, since each is kept and copied by its name alone; and is never a [policy] mask entry. For link, it is strictly inside one of creds’ directories, where the link is laid; for file, one of creds’ files or inside one of its directories; for dir, a file name in root. A file preferences file that is a [policy] copy too is one copy: the account file’s rest, with the session’s preferences required
quiet keys of the same files the harness writes on its own — a notice dismissed — kept without asking, and named when kept. rm --yes keeps none, and a value whose type changed is kept nowhere. A key whose value names a model is not bookkeeping, and belongs in files none
fallback for a link file, the files the harness reads in its place while it is absent, in the order it tries them — omp’s config.yaml for its config.yml. Before the link, the account’s preferences are migrated from the first one there, which then leaves the account; after it, the harness never reads one again, and nothing in one is kept. Each is beside the file it stands in for, TOML, JSON or YAML, and neither that file nor another preferences file none

The rule a key is judged by: a preference never names a program or a capability, never grants a consent, a trust or a permission, never carries text the harness puts in front of the model, and never picks where traffic goes. omh holds a list of what that rules out per harness — for codex mcp_servers, notify, model_provider, model_providers, profiles, features, developer_instructions, projects, hooks, approval_policy, sandbox_mode, notice.hide_full_access_warning and their kin; for claude hooks, disableAllHooks, permissions, enabledPlugins, extraKnownMarketplaces, mcpServers, statusLine, apiKeyHelper, env, attribution, outputStyle, projects and their kin; for omp extensions, mcp, tools, bash, skills, personality, contextFiles, auth, providers, enabledProviders, disabledProviders, modelProviderOrder, retry and their kin; for opencode plugin_enabled and share_consent — and an adapter declaring a key at, inside or around one of them is refused when it loads. Every list is explicit: a key no list names is not kept.

[provider]: the account’s model provider

Section titled “[provider]: the account’s model provider”
adapters/omp.toml
[provider]
format = "omp-models"
path = "$HOME/.omp/agent/models.yml"
import = "$HOME/.omp/agent/models.yml"
drop = [
"$HOME/.omp/agent/models.db",
"$HOME/.omp/agent/models.db-wal",
"$HOME/.omp/agent/models.db-shm",
]

A custom model provider is the user’s, and the file a harness keeps it in is one every session on the account can rewrite. The account keeps it instead, in a provider file at its root in the harness’s own schema, and each launch renders it where the session cannot write — see A model provider of your own.

Field What it says Default
format the provider file’s schema, and where it is rendered. codex-config: model_provider and [model_providers.*], kept in provider.toml and rendered into Codex’s system layer beside omh’s hooks. omp-models: omp’s models.yml, kept in provider.yml and rendered over path required
import the host file, under $HOME, that omh account login --import reads the harness’s own provider from required
path for omp-models, the file the harness reads providers from, strictly inside one of creds’ directories, with no ..: the render is mounted over it read-only, {} when the account has none. codex-config takes none —
drop files in the account, each strictly inside one of creds’ directories, that can carry a provider’s address into the next session and that the harness rebuilds: removed before each launch none

omh enforces each of these, with an error naming the adapter:

  • a key it does not know — see Unknown fields are rejected
  • a name that is anything but letters, digits, - and _
  • a version that install never interpolates, or a {{version}} with no version to fill it
  • an install that runs sha256sum -c while declaring no checksums, or a declared sum install never checks
  • an install that spans lines: it is one RUN, and each line after the first would be a Dockerfile instruction of its own
  • token and token-probe together
  • no capabilities at all — it would launch a harness that can see none of your catalogue
  • hooks with no [tools], [tools] with no hooks, hooks with no events, and a hook map on any capability but hooks
  • a [provider] path or drop entry not strictly inside a creds directory, or with a ..; an omp-models with no path or a codex-config with one; an import outside $HOME
  • a [policy] mask entry that is a capability’s own path — two mounts at one path
  • a mask entry not strictly inside a creds directory, with a .., inside a masked directory, a [policy] mask too, or a capability’s own path
  • a [preferences] file outside where its mount puts it, in a format other than TOML, JSON or YAML, with a .., masked, or named like another; a key that is not a dotted key, declared twice, inside another, or at, inside or around one the harness’s never-list names; a fallback for a file that is not declared, for a file mount, outside a credential directory, not beside its file, in another format, or that is a preferences file itself; a dir mount with no root, a root for any other mount, outside the home, with a .., or inside or around a credential path, and a dir file that is not a name
  • a flag that is not a long option (-- and letters, digits, dashes), on anything but a claude-settings render, with a path not under /etc/, or with no eject; an eject outside $HOME, with a .., or without a flag

One rule is yours to keep: declare verify and ready together. doctor runs the check only when both are set, and skips half a pair without a word.

Why a pin and a checksum are checked as the adapter loads

A pin can be inert two ways, and both compile and both ship an image nobody meant: a version the command has no {{version}} to receive installs the latest release while claiming a pin, and a {{version}} with nothing to fill it installs a package literally called {{version}}.

A checksum verified but never declared is a check against nothing: the install would sha256sum -c a file it also wrote, or fail on a sum that is not there. If the command checks one, the field carries it — which is where omh doctor reads it too.

The name is checked because it is a path: omh resolves a harness by joining the word into <dir>/<word>.toml, and keys the credential directory on it. A name of .. would name a file outside the catalogue, whose install becomes a line of a host docker build.

The catalogue carries the superset of what any harness might express. An adapter declares which parts its harness can actually express, and where. The capability classes are rules, skills, mcp, commands, subagents and hooks.

An absent key means the harness cannot do that thing. Degradation is a missing map entry, not special-case logic — which is why adding a harness needs no branch anywhere in the codebase.

It is announced once, at launch, rather than silently swallowed:

Terminal window
$ omh new codex
codex on omh/s02 — dropped 0 commands, 0 subagents (unsupported); dropped hooks: graph-first (no `search` tool), graph-read (no `read` tool)

Adapters parse with deny_unknown_fields. Without it, a stale adapter parses cleanly with zero capabilities and every harness silently degrades to nothing — the worst possible failure for a tool whose entire promise is that your setup is already there.

Declare one way to prove a login. omh refuses an adapter that declares both, at load.

Declare When It passes when
token the harness writes its login to a file omh can stat the file holds bytes
[token-probe] the credentials are somewhere omh cannot read — omp keeps them in SQLite run succeeds inside the sandbox and its output names ready
[token-probe]
run = "omp usage --json" # asked inside the sandbox, where the credentials are
ready = "accountId" # what that answer says when the login is real

omh doctor settles a probe, not omh account login. omh account login tears the container down before it can ask, so on such a harness it records the account and says the login is unconfirmed.

Why a file beats a probe, and why declaring both is refused

token is the better answer wherever it applies, because a stat needs no container and cannot be wrong about what it saw.

Some harnesses keep credentials somewhere omh cannot read. There the only file that could be named is created by the harness starting, so declaring it would report a login for every session that merely opened the tool. Such an adapter declares a probe instead.

The pair is refused at load, not resolved by precedence: an adapter declaring both leaves omh with two answers to one question and no rule for which wins. It is the same shape as verify/ready one level up — when the claim is about software omh did not write, ask that software — and the same rule applies to the answer: the probe passes only when the command succeeds and its output names ready, because a harness that errors out will happily print the marker inside its error text.

Render Used by Effect
dir skills, commands, subagents link the catalogue’s entries by name, mount read-only
concat rules join the sections into one document, staged and mounted read-only
mcp-json, codex-toml, opencode-json mcp reshape the canonical server list
claude-settings hooks reshape the canonical hook list
codex-hooks hooks write Codex’s [[hooks.<Event>]] tables into its system layer, /etc/codex/config.toml, which Codex runs as managed — with the account’s model provider beside them, when it has one
opencode-plugin, omp-plugin hooks generate a JavaScript module

Nothing is written into the worktree. concat, and any path under /work whichever renderer produced it, is mounted: omh places the mountpoint and binds over it, so a project’s own file is hidden for the session and returned untouched.

Every renderer must round-trip through its parser. omh settings mcp import is the exact inverse of rendering, so a format that renders but parses lossily means import silently drops fields. That is a test, not a hope.

Why the two plugin renders are separate, and why concat stopped writing to the worktree

Two of the renders emit a program, and they are separate renders rather than one parameterised by the harness. They agree on the shell bridge — how a hook body runs, how a failed predicate is reported, how $OMH_* expands — and on nothing else: opencode registers one object of named handlers and reads a call’s arguments off output or input depending on the moment, while omp gives each hook its own pi.on(...) registration and hands every handler an event. Sharing the generator would mean a match on the harness in every line of it.

concat once wrote into the worktree. That made omh’s staging indistinguishable from the agent’s work, and carried omh’s rules into users’ pull requests.

verify is the harness’s own command for listing what it loaded; ready is the word its output uses for a server the harness has taken up. omh doctor runs the one and greps for the other, on the same line as the server’s name.

Harness verify ready What a pass proves
claude claude mcp list Connected the server is running
codex codex mcp list enabled Codex’s parsed config holds the server — the file was read, not that the server starts
opencode, omp — — not asked

That is as much as each harness can say. The pass line names the word, so the two do not read as the same fact.

Both are optional. An adapter that declares neither is simply not asked — the same degradation as any absent key.

Why a name in the list is not enough, and what an unasked path cost

A path is a claim about software omh did not write, and the suite cannot check it. What leaving it unasked costs is on record: the claude binding pointed mcp at $HOME/.mcp.json, which nothing reads, and every check omh had stayed green while no session ever loaded a server.

Matching the name alone is not enough, and that is the point of ready. A project-scoped document Claude Code has not been told to trust is listed in full and loaded not at all, so a name-only check passes in exactly the state the feature is broken in.

A hook is authored in omh’s vocabulary and translated when it is staged, so the adapter is where a harness’s spelling lives — the same rule as every path.

# Adapter-level: hooks match on these, and the Agent Skills standard and
# subagent frontmatter both carry harness tool names too. One vocabulary.
# No `search`: Claude Code's native builds reach grep and glob through `Bash`.
[tools]
edit = "Edit|Write|MultiEdit"
read = "Read"
shell = "Bash"
[capabilities.hooks]
path = "/etc/omh/claude/settings.json"
render = "claude-settings"
flag = "--settings"
eject = "$HOME/.claude/settings.json"
[capabilities.hooks.events] # omh's moments → this harness's names
session-start = "SessionStart"
turn-end = "Stop"
before-tool = "PreToolUse"
after-tool = "PostToolUse"
[capabilities.hooks.fields] # where this harness keeps each field
tool-file = ".tool_input.file_path"
tool-command = ".tool_input.command"
[capabilities.hooks.inject] # how it accepts advisory text for the agent
template = """jq -nc --arg m {{text}} '{"hookSpecificOutput":{"hookEventName":"{{event}}","additionalContext":$m}}'"""
[capabilities.hooks.refuse] # and how it blocks a call, with a reason
template = """jq -nc --arg m {{text}} '{"hookSpecificOutput":{"hookEventName":"{{event}}","permissionDecision":"deny","permissionDecisionReason":$m}}'"""
Key Maps
[tools] omh’s tool words — edit, read, shell, search — to the harness’s tool names. Adapter-level
events omh’s moments — session-start, turn-end, before-tool, after-tool — to the harness’s names. Required on a hooks binding
fields where the harness keeps tool-file and tool-command, in the renderer’s own language
fields-by-tool.<tool> a field that lives somewhere else on one tool: a complete expression, used verbatim. A tool with no entry falls back to fields
inject the template for advisory text the agent reads; the call goes ahead
refuse the template that blocks a call, with a reason

In a template, {{text}} receives a shell word and {{event}} the harness’s own word for the moment — some protocols name the event back in the payload.

An absent entry means this harness has no such thing — the capability map’s rule, one level down. The hooks wanting it are dropped by name, saying what they asked for, and the rest still ship: a harness with turn-end and no before-tool keeps most of them, and a count per capability would report that as “hooks: 0”.

fields is read in the renderer’s own language. Claude Code’s and Codex’s are jq paths, because they hand a hook its payload on stdin; opencode’s and omp’s are property names, because a plugin receives the tool’s arguments as an object. An adapter says which by its render.

Advising and blocking are two templates, and either may be absent. A harness that can do one and not the other drops the hooks wanting the other by name — never substituting, in either direction.

events is required on a hooks binding, and these maps are refused on any other capability, where nothing would read them. omh enforces both.

Why there is no single syntax for fields, why advise and block stay apart, and what fields-by-tool is for

The fields map answers “where does this harness keep the file path”, and there is no single syntax for which that is true — so each render reads it in the language it emits.

Advising and blocking are one field apart on Claude Code and genuinely different mechanisms elsewhere: on opencode advisory text has no channel before a tool runs, and the only way to speak there is to throw, which blocks.

Without events nothing can be expressed: every hook is dropped, and the harness receives an empty settings document — indistinguishable from a harness that declares no hooks, except that this one claimed to have them.

fields-by-tool is the escape hatch fields cannot express. omp’s edit is the reason it exists: its path is not a property at all but a substring of one input string, one or more [PATH#TAG] sections. Codex’s edit is the same problem in another shape — its file is read out of the patch’s first *** Update File:, *** Add File: or *** Delete File: header. On both, a call touching several files shows a guard only the first.

Adapter breadth is capped until the base set is earned. Four harnesses ship: the cap moved from two to three for omp (#37, in 0.6.0), and codex came in as the fourth in 0.10.0 (#107), adopted so that “the second-most-used harness is in”.

Breadth before depth is how distributions die. Ten adapters that each half-work is a worse product than three that are verified, and it is a much worse product to maintain, because every one of them is a standing claim about software you do not control.

See the roadmap for when that cap lifts.

Why omp moved the cap, and the argument a further one has to beat

omp was not adopted for coverage. It was adopted because it is the second harness to express hooks as a program rather than as configuration, and that is a fact about the design omh could not learn from one example: opencode-plugin’s own doc called declarative hook config the norm and a plugin the exception, and it had exactly one data point. Two independent harnesses agreeing that hooks are code is what turns the hook vocabulary from a convenience into the only place a hook is written once.

A third adapter that had taught omh nothing would not have been worth its maintenance, whatever it cost to write. codex, the fourth, came in for coverage, and #107 says so.

  • Troubleshooting
    What each row of omh doctor means, and what to do when one fails.
  • Configuration
    The catalogue an adapter renders from, and what a repository takes from it.
  • Editors
    Open a session in VS Code, Zed, Cursor or Neovim.
  • Roadmap
    When the cap on harnesses lifts.