Multi-harness · cómo se comporta el repo
El repo corre en Claude Code, OpenCode y Codex (CLI y Desktop). Cada instrucción y cada skill existe una sola vez. Donde los tres harnesses difieren de verdad (formato de MCP, API de hooks) cada uno tiene un adaptador fino, versionado o generado, y un check lo compara contra la fuente.
OpenCode y Codex leen AGENTS.md y .agents/skills/ de forma
nativa, así que ahí no hay adaptador. Las secciones de
.agents/instructions/ no las carga ningún host por su cuenta: el router de
AGENTS.md dice cuándo leer cada una, igual en los tres. Claude Code necesita
dos superficies generadas para llegar al mismo contenido. El hook y el MCP tienen un
archivo por host porque cada uno habla un formato distinto.
| Superficie | Claude Code | OpenCode | Codex CLI + Desktop |
|---|---|---|---|
| Instrucciones |
CLAUDE.md → @AGENTS.md
generado
|
AGENTS.md nativo |
AGENTS.md nativo |
| Secciones de instrucciones | .agents/instructions/ a pedido, por el router de L0 |
.agents/instructions/ a pedido |
.agents/instructions/ a pedido |
| Skills | .claude/skills alias generado |
.agents/skills/ nativo |
.agents/skills/ nativo |
| Comandos | ninguno: /<skill> <modo> |
ninguno: skill y modo en prosa | ninguno: skill y modo en prosa |
| Hook | .claude/settings.json → UserPromptSubmit |
.opencode/plugins/personality-reinject.js |
.codex/hooks.json → UserPromptSubmit |
| MCP | .mcp.json |
opencode.jsonc |
.codex/config.toml |
AGENTS.md es el núcleo que carga siempre: la frase que
ata de cada regla crítica, la capa comportamental, el núcleo de orquestación y un router
fijo. El resto vive en una sección por tema, que el agente lee cuando el router o una
línea ROUTE: del hook la nombra. Para Claude Code, el router además importa
package.json y .agents/project.yaml; OpenCode y Codex no
expanden imports y siguen la instrucción reforzada de la misma fila. Cómo funciona, en
detalle:
Las instrucciones, en capas.
harnesses: de
.agents/project.yaml dice cuáles usa el proyecto, y cada gate
(agents:compat:check, setup:doctor, el instalador,
bun sync) revisa solo esos. Sin la lista, o en null, se
detectan por los archivos presentes; una lista explícita manda. El boilerplate mismo
revisa siempre los tres. Decisión:
.context/ADR/ADR-0013-declared-harnesses.md.
Se edita la fuente y se regenera la salida con bun run agents:compat. Es la
Critical Rule #15: escribir prosa operativa en CLAUDE.md o tocar el alias es
drift estructural, y el check lo marca.
AGENTS.md, .agents/instructions/,
.agents/skills/, .agents/hooks/
bun run agents:compat
CLAUDE.md: shim de una línea, commiteado. .claude/skills:
symlink POSIX o junction de Windows, ignorado por git.
CLAUDE.md es un import documentado y no un symlink porque
un archivo de una línea (@AGENTS.md más un salto de línea) sobrevive a un
checkout en Windows. Nunca lleva prosa propia: todo lo que diga va en
AGENTS.md o en su sección de .agents/instructions/.
No hay archivos de comando ni capa de alias: el boilerplate no trae
.claude/commands/ ni .opencode/commands/. La skill es la única
forma de invocación en los tres hosts, y una skill con varios modos los lista en su propia
sección ## Mode routing.
| Host | Cómo se pide |
|---|---|
| Claude Code |
/project-context data, el slash de la skill a través de
.claude/skills
|
| OpenCode | "cargá project-context, modo data" |
| Codex | "cargá project-context, modo data" |
El primer token de los argumentos que coincide con un modo es el modo, y el resto se reenvía. Si ninguno coincide, la skill lo deduce de la frase o pregunta.
bun run agents:compat:check falla con ese caso y
bun run agents:compat mueve el archivo a
.backups/shadowing-commands/, recuperable y nunca borrado. La decisión está
en .context/ADR/ADR-0006-skill-plus-mode-invocation.md.
.agents/hooks/personality-reinject.mjs tiene el texto que se reinyecta en
cada prompt. Existe en ese archivo y en ningún otro; cada host llega a él por un
adaptador. Emite, en este orden:
AGENTS.md §2, para que la voz no se
diluya a medida que se llena la ventana de contexto.
AGENT IDENTITY: con el worktree, la etiqueta de
sesión y el harness. Es metadato forense: de ahí salen los trailers de cada commit.
ORCA:, solo cuando el binario orca
es alcanzable. Una máquina sin Orca nunca oye hablar de él.
.env en el checkout, o
es un worktree enlazado que nunca corrió bun run worktree:provision.
ROUTE: read <archivo>: las secciones de instrucciones que el prompt necesita y la sesión todavía no leyó,
clasificadas con el router de AGENTS.md y los triggers: de
cada sección. Un SessionStart con compact o
clear las re-arma en Claude Code y Codex; OpenCode 1 rutea desde
chat.message y OpenCode 2 se queda con el router.
AGENT IDENTITY: worktree=<nombre|primary> session=<etiqueta> harness=<claude-code|codex|opencode>
.claude/settings.json
UserPromptSubmit corre
node "$CLAUDE_PROJECT_DIR/.agents/hooks/personality-reinject.mjs" con un
timeout corto. Es el único host que además acepta un título de sesión propuesto por el
hook.
.opencode/plugins/personality-reinject.js
Un plugin fino importa agentContextLines y empuja las mismas líneas al
system prompt, en el lugar y sin duplicar. Un solo export por defecto
{ id, setup, server } carga en las dos generaciones de plugins de
OpenCode.
OpenCode no le pasa un nombre de sesión al plugin, solo el id, así que la etiqueta queda como el id crudo.
.codex/hooks.json
UserPromptSubmit resuelve la raíz con
git rev-parse --show-toplevel y corre el mismo emisor, con una variante
commandWindows en PowerShell.
Solo carga en un repositorio que Codex marcó como confiable.
Hay un segundo hook, separado de este: .agents/hooks/doc-contracts.mjs.
Claude Code y Codex lo corren después de cada edición de archivo
(PostToolUse). Cuando la edición cae dentro de una región marcada con
LINT.IfChange, agrega una línea DOCS: con las páginas que
describen ese código, una vez por sesión. El que bloquea es el gate de pre-push y de CI;
el hook solo adelanta el aviso al momento de editar. OpenCode no lo registra. Vale solo en
el repo del boilerplate (ADR-0017).
Todo commit que escribe una sesión de agente termina con dos líneas copiadas de la línea
AGENT IDENTITY:, en cualquier host. Dicen qué checkout y qué sesión
produjeron el cambio, para encontrar la transcripción correcta en un bisect o en un
post-mortem. No nombran ninguna herramienta.
docs: rewrite the harnesses page as a behaviour page Worktree: <nombre|primary> Session: <etiqueta>
Worktree: y después Session:, al final y sin nada debajo. Si
un valor no se pudo resolver se escribe unknown, nunca se adivina ni se
omite la clave.
Claude-Session: o cualquier trailer con marca de un harness, un
Co-Authored-By: de una IA y cualquier línea "Generated with". En Claude
Code, .claude/settings.json deja attribution en blanco y
includeCoAuthoredBy en false.
.husky/commit-msg llama a .husky/framework-gates.sh, que
corre scripts/check-commit-trailers.ts. Avisa y nunca bloquea: un commit
humano, un merge o un arreglo de emergencia siempre pasan.
La regla es la Critical Rule #3 de AGENTS.md; la decisión está en
.context/ADR/ADR-0004-harness-agnostic-commit-trailers.md.
El conjunto canónico de servidores es el que declara .mcp.json (sin Claude,
el archivo del primer harness declarado). Cada uno tiene que existir también en la
configuración de los otros harnesses en uso (opencode.jsonc,
.codex/config.toml), con las mismas dependencias de .env y los
mismos valores literales. Los tres archivos declaran solo servidores locales.
| Host | Archivo | Cómo referencia una credencial | Si la credencial falta |
|---|---|---|---|
| Claude Code | .mcp.json |
el cargador de .env, con --filter = sus variables |
el servidor recibe la variable vacía o sin definir y muere en su primera llamada autenticada |
| OpenCode | opencode.jsonc |
el mismo cargador, el mismo filtro | igual: falla en la primera llamada autenticada |
| Codex | .codex/config.toml |
el mismo cargador, el mismo filtro, más startup_timeout_sec |
igual en un servidor stdio; solo un servidor remoto sin su
bearer_token_env_var es error duro que nombra el servidor
|
.env y reiniciar la sesión (Critical Rule
#9). El entorno se lee cuando el harness lanza los servidores y no se refresca a mitad
de sesión.
bun run agents:compat:check normaliza JSON, JSONC y TOML a una forma común
(transporte, comando, argumentos, URL, variables de entorno, habilitado) y compara eso; un
servidor detrás del cargador se lee como su propio comando, con su
--filter como dependencias de .env. Un servidor presente en un
solo host falla nombrando el servidor y el host. Los servidores que trae el boilerplate
(KNOWN_MCP_IDS en cli/lib/agent-compatibility-contracts.ts)
pasan además un chequeo de forma estricto por host; cualquier otro, solo el genérico. Un
proyecto puede sacar o agregar servidores sin pelear con el check.
library-docs, web-search,
db…) y el agente la resuelve por el sufijo del nombre de la tool, sea cual
sea el prefijo del servidor. Si ninguna tool la provee, el agente para y dice qué
capacidad falta y cómo habilitarla. Detalle en
.agents/skills/agentic-dev-core/references/mcp-capabilities.md y
.context/ADR/ADR-0005-harness-level-mcps-and-capabilities.md.
Un harness lanza sus servidores MCP antes de que corra cualquier hook, así que las credenciales tienen que estar disponibles en el momento del arranque. Las lee cada servidor por su cuenta, con el mismo cargador en los tres hosts, se haya abierto la sesión como sea.
.env
Cada servidor stdio que necesita valores arranca con
bunx -p varlock@<pin> varlock run --no-redact-stdout --inject vars --filter
<sus variables> -- <comando real>. Lee .env (o el gestor de secretos que nombra el schema) al arrancar y
le pasa al servidor solo las variables de su filtro.
La app de escritorio o un worker supervisado lanzado de forma nativa no tienen comando
que envolver ni entorno de proceso, y el cargador lee .env igual. No se
copia ningún valor a otro archivo.
claude · opencode · codex
El binario pelado, sin script envoltorio: nada carga .env en el proceso
del agente, así que ningún secreto llega al shell de la IA. Un comando que necesita un
valor lo corre por el cargador en un subproceso,
bunx varlock run -- sh -c '<comando>'. Si un servidor se comporta
como si .env no existiera, bun run vars:env:check nombra la
variable que el shell exporta con otro valor (bajo varlock gana el heredado).
bun run harness:env| Copia vieja | Qué hace |
|---|---|
igual a .env |
la borra: .env ya tiene el valor |
distinta de .env, o una variable que .env no declara
|
la mueve a .auth/harness-env-backup/<VAR> (modo
0600) y la nombra: pasás el valor correcto a .env y
después borrás ese directorio
|
| leída todavía por una config vieja | la deja hasta que esa config pase al cargador |
.env
Reiniciar la sesión. En OpenCode también su servicio de fondo (opencode service restart), porque cachea la config resuelta por directorio. Las copias en texto plano que
escribía una instalación vieja (el bloque env de
.claude/settings.local.json, .auth/opencode/<VAR>) ya no
las lee nadie; bun run harness:env:check sale con 1 mientras quede
una.
| Comando | Qué hace |
|---|---|
bun run agents:compat |
Regenera el shim y el alias y mueve aparte los comandos que tapan una skill.
También lo corren bun sync y bun setup.
|
bun run agents:compat:check |
El gate del contrato: bytes del shim, destino del alias, ningún comando con nombre
de skill, adaptadores de hook, paridad MCP y que
eslint.config.js conecte cada bloque que exporta
eslint.config.base.js. Es parte de bun run repo:check.
|
bun run setup:doctor |
Diagnóstico por superficie (instrucciones, alias, comandos, hooks, MCP, lint), la línea de estado del alias, las copias en texto plano de credenciales MCP que quedan, de dónde salen los secretos y la confianza de Codex como aviso. |
bun setup |
Detecta los tres harnesses y pregunta cuáles configurar (INSTALL_AGENTS=claude-code,opencode,codex
para un run sin preguntas), guarda la elección en harnesses: y ofrece
borrar los archivos de los que quedaron afuera (por defecto los conserva). Retira
las copias en texto plano de una instalación vieja y, después de escribir
.env, avisa que hay que reiniciar la sesión.
|
.codex/ solo
cargan en un repositorio confiable. Es estado de runtime y ningún archivo lo prueba, por
eso el doctor lo muestra como aviso.
engram setup <agente> registra
el servidor MCP de Engram en los tres hosts, y las reglas de memoria no hacen nada donde
ese servidor falta.