Twelve agents. One map.
Where each harness keeps its config, sessions, instructions, skills and hooks, per platform, with an evidence level on every path. Detect which one you're running inside. Run a prompt through another one without tools, read-only, or as a full agent. Keep every MCP list and AGENTS.md in sync from one file. Library, CLI, MCP server, Pi and OMP extensions.
- 12
- harnesses
- 161
- mapped paths
- 10
- headless CLIs
- 9
- agent tools
Paths
Every path, with its evidence
getHarness("codex") is one object with config, sessions, instructions, skills, commands and hooks. Nothing is fetched or scanned; it's a table someone checked against the CLI's own docs, source, or a local probe, and wrote the level down. inferred means exactly that. This panel walks through 12 harnesses, values expanded the way the library expands them.
- Each entry carries scope (user, project, system, data) and level (official, community, inferred)
- resolve({ platform, homeDir, projectRoot }) expands ~, ${HOME} and %VAR% and drops paths for other platforms
- A note where the path needs one: dash-encoded cwd, XDG on every platform, deprecated location
import { getHarness } from "@agntn/harnesses";
// Anthropic Claude Code, binary claude
const harness = getHarness("claude");
const paths = harness.resolve({ platform: "linux", homeDir: "/home/dev" });
paths.config[0]?.path; // "/home/dev/.claude/settings.json"
paths.sessions[0]?.path; // "/home/dev/.claude/projects/<dash-encoded-cwd>/*.jsonl"
paths.skills[0]?.path; // ".claude/skills/"
harness.detection.envVars; // ["CLAUDE_CODE","CLAUDECODE"]Invoke
Six modes, none of them a fallback
invoke(prompt, options) expands one argument template per mode and spawns the binary. A harness whose CLI cannot switch tools off rejects advisor mode instead of pretending. The command line below is what would run for the current harness, and the second block is the error you get for a mode it doesn't have.
- tools defaults to false: an advisor with tools removed by a native flag, not by prompt wording
- readOnly needs a sandbox the CLI enforces itself. No recipe, no run, and an error that says which retry would work
- timeoutMs and signal stop the whole process group; stopped results keep their output
invocationModesclaude
4 of 6
advisor
{}
advisor · json
{ structured: true }
read-only
{ readOnly: true }
read-only · json
{ readOnly: true, structured: true }
agent
{ tools: true }
agent · json
{ tools: true, structured: true }
advisor · spawned
claude -p 'Review this patch' --tools ''read-only · rejected
Harness claude has no read-only full agent invocationMCP servers
One list, every dialect
~/.config/agntn/mcp.jsonc is the only place a server is declared. ~ and ${HOME} expand at sync time because harnesses spawn MCP servers without a shell, and a tilde in a config is a server that silently never starts. TOML files get a surgical edit that keeps their comments. JSON files are rewritten.
- listMcpServers reads JSON and TOML, standard, OpenCode, VS Code and Antigravity shapes, into one McpServerConfig
- sync makes each user scope config exactly the master list; extras are removed, not merged around
- excludes keeps a harness's own servers, but names on the master list are withdrawn from it
master~/.config/agntn/mcp.jsonc
9 targets
{
"excludes": ["codex"],
"mcpServers": {
"harnesses": {
"command": "npx",
"args": ["-y", "@agntn/harnesses", "mcp"]
}
}
}- ~/.gemini/config/mcp_config.jsonjson · antigravity
- ~/.claude.jsonjson · standard
- ~/.codex/config.tomltoml · standard
- ~/.cursor/mcp.jsonjson · standard
- ~/.agents/mcp.jsonjson · standard
- ~/.gemini/settings.jsonjson · standard
- ~/.grok/config.tomltoml · standard
- ~/.omp/agent/mcp.jsonjson · standard
- ~/.config/opencode/opencode.jsonjson · opencode
Harnesses
Twelve harnesses, three platforms
Each page lists the binaries, capabilities, invocation templates, every path with its scope and level, the MCP config dialect and what the harness reads from other harnesses' folders. Cursor and Copilot scan .claude/skills/, Grok loads CLAUDE.md. That's written down too, because it decides where your file has to live.
- Linux, macOS and Windows: Linux, macOS, Windows paths tagged where they differ
- Detection by environment variable first, then by project markers; two matches is null, not a guess
- Audio and video mean a verified native route into model context, not a conversion or an MCP tool
Agents
Nine tools, three hosts
harnesses mcp serves the tools over stdio, the Pi and OMP extensions render the same executors in the terminal. So Claude can ask Codex for a second opinion on a patch, read-only, and get the answer back as one tool result. That's what the tools are for; the table of paths alone wouldn't need a server.
- harnesses_detect, harnesses_info, harnesses_models, harnesses_run, and five for MCP lists and AGENTS.md
- harnesses_run makes the model choose tools explicitly; unsupported modes never widen access
- The host's own request signal cancels a run, not a JSON argument the model could forget
toolharnesses_info
MCP · Pi · OMP
input
{
"id": "claude"
}output
{
"id": "claude",
"name": "Anthropic Claude Code",
"binaries": ["claude"],
"capabilities": ["mcp","vision","tools","streaming"],
"invocationModes": ["advisor","advisorStructured","agent","agentStructured"],
"modelListing": false,
"agentsFile": "~/.claude/CLAUDE.md",
"paths": { config, sessions, instructions, skills, commands, hooks }
}Your harness
Extend Harness, call registerHarness
Every built-in is a concrete class extending the exported abstract Harness. Yours is the same shape, one file. Put level: "inferred" on a path you haven't checked and change it when you have. The type won't let you skip the field, which is the point.
- id, name, binaries, the six path groups, capabilities, detection and invocation. Same fields the built-ins fill
- registerHarness(Class) makes it visible to getHarness, detectHarness and the CLI in your process
- invocation: null is honest for a CLI without a headless mode. invoke() then throws before spawning
import { Harness, registerHarness, type HarnessId } from "@agntn/harnesses";
class Aider extends Harness {
readonly id = "aider" as HarnessId;
readonly name = "Aider";
readonly binaries = ["aider"];
readonly capabilities = {
mcp: false, vision: true, audio: false,
video: false, tools: true, streaming: true,
};
readonly config = [
{ path: "~/.aider.conf.yml", scope: "user", level: "official" },
];
readonly sessions = [
{ path: ".aider.chat.history.md", scope: "project", level: "official" },
];
readonly persistence = [{ format: "YAML", level: "official" }];
readonly instructions = [];
readonly skills = [];
readonly commands = [];
readonly hooks = [];
readonly detection = { envVars: [], projectMarkers: [".aider.conf.yml"] };
readonly invocation = {
args: ["--message", "{prompt}", "--yes-always"],
modelArgs: ["--model", "{model}"],
level: "inferred",
};
}
registerHarness(Aider);Start with one command
Pre-1.0, so pin exact versions. Paths follow upstream CLIs, and upstream CLIs move. When one does, the fix is a line in a table and a contract test, and I'd rather get the issue.