Harness engineering: todo lo que rodea al modelo
El modelo razona; el harness decide qué lee, qué puede tocar y qué se le impide romper. Esta página explica de qué piezas está hecho un harness y cómo este repo sostiene uno solo para tres herramientas distintas sin que se desincronicen.
Qué es un harness
Un modelo de lenguaje, solo, recibe texto y devuelve texto. Para que lea un repositorio, ejecute un comando o cree un issue en Jira necesita una capa que le entregue contexto, le dé herramientas y ponga límites. Esa capa es el harness. Claude Code, OpenCode y Codex son harnesses: programas que envuelven al modelo, arman lo que ve en cada turno y ejecutan lo que pide.
Harness engineering es el trabajo de diseñar y mantener esa capa para un proyecto concreto. En la práctica consiste en escribir archivos: instrucciones, skills, hooks, configuración de MCP y scripts de verificación. Con el mismo modelo, dos proyectos obtienen resultados muy distintos según lo que cada uno le pone alrededor.
Las seis piezas
Cada harness usa nombres propios, pero las piezas se repiten. Esta tabla las ordena por la pregunta que responde cada una.
| Pieza | Pregunta que responde | Cuándo actúa |
|---|---|---|
| Instrucciones | Qué reglas valen siempre en este repo? | Se cargan al abrir la sesión |
| Skills | Cómo se hace esta tarea concreta? | Bajo demanda, cuando la tarea coincide con su trigger |
| Hooks | Qué tiene que pasar sí o sí en cierto momento? | En eventos del harness (por ejemplo, antes de procesar cada prompt) |
| MCP | A qué sistemas externos puede llegar el agente? | Cuando el modelo llama a una tool del servidor |
| Memoria | Qué aprendimos en sesiones anteriores? | Entre sesiones; sobrevive al cierre de la ventana de contexto |
| Gates | Sigue todo lo anterior siendo coherente? | Antes de un commit, antes de un push y en CI |
Las cinco primeras le hablan al modelo. La sexta le habla al repositorio: los gates impiden que las otras piezas se contradigan con el paso de los commits. Un harness sin gates se degrada en silencio, porque nadie se entera de que una skill cita un archivo que ya no existe hasta que un agente lo busca y falla.
La diferencia entre instrucciones y hooks es de garantía. Una instrucción es un pedido que el modelo puede diluir en una sesión larga. Un hook es código que el harness ejecuta cada vez, lo recuerde el modelo o no. Si una regla tiene que cumplirse siempre, va en un hook o en un gate.
Una fuente, adaptadores finos
Este repo funciona en tres harnesses. Un proyecto puede declarar solo los que usa en
harnesses: (.agents/project.yaml): los gates revisan solo esos, y
el boilerplate siempre revisa los tres
(.context/ADR/ADR-0012-declared-harnesses.md,
.agents/instructions/agent-harnesses.md). La tentación obvia sería mantener
tres copias de todo (un CLAUDE.md con las reglas, otras en la configuración de
OpenCode, otras para Codex). Cada copia empezaría igual y a la tercera semana diría algo
distinto.
La alternativa es tener un solo cuerpo de instrucciones y skills, y dejar en cada host un
adaptador mínimo que solo resuelva lo que ese host hace distinto: el formato del archivo de
MCP y la API de hooks. AGENTS.md es un formato abierto que OpenCode y Codex
leen de forma nativa; Claude Code lee CLAUDE.md, así que en este repo ese
archivo contiene una sola línea, @AGENTS.md, que importa el original.
Ese cuerpo está en capas. AGENTS.md carga en cada sesión y guarda solo lo que
ata en cada turno, más un router; cada tema vive en su propio archivo bajo
.agents/instructions/ y se lee cuando el router, o una línea
ROUTE: del hook, lo nombra. Así ningún host paga en cada sesión lo que no usa,
y Codex, que corta las instrucciones del proyecto en su tope de bytes, recibe el archivo
entero. El mecanismo está en el deck
Progressive disclosure.
Gates contra el drift
Drift es la distancia que se abre entre dos cosas que deberían decir lo mismo: una skill y
el archivo que cita, .env.example y el manifiesto de variables, el
.mcp.json y su equivalente en Codex. Nadie lo introduce a propósito. Aparece
porque alguien edita un lado y se olvida del otro, y el agente sigue leyendo la versión
vieja con total confianza.
Un gate es un script que compara los dos lados y termina con código de salida distinto de cero si no coinciden. Lo que lo vuelve útil es dónde corre: en el hook de pre-commit, en el de pre-push y en CI. Así el drift se detecta en el commit que lo crea, cuando arreglarlo cuesta un minuto.
Generado contra versionado
Hay dos clases de archivo en un harness y conviene distinguirlas a simple vista. Los versionados son la fuente: se editan a mano y se revisan en un PR. Los generados son salida de un script: se regeneran, y editarlos a mano es trabajo perdido, porque la próxima regeneración lo pisa (o peor, un gate lo rechaza y no sabes por qué).
La regla práctica: antes de editar un archivo, pregunta de dónde sale. Si sale de un script, edita la entrada del script y vuelve a correrlo.
Así lo hace este repo
La sección 4.5, en .agents/instructions/agent-harnesses.md (el router de
AGENTS.md la nombra), describe el contrato completo. Este es el mapa por
superficie:
| Superficie | Claude Code | OpenCode | Codex |
|---|---|---|---|
| Instrucciones |
CLAUDE.md (generado, importa AGENTS.md), más las secciones
de .agents/instructions/ a pedido
|
AGENTS.md, más las secciones a pedido |
AGENTS.md, más las secciones a pedido |
| Skills | .claude/skills (alias generado) |
.agents/skills/ |
.agents/skills/ |
| Hook | .claude/settings.json |
.opencode/plugins/personality-reinject.js |
.codex/hooks.json |
| MCP | .mcp.json |
opencode.jsonc |
.codex/config.toml |
Un emisor, tres adaptadores
El texto del hook vive una sola vez, en .agents/hooks/personality-reinject.mjs.
En cada prompt agrega una línea AGENT IDENTITY: (worktree, sesión y harness,
que luego terminan en los trailers de cada commit), avisa si falta el .env y
clasifica el pedido contra el router de AGENTS.md: por cada sección que el
pedido necesita y la sesión todavía no leyó, suma una línea
ROUTE: read <archivo>, con un tope de pocas por pedido: el resto va en
una sola línea ROUTE-OPTIONAL: que no obliga. Un aviso de tarea del harness (un
prompt que empieza con <task-notification>) no se rutea. Claude Code y
Codex lo ejecutan como hook de comando en el evento UserPromptSubmit, y un
SessionStart con matcher compact o clear re-arma las
rutas después de compactar o de un /clear. En Claude Code, un
PostToolUse corre el mismo archivo y, si el agente usa una herramienta sin
haber leído lo ruteado, le recuerda una sola vez qué falta (ROUTE-PENDING:).
OpenCode no tiene hooks de comando, así que su plugin importa las mismas funciones y las
empuja al prompt de sistema; en OpenCode 2, que no le pasa el texto del pedido, queda solo
el router.
Hay un segundo hook, aparte y mucho más chico: .agents/hooks/doc-contracts.mjs.
Claude Code y Codex lo corren después de cada edición de archivo (evento
PostToolUse). Si la edición cae dentro de una región marcada con
LINT.IfChange, suma una línea DOCS: con las páginas que describen
ese código, una vez por sesión. Es el aviso; el gate que lo hace cumplir corre en el
pre-push y en CI (ver la tabla de abajo). OpenCode no lo registra y depende solo del gate.
Vale únicamente en el repo del boilerplate: en un proyecto queda en silencio (ADR-0016).
Una skill, un modo
No hay archivos de comando. Una skill se invoca por su nombre más un modo: en Claude Code,
/project-context api o /test-automation explain <target>; en
OpenCode y Codex, nombrando la skill y el modo en prosa. El primer token de
$ARGUMENTS que coincide con un modo es el modo, y el resto se reenvía; si
ninguno coincide, la skill pregunta.
Un comando del proyecto con el mismo nombre que una skill la taparía, así que
agents:compat:check lo rechaza y bun run agents:compat lo mueve a
.backups/shadowing-commands/, donde se puede recuperar.
La cadena de repo:check
bun run repo:check encadena los gates del repo; la cadena exacta y su orden son
los del script repo:check en package.json (regla 11 de
AGENTS.md: se lee ahí, nunca de una copia).
Los que cuidan el harness en sí son estos:
| Script | Archivo | Qué detecta |
|---|---|---|
skills:check |
scripts/lint-skills.ts |
Frontmatter roto, categorías fuera del vocabulario, tiers incoherentes con
cli/install.ts y rutas citadas en una skill que ya no existen
|
skills:registry:check |
scripts/build-skill-registry.ts |
Que .agents/skills/REGISTRY.md (las reglas compactas que se copian en
cada briefing de subagente) esté al día con las skills
|
agents:compat:check |
scripts/agent-compatibility.ts |
El shim de CLAUDE.md, el alias de skills, los comandos del proyecto que
tapan una skill, los adaptadores de hooks y la paridad de servidores MCP entre los
formatos de los harnesses en uso
|
vars:check / vars:env:check |
scripts/lint-vars.ts / scripts/check-vars.ts |
Variables de proyecto mal usadas, y que .env.example y
cli/lib/variables-manifest.ts coincidan
|
instructions:check |
scripts/lint-instructions.ts |
El cuerpo de instrucciones: presupuesto de AGENTS.md, el ROUTER
bloqueado detrás de un ADR, el eval del router con pisos de recall y precisión en
cada corrida y que cada sección nueva llegue completa
|
vars:schema:check |
scripts/env-schema.ts |
Que .env.core.schema esté al día con el manifiesto y que toda clave con
nombre de secreto (TOKEN, SECRET, PASSWORD, API_KEY...) esté marcada
@sensitive
|
docs:check |
scripts/lint-docs.ts |
Links relativos rotos (también los links markdown de los .md de
.agents/, .context/ y .claude/), rutas
citadas en docs/ que no existen y páginas sin
<title> o descripción. Corre en cada push y en CI
|
| contratos de docs | scripts/lint-doc-contracts.ts |
Que un cambio dentro de una región LINT.IfChange llegue con todas las
páginas que nombra su LINT.ThenChange, o con un commit que diga
Docs-Checked: <label> <motivo>. Avisa en el commit, bloquea
en el push y en CI. Solo en el repo del boilerplate (ADR-0016)
|
Aparte de los gates, bun run instructions:audit mide en las transcripciones
locales de Claude Code cuántas de las secciones que el hook ruteó leyó de verdad el agente
(solo nombres y conteos, nunca contenido).
No todos corren en todos lados. Qué corre en cada hook está en
.husky/framework-gates.sh: el pre-push corre un subconjunto amplio, el
pre-commit corre unos pocos siempre y el resto solo cuando el commit toca los archivos que
vigilan. repo:check es la pasada completa, la que conviene correr antes de
abrir un PR.
Editar un archivo generado. CLAUDE.md y
.agents/skills/REGISTRY.md son salida de scripts. Escribir reglas en
CLAUDE.md rompe agents:compat:check. Edita la fuente y corre
bun run agents:compat o bun run skills:registry.
Confiar en que el MCP avisa si falta una variable. Si una variable de
.env no está definida, el loader de .env arranca el servidor sin
ella en los tres harnesses, y el servidor muere más tarde, en su primera llamada
autenticada. Ninguno falla al arrancar. Un 401 o una tool que falla sin explicación casi
siempre es eso: corrige el .env y reinicia la sesión, porque las variables se
leen cuando el harness levanta los servidores.
Exportar una variable del proyecto en tu shell. El harness se abre tal
cual (claude, opencode, codex o su app de
escritorio) y cada servidor MCP lee .env por su cuenta. Pero una variable que
exportaste tú en el perfil de tu shell gana sobre el archivo en todo lo que arranques
desde esa terminal, y un valor viejo ahí tapa el .env corregido sin avisar.
bun run vars:env:check te dice cuál (solo el nombre y el largo): quítala del
perfil o abre una terminal limpia.
Para profundizar
- Effective harnesses for long-running agents (Anthropic Engineering): cómo un harness sostiene el trabajo de un agente a lo largo de muchas ventanas de contexto.
- AGENTS.md: el formato abierto de instrucciones que leen OpenCode, Codex y otros agentes.
-
Claude Code: memoria y CLAUDE.md: cómo carga instrucciones Claude Code, incluidos los imports
@path. -
Claude Code: hooks reference: eventos como
UserPromptSubmity el campoadditionalContext. -
Codex: Customization:
AGENTS.md, skills, MCP y memorias desde el lado de Codex. - Model Context Protocol: especificación: el protocolo que usan los tres hosts para hablar con servidores externos.