Ayer escribí un commit message con Claude Code. El diff era un cambio de una línea: un typo en un comentario. Claude Opus leyó el diff, pensó durante dos segundos, y generó fix: correct typo in auth comment. Para eso consumió unos 800 tokens de entrada y 30 de salida, a $15 y $75 por millón respectivamente. Coste: una fracción de centavo. Pero multiplica eso por 40 commits al día, 250 días al año, en una empresa con 200 desarrolladores usando coding agents, y la fracción de centavo se convierte en miles de dólares gastados en la tarea intelectual equivalente a poner tiritas.
El problema no es que Opus sea caro. El problema es que los coding agents no distinguen entre tareas de $0.001 y tareas de $0.10. Todo pasa por el mismo modelo. Generar un commit message, clasificar una issue, validar un formato — todo va al modelo grande al mismo coste que diseñar una arquitectura de microservicios. Es el equivalente de contratar a un cirujano para poner tiritas.
Los números
Vamos a hacer las cuentas con los precios de Claude Opus 4 (la generación anterior, la que la mayoría sigue usando en producción):
| Tarea | Tokens entrada | Tokens salida | Coste |
|---|---|---|---|
| Commit message (diff pequeño) | ~800 | ~30 | $0.014 |
| Clasificar una issue | ~500 | ~50 | $0.011 |
| Validar formato de commit | ~300 | ~20 | $0.006 |
| Standup summary | ~2000 | ~200 | $0.045 |
Ninguna de estas tareas necesita un modelo con 2 trillones de parámetros y capacidad de razonamiento multi-paso. Son tareas de clasificación y generación con restricciones fuertes. El equivalente de ordenar cartas por color.
Con el modelo on-device de Apple Intelligence (3B parámetros, incluido en macOS 26): coste $0.00, latencia ~300ms, sin red, sin API key.
foundation-hooks
foundation-hooks es un conjunto de 4 binarios Swift que usan el framework Foundation Models de Apple para automatizar tareas de desarrollo que no justifican un modelo cloud:
| Binario | Función | Hook git |
|---|---|---|
fm-commit-msg |
Genera commit messages convencionales a partir del diff | prepare-commit-msg |
fm-validate-msg |
Valida formato y sugiere correcciones | commit-msg |
fm-lql-create |
Clasifica y crea issues en Linear via lql | CLI |
fm-lql-standup |
Genera standup summary desde git log + issues | CLI |
Los cuatro comparten el mismo patrón: definir un struct Swift con @Generable, alimentar el modelo con contexto mínimo, obtener salida estructurada en milisegundos.
Instalación:
git clone https://github.com/frr/foundation-hooks
cd foundation-hooks
make build && make install-hooks REPO=/path/to/your/repo
A partir de ese momento, cada git commit genera automáticamente un mensaje convencional. El hook está instalado en 11 repositorios de producción desde hace dos semanas.
Cómo funciona: @Generable y constrained decoding
Esta es la parte que merece atención técnica. @Generable no es «pedirle al modelo que devuelva JSON y cruzar los dedos». Es constrained decoding — el modelo literalmente no puede generar tokens que violen el schema.
El mecanismo
@Generablees un macro Swift que genera un JSON Schema en tiempo de compilación a partir del struct.- El framework inyecta ese schema en el prompt como especificación de formato de respuesta.
- Durante la inferencia, en cada paso de decodificación, se aplica token masking: los tokens del vocabulario que producirían una salida inválida según el schema se enmascaran (probabilidad 0 en el softmax).
- El modelo solo puede elegir entre tokens válidos.
Apple describe esto como «guided generation» en la documentación de WWDC25. Es la misma técnica que OpenAI usa con response_format: json_schema y que Anthropic aplica en tool use. La diferencia: Apple lo integra en el sistema de tipos de Swift. Defines el struct, el compilador genera el schema, el runtime lo aplica en inferencia. Type safety end-to-end.
Los tres niveles de restricción
@Generable
struct CommitMessage {
// Nivel 1: restricción DURA — enum efectivo
// Token masking activo: solo "fix", "feat", "refactor", etc.
// Los tokens que formarían "bug" o "update" tienen probabilidad 0.
@Guide(.anyOf(["fix", "feat", "refactor", "test", "docs", "chore", "style"]))
var type: String
// Nivel 2: restricción SUAVE — como un system prompt para este campo
// El modelo tiende a seguirlo pero no está forzado.
@Guide(description: "Scope of the change, e.g. auth, ui, db. One word, lowercase.")
var scope: String
// Nivel 3: sin restricción — string libre, el modelo decide
var subject: String
}
La analogía: anyOf es un dropdown, description es un input con placeholder, y un campo sin Guide es un textarea vacío. La diferencia entre los tres no es de grado sino de mecanismo. El primero opera a nivel de tokens (el modelo no puede salirse), el segundo opera a nivel de prompt (el modelo tiende a seguirlo), el tercero no tiene guía.
Esto es relevante porque el caso de uso de los hooks es exactamente el escenario donde la restricción dura brilla. Un commit type tiene que ser uno de 7 valores. No hay ambigüedad, no hay creatividad, no hay razonamiento. Es clasificación pura. Un modelo de 3B parámetros con constrained decoding lo hace tan bien como uno de 200B. La diferencia es que uno tarda 300ms y es gratis, y el otro tarda 2 segundos y cuesta dinero.
El código completo de un hook
Este es fm-commit-msg, el hook de prepare-commit-msg. Son 106 líneas de Swift, sin dependencias externas:
import Foundation
import FoundationModels
@Generable
struct CommitMessage {
@Guide(description: "Type of change")
@Guide(.anyOf(["fix", "feat", "refactor", "test", "docs", "chore", "style"]))
var type: String
@Guide(description: "Scope of the change, e.g. auth, ui, db, api. One word, lowercase.")
var scope: String
@Guide(description: "Imperative summary of the change, max 50 chars, lowercase, no period")
var subject: String
}
guard SystemLanguageModel.default.isAvailable else {
exit(0) // No Apple Intelligence — exit silently, user writes their own
}
Tres cosas que destacar:
-
Graceful degradation: si Apple Intelligence no está disponible (Mac sin Apple Silicon, modelo no descargado), el hook sale con código 0 y git continúa normalmente. Nunca bloquea.
-
No inventa: el modelo recibe
git diff --cached --staty un patch truncado a 3000 caracteres. Suficiente para clasificar y resumir, insuficiente para confabular. -
No reemplaza al humano: el mensaje se escribe al fichero de commit con comentarios git (
#), así quegit commitlo muestra en el editor. El usuario puede modificarlo o descartarlo.
La generación:
let session = LanguageModelSession(instructions: """
You generate git commit messages in conventional commits format.
Focus on WHY the change was made, not WHAT changed.
The subject must be imperative mood, lowercase, no period, max 50 chars.
""")
let result = try await session.respond(to: prompt, generating: CommitMessage.self)
let msg = result.content
let message = "\(msg.type)(\(msg.scope)): \(msg.subject)"
session.respond(to:generating:) devuelve una instancia de CommitMessage, no un String. No hay parsing. No hay regex. No hay try? JSONDecoder().decode(...). El struct es el contrato y el compilador lo garantiza.
Integración con issue tracking: fm-lql-create
El mismo patrón funciona para issue tracking. fm-lql-create clasifica una descripción en lenguaje natural y crea una issue en Linear via lql, un CLI de Linear escrito en Rust:
@Generable
struct IssueClassification {
@Guide(.anyOf(["bug", "feature", "improvement", "task", "chore"]))
var type: String
@Guide(.anyOf(["urgent", "high", "medium", "low", "none"]))
var priority: String
@Guide(description: "Clean, professional issue title. Max 80 chars.")
var title: String
@Guide(description: "One-line description for the issue body")
var description: String
}
Uso:
$ fm-lql-create "auth token refresh crashes when expired"
PROD | high | bug | TOK: Auth: token refresh crashes on expiry
Token refresh fails silently when the OAuth token has expired, causing auth loop.
Press Enter to create, Ctrl-C to cancel:
El modelo local clasifica la issue en ~500ms: tipo bug, prioridad high, título limpio, descripción de una línea. Luego lql create la crea en Linear. El --dry-run flag muestra la propuesta sin ejecutar nada.
Dos campos con anyOf (type, priority) garantizan que la clasificación es válida. No puede devolver «priority: very important» ni «type: bugfix». Los tokens están enmascarados. Dos campos con description (title, description) dan libertad controlada al modelo.
Antes y después
| Paso | Con coding agent (Opus) | Con foundation-hooks |
|---|---|---|
| Generar commit message | ~2s, ~800 tokens, ~$0.014 | ~300ms, 0 tokens, $0.00 |
| Validar formato | ~1.5s, ~300 tokens, ~$0.006 | ~200ms, 0 tokens, $0.00 |
| Clasificar issue | ~2s, ~500 tokens, ~$0.011 | ~500ms, 0 tokens, $0.00 |
| Generar standup | ~3s, ~2000 tokens, ~$0.045 | ~800ms, 0 tokens, $0.00 |
| Requiere red | Si | No |
| Requiere API key | Si | No |
| Funciona en avion | No | Si |
Los tiempos del modelo local son mediciones reales en un MacBook Pro M4 Pro. No son benchmarks sintéticos.
Lo que no puede hacer
El modelo on-device de Apple es un modelo de 3B parámetros con una ventana de contexto de 4096 tokens. Tiene límites claros:
-
Diffs largos: por encima de ~3000 caracteres de patch, el contexto se trunca. Para refactors masivos que tocan 20 ficheros, el modelo solo ve el resumen estadístico (
--stat), no el patch completo. El commit message será genérico pero correcto en formato. -
Decisiones arquitectónicas: «Should I use a protocol or a concrete type here?» es una pregunta que necesita contexto de proyecto, historia del codebase, y razonamiento multi-paso. Eso sigue siendo territorio de modelos grandes.
-
Generación de código: foundation-hooks no genera código. Genera metadata sobre código: commit messages, clasificaciones, resúmenes. La frontera es clara: si la tarea es «escribir» algo que un humano revisará, usa el modelo grande. Si la tarea es «etiquetar» algo que un humano ya escribió, usa el modelo local.
-
Solo macOS 26+ con Apple Silicon: no funciona en Linux, no funciona en Macs Intel. Para equipos heterogéneos, el hook sale silenciosamente y el usuario escribe su propio mensaje.
Instalación
# Prerequisitos: macOS 26, Xcode 26, Apple Intelligence activado
git clone https://github.com/frr/foundation-hooks
cd foundation-hooks
make build
# Instalar hooks en un repo especifico
make install-hooks REPO=/path/to/your/repo
# Instalar binarios CLI en ~/.local/bin
make install-lql
# Instalar hooks en todos los repos conocidos (editar Makefile para ajustar la lista)
make install-all
El Makefile copia los binarios compilados directamente a .git/hooks/. No hay runtime, no hay daemon, no hay configuración. Si el binario está en el hook, funciona. Si no quieres AI en un commit, git commit --no-verify.
La tesis
Los coding agents son herramientas extraordinarias para tareas que requieren razonamiento complejo. Pero el modelo de precios actual no distingue entre complejidad. Cada interacción con el modelo — desde diseñar una arquitectura hasta escribir «fix: typo» — pasa por el mismo pipeline, al mismo coste, con la misma latencia.
La solución no es dejar de usar coding agents. Es dejar de usarlos para todo. Las tareas de clasificación, validación y generación con restricciones fuertes son resolubles con un modelo de 3B parámetros corriendo en local. El hardware ya está en tu máquina. El framework ya está en el sistema operativo. Solo falta el código que los conecte.
foundation-hooks son 400 líneas de Swift que conectan esos puntos. make install-hooks REPO=. y cada commit genera su propio mensaje, cada issue se clasifica sola, cada standup se escribe en 800ms. Sin red, sin tokens, sin coste.
El cirujano puede dejar de poner tiritas.
Read this article in English.



