# Objective Guard en Claude Code

Cómo correr Objective Guard dentro de Claude Code: qué hooks instalar, cómo probar que se dispararon y qué no observa la integración. Para la puesta en marcha inicial y tu primera ronda, leé antes [../getting-started.es.md](../getting-started.es.md). Versión en inglés: [claude-code.md](claude-code.es.md).

## Qué aporta esta integración

- **Un gate de preflight pre-edición.** Un hook `PreToolUse` sobre `Edit|Write` ejecuta `src/pre-edit-gate.mjs`. Si el repositorio tiene contrato tipado pero no una autorización vigente, el hook escribe `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny", ...}}` y sale con código 2, así que Claude Code rechaza la llamada a la herramienta. Esto prueba que el agente leyó el contrato antes de mutar; no prueba nada sobre si la edición es correcta.
- **Detectores en tiempo de edición.** Un hook `PostToolUse` sobre `Edit|Write` ejecuta `src/claude-hook.mjs`, que resuelve el archivo editado a una ruta relativa a la raíz Git y corre `oguard check --file <ruta> --json` sobre él. El conjunto de detectores son sólo cuatro checks: `weak-test`, `stub-prod`, `new-dep` (únicamente sobre `package.json`) y sintaxis JS (únicamente `.js`, `.mjs`, `.cjs`). El silencio significa que esos cuatro no encontraron nada en ese archivo, no que el cambio sea sólido.
- **Inyección de prompt de la fase actual.** `scripts/claude-awareness-oguard-prompt.sh` en `UserPromptSubmit` sube desde el directorio de trabajo buscando `oguard.contract.json` y le dice al agente en qué estado está el repositorio — contrato placeholder, contrato activo o sin contrato — y el próximo comando a correr (`intent`, `start --from-intent` o `task-flow`). Para un contrato activo también indica no reintentar una denegación de preflight: hay que correr `task-flow` desde la raíz del contrato y continuar sólo tras obtener `ready`. Es un consejo inyectado como contexto; no fuerza nada.
- **Evidencia de ronda.** `oguard align` mide la superficie modificada contra el scope y los key results declarados; `oguard round-end` corre los checks declarados, evalúa los criterios de aceptación y conserva un registro append-only. Un `pass` significa que los checks en alcance no encontraron violación.
- **Telemetría sanitizada.** Cada decisión pre-edición agrega un evento al inventario global: harness, hash del repositorio, nombre base del repositorio, `allow`/`deny`, un código de razón y una duración. Sin paths, sin prompt, sin contenido de archivos.

## Requisitos

- Node 20 o superior (`package.json#engines` declara `"node": ">=20"`), y `node` resoluble en el `PATH` — los comandos del hook invocan `node` directamente, no a través de un gestor de paquetes.
- Un repositorio Git. Todo comando resuelve su raíz con `git rev-parse --show-toplevel`; fuera de un repositorio la CLI falla y los hooks se saltean con `not-a-git-project`.
- Una versión de Claude Code que soporte hooks de comando `PreToolUse`, `PostToolUse` y `UserPromptSubmit` y respete `permissionDecision: "deny"` dentro de `hookSpecificOutput`. El código lee sólo `input.cwd` e `input.tool_input.file_path` (el gate también acepta `tool_input.file`) y no fija ninguna versión de harness.

## Instalación

Instalá el paquete público y usá la CLI publicada desde PATH:

```bash
npm install -g @fyso/objective-guard
oguard-hooks install --harness claude
```

El enlace de npm se verifica ejecutándolo en una regresión de CLI. Para desarrollo podés usar `node src/cli.mjs <cmd>`. El instalador reemplaza sus hooks de prefijos anteriores, preserva integraciones independientes y acepta `--home PATH` para probar una configuración aislada.

Awareness es opcional: el helper compartido consulta `awareness status` una sola vez, desde el directorio actual y con timeout de 1,5 segundos. Respeta el routing por worktree/sesión del CLI, omite focos divergentes y no exige mantenimiento por cada respuesta. Un error de Awareness no suprime el preflight del guard. Si ya existe un hook de prompt de Awareness, el instalador configura `OGUARD_AWARENESS_CONTEXT=off` para evitar inyección duplicada. El JSON se serializa de forma segura.

Las instrucciones de preflight, alineación y cierre siguen vigentes: esto no relaja la autorización de ediciones.

La configuración de hooks es **a nivel usuario**, en `~/.claude/settings.json`. No es a nivel proyecto: una sola instalación protege todo repositorio que contenga un `oguard.contract.json`. Las rutas en `command` deben ser absolutas — los hooks corren con el directorio de trabajo del harness, no con el del checkout.

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "OGUARD_HARNESS=claude node /absolute/path/to/objective-guard/src/pre-edit-gate.mjs",
            "timeout": 10,
            "statusMessage": "Validating Objective Guard task flow"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "OGUARD_HOOK_EXECUTE=1 node /absolute/path/to/objective-guard/src/claude-hook.mjs",
            "timeout": 10,
            "async": true
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash /absolute/path/to/objective-guard/scripts/claude-awareness-oguard-prompt.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
```

`OGUARD_HARNESS=claude` es lo que etiqueta la telemetría; sin eso el evento pre-edición registra `tool: "unknown"`. `OGUARD_HOOK_EXECUTE=1` fuerza a `src/claude-hook.mjs` a ejecutar su `main()` cuando el harness lo invoca a través de un wrapper de shell. `async: true` en el hook post-edición significa que los findings llegan como contexto posterior y nunca bloquean la edición que los produjo; quitalo si querés que el detector termine antes de que el turno continúe.

Después, por repositorio:

```bash
oguard init   # crea solamente oguard.contract.json
```

`oguard init` no instala hooks ni toca la configuración del harness — escribe un contrato placeholder con `objective.id = "O-UNSPECIFIED"` y se niega a sobreescribir uno existente (`test/init-cli.test.mjs`). La instalación del hook es el paso manual de arriba.

## Verificar la integración

Corré esto desde la raíz de un repositorio con contrato tipado.

```bash
# 1. Preflight. Imprime status ready o action-required; action-required sale con código 1.
node src/cli.mjs task-flow --json

# 2. Pasale al gate el payload que envía Claude Code, con telemetría aislada.
export OGUARD_INVENTORY_DIR=/tmp/oguard-verify
printf '{"cwd":"%s","tool_name":"Edit","tool_input":{"file_path":"%s/README.md"}}' "$PWD" "$PWD" \
  | OGUARD_HARNESS=claude node src/pre-edit-gate.mjs
echo "exit=$?"

# 3. Leé la decisión que quedó registrada.
tail -1 "$OGUARD_INVENTORY_DIR/events.jsonl"
```

Un repositorio autorizado no produce stdout y devuelve `exit=0`; el evento agregado lleva `"preflight_decision":"allow","preflight_reason":"fresh-preflight"`. Uno no autorizado imprime el JSON con `permissionDecision: deny`, sale con código 2 y registra `"preflight_decision":"deny","preflight_reason":"missing-or-expired-preflight"`. Ambas transiciones están aseveradas en `test/pre-edit-gate.test.mjs`.

Para ver disparar el detector post-edición sin depender del estado de tu propio repositorio, corré los tests que ejercitan el mismo camino de código contra un fixture descartable:

```bash
node --test test/pre-edit-gate.test.mjs test/hooks-e2e.test.mjs test/claude-hook.test.mjs
```

`test/hooks-e2e.test.mjs` siembra un `test.skip`, llama a `runClaudeHook` desde un directorio anidado y asevera tanto el finding `weak-test` como el evento sanitizado del inventario. `test/claude-hook.test.mjs` asevera que el hook nunca registra contenido de archivos. Si el hook real parece no hacer nada durante una sesión, poné `OGUARD_HOOK_DEBUG=1` en el comando del hook: el hook devuelve entonces la razón del salteo (`missing-file`, `missing-contract`, `not-a-git-project`, `file-outside-project`) en lugar de quedarse callado.

## El loop en la práctica

```bash
oguard init
oguard intent --objective-id O-1 --objective "Protect checkout" --task-type bugfix --scope "src/**,test/**"
oguard intent-scan
oguard start --from-intent --round-id R-1 --round "Add regression" --kr-id KR-1 --kr "Checkout has a regression test"
git add oguard.contract.json && git commit -m "open round R-1"
oguard task-flow --json
# ... Edit/Write, cada llamada frenada por PreToolUse e inspeccionada por PostToolUse
oguard check --file src/checkout.mjs
oguard align --json
oguard round-end --mode final
```

`init` escribe el contrato placeholder una vez por repositorio. `intent` registra un borrador local y revisable de objetivo, tipo de tarea y scope candidato en `.oguard/intent.json`; todavía no hay nada congelado. `intent-scan` resuelve sólo archivos observables por Git que coinciden con esos globs y lista candidatos de test — evidencia estructural, nunca prueba semántica, así que corregí la intención y volvé a escanear si la superficie está mal. `start --from-intent` copia el borrador revisado al contrato congelado y ancla el baseline; después de esto, cambiar de dirección exige una ronda sucesora, no otro `intent`. Commiteá `oguard.contract.json` para que el contrato congelado quede versionado con el trabajo. `task-flow --json` debe devolver `ready` antes de la primera edición material; es lo que concede la autorización que busca el gate. Cada `Edit`/`Write` queda entonces frenado e inspeccionado automáticamente. `check --file` corre los mismos cuatro detectores a demanda. `align` compara la superficie modificada con el scope y los key results declarados. `round-end --mode final` corre los checks declarados y bloquea si un criterio requerido sigue en `unknown`; `--mode fast` conserva esa incertidumbre en lugar de bloquear. Para el próximo trabajo, abrí una sucesora con `oguard next-round ...`, que exige árbol limpio y archiva el contrato y el baseline anteriores.

## Qué no puede ver el harness

- El gate sólo inspecciona payloads estructurados de herramientas. Lee `tool_input.file_path` (o `tool_input.file`) de las llamadas `Edit`/`Write`. Una escritura hecha dentro de un comando `Bash` — `sed -i`, `tee`, una redirección de shell, `git apply`, un script que reescribe archivos — no queda frenada ni clasificada, porque el matcher nunca se dispara y no se expone ninguna ruta de archivo.
- Un repositorio sin `oguard.contract.json` nunca queda bloqueado. El gate devuelve `allow` con razón `no-contract`, y un archivo resuelto fuera de la raíz del contrato devuelve `allow` con razón `file-outside-contract` (`test/pre-edit-gate.test.mjs`). La ausencia de una denegación no es evidencia de gobernanza.
- La autorización `ready` es cacheada, no continua. Se guarda en `.oguard/state.json` contra el hash del contrato y el hash de la política de KPIs versionada, y expira una hora después de otorgada. Editar el contrato u `oguard.kpis.json` la invalida de inmediato y la próxima edición queda denegada hasta que corras `task-flow` otra vez.
- El detector post-edición es informativo. Con `async: true` no puede detener la edición que inspecciona; los findings aparecen después. Su alcance son cuatro checks sobre un solo archivo, y `new-dep` sólo mira `package.json`.
- La inyección de prompt no es enforcement. `UserPromptSubmit` agrega contexto que el modelo puede ignorar. Sólo el gate `PreToolUse` produce una denegación.
- La telemetría prueba uso, no beneficio. El inventario registra que hubo una decisión, no que evitó un desvío; `oguard inventory dashboard --json` reporta `causal_effect: not-evaluable` justamente por eso.

## Resolución de problemas

| Síntoma | Causa probable | Comando que muestra la verdad |
| --- | --- | --- |
| Toda `Edit` queda denegada | No hay autorización vigente, o el contrato o la política de KPIs cambió desde la última | `node src/cli.mjs task-flow --json` |
| La denegación menciona una raíz de contrato inesperada | Un directorio padre tiene `oguard.contract.json`; el gate sube desde el archivo y desde el cwd | `printf '{"cwd":"%s","tool_input":{"file_path":"%s/file.mjs"}}' "$PWD" "$PWD" \| node src/pre-edit-gate.mjs` |
| `task-flow` devuelve `action-required` | Falta contrato tipado, hay mismatch de baseline, un finding de gobernanza o una violación bloqueante de la política de KPIs — el array `actions` dice cuál | `node src/cli.mjs task-flow --json` |
| Nunca se deniega nada | No hay contrato en ese repositorio, o el matcher de `PreToolUse` no cubre la herramienta | `ls oguard.contract.json` y revisar `hooks.PreToolUse` en `~/.claude/settings.json` |
| No aparecen findings tras una edición obviamente mala | El hook se salteó, o el cambio está fuera de los cuatro detectores | reejecutar el comando del hook con `OGUARD_HOOK_DEBUG=1`, luego `node src/cli.mjs check --file <ruta> --json` |
| El archivo de inventario queda vacío | `OGUARD_INVENTORY_DIR` apunta a otro lugar, o el hook nunca corrió | `tail -3 ~/.oguard/inventory/events.jsonl` y `node src/cli.mjs inventory status --json` |
| `round-end` bloquea por criterios `unknown` | La ronda no declaró un check reproducible para un criterio requerido | `node src/cli.mjs round-end --mode fast --json` |
