Your Claude Skill Is Invisible to Codex. Here's How to Fix It.
DEV Community

Your Claude Skill Is Invisible to Codex. Here's How to Fix It.

The Problem: One Skill, One Agent

You spent an afternoon writing a good skill. A SKILL.md, a couple of scripts, a crisp description so the model knows when to reach for it. Claude Code picks it up and uses it perfectly.

Now open Codex in the same repo. Nothing. The skill is invisible - wrong folder, wrong format, wrong dialect. Point a cheap local model at the task and you get the same blank stare. You wrote the capability once, and exactly one agent can see it.

The One Idea

Everyone converged on folders of markdown with zero interop. It's Ethereum before ERC-20 - the primitive is proven, and nothing can hold anyone else's. Everyone reads files. Nobody agrees on the field names.

Open any serious agent-driven repo in 2026 and you find the same sediment: a CLAUDE.md, an AGENTS.md, maybe a GEMINI.md saying mostly the same thing; a .claude/skills/ folder full of SKILL.md; Codex agents in TOML; a .mcp.json; somebody's OPERATOR.md. Every tool independently discovered the same primitive - agent behavior configured as files in the repo - and every tool speaks its own dialect of it.

This is a genuinely good primitive, and the convergence is evidence it's right. Files are diffable, reviewable, versionable; they ride along in git; agents can read and write them, which is what lets an agent improve its own tooling. Anthropic's harness team, describing their long-running app builder (March 2026), put it plainly: "communication was handled via files." Repository as constitution.

Here's why this shape won: each new agent session arrives like a new contributor with shell access but no knowledge of your team's norms, so a short AGENTS.md acts as the repo's constitution for agents. The pattern is settled. The interop is what's missing.

The Dialects Are the Whole Problem

They don't even agree on names: it's skill.title in one flavor, skill.name in another, three incompatible spins on AGENTS.md. A capability you wire into one product's config is invisible to the other four agents in your fleet - so you re-wire it, by hand, every time you add a brand.

That manual re-wiring, with your name on it, is the interop tax - and it's the part of "orchestration" nobody sells you a fix for.

Self-Diagnosis: Count Your Duplicated Declarations

Before we build one, do the self-diagnosis: count how many separate places you re-declare the same capability - the docs-search glue, the deploy runbook, the linter rule - once per agent that needs it.

  • 0 duplicated declarations - you're single-agent, not your problem yet.
  • 2–3 (a CLAUDE.md here, an AGENTS.md there) - you're paying the tax quietly, once per new brand.
  • 4+ - you are the interop layer, by hand, and it costs an afternoon per new agent.

The Vendors Already Proved the Fix - Then Locked It in Their House

Watch where the platform layer is heading, because it validates the whole idea. Anthropic's Managed Agents (April 2026) virtualizes the runtime: the session is an append-only log, the harness is disposable, the sandbox sits behind one generic execute interface. Stable contracts between layers, so each layer swaps freely. Design it like an OS.

The Managed Agents write-up is explicit about the principle: to build infrastructure that accommodates "programs as yet unthought of," you "decouple the brain from the hands via a uniform tool interface" and put stable interfaces over swappable implementations. That's exactly the right instinct - and it's hosted, and it's Claude-only.

The same layering, done in the open, is just as valuable one floor below the runtime: stable contracts for the files. What a tool promises, how a driver implements it, what a skill declares. Make those contracts public and versioned, and any host can load any capability - the way any wallet holds any ERC-20.

So Let's Actually Build One and Watch It Reach Three Different Agents

Step 1 - Author the Contract (What the Tool Promises)

The trick is to split what most formats fuse. A capability is two things: a contract (its name, its inputs and outputs, whether it's allowed to mutate anything) and an implementation (the code that runs). Fuse them and the capability is welded to one runtime. Separate them and the contract is the part that ports.

Here's a docs-search tool as a pure contract - agentproto's defineTool (the AIP-14 TOOL.md spec). Note what's not here: no execute body.

// tools/docs-search/TOOL.ts - the contract, and nothing else
import { defineTool } from "@agentproto/tool"
import { z } from "zod"

export const docsSearch = defineTool({
  id: "docs.search",
  description: "Search the team's internal docs. Returns titles + URLs.",
  inputSchema: z.object({
    query: z.string(),
    limit: z.number().default(5)
  }),
  outputSchema: z.object({
    hits: z.array(z.object({
      title: z.string(),
      url: z.string()
    })),
  }),
  mutates: [], // read-only β†’ no approval gate needed
  approval: "auto",
})

The contract carries identity, schemas, and a side-effect profile - mutates and approval are how a supervisor later decides whether this call needs a human (read-only tools sail through; a tool that writes files gets gated). One file, one declared promise, zero opinion about how it's fulfilled. This is the piece that makes a capability portable: a promise any runtime can read without running anyone's code.

Step 2 - Bind an Implementation (a DRIVER)

Now the code. A DRIVER (AIP-30's defineDriver) bundles one or more implementations, each bound to a tool contract by implementTool. The binding is type-checked against the contract's schemas.

// drivers/docs-meili.ts - one implementation of the contract above
import { defineDriver, implementTool } from "@agentproto/driver"
import { docsSearch } from "../tools/docs-search/TOOL.js"

export default defineDriver({
  id: "docs.search.meili",
  name: "Docs search via Meilisearch",
  kind: "http",
  implementations: [
    implementTool(docsSearch, async ({ input }) => {
      const res = await fetch(`${process.env.MEILI_URL}/indexes/docs/search`, {
        method: "POST",
        body: JSON.stringify({ q: input.query, limit: input.limit }),
      })
      const { hits } = await res.json()
      return { hits: hits.map((h) => ({ title: h.title, url: h.url })) }
    }),
  ],
  network: { egress: ["MEILI_URL"] }, // declared reach, not ambient
})

The codebase makes the analogy for you. The implementTool doc comment calls it "equivalent to Solidity's MyToken is IERC20 pattern" - the compiler enforces that the body's inputs and outputs match the contract's generics, no string-keyed guessing. The contract is the interface; the driver is the class that implements it. Swap Meilisearch for Postgres FTS later, keep the contract, and every agent that used docs.search keeps working.

Why the Split Earns Its Keep

A driver declares its own network.egress, its auth, its install steps - so the same docs.search contract can have a local implementation, a hosted one, and a mock for tests, and the resolver picks one per call. That's "stable interfaces over swappable implementations," but for your tools, in your repo, not behind a vendor's API.

Two files. Now the payoff.

Step 3 - Serve It to Every Agent, from One Place

Start the daemon in your repo. It reads your tools/ and drivers/ and exposes them over MCP:

npm i -g @agentproto/cli
agentproto install claude-code   # also installs the adapter package
agentproto serve                 # long-lived daemon, serves the /mcp gateway

Now spawn agents. Each adapter is pointed at the daemon's /mcp gateway as it starts, so all of them see the same served tools - including your docs.search:

# a frontier agent…
agentproto sessions start claude-code --cwd . \
  --prompt "Use docs.search to find our retry-policy page, then summarize it."

# …the exact same tool, a different vendor…
agentproto sessions start codex --cwd . \
  --prompt "Use docs.search to check whether we document the queue timeout."

# …and a cheap local model, no MCP config of its own
agentproto sessions start hermes --cwd . \
  --prompt "Use docs.search for 'deploy rollback' and list the top 3 hits."

One contract, one driver, three vendors - Claude Code, Codex, and a local model via Hermes all call docs.search with no per-agent wiring. The daemon is the single place capabilities live; agents mount it, they don't each re-declare it. (Hermes ships with zero MCP by default, so the daemon mounts the gateway for it - the cheap open model gets the same toolbox as the frontier one.)

This is the answer to the question the hub piece left open - when you give one agent a new capability, how many of the others get it? Here the answer is "all of them, for free," and that's the difference between managing agents and building with them.

Import Someone Else's MCP Once, Hand It to All of Them

The same "one place, every agent" trick works for tools you didn't write. MCP servers are the industry's one real interop win - but each agent brand configures them separately, so a server wired into Claude Code is still invisible to Codex.

agentproto imports an MCP server once into the daemon's curated set, and then every session can reach it. The flow is three real tool calls:

  1. mcp_discovered_list - find MCPs already configured in claude/cursor/etc.
  2. mcp_import - snapshot one into the daemon (survives the source config being deleted later)
  3. mcp_imported_tool_list β†’ search-then-call: list the imported server's tools…
  4. mcp_imported_call β†’ …and invoke one

The import captures a snapshot at import time, so the tool stays usable even if you later remove the original config. You wire an MCP server up once, in one daemon, and Claude Code, Codex, and your local model all inherit it - instead of copy-pasting a .mcp.json block into four different agent configs and keeping them in sync by hand. That's the whole interop tax, refunded: author-once for tools you write, import-once for tools you borrow.

Numbered Specs, Not Yet Another Framework

Here's the honest edge, because that's what makes the rest trust me. You could do all of this with a bespoke framework. The bet agentproto makes is not a framework - it's a registry of numbered specs, modeled deliberately on ERCs, BIPs, and PEPs. TOOL.md is AIP-14. DRIVER.md is AIP-30. Skills, operators, workflows, policies, sandbox definitions each get a number.

The reason to prefer numbers over a framework is projection. A contract that's a public spec, not a class in someone's SDK, can be adapted to any host:

  • Served over MCP by the daemon (what you just saw - this is live)
  • Projected in-process with toMastraTool(impl) for a Mastra agent
  • Or toAiSdkTool(impl) for the Vercel AI SDK
  • Authored by a human in TypeScript, or by an agent as a plain YAML TOOL.md (AIP-16 declares the IO as JSON Schema, no compiled module required)

Aligned-with, not forked-from. A Claude Code skill is already most of an AIP skill; the specs are meant to standardize the format people converged on, not replace it. And they're ours, plural: Apache-2.0, open to amendment by issue - the entire point of a numbered public registry is that no single vendor owns the meaning of docs.search.

The Honest Caveats, Stated Plainly

agentproto is 0.5.0-alpha. The daemon, the MCP surface, the driver doctype, and MCP import are live and hands-on today; the in-process Mastra / AI-SDK adapters and the wider ~52-spec family are earlier-stage - the repo publishes a live-vs-roadmap split precisely so you can check.

Betting on a young registry is a real cost. So is re-wiring the same tool by hand forever. Pick your poison with open eyes.

The Endgame: Agents That Write Their Own Tools

Once a capability is files with a contract, the loop closes in a way no config format allows. An agent can write a new TOOL.md, bind a driver, or scaffold a skill - and because those are file writes, they're diffs you can gate exactly like any other code.

This is where this piece hands off to the supervision ladder: stage the agent-authored tool behind a check and a human ack, and your fleet's capabilities grow the way your codebase grows - incrementally, reviewed, in git - instead of the way config sprawl grows.

The daemon even exposes the doctype verbs (create_tool, create_driver, list_driver, self_inspect) as MCP tools, so an agent authors its own next capability through the same interface it uses to call the last one. That's the actual endgame of files-with-contracts: not tidier config, but agents that safely extend themselves, under the same review discipline as a human PR.

What to Do Monday, Whatever You Run

You don't need my daemon to start paying down the tax:

  • Stop duplicating dialects. Pick one canonical instructions file; make the others thin pointers so they can't drift.
  • Separate contract from implementation in your internal tools, even informally. The day you add a second agent brand, the contract is the only part that ports - so it's worth naming today.
  • Treat agent-authored config as code: PR review, tests where you can, no direct writes to main. Files-as-behavior means files-as-attack-surface, and routing cheap execution through a judge only pays off if the capabilities those cheap agents wield are gated too.

If the interop problem is biting you, the AIPs are Apache-2.0 and open to amendment. The point of numbered public specs is that fixing them is your right, not a feature request in someone's queue.

Count your capability re-declarations one more time. Every number above one is a place a dialect can drift, an agent that's blind to a tool it should have, and an afternoon of your life spent being human glue. The pattern is settled; the plumbing is the only thing left to build - and it's a couple of files, not a platform migration.

We had folders of markdown and called it interoperability. It never was. The fix isn't another framework that everyone reinvents next year.

Comments

No comments yet. Start the discussion.