One config for every coding agent — Cursor, Claude Code, Codex, OpenCode, Pi.
Keep your instructions, subagents, commands, and MCP servers in a single
iso/ directory. iso-harness build transpiles that source to the
file layout each harness actually reads.
iso/ → CLAUDE.md (Claude Code)
├── instructions.md .claude/agents/*.md
├── instructions.agents.md* .claude/commands/*.md
├── instructions.claude.md* .mcp.json
├── instructions.cursor.md* → AGENTS.md (Codex + OpenCode + Pi)
├── instructions.opencode.md* .codex/config.toml
├── mcp.json .opencode/agents/*.md
│ .opencode/skills/*.md
│ .opencode/instructions.md* (OpenCode-only addendum)
│ .opencode/opencode-model-fallback.json (optional; from `opencodeModelFallback` in iso/config.json)
│ opencode.json
│ → .cursor/rules/*.mdc (Cursor)
│ .cursor/mcp.json
│ → .pi/skills/*/SKILL.md (Pi)
│ .pi/prompts/*.md
├── agents/ .mcp.json
│ └── researcher.md
└── commands/
└── review.md
Files marked with * are optional.
npm install
node bin/iso-harness.mjs build --source examples/minimal/iso --out /tmp/iso-demoOr once installed as a CLI:
iso-harness build # reads ./iso, writes to ./
iso-harness build --target claude,cursor # only two targets
iso-harness build --source path/to/iso --out path/to/project
iso-harness build --dry-run # print planned writes, no disk changes
iso-harness build --watch # rebuild on every change under iso/--dry-runvalidates and renders the full output plan, but prints what would be written instead of touching disk.--watchkeeps a filesystem watcher on the source directory and reruns the build after changes. Combine it with--targetwhen you only care about one harness while iterating.
iso/
├── instructions.md # root prompt → CLAUDE.md / AGENTS.md / .cursor/rules/main.mdc
├── instructions.agents.md # optional addendum for shared AGENTS.md targets
├── instructions.claude.md # optional Claude-only root addendum
├── instructions.cursor.md # optional Cursor-only root addendum
├── instructions.opencode.md # optional OpenCode-only addendum, loaded via opencode.json.instructions
├── config.json # optional — targets.* merges + opencodeModelFallback file emit
├── mcp.json # shared MCP server definitions
├── agents/ # subagents
│ └── <slug>.md # YAML frontmatter + body
└── commands/ # slash commands / skills
└── <slug>.md # YAML frontmatter + body
A harness-neutral schema. Each server has command, optional args, optional
env. The emitter translates to the shape each harness expects (e.g.
OpenCode wants type: "local" and command as an array).
{
"servers": {
"example": {
"command": "npx",
"args": ["-y", "@example/mcp"],
"env": { "EXAMPLE_MODE": "demo" }
}
}
}By design, mcp.json has no per-harness override mechanism. The
same MCP server should behave the same way no matter which harness
launches it — if it doesn't, that's an MCP/config issue to fix at the
server level, not something the shared config should paper over.
---
name: researcher
description: Researches technical topics.
model: sonnet
tools: [Read, Grep, WebFetch]
targets:
cursor: skip # don't emit for Cursor
codex: skip
opencode: # per-target overrides pass through verbatim
temperature: 0.2
fallback_models: [foo, bar]
---
Agent prompt body goes here.---
name: review
description: Review the current git diff.
args: "[scope]" # argument hint
targets:
cursor: skip
---
Slash-command body goes here.| Harness | Instructions | Agents | Commands | MCP |
|---|---|---|---|---|
| Claude Code | CLAUDE.md |
.claude/agents/*.md |
.claude/commands/*.md |
.mcp.json |
| Cursor | .cursor/rules/main.mdc |
.cursor/rules/agent-*.mdc |
(no native form) | .cursor/mcp.json |
| Codex | AGENTS.md |
(no native form) | (no native form) | .codex/config.toml |
| OpenCode | AGENTS.md |
.opencode/agents/*.md |
.opencode/skills/*.md |
opencode.json |
| Pi | AGENTS.md |
.pi/skills/*/SKILL.md |
.pi/prompts/*.md |
(extension/package only) |
The abstraction is only as good as its lowest common denominator. Four explicit hatches keep harness-specific features possible:
- Per-target frontmatter under
targets:(agents & commands). Harness-specific fields undertargets.<name>are mapped or passed through where that target supports them. Use this for OpenCodetemperature/fallback_models, Claude Codeallowed-tools, Pi skill metadata, etc. targets.<name>: skipomits the item from a specific target — useful when a subagent only makes sense in harnesses that support subagents.iso/config.jsonwithtargets.<name>: { … }for top-level harness config (not per-item). Keys undertargets.opencodeare merged into the generatedopencode.json— use this for OpenCode's top-levelinstructions: [...]array, for example. Keys undertargets.piare merged into.pi/settings.json.iso/config.jsontop-levelopencodeModelFallback— JSON object written verbatim to.opencode/opencode-model-fallback.jsonfor the@agent-pattern-labs/opencode-model-fallbackplugin (retryable_error_patterns, globalfallback_models, etc.). OpenCode-only; other harnesses ignore it.
instructions.md is still the shared base prompt. Optional sibling files let
you add harness-specific root guidance without forking the whole source:
instructions.agents.mdappends only to the sharedAGENTS.mdoutput used by Codex, OpenCode, and Pi.instructions.claude.mdappends only toCLAUDE.md.instructions.cursor.mdappends only to.cursor/rules/main.mdc.instructions.opencode.mdis emitted to.opencode/instructions.mdand automatically added toopencode.json.instructions.
This is especially useful when OpenCode needs extra orchestration guidance
that should not leak into the shared AGENTS.md file that Codex and Pi also read.
// iso/config.json
{
"targets": {
"opencode": {
"instructions": ["templates/states.yml"]
},
"pi": {
"prompts": ["prompts"],
"enableSkillCommands": true
}
},
"opencodeModelFallback": {
"cooldown_seconds": 60,
"retryable_error_patterns": ["(?i)venice.*insufficient"],
"fallback_models": ["openrouter/openai/gpt-oss-120b:free"]
}
}Per-agent OpenCode fallback_models still belong in agent frontmatter
under targets.opencode. Use opencodeModelFallback only for the
global plugin file OpenCode loads from .opencode/.
When @agent-pattern-labs/iso-route
writes its resolved role map to <out>/.claude/iso-route.resolved.json
(normally by running iso-route build into the same output directory
before iso-harness build), iso-harness picks it up automatically and
stamps model: onto each Claude subagent frontmatter.
Resolution order per subagent, highest to lowest:
- Per-target
targets.claude.modelfrom the agent's frontmatter. - Inline
model:from the agent's frontmatter. roles[agent.role ?? agent.slug].modelfrom the resolved map.- Nothing — the emitted frontmatter has no
model:field.
So an author can (a) hard-pin a model in the source file, (b) let iso-route drive it from policy, or (c) leave it to Claude Code's session default — without editing the agent body.
Non-Anthropic roles in the resolved map are skipped (Claude Code subagents can only run Anthropic models) and logged on stderr. Missing roles are silent — not every agent needs a role entry.
For Pi, iso-route owns .pi/settings.json model defaults. If
iso/config.json also contains targets.pi, iso-harness merges those
project settings into the existing file instead of replacing model
settings.
The contract is file-based on purpose: iso-harness and iso-route publish and version independently, so an on-disk JSON file is more robust than a TypeScript import across the two.
Releases are cut via a GitHub Release, which triggers
.github/workflows/release.yml to publish @agent-pattern-labs/iso-harness to npm
with provenance.
Prerequisites (one-time):
- Repo secret
NPM_TOKEN— an npm automation token with publish rights on the@agent-pattern-labsscope. Set athttps://github.com/Agent-Pattern-Labs/iso-harness/settings/secrets/actions. - Npm scope
@agent-pattern-labsmust exist and the token must have access.
Cutting a release:
# 1. Bump version, commit, push. CI (Quality checks) must pass on the
# pushed commit — the release workflow refuses to publish otherwise.
npm version patch # or minor/major — bumps package.json + tags
git push && git push --tags
# 2. Create the GitHub Release off the tag. This fires release.yml.
gh release create "v$(node -p 'require(\"./package.json\").version')" \
--generate-notesThe release workflow will:
- Wait for the Quality checks run on the release commit to complete (up to 30 min). Refuses to publish on red.
- Verify
package.jsonversion matches the tag viascripts/release/check-source.mjs. npm publish --provenance --access public.
If the publish step fails (e.g. token, 2FA, name conflict), fix the cause, delete the GitHub release + tag, and re-cut — do not amend.
v0.1 — instructions, agents, commands, MCP. Hooks, permissions, and per-harness-only features are out of scope for v1.