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
Registry and paths
TSclaude.ts
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"]
Anthropic Claude Code · 18 paths

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
Running another harness

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 invocation

MCP 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
Syncing MCP servers

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
Claude Code: JSON rewritten under mcpServers

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
All harnesses

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
MCP, Pi and OMP

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
Custom harnesses
TSaider.ts
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.

@agntn/harnesses·MIT license· Paths follow the upstream CLIs and carry their evidence level. This site reads nothing from your machine.