Apple開発のためのフック:プロジェクトを救うパターン
iOSプロジェクトに向けたClaude Codeセッションは、汎用的なPythonやWebプロジェクトにはない広いリーチを持ちます。エージェントはBashツールを通じてxcodebuildやxcrunを実行できます。.pbxprojファイル(デフォルトでは旧式のASCIIプロパティリスト形式、ツールでの変換後にはXMLやJSON形式になることもあり、いずれの形式でも破損すれば致命的です)の読み書きや編集ができます。開発者のマシン上で動作している以上、開発者の署名IDも保持しています。シミュレーターを消去することも、誤ったスキームでプロジェクトをリビルドすることも、コミットしてプッシュすることもできます。プロトコルはこれらを一切ゲートしません。開発者のファイルシステムはエージェントのファイルシステムであり、Claude Codeの--dangerously-skip-permissionsフラグはキー1つで完全自動化に到達します。
緩和策は「エージェントを信頼する」ことではありません。緩和策はフックです。これは、ホストがライフサイクル境界(PreToolUse、PostToolUse、UserPromptSubmit、SessionStart、Stop)で実行する決定論的なシェルスクリプトです。1 フックは危険な入力に対してエージェントを減速させ、破壊的な出力を検証し、グリーンビルドで完了をゲートします。これらは、エージェントを実行する前にすべてのiOS開発者が設定すべき、安全性の根幹を担うプリミティブです。
iOSプロジェクトで真価を発揮するフックパターンは4つあります。これらはフレームワークレベルのものであり、プロジェクト固有のものではありません。クラスター内のアプリ(Return、Get Bananas、Reps、Water、Ace Citizenship)はすべて、これらのバリアントを実行しています。各パターンは、実際の障害モード、具体的なスクリプト、そして影響範囲を限定するライフサイクルイベントを示しています。
TL;DR
- iOSで重要な4つのフックパターン:
.pbxproj検証(PostToolUse、エラーをエージェントにフィードバック)、危険なbashのゲート(PreToolUse、実行前にブロック)、グリーンビルドのStopゲート、シミュレーター状態の整理(Stop)。 - フックの終了コードは重要であり、イベントごとに動作が異なります。
exit 2はPreToolUseで提案されたアクションをブロックします(ツールは実行されません)。PostToolUseではブロックできません(ツールはすでに実行済み)が、stderrをエージェントに返すことで、修復やリバートが可能になります。Stopでは、エージェントがセッションを終了するのを防ぎます。exit 0は許可します。exit 1は通常ログに記録しますがブロックはしません。1 - フックスクリプトはリポジトリ内の
.claude/hooks/*.shに置き、.claude/settings.jsonから相対パスで参照されます。コードレビューの対象となります。 - エージェントの権限は開発者の権限です。フックは、その権限を開発者が承認した一連のアクションへと意図的に切り出すための仕組みです。
パターン1:すべての編集における.pbxproj検証
Xcodeのプロジェクトファイルは、エージェントが日常的に変更するファイルの中で、行あたりの影響範囲が最も大きいファイルです。project.pbxproj内のブラケットを1つ間違えるだけで、チームのすべての開発者のビルドが密かに壊れます。ビルドエラーは編集時ではなく次のxcodebuild実行時に現れるため、エージェントは破損が表面化する前に変更が成功したと主張するのが普通です。
このフックは、.pbxprojへの書き込みに対してplutil -lintを実行します。PostToolUseは書き込み自体をブロックできません(フックが発動する時点でファイルはすでにディスク上にあるため)が、exit 2は検証エラーをツール呼び出しの失敗として即座にエージェントに返します。エージェントは失敗を読み取り、ファイルが壊れていることを認識し、セッションが続行する前にリバートまたは修復できます。
#!/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は構造的な破損を検出します。中括弧や括弧の不均衡、セミコロンの欠落、無効なplistトークン、XMLのネストの破損などです。2 ただし、構文的には有効なplistテキストでありながら不正なUUIDや、存在しないファイルを参照するビルドフェーズといったXcodeの意味論的なエラーは検出しません。これらはエージェントが通常通りデバッグできる、通常のビルドエラーとして現れます。plutilゲートは致命的な解析失敗のクラスを捕捉し、意味論的なエラーはビルド自体に委ねられます。
.claude/settings.json内のフック設定は次のとおりです(スペースを含むパスのために$CLAUDE_PROJECT_DIRを引用符で囲んでいる点に注意してください)。
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-write-pbxproj.sh"
}]
}]
}
}
マッチャーはWriteとEditのツール呼び出しに対してのみフックを発動します。スクリプトの最初のアクションは、.pbxproj以外のパスでショートサーキットすることです。すべてのEditで実行するコストは、パスフィルターが最初のチェックであるため無視できる程度です。
パターン2:実行前に破壊的なBashコマンドをゲート
xcrun simctl eraseはシミュレーターのデータを消去します。xcodebuild archiveは署名を呼び出し、開発者が意図しない署名済みアーティファクトを生成する可能性があります。git push --forceは履歴を書き換えます。エージェントはBashツールを通じてこれらすべてにアクセスできます。Bashに対するPreToolUseフックは、提案されたコマンドのパターンと照合し、続行するかどうかを決定します。
その形は次のようになります。
#!/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
このフックはコマンドパターンの部分文字列に対するスイッチです。ブロック対象のクラスは不可逆アクションのクラス、つまり消去、署名、強制プッシュ、再帰的削除です。可逆的な操作(通常のビルド、テスト、強制なしのgit commit)は通過します。
よく使われる改良として、開発者が会話内のenv varやフラグで明示的にオプトインした場合に一部のコマンドを許可することがあります。たとえば、xcodebuild archiveは、特定のアーカイブタスクのためにセッション前に開発者が設定したCLAUDE_ALLOW_ARCHIVE=1が環境にある場合に許可できます。フックはenvを読み取り、ブロックをバイパスします。
*"xcodebuild archive"*)
if [[ "${CLAUDE_ALLOW_ARCHIVE:-0}" == "1" ]]; then
exit 0
fi
echo "ERROR: xcodebuild archive requires CLAUDE_ALLOW_ARCHIVE=1" >&2
exit 2
;;
パターンとしては、不可逆クラスはデフォルトで拒否し、開発者がエージェントに処理させたいケースのためのオプトインの脱出弁を設けるという形です。
パターン3:グリーンビルドで完了をゲートするStopフック
エージェントは、会話が解決したように見えるとタスクを完了したと宣言したがります。ゲートがなければ、「完了」とは「ビルドが依然としてコンパイル可能である」ではなく「ファイルを編集してチャットが一貫した状態になった」を意味することがあります。Stopフックは、正しい意味を強制するための場所です。
#!/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
PROJECT、SCHEME、DESTINATIONをハードコーディングするのは、リポジトリにコミットされるフックにとって正しい形です。値がドリフトすることがなく、ワークスペースや複数プロジェクトのリポジトリでもマシンごとの調整なしで動作し、同じフックを使うCIビルドシステムはenv var経由でdestinationを切り替えられます。自動検出(ls *.xcodeproj、xcodebuild -list | awk)は最もシンプルな単独のケースでは機能しますが、.xcworkspaceをルートとするプロジェクト、複数の.xcodeprojファイルを持つリポジトリ、shared対userスキームの区別では失敗します。destination文字列はxcodebuildの文書化されたplatform=...,name=...構文に従います。3 これは、開発者のマシンに実際に存在するシミュレーターでなければなりません。そうでなければ、フックはコード上の理由ではなく環境上の理由で失敗します。
このフックが行う2つの製品上の決定があります。
Stopはエージェントの「完了しました」というシグナルをブロックするのであって、人間のシグナルではありません。 開発者は常にCtrl+C、ターミナルを閉じる、またはオーバーライドができます。フックはエージェントの楽観主義に対するガードレールであり、人間に対するロックインではありません。
フックはシンタックスチェックではなく、実際のビルドを実行します。 iOS固有のプロジェクトに対するswift buildは、iOS固有のコンパイル手順を省略します。iOSターゲットがコンパイルされることを証明するのはxcodebuildだけです。コストはビルド時間そのものです(ほとんどのプロジェクトで10〜60秒)。価値は、ビルドが壊れているのに完了とマークされるケースを毎回検出できることです。
パターン4:シミュレーター状態の整理
長時間のエージェントセッションの後、シミュレーターは積み重なる可能性があります。エージェントがシャットダウンを忘れた起動済みシミュレーター、古い状態をキャッシュしている古いアプリのインストール、セッションをまたいで残り再現困難なバグを生み出すランタイムデータなどです。Stopフックでクリーンアップできます。
#!/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
その形は意図的に非ブロッキングです。フックは状態を報告しますが、開発者がシャットダウン行のコメントを外さない限り行動はしません。理由は、エージェントの次のセッションが起動済みシミュレーターを保持したい場合があるためです(シミュレーターがすでに実行中であればコールドスタートが速くなります)。決定は開発者ごとに異なります。複数セッションにまたがってシミュレーターが積み重なり、コストが現実的であればシャットダウンのコメントを外します。そうでなければログシグナルとして残します。
より積極的なバリアントはセッション間でシミュレーターを消去しますが、これはパターン2の破壊的操作の領域に踏み込みます。EraseはStopの自動化ではなくPreToolUseのブロッキングに属するべきです。
フックでは解決できないこと
上記の4つのパターンは作業セットであり、全体像ではありません。フックでは捕捉できない3つのクラスの障害があります。
エージェントのコード内のロジックバグ。 フックは構造を検証するのであって、意味論を検証するのではありません。エージェントは、コンパイルされ、プロジェクトファイルのlintを通過し、グリーンでビルドされ、それでも意味論的に間違っている@Modelクラスを書く可能性があります(マイグレーションの欠落、ユニーク制約の破損、逆方向のないSwiftDataの関係など)。ロジックの正しさはテスト、コードレビュー、開発者の目に宿ります。フックは構造とライフサイクルの懸念のためのものです。
エージェントの品質におけるゆっくりとしたドリフト。 個々のフックは初回の遭遇でクラスの障害を停止しますが、多くのセッションにわたる累積的なドリフト(徐々に乱雑になるコード、徐々に弱くなるテスト、徐々に古くなるCLAUDE.mdの指示)はフックが測定するものではありません。これはセッションレビューの問題であり、フックの問題ではありません。
エージェントのツール表面の外側での信頼境界違反。 BashとEditに対するフックは一般的な経路をカバーします。エージェントが呼び出す可能性のあるすべてのMCPツールに対するフックには、ツールごとのマッチャーが必要です。一部のMCPサーバーは数十から数百のツールを公開しており(XcodeBuildMCPは約80を広告しています)、ツールごとにフックを書くのは非現実的です。そこで適切なパターンは、すべての個別ツールにフックを設定するのではなく、MCPサーバーへのアクセスをスコープすること(プロジェクトレベルの.mcp.json、初回使用時の承認フロー)であり、MCPサーバーを操作するエージェントが、その認可された権限の一部であることを受け入れることです。
フックとより広範な信頼姿勢の関係は、The Repo Shouldn’t Get to Vote on Its Own Trustで扱っています。信頼はロード順の不変条件であり、下流のチェックではありません。フックは、すでに信頼されたエージェントが取るアクションに対する下流のガードであり、エージェントを信頼すべきかどうかという上流の決定に取って代わるものではありません。
私が違うふうに作るとしたら
クラスター内のアプリが出荷している、または出荷していたらと願う3つのパターンです。
フックスクリプトをプロジェクトの残りの部分とともにバージョン管理する。 フックスクリプトはリポジトリ内の.claude/hooks/*.shにあります。.claude/settings.jsonは相対パスでそれらを参照します。チームはマシン全体で同じセーフティネットを得られ、フックの変更にコードレビューが適用され、新しい開発者のオンボーディングはコピーペーストの作業ではなくgit cloneになります。~/.claude/settings.jsonにあるユーザースコープのフックは、プロジェクト固有のゲーティングに対しては誤った粒度です。
アクティブなフック構成を出力するSessionStartフック。 フックは発動するまで沈黙しています。すべてのClaude Codeセッションの開始時に実行され、「Active hooks: pbxproj-validation, dangerous-bash-gate, build-check-on-stop」と出力するSessionStartフックは、開発者(およびエージェント)にどのガードが実行されているかを思い出させます。コストはセッションあたりstderrの1行です。価値は、誰もセーフティネットがあることを知らずに開発しないことです。
エージェントのツール呼び出しのリポジトリレベルの監査ログ。 すべてのツール呼び出し(タイムスタンプ、ツール名、引数を含む)を.claude/logs/内のJSONLファイルに追記するPostToolUseフックです(gitignored)。ログは、チャット履歴をスクロールする代わりにjqクエリで「このセッションでエージェントは何をしたか?」に答えます。フックはツール呼び出しごとに数ミリ秒を追加し、何かがおかしくなったときに開発者がgrepできる永続的な監査データを生成します。
フックが間違った答えになる場合
フック層が問題を解決すべき場所ではない2つのケースです。
エージェントのMCPサーバー自体。 不正なことをしている悪いMCPサーバーはフックの問題ではなく、MCPサーバーの問題です。修正は、プロジェクトが信頼するMCPサーバーをスコープすること(.mcp.jsonのレビュー、プロジェクトスコープでの初回使用承認)と、ソースコードがオープンであれば読むことです。すべてのMCPツール呼び出しに対するフックは、信頼の問題に対処することなくオーバーヘッドを追加します。
エージェントが無人で実行される場合。 完全なフック姿勢は、開発者がセッションの近くにいて、失敗したフックを解釈できることを前提としています。人間がループにいないCIで実行されるエージェントには異なる姿勢が必要です。より厳格なMCPスコープ、より狭いツールセット、異なる信頼モデルです。フックだけでは、有人開発と無人自動化の間のギャップを埋めることはできません。そのギャップは意図的なものです。
このパターンがiOS 26+で出荷されるiOSアプリにとって何を意味するか
3つのテイクアウェイです。
-
不可逆操作にはデフォルトで拒否、構造的な影響範囲のファイルを検証、グリーンビルドで完了をゲート。 3つのフックライフサイクルイベント(
PreToolUse、PostToolUse、Stop)、4つのパターンで、一般的なiOS障害モードをカバーします。組み合わせたセットは午後に書ける程度に小さく、特定のエージェントやモデルよりも長く生き残るほど耐久性があります。 -
終了コードは重要であり、イベントによって異なります。
exit 2はPreToolUseでアクションをブロックします(ツールは実行されません)。PostToolUseではブロックできません(ツールはすでに実行済み)が、stderrをエージェントに返すことで、エージェントは修復またはリバートできます。Stopでは、エージェントがセッションを終了するのを防ぎます。exit 1はほとんどのイベントでブロックしません。依存する前に、意図的に失敗するケースですべてのフックをテストしてください。 -
フックは権限を限定します。権限を付与するわけではありません。 エージェントの開発者マシンへのリーチは、OSが開発者のターミナルセッションに許可するものすべてです。フックは、開発者がその権限から特定のアクションを切り出し、明示的な承認を要求することを可能にします。デフォルトはOSが付与するものであり、フックの目的はデフォルトをより大きくするのではなく、より小さくすることです。
完全なApple Ecosystemクラスター:Apple Intelligenceの表面のための型付きApp Intents、エージェント表面のためのMCPサーバー、それらの間のルーティングの問題、アプリ内オンデバイスLLM機能のためのFoundation Models、ランタイム対ツーリングLLMの区別、3つの表面の統合、単一の真実の源パターン、これらのフックと組み合わせるXcode統合のためのTwo MCP Servers、iOSロック画面状態マシンのためのLive Activities、Apple WatchのwatchOSランタイム契約、フレームワーク基盤のためのSwiftUI内部、visionOSシーンのためのRealityKitの空間メンタルモデル、永続性のためのSwiftDataスキーマ規律、ビジュアル層のためのLiquid Glassパターン、デバイス間のリーチのためのマルチプラットフォーム出荷。ハブはApple Ecosystem Seriesにあります。AIエージェントを伴うより広範なiOSコンテキストについては、iOS Agent Developmentガイドをご覧ください。
FAQ
Claude Codeフックとは何で、iOS開発でなぜ重要なのか
Claude Codeフックは、ライフサイクルイベント(PreToolUse、PostToolUse、UserPromptSubmit、SessionStart、Stop)で実行される決定論的なシェルスクリプトです。iOS開発では、エージェントの破壊的操作に対する権限を限定します。シミュレーターの消去、コード署名、プロジェクトファイルの変更、強制プッシュなどです。フックがなければ、エージェントは開発者のマシン全体の権限を持ちます。フックがあれば、特定の危険なアクションには明示的な承認が必要になります。
iOS開発者が優先すべきフックイベントはどれか
破壊的なコマンド(simctl erase、xcodebuild archive、git push --force)をブロックするためのBashに対するPreToolUse。.pbxprojの整合性を検証するためのEdit/Writeに対するPostToolUse。グリーンビルドでゲートするためのStop。アクティブなフック構成をログに記録するためのSessionStart。これら4つで、最も一般的なiOS固有のエージェント障害をキャッチできます。
終了コード0、1、2の違いは何か
Exit 0はアクションを許可し続行します。Exit 2はイベントによって動作が異なります。PreToolUseでは提案されたアクションをブロックします(ツールは実行されません)。PostToolUseでは、ツールがすでに実行されているためブロックできませんが、stderrをエージェントに返してエージェントが修復またはリバートできるようにします。Stopでは、エージェントがセッションを終了するのを防ぎます。Exit 1はエラーをログに記録しますが、ほとんどのフックイベントでブロックしません。実行前に実際にアクションを防ぐ必要のある安全パターンの場合は、PreToolUseでexit 2を使用してください。破壊的書き込み後の検証の場合は、PostToolUseでexit 2を使用して失敗をエージェントに返します。すべてのフックを意図的に失敗する入力でテストし、特定のイベントで期待通りに動作することを確認してください。
フックスクリプトはどこに置くべきか
プロジェクトルートの.claude/hooks/*.shに置き、.claude/settings.jsonから相対パスで参照します。プロジェクトの残りの部分とともにバージョン管理し、コードレビューを行います。~/.claude/settings.jsonのユーザースコープのフックも機能しますが、プロジェクト固有のiOSゲーティングには誤った粒度です。
フックはコードレビューの必要性を置き換えるのか
いいえ。フックは構造的なエラー(破損したプロジェクトファイル、危険なbash、壊れたビルド)が出荷される前にキャッチします。コードレビューは意味論的なエラー(ロジックバグ、欠落したマイグレーション、弱いテスト)をキャッチします。2つの層は互いに補完します。フックは内側のループでエージェントを安全にデプロイし、コードレビューは境界でエージェントの出力を誠実に保ちます。
References
-
Anthropic, “Claude Code reference: Hooks”. Lifecycle events (
PreToolUse,PostToolUse,UserPromptSubmit,SessionStart,Stop), matcher syntax, command shape, and the role of exit codes. Exit 2 behaves differently per event: onPreToolUseandStopit blocks the action; onPostToolUseit cannot block (the tool already ran) but it surfaces stderr back to the agent. Exit 0 allows; exit 1 generally logs but does not block. Author’s analysis in When the LLM Lives in Your App vs in Your Tooling covers the runtime-vs-tooling LLM trust posture that hooks operationalize. ↩↩ -
Apple,
plutil(1)man page. The-lintflag validates property list syntax across old-style ASCII, XML, and binary formats. It detects parse-level breakage but does not check Xcode-specific semantics like build phase references or UUID validity within the project graph. ↩ -
Apple Developer, “xcodebuild Destination Specifier” and the
xcodebuildman page. The-destination 'platform=...,name=...'syntax is the canonical way to pin a build target; CI environments override the simulator name via env vars or scripted device-availability detection. ↩