iso-harness
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.
Quickstart
npm install
node bin/iso-harness.mjs build --source examples/minimal/iso --out /tmp/iso-demo
Or once installed as a CLI:
iso-harness build
iso-harness build --target claude,cursor
iso-harness build --source path/to/iso --out path/to/project
iso-harness build --dry-run
iso-harness build --watch
Build modes
--dry-run validates and renders the full output plan, but prints what
would be written instead of touching disk.
--watch keeps a filesystem watcher on the source directory and reruns the
build after changes. Combine it with --target when you only care about one
harness while iterating.
Source format
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
mcp.json
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.
Agent frontmatter
---
name: researcher
description: Researches technical topics.
model: sonnet
tools: [Read, Grep, WebFetch]
targets:
cursor: skip
codex: skip
opencode:
temperature: 0.2
fallback_models: [foo, bar]
---
Agent prompt body goes here.
Command frontmatter
---
name: review
description: Review the current git diff.
args: "[scope]"
targets:
cursor: skip
---
Slash-command body goes here.
Targets
| 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) |
Escape hatches
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 under targets.<name> are mapped or passed
through where that target supports them. Use this for OpenCode
temperature / fallback_models, Claude Code allowed-tools, Pi
skill metadata, etc.
targets.<name>: skip omits the item from a specific target —
useful when a subagent only makes sense in harnesses that support
subagents.
iso/config.json with targets.<name>: { … } for top-level
harness config (not per-item). Keys under targets.opencode are
merged into the generated opencode.json — use this for OpenCode's
top-level instructions: [...] array, for example. Keys under
targets.pi are merged into .pi/settings.json.
iso/config.json top-level opencodeModelFallback — JSON object
written verbatim to .opencode/opencode-model-fallback.json for the
@agent-pattern-labs/opencode-model-fallback
plugin (retryable_error_patterns, global fallback_models, etc.).
OpenCode-only; other harnesses ignore it.
Root instruction addenda
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.md appends only to the shared AGENTS.md output used by Codex, OpenCode, and Pi.
instructions.claude.md appends only to CLAUDE.md.
instructions.cursor.md appends only to .cursor/rules/main.mdc.
instructions.opencode.md is emitted to .opencode/instructions.md and automatically added to opencode.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.
{
"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/.
Composition with @agent-pattern-labs/iso-route
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.model from the agent's frontmatter.
- Inline
model: from the agent's frontmatter.
roles[agent.role ?? agent.slug].model from 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.
Releasing
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-labs scope. Set at
https://github.com/Agent-Pattern-Labs/iso-harness/settings/secrets/actions.
- Npm scope
@agent-pattern-labs must exist and the token must have access.
Cutting a release:
npm version patch
git push && git push --tags
gh release create "v$(node -p 'require(\"./package.json\").version')" \
--generate-notes
The 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.json version matches the tag via
scripts/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.
Status
v0.1 — instructions, agents, commands, MCP. Hooks, permissions, and
per-harness-only features are out of scope for v1.