Hooks para el desarrollo en Apple: patrones que salvan el proyecto
Una sesión de Claude Code apuntada a un proyecto de iOS tiene un alcance que un proyecto Python genérico o web no tiene. El agente puede ejecutar xcodebuild y xcrun mediante su herramienta Bash. Puede leer y editar archivos .pbxproj (un property list ASCII de estilo antiguo por defecto, a veces XML o JSON tras la conversión por herramientas, e igualmente fatal si se corrompe en cualquiera de esos formatos). Tiene en sus manos las identidades de firma del desarrollador por el simple hecho de ejecutarse en su máquina. Puede borrar un simulador. Puede recompilar un proyecto con el esquema equivocado. Puede hacer commit y push. El protocolo no controla nada de esto: el sistema de archivos del desarrollador es el sistema de archivos del agente, y la flag --dangerously-skip-permissions de Claude Code está a una sola pulsación de teclas de la automatización completa.
La mitigación no es “confía en el agente”. La mitigación son los hooks: scripts de shell deterministas que el host ejecuta en los límites del ciclo de vida (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop).1 Los hooks frenan al agente ante entradas peligrosas, validan salidas destructivas y condicionan la finalización a una build verde. Son la primitiva de seguridad portante que todo desarrollador de iOS que use un agente debería configurar antes de que el agente ejecute cualquier cosa.
Cuatro patrones de hooks se ganan su lugar en proyectos iOS. Son a nivel de framework, no específicos del proyecto; las apps del clúster (Return, Get Bananas, Reps, Water, Ace Citizenship) ejecutan variantes de estos. Cada patrón nombra un modo de fallo real, un script concreto y el evento del ciclo de vida que acota el radio de impacto.
TL;DR
- Cuatro patrones de hooks que importan en iOS: validación de
.pbxproj(PostToolUse, devuelve los errores al agente), control de bash peligroso (PreToolUse, bloquea antes de ejecutar), gate de build verde en Stop, higiene del estado del simulador (Stop). - Los códigos de salida de los hooks importan, y se comportan distinto según el evento.
exit 2bloquea la acción propuesta enPreToolUse(la herramienta nunca se ejecuta); enPostToolUseno puede bloquear (la herramienta ya se ejecutó), pero hace que el stderr llegue al agente para que pueda reparar o revertir; enStopimpide que el agente concluya la sesión.exit 0permite.exit 1generalmente registra pero no bloquea.1 - Los scripts de hooks viven en
.claude/hooks/*.shdentro del repo, referenciados por ruta relativa desde.claude/settings.json. Aplica revisión de código. - La autoridad del agente es la autoridad del desarrollador. Los hooks son la forma en que el desarrollador recorta esa autoridad para devolverla a un conjunto deliberado de acciones aprobadas.
Patrón uno: validación de .pbxproj en cada edición
El archivo de proyecto de Xcode es el único archivo que un agente muta con regularidad y que tiene la mayor proporción de radio-de-impacto-por-línea. Un corchete mal puesto en project.pbxproj rompe silenciosamente la build para todos los desarrolladores del equipo. El error de build aparece en la siguiente invocación de xcodebuild, no en el momento de la edición, así que el agente normalmente afirma que el cambio funcionó antes de que aflore el daño.
El hook ejecuta plutil -lint contra cualquier escritura en .pbxproj. PostToolUse no puede bloquear la escritura en sí (el archivo ya está en disco cuando el hook se dispara), pero exit 2 hace llegar el error de validación al agente de inmediato como un fallo de llamada de herramienta: el agente lee el fallo, sabe que el archivo está roto y puede revertir o reparar antes de que la sesión continúe:
#!/bin/bash
# .claude/hooks/post-write-pbxproj.sh
# Runs after every Edit or Write tool call. Exits 2 to surface the
# validation failure to the agent so it can revert/repair the broken
# .pbxproj before the session moves on. (PostToolUse cannot prevent
# the write itself; it can only feed the error back.)
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" != *.pbxproj ]]; then
exit 0
fi
if ! plutil -lint "$FILE" >/dev/null 2>&1; then
echo "ERROR: $FILE failed plutil -lint after write" >&2
echo "The Xcode project file is structurally broken. Revert and try again." >&2
exit 2
fi
exit 0
plutil -lint detecta rupturas estructurales: llaves o paréntesis desbalanceados, punto y comas faltantes, tokens inválidos del plist, anidamiento XML roto.2 No detecta errores semánticos de Xcode como un UUID malformado que resulta ser texto plist sintácticamente válido, o una fase de build que referencia un archivo inexistente. Esos producen errores de build ordinarios que el agente puede depurar normalmente. El gate de plutil atrapa la clase de fallos catastróficos de parseo; los errores semánticos pasan a la build misma.
La configuración del hook en .claude/settings.json (nota las comillas en $CLAUDE_PROJECT_DIR para rutas con espacios):
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-write-pbxproj.sh"
}]
}]
}
}
El matcher dispara el hook solo en llamadas a las herramientas Write y Edit; la primera acción del script es cortocircuitar en rutas que no sean .pbxproj. El costo de ejecutarse en cada Edit es despreciable porque el filtro de ruta es la primera comprobación.
Patrón dos: controlar comandos bash destructivos antes de que se ejecuten
xcrun simctl erase borra los datos de un simulador. xcodebuild archive invoca firma y puede producir artefactos firmados que el desarrollador no pretendía. git push --force reescribe la historia. El agente tiene acceso a todos ellos a través de su herramienta Bash. Un hook PreToolUse sobre Bash empareja el patrón del comando propuesto y decide si proceder.
La forma:
#!/bin/bash
# .claude/hooks/pre-bash-xcode.sh
# Runs before every Bash tool call. Exits 2 (blocking) on irreversible
# Xcode/signing/git operations the developer hasn't explicitly approved.
# PreToolUse hooks CAN block: exit 2 prevents the tool from running.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
case "$COMMAND" in
*"simctl erase"*)
echo "ERROR: simulator erase requires explicit human approval" >&2
echo "Tell the developer what you wanted to erase and why; let them run it." >&2
exit 2
;;
*"xcodebuild archive"*|*"xcodebuild -exportArchive"*)
echo "ERROR: xcodebuild archive/export invokes signing; requires human approval" >&2
exit 2
;;
*"git push --force"*|*"git push -f"*)
echo "ERROR: force-push rewrites history; requires human approval" >&2
exit 2
;;
*"rm -rf"*)
echo "ERROR: rm -rf requires explicit approval" >&2
exit 2
;;
esac
exit 0
El hook es un switch sobre subcadenas de patrones de comandos. La clase bloqueante es la clase de acciones irreversibles: borrar, firmar, force-push, borrado recursivo. Las operaciones reversibles (builds normales, tests, commits de git sin force) pasan.
Un refinamiento común es permitir algunos comandos cuando el desarrollador ha optado explícitamente vía una variable de entorno o flag en la conversación. Por ejemplo: xcodebuild archive podría permitirse si CLAUDE_ALLOW_ARCHIVE=1 está en el entorno, configurada por el desarrollador antes de la sesión para una tarea específica de archivado. El hook lee la variable y omite el bloqueo:
*"xcodebuild archive"*)
if [[ "${CLAUDE_ALLOW_ARCHIVE:-0}" == "1" ]]; then
exit 0
fi
echo "ERROR: xcodebuild archive requires CLAUDE_ALLOW_ARCHIVE=1" >&2
exit 2
;;
El patrón: deny-por-defecto sobre la clase irreversible, válvula de escape opt-in para los casos que el desarrollador quiere que el agente maneje.
Patrón tres: hook Stop que condiciona la finalización a una build verde
Al agente le gusta declarar una tarea hecha cuando la conversación parece resuelta. Sin un gate, “hecho” puede significar “edité los archivos y el chat está en un estado coherente” en lugar de “la build aún compila”. El hook Stop es el lugar para imponer el significado correcto.
#!/bin/bash
# .claude/hooks/stop-build-check.sh
# Runs when the agent tries to stop. Exits 2 if the build is broken,
# which prevents the session from concluding until the agent fixes it.
cd "$CLAUDE_PROJECT_DIR" || exit 0
# Hard-code project, scheme, and destination per repo. Do not rely on
# auto-discovery: workspaces, multiple projects, or shared-vs-user
# schemes all break naive heuristics.
PROJECT="MyApp.xcodeproj"
SCHEME="MyApp"
DESTINATION="platform=iOS Simulator,name=iPhone 17 Pro"
LOG=/tmp/claude-stop-build.log
if ! xcodebuild -project "$PROJECT" -scheme "$SCHEME" \
-configuration Debug \
-destination "$DESTINATION" \
build > "$LOG" 2>&1; then
echo "ERROR: build failed; cannot stop with a broken build" >&2
echo "See $LOG for the full xcodebuild output." >&2
tail -50 "$LOG" >&2
exit 2
fi
exit 0
Codificar duro PROJECT, SCHEME y DESTINATION es la forma adecuada para hooks comprometidos al repo: los valores nunca se desvían, los workspaces y los repos multi-proyecto funcionan sin ajustes por máquina, y un sistema de build CI que use el mismo hook puede intercambiar el destino vía variable de entorno. La autodetección (ls *.xcodeproj, xcodebuild -list | awk) funciona para el caso solo más simple, pero falla en proyectos con raíz .xcworkspace, en repos con múltiples archivos .xcodeproj y en la distinción entre esquemas compartidos y de usuario. La cadena de destino sigue la sintaxis documentada de xcodebuild platform=...,name=...;3 tiene que ser un simulador que la máquina del desarrollador realmente tenga, de lo contrario el hook falla por razones de entorno y no de código.
Dos decisiones de producto que toma el hook:
Stop bloquea la señal de “estoy listo” del agente, no la del humano. El desarrollador siempre puede hacer Ctrl+C, cerrar la terminal o anular. El hook es una baranda contra el optimismo del agente, no un cierre forzado para el humano.
El hook ejecuta una build real, no una verificación de sintaxis. swift build contra un proyecto específico de iOS omite los pasos de compilación específicos de iOS; solo xcodebuild demuestra que el target de iOS compila. El costo es el tiempo de build en sí (10-60 segundos en la mayoría de proyectos); el valor es atrapar siempre el caso de build-rota-marcada-como-hecha.
Patrón cuatro: higiene del estado del simulador
Tras una sesión larga del agente, los simuladores pueden acumularse: simuladores arrancados que el agente olvidó apagar, instalaciones viejas de apps que cachean estado obsoleto, datos de runtime que sobreviven entre sesiones y producen bugs irreproducibles. Un hook Stop puede limpiar.
#!/bin/bash
# .claude/hooks/stop-simulator-cleanup.sh
# Soft cleanup: shuts down booted simulators we don't need anymore.
# Does NOT erase data; does NOT block. Logs only.
BOOTED=$(xcrun simctl list devices booted 2>/dev/null | grep -E "Booted" | wc -l | xargs)
if (( BOOTED > 0 )); then
echo "[hook] $BOOTED booted simulators at session end; consider shutdown" >&2
# Uncomment to auto-shutdown:
# xcrun simctl shutdown all 2>/dev/null
fi
exit 0
La forma es no bloqueante por diseño: el hook reporta el estado pero no actúa a menos que el desarrollador descomente la línea de apagado. La razón es que la siguiente sesión del agente podría querer el simulador arrancado preservado (arranque en frío más rápido cuando el simulador ya está corriendo). La decisión es por desarrollador: si los simuladores se acumulan a través de múltiples sesiones y el costo es real, descomenta el apagado; de lo contrario, déjalo como una señal de log.
Una variante más agresiva borra simuladores entre sesiones, pero eso cruza al territorio de operación destructiva del Patrón dos. El borrado pertenece al bloqueo en PreToolUse, no a la automatización en Stop.
Lo que los hooks no resuelven
Los cuatro patrones anteriores son el conjunto de trabajo, no el panorama completo. Tres clases de fallo que los hooks no pueden atrapar:
Bugs de lógica en el código del agente. Un hook valida estructura, no semántica. El agente puede escribir una clase @Model que compila, pasa el lint del archivo de proyecto, hace build verde y aun así es semánticamente incorrecta (una migración faltante, una restricción de unicidad rota, una relación SwiftData sin inverso). La corrección lógica vive en los tests, en la revisión de código y en los ojos del desarrollador; los hooks son para preocupaciones estructurales y de ciclo de vida.
Deriva lenta en la calidad del agente. Cada hook individual detiene una clase de fallo en su primer encuentro, pero la deriva acumulada a lo largo de muchas sesiones (código gradualmente más desordenado, tests gradualmente más débiles, instrucciones de CLAUDE.md gradualmente desactualizadas) no es lo que los hooks miden. Eso es un problema de revisión de sesión, no un problema de hooks.
Violaciones de la frontera de confianza fuera de la superficie de herramientas del agente. Un hook sobre Bash y Edit cubre el camino común. Un hook sobre cada herramienta MCP que el agente pueda invocar requiere matchers por herramienta; algunos servidores MCP exponen docenas o cientos de herramientas (XcodeBuildMCP anuncia alrededor de 80) y escribir un hook por herramienta es impráctico. El patrón correcto allí es acotar el acceso a los servidores MCP (.mcp.json a nivel de proyecto, flujo de aprobación al primer uso) en lugar de poner un hook a cada herramienta individual, y aceptar que el agente operando sus servidores MCP es parte de su autoridad sancionada.
La relación entre los hooks y la postura de confianza más amplia se cubre en El repo no debería tener voto sobre su propia confianza: la confianza es un invariante de orden de carga, no una verificación posterior. Los hooks son guardas posteriores sobre las acciones que toma un agente ya confiable; no reemplazan la decisión previa sobre si el agente debería ser confiable en primer lugar.
Lo que construiría diferente
Tres patrones que las apps del clúster o bien envían o bien desearían enviar.
Scripts de hooks en control de versiones junto con el resto del proyecto. Los scripts de hooks viven en .claude/hooks/*.sh en el repo. El .claude/settings.json los referencia por ruta relativa. El equipo obtiene las mismas redes de seguridad en todas las máquinas, la revisión de código aplica a los cambios de hooks y dar de alta a un nuevo desarrollador es un git clone en lugar de un ejercicio de copiar y pegar. Los hooks de ámbito de usuario en ~/.claude/settings.json son la granularidad equivocada para gating específico de proyecto.
Un hook SessionStart que imprima la configuración de hooks activa. Los hooks son silenciosos hasta que se disparan. Un hook SessionStart que se ejecute al inicio de cada sesión de Claude Code e imprima “Active hooks: pbxproj-validation, dangerous-bash-gate, build-check-on-stop” recuerda al desarrollador (y al agente) qué guardas están corriendo. El costo es una línea de stderr por sesión; el valor es que nadie desarrolla sin saber que la red de seguridad está ahí.
Un log de auditoría a nivel de repo de las llamadas de herramienta del agente. Un hook PostToolUse que añade cada llamada de herramienta (con timestamp, nombre de herramienta, argumentos) a un archivo JSONL en .claude/logs/ (gitignoreado). El log responde “¿qué hizo el agente esta sesión?” con una consulta jq en lugar de un scroll por el historial de chat. El hook añade unos pocos milisegundos por llamada de herramienta y produce datos de auditoría duraderos que el desarrollador puede grepear cuando algo sale mal.
Cuándo los hooks son la respuesta equivocada
Dos casos donde la capa de hooks es el lugar equivocado para resolver el problema.
Los servidores MCP del agente en sí. Un servidor MCP malo haciendo lo equivocado no es un problema de hooks; es un problema del servidor MCP. La solución es acotar qué servidores MCP confía el proyecto (revisión de .mcp.json, aprobación de primer uso a nivel de proyecto) y leer el código fuente del servidor si es abierto. Un hook sobre cada llamada de herramienta MCP añade overhead sin abordar la cuestión de la confianza.
Agentes corriendo desatendidos. La postura completa de hooks asume que un desarrollador está cerca de la sesión y puede interpretar un hook fallido. Un agente corriendo en CI sin un humano en el bucle necesita una postura distinta: scoping MCP más estricto, conjuntos de herramientas más estrechos, un modelo de confianza distinto. Los hooks por sí solos no salvan el puente entre desarrollo atendido y automatización desatendida; esa brecha es intencional.
Lo que el patrón significa para apps iOS que envían en iOS 26+
Tres conclusiones.
-
Deny-por-defecto en operaciones irreversibles, validar los archivos estructurales con radio-de-impacto, condicionar la finalización a builds verdes. Tres eventos del ciclo de vida de hooks (
PreToolUse,PostToolUse,Stop), cuatro patrones, que cubren los modos de fallo comunes en iOS. El conjunto combinado es lo bastante pequeño para escribirlo en una tarde y lo bastante duradero para sobrevivir a cualquier agente o modelo específico. -
El código de salida importa, y difiere por evento.
exit 2bloquea la acción enPreToolUse(la herramienta nunca se ejecuta); enPostToolUseno puede bloquear (la herramienta ya se ejecutó), pero hace que el stderr llegue al agente para que pueda reparar o revertir; enStopimpide que el agente concluya la sesión.exit 1no bloquea en la mayoría de los eventos. Prueba cada hook con un caso deliberadamente fallido antes de confiar en él. -
Los hooks acotan la autoridad. No la conceden. El alcance del agente sobre la máquina del desarrollador es el que el SO le permite a la sesión de terminal del desarrollador. Los hooks le permiten al desarrollador recortar acciones específicas de esa autoridad y exigir aprobación explícita. El valor por defecto es lo que el SO concede; el objetivo de los hooks es hacer ese valor por defecto más pequeño, no más grande.
El clúster completo del Ecosistema Apple: App Intents tipados para la superficie de Apple Intelligence; servidores MCP para la superficie del agente; la cuestión de enrutamiento entre ellos; Foundation Models para funciones de LLM on-device dentro de la app; la distinción entre LLM de runtime y de tooling; la síntesis de las tres superficies; el patrón de fuente única de verdad; Dos servidores MCP para la integración de Xcode que se complementa con estos hooks; Live Activities para la máquina de estados de la pantalla de bloqueo de iOS; el contrato del runtime de watchOS en Apple Watch; los internos de SwiftUI para el sustrato del framework; el modelo mental espacial de RealityKit para escenas de visionOS; la disciplina de esquema de SwiftData para persistencia; los patrones de Liquid Glass para la capa visual; el envío multiplataforma para alcance entre dispositivos. El hub está en la Serie del Ecosistema Apple. Para contexto más amplio sobre iOS con agentes de IA, consulta la guía de desarrollo iOS con agentes.
FAQ
¿Qué son los hooks de Claude Code y por qué importan para el desarrollo iOS?
Los hooks de Claude Code son scripts de shell deterministas que se ejecutan en eventos del ciclo de vida (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop). Para desarrollo iOS, acotan la autoridad del agente sobre operaciones destructivas: borrado de simuladores, firma de código, mutaciones de archivos de proyecto, force-pushes. Sin hooks, el agente tiene la autoridad total sobre la máquina del desarrollador; con hooks, acciones peligrosas específicas requieren aprobación explícita.
¿Qué eventos de hook debería priorizar un desarrollador iOS?
PreToolUse en Bash para bloquear comandos destructivos (simctl erase, xcodebuild archive, git push --force). PostToolUse en Edit/Write para validar la integridad de .pbxproj. Stop para condicionar a una build verde. SessionStart para registrar la configuración de hooks activa. Los cuatro juntos atrapan los fallos del agente más comunes específicos de iOS.
¿Cuál es la diferencia entre los códigos de salida 0, 1 y 2?
exit 0 permite la acción y procede. exit 2 se comporta distinto por evento: en PreToolUse bloquea la acción propuesta (la herramienta nunca se ejecuta); en PostToolUse no puede bloquear porque la herramienta ya se ejecutó, pero hace que el stderr llegue al agente para que pueda reparar o revertir; en Stop impide que el agente concluya la sesión. exit 1 registra un error pero no bloquea en la mayoría de los eventos de hook. Para patrones de seguridad que necesitan prevenir efectivamente la acción antes de que se ejecute, usa exit 2 en PreToolUse. Para validación tras una escritura destructiva, usa exit 2 en PostToolUse para hacer llegar el fallo al agente. Prueba cada hook con una entrada deliberadamente fallida para confirmar que se comporta como esperas para el evento específico.
¿Dónde deberían vivir los scripts de hooks?
En .claude/hooks/*.sh en la raíz del proyecto, con .claude/settings.json referenciándolos por ruta relativa. Versionados en control de código y revisados junto con el resto del proyecto. Los hooks de ámbito de usuario en ~/.claude/settings.json también funcionan, pero son la granularidad equivocada para gating específico de iOS a nivel de proyecto.
¿Los hooks reemplazan la necesidad de revisión de código?
No. Los hooks atrapan errores estructurales (archivos de proyecto rotos, bash peligroso, builds rotas) antes de que se envíen. La revisión de código atrapa errores semánticos (bugs de lógica, migraciones faltantes, tests débiles). Las dos capas se complementan: los hooks hacen al agente más seguro de desplegar en el bucle interno, la revisión de código mantiene honesta la salida del agente en el límite.
Referencias
-
Anthropic, “Claude Code reference: Hooks”. Eventos del ciclo de vida (
PreToolUse,PostToolUse,UserPromptSubmit,SessionStart,Stop), sintaxis del matcher, forma del comando y el papel de los códigos de salida.exit 2se comporta distinto por evento: enPreToolUseyStopbloquea la acción; enPostToolUseno puede bloquear (la herramienta ya se ejecutó), pero hace que el stderr llegue al agente.exit 0permite;exit 1generalmente registra pero no bloquea. El análisis del autor en Cuando la LLM vive en tu app vs en tu tooling cubre la postura de confianza de LLM runtime-vs-tooling que los hooks operacionalizan. ↩↩ -
Apple, página man de
plutil(1). El flag-lintvalida la sintaxis de property lists en formatos ASCII de estilo antiguo, XML y binario. Detecta rupturas a nivel de parseo, pero no comprueba semántica específica de Xcode como referencias de fases de build o validez de UUIDs dentro del grafo del proyecto. ↩ -
Apple Developer, “xcodebuild Destination Specifier” y la página man de
xcodebuild. La sintaxis-destination 'platform=...,name=...'es la forma canónica de fijar un target de build; los entornos de CI sobreescriben el nombre del simulador vía variables de entorno o detección de disponibilidad de dispositivos por script. ↩