← Todos os Posts

Hooks para desenvolvimento Apple: padrões que salvam o projeto

From the guide: Claude Code Comprehensive Guide

Uma sessão Claude Code apontada para um projeto iOS tem um alcance que um projeto Python genérico ou web não tem. O agente pode executar xcodebuild e xcrun através de sua ferramenta Bash. Ele pode ler e editar arquivos .pbxproj (uma property list ASCII no estilo antigo por padrão, às vezes XML ou JSON após conversão por ferramentas, e igualmente fatal de corromper em qualquer um desses formatos). Ele detém as identidades de assinatura do desenvolvedor pelo simples fato de estar rodando na máquina do desenvolvedor. Ele pode apagar um simulador. Ele pode reconstruir um projeto com o scheme errado. Ele pode fazer commit e push. O protocolo não restringe nada disso: o sistema de arquivos do desenvolvedor é o sistema de arquivos do agente, e a flag --dangerously-skip-permissions do Claude Code está a uma tecla de distância da automação completa.

A mitigação não é “confiar no agente”. A mitigação são os hooks: scripts shell determinísticos que o host executa em fronteiras do ciclo de vida (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop).1 Hooks freiam o agente em entradas perigosas, validam saídas destrutivas e condicionam a conclusão a um build verde. Eles são a primitiva de segurança fundamental que todo desenvolvedor iOS rodando um agente deveria configurar antes que o agente execute qualquer coisa.

Quatro padrões de hook merecem seu lugar em projetos iOS. Eles são em nível de framework, não específicos de projeto; os apps do cluster (Return, Get Bananas, Reps, Water, Ace Citizenship) todos rodam variantes desses padrões. Cada padrão nomeia um modo de falha real, um script concreto e o evento de ciclo de vida que delimita o raio de impacto.

TL;DR

  • Quatro padrões de hook que importam no iOS: validação de .pbxproj (PostToolUse, devolve erros para o agente), bloqueio de bash perigoso (PreToolUse, bloqueia antes da execução), gate de build verde no Stop, higiene do estado do simulador (Stop).
  • Códigos de saída de hooks importam, e se comportam de forma diferente por evento. exit 2 bloqueia a ação proposta no PreToolUse (a ferramenta nunca roda); no PostToolUse ele não pode bloquear (a ferramenta já rodou) mas devolve o stderr ao agente para que ele possa reparar ou reverter; no Stop ele impede o agente de concluir a sessão. exit 0 permite. exit 1 geralmente registra mas não bloqueia.1
  • Scripts de hook ficam em .claude/hooks/*.sh no repositório, referenciados por caminho relativo a partir de .claude/settings.json. Code review se aplica.
  • A autoridade do agente é a autoridade do desenvolvedor. Hooks são como o desenvolvedor recorta essa autoridade de volta em um conjunto deliberado de ações aprovadas.

Padrão um: validação de .pbxproj em cada edição

O arquivo de projeto do Xcode é o único arquivo que um agente regularmente modifica que tem a maior razão de raio-de-impacto-por-linha. Um colchete errado em project.pbxproj quebra silenciosamente o build para todo desenvolvedor da equipe. O erro de build aparece na próxima invocação do xcodebuild, não no momento da edição, então o agente normalmente afirma que a mudança funcionou antes que a quebra venha à tona.

O hook executa plutil -lint contra qualquer escrita em .pbxproj. PostToolUse não pode bloquear a escrita em si (o arquivo já está em disco quando o hook dispara), mas exit 2 devolve o erro de validação imediatamente ao agente como uma falha de chamada de ferramenta: o agente lê a falha, sabe que o arquivo está quebrado e pode reverter ou reparar antes que a sessão continue:

#!/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 captura quebras estruturais: chaves ou parênteses desbalanceados, ponto e vírgula faltando, tokens plist inválidos, aninhamento de XML quebrado.2 Ele não captura erros semânticos do Xcode como um UUID malformado que por acaso é texto plist sintaticamente válido, ou uma fase de build referenciando um arquivo inexistente. Esses produzem erros de build comuns que o agente pode depurar normalmente. O gate do plutil captura a classe de falha catastrófica de parsing; erros semânticos passam para o próprio build.

A configuração do hook em .claude/settings.json (note o $CLAUDE_PROJECT_DIR entre aspas para caminhos com espaços):

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-write-pbxproj.sh"
      }]
    }]
  }
}

O matcher dispara o hook apenas em chamadas das ferramentas Write e Edit; a primeira ação do script é fazer um curto-circuito em caminhos não-.pbxproj. O custo de rodar em cada Edit é desprezível porque o filtro de caminho é a primeira verificação.

Padrão dois: bloqueando comandos bash destrutivos antes que rodem

xcrun simctl erase apaga os dados de um simulador. xcodebuild archive invoca a assinatura e pode produzir artefatos assinados que o desenvolvedor não pretendia gerar. git push --force reescreve histórico. O agente tem acesso a todos eles através de sua ferramenta Bash. Um hook PreToolUse em Bash faz match com o padrão de comando proposto e decide se prossegue.

A 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

O hook é um switch sobre substrings de padrões de comando. A classe que bloqueia é a classe de ação irreversível: erase, sign, force-push, recursive delete. Operações reversíveis (builds normais, testes, git commits sem force) passam adiante.

Um refinamento comum é permitir alguns comandos quando o desenvolvedor optou explicitamente via uma variável de ambiente ou flag na conversa. Por exemplo: xcodebuild archive poderia ser permitido se CLAUDE_ALLOW_ARCHIVE=1 estiver no ambiente, definido pelo desenvolvedor antes da sessão para uma tarefa de archive específica. O hook lê o env e contorna o bloqueio:

*"xcodebuild archive"*)
    if [[ "${CLAUDE_ALLOW_ARCHIVE:-0}" == "1" ]]; then
        exit 0
    fi
    echo "ERROR: xcodebuild archive requires CLAUDE_ALLOW_ARCHIVE=1" >&2
    exit 2
    ;;

O padrão: negação por padrão na classe irreversível, válvula de escape opt-in para os casos em que o desenvolvedor quer que o agente cuide.

Padrão três: hook Stop que condiciona a conclusão a um build verde

O agente gosta de declarar uma tarefa concluída quando a conversa parece resolvida. Sem um gate, “concluído” pode significar “editei os arquivos e o chat está em um estado coerente” em vez de “o build ainda compila”. O hook Stop é o lugar para impor o significado correto.

#!/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 PROJECT, SCHEME e DESTINATION no script é a forma certa para hooks comitados no repositório: os valores nunca derivam, workspaces e repos com múltiplos projetos funcionam sem ajustes por máquina, e um sistema de build CI usando o mesmo hook pode trocar o destination via env var. Auto-descoberta (ls *.xcodeproj, xcodebuild -list | awk) funciona para o caso solo mais simples mas falha em projetos enraizados em .xcworkspace, em repos com múltiplos arquivos .xcodeproj e na distinção entre schemes shared e user. A string de destination segue a sintaxe documentada do xcodebuild platform=...,name=...;3 precisa ser um simulador que a máquina do desenvolvedor realmente possua, caso contrário o hook falha por razões de ambiente em vez de razões de código.

Duas decisões de produto que o hook faz:

Stop bloqueia o sinal “estou pronto” do agente, não do humano. O desenvolvedor sempre pode dar Ctrl+C, fechar o terminal ou contornar. O hook é uma barreira contra o otimismo do agente, não uma trava sobre o humano.

O hook executa um build real, não uma verificação de sintaxe. swift build contra um projeto específico de iOS pula as etapas de compilação específicas do iOS; apenas xcodebuild prova que o target iOS compila. O custo é o tempo de build em si (10-60 segundos na maioria dos projetos); o valor é capturar o caso de build-quebrado-marcado-como-pronto toda vez.

Padrão quatro: higiene do estado do simulador

Depois de uma longa sessão de agente, simuladores podem se acumular: simuladores em execução que o agente esqueceu de desligar, instalações antigas de apps que cacheiam estado obsoleto, dados de runtime que sobrevivem entre sessões e produzem bugs irreproduzíveis. Um hook Stop pode fazer a limpeza.

#!/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

A forma é não-bloqueante por design: o hook reporta o estado mas não age a menos que o desenvolvedor descomente a linha de shutdown. A razão é que a próxima sessão do agente pode querer o simulador em execução preservado (cold-start mais rápido quando o simulador já está rodando). A decisão é por desenvolvedor: se simuladores se acumulam ao longo de múltiplas sessões e o custo é real, descomente o shutdown; caso contrário deixe como sinal de log.

Uma variante mais agressiva apaga simuladores entre sessões, mas isso atravessa para o território de operação destrutiva do Padrão Dois. Erase pertence ao bloqueio em PreToolUse, não à automação no Stop.

O que hooks não resolvem

Os quatro padrões acima são o conjunto operacional, não o quadro completo. Três classes de falha que hooks não conseguem capturar:

Bugs de lógica no código do agente. Um hook valida estrutura, não semântica. O agente pode escrever uma classe @Model que compila, passa no lint do arquivo de projeto, faz build verde e ainda está semanticamente errada (uma migração faltando, uma constraint de unicidade quebrada, um relacionamento SwiftData sem inverso). Correção lógica vive em testes, code review e nos olhos do desenvolvedor; hooks são para preocupações estruturais e de ciclo de vida.

Deriva lenta na qualidade do agente. Cada hook individual interrompe uma classe de falha no primeiro encontro, mas a deriva cumulativa ao longo de muitas sessões (código gradualmente mais bagunçado, testes gradualmente mais fracos, instruções de CLAUDE.md gradualmente desatualizadas) não é o que hooks medem. Isso é um problema de revisão de sessão, não um problema de hook.

Violações de fronteira de confiança fora da superfície de ferramentas do agente. Um hook em Bash e Edit cobre o caminho comum. Um hook em cada ferramenta MCP que o agente possa chamar requer matchers por ferramenta; alguns servidores MCP expõem dezenas ou centenas de ferramentas (XcodeBuildMCP anuncia cerca de 80) e escrever um hook por ferramenta é impraticável. O padrão certo ali é restringir o escopo de acesso a servidores MCP (.mcp.json em nível de projeto, fluxo de aprovação no primeiro uso) em vez de fazer hook em cada ferramenta individual, e aceitar que o agente operando seus servidores MCP é parte de sua autoridade sancionada.

A relação entre hooks e a postura de confiança mais ampla é abordada em The Repo Shouldn’t Get to Vote on Its Own Trust: confiança é uma invariante de ordem de carregamento, não uma verificação a jusante. Hooks são guardas a jusante sobre ações que um agente já confiável toma; eles não substituem a decisão a montante sobre se o agente deveria ser confiável em primeiro lugar.

O que eu construiria de forma diferente

Três padrões que os apps do cluster ou já entregam ou desejariam ter entregado.

Scripts de hook em controle de versão junto com o resto do projeto. Os scripts de hook ficam em .claude/hooks/*.sh no repositório. O .claude/settings.json os referencia por caminho relativo. A equipe ganha as mesmas redes de segurança em todas as máquinas, code review se aplica a mudanças de hook e o onboarding de um novo desenvolvedor é um git clone em vez de um exercício de copiar e colar. Hooks em escopo de usuário em ~/.claude/settings.json são a granularidade errada para gating específico de projeto.

Um hook SessionStart que imprime a configuração ativa de hooks. Hooks são silenciosos até dispararem. Um hook SessionStart que roda no início de cada sessão Claude Code e imprime “Hooks ativos: pbxproj-validation, dangerous-bash-gate, build-check-on-stop” lembra o desenvolvedor (e o agente) de quais guardas estão rodando. O custo é uma linha de stderr por sessão; o valor é que ninguém desenvolve sem saber que a rede de segurança está lá.

Um log de auditoria em nível de repositório das chamadas de ferramenta do agente. Um hook PostToolUse que anexa cada chamada de ferramenta (com timestamp, nome da ferramenta, argumentos) a um arquivo JSONL em .claude/logs/ (gitignored). O log responde “o que o agente fez nesta sessão?” com uma query jq em vez de uma rolagem do histórico do chat. O hook adiciona alguns milissegundos por chamada de ferramenta e produz dados de auditoria duráveis que o desenvolvedor pode usar com grep quando algo dá errado.

Quando hooks são a resposta errada

Dois casos em que a camada de hooks é o lugar errado para resolver o problema.

Os próprios servidores MCP do agente. Um servidor MCP ruim fazendo a coisa errada não é um problema de hook; é um problema de servidor MCP. A correção é restringir quais servidores MCP o projeto confia (revisão de .mcp.json, aprovação de primeiro uso em escopo de projeto) e ler o código-fonte do servidor se for aberto. Um hook em cada chamada de ferramenta MCP adiciona overhead sem abordar a questão de confiança.

Agentes rodando sem supervisão. A postura completa de hooks assume que um desenvolvedor está próximo da sessão e pode interpretar um hook que falhou. Um agente rodando em CI sem um humano no loop precisa de uma postura diferente: escopo MCP mais rigoroso, conjuntos de ferramentas mais estreitos, um modelo diferente de confiança. Hooks sozinhos não preenchem a lacuna entre desenvolvimento supervisionado e automação não-supervisionada; essa lacuna é intencional.

O que o padrão significa para apps iOS sendo lançados em iOS 26+

Três conclusões.

  1. Negação por padrão em operações irreversíveis, validação dos arquivos com raio-de-impacto estrutural, gate de conclusão em builds verdes. Três eventos de ciclo de vida de hooks (PreToolUse, PostToolUse, Stop), quatro padrões, cobrindo os modos de falha iOS comuns. O conjunto combinado é pequeno o suficiente para escrever em uma tarde e durável o suficiente para sobreviver a qualquer agente ou modelo específico.

  2. O código de saída importa, e difere por evento. exit 2 bloqueia a ação no PreToolUse (a ferramenta nunca roda); no PostToolUse ele não pode bloquear (a ferramenta já rodou) mas devolve o stderr ao agente para que ele possa reparar ou reverter; no Stop ele impede o agente de concluir a sessão. exit 1 não bloqueia na maioria dos eventos. Teste cada hook com um caso deliberadamente falho antes de depender dele.

  3. Hooks delimitam autoridade. Eles não a concedem. O alcance do agente sobre a máquina do desenvolvedor é o que o sistema operacional permitir que a sessão de terminal do desenvolvedor faça. Hooks permitem ao desenvolvedor recortar ações específicas dessa autoridade e exigir aprovação explícita. O padrão é o que o sistema operacional concede; o objetivo dos hooks é tornar o padrão menor, não maior.

O cluster Apple Ecosystem completo: App Intents tipados para a superfície da Apple Intelligence; servidores MCP para a superfície do agente; a questão de roteamento entre eles; Foundation Models para recursos LLM on-device dentro do app; a distinção LLM runtime vs tooling; a síntese das três superfícies; o padrão de fonte única da verdade; Two MCP Servers para a integração com Xcode que se combina com esses hooks; Live Activities para a máquina de estados na Lock Screen do iOS; o contrato do runtime watchOS no Apple Watch; SwiftUI internals para o substrato do framework; o modelo mental espacial do RealityKit para cenas visionOS; disciplina de schema SwiftData para persistência; padrões Liquid Glass para a camada visual; shipping multiplataforma para alcance entre dispositivos. O hub está na Apple Ecosystem Series. Para contexto mais amplo de iOS com agentes de IA, veja o iOS Agent Development guide.

FAQ

O que são hooks do Claude Code e por que importam para o desenvolvimento iOS?

Hooks do Claude Code são scripts shell determinísticos que rodam em eventos de ciclo de vida (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop). Para desenvolvimento iOS, eles delimitam a autoridade do agente sobre operações destrutivas: apagar simuladores, assinatura de código, mutações em arquivos de projeto, force-pushes. Sem hooks, o agente tem a autoridade total da máquina do desenvolvedor; com hooks, ações perigosas específicas requerem aprovação explícita.

Quais eventos de hook um desenvolvedor iOS deveria priorizar?

PreToolUse em Bash para bloquear comandos destrutivos (simctl erase, xcodebuild archive, git push --force). PostToolUse em Edit/Write para validar a integridade de .pbxproj. Stop para condicionar a um build verde. SessionStart para registrar a configuração ativa de hooks. Os quatro juntos capturam as falhas de agente específicas de iOS mais comuns.

Qual é a diferença entre os códigos de saída 0, 1 e 2?

Exit 0 permite a ação e prossegue. Exit 2 se comporta diferentemente por evento: no PreToolUse ele bloqueia a ação proposta (a ferramenta nunca roda); no PostToolUse ele não pode bloquear porque a ferramenta já executou, mas devolve o stderr ao agente para que ele possa reparar ou reverter; no Stop ele impede o agente de concluir a sessão. Exit 1 registra um erro mas não bloqueia na maioria dos eventos de hook. Para padrões de segurança que precisam realmente impedir a ação antes que rode, use exit 2 no PreToolUse. Para validação após uma escrita destrutiva, use exit 2 no PostToolUse para devolver a falha ao agente. Teste cada hook com uma entrada deliberadamente falha para confirmar que ele se comporta como esperado para o evento específico.

Onde devem ficar os scripts de hook?

Em .claude/hooks/*.sh na raiz do projeto, com .claude/settings.json os referenciando por caminho relativo. Versionados e revisados em code review junto com o resto do projeto. Hooks em escopo de usuário em ~/.claude/settings.json também funcionam mas são a granularidade errada para gating iOS específico de projeto.

Hooks substituem a necessidade de code review?

Não. Hooks capturam erros estruturais (arquivos de projeto quebrados, bash perigoso, builds quebrados) antes que cheguem à produção. Code review captura erros semânticos (bugs de lógica, migrações faltando, testes fracos). As duas camadas se complementam: hooks tornam o agente mais seguro de implantar no loop interno, code review mantém a saída do agente honesta na fronteira.

References


  1. Anthropic, “Claude Code reference: Hooks”. Eventos de ciclo de vida (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop), sintaxe de matcher, forma do comando e o papel dos códigos de saída. Exit 2 se comporta diferentemente por evento: no PreToolUse e Stop bloqueia a ação; no PostToolUse não pode bloquear (a ferramenta já rodou) mas devolve o stderr ao agente. Exit 0 permite; exit 1 geralmente registra mas não bloqueia. Análise do autor em When the LLM Lives in Your App vs in Your Tooling cobre a postura de confiança LLM runtime-vs-tooling que os hooks operacionalizam. 

  2. Apple, plutil(1) man page. A flag -lint valida a sintaxe de property list nos formatos ASCII antigo, XML e binário. Detecta quebras em nível de parsing mas não verifica semânticas específicas do Xcode como referências de fase de build ou validade de UUID dentro do grafo do projeto. 

  3. Apple Developer, “xcodebuild Destination Specifier” e o man page do xcodebuild. A sintaxe -destination 'platform=...,name=...' é a forma canônica de fixar um target de build; ambientes de CI sobrescrevem o nome do simulador via env vars ou detecção de disponibilidade de dispositivo via script. 

Artigos relacionados

Single Source Of Truth: SwiftData, MCP, iCloud

Three callers can write to the same shopping list: a human, Apple Intelligence, and an external agent. Truth has to live…

18 min de leitura

Claude Code as Infrastructure

Claude Code is not an IDE feature. It is infrastructure. 84 hooks, 48 skills, 19 agents, and 15,000 lines of orchestrati…

15 min de leitura