2014年に最初の壁紙アプリをリリースしてから、私は12年間ずっと一人でアプリを作り続けています。アーティスト・クリエイターの廣川政樹です。アート活動と並行して、累計5,000万DL超の小さなアプリ事業を一人で回しています。
Claude Code を本格導入してから、コード生成のスピードは確かに上がりました。けれど一人で複数アプリを保守していると、生成されたコードがプロジェクト全体のスタイルからわずかにずれていく違和感が、少しずつ積み重なっていきます。インデント幅、命名規則、import の順序、guard と if let の使い分け。1ファイルなら気づきますが、10ファイル20ファイルと進むうちに、後で読み返したときの「私が書いた感」が薄れていくのが気になっていました。
そこで6週間前から、Claude Code の PostToolUse Hook に SwiftLint を組み込んで、ファイル編集の直後に自動でスタイルチェックがかかる仕組みを運用しています。完璧な仕組みではありません。けれど「私らしいコードベース」を保つ意味では、想像以上に効いています。この記事は、その6週間の運用記録です。
導入の動機 — 私の手元コードを「私らしく」保ちたい
両家の祖父はともに宮大工でした。宮大工の現場では、新しく入る材木も、既存の柱や梁との「揃い」を必ず確認します。釘1本の打ち方、面取りの角度、木目の方向。完成してから見れば気づかれない部分にも、揃いの基準があります。私はコードに対しても、同じ感覚を持ち続けたいと思っています。
Claude Code が出力する Swift コードは、ベースの品質としては申し分ありません。けれど SwiftUI のクロージャ内で return を省略するか書くか、@State プロパティの並び順、コメントの書き方の細部は、プロンプトでは指示しきれない揺らぎが残ります。私は手動レビューでそれを直していましたが、複数アプリ × 数十ファイルの編集が走るとレビュー負荷が積み上がりました。
「編集直後に SwiftLint を走らせて、違反があれば Claude Code 自身に直してもらう」というのが、今回の運用設計です。リンタを CI だけで動かすのでは遅すぎます。Claude Code が編集したまさにその瞬間に走らせる必要がありました。
PostToolUse Hook の最小構成
Claude Code の Hooks は、ツール呼び出しの前後に任意のシェルコマンドを実行できる仕組みです。Edit / Write / MultiEdit の直後に SwiftLint をかけるのが、今回の目的です。
~/.claude/settings.json に PostToolUse Hook を追加します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash $HOME/.claude/scripts/swiftlint-after-edit.sh"
}
]
}
]
}
}matcher で対象ツールを絞っているのは、Read / Grep のたびに SwiftLint が走るのを避けるためです。最初はマッチャを書かずに全イベントで走らせていましたが、思考の流れまで遅くなるのに気づき、編集系だけに絞りました。
スクリプト本体は次のような最小構成です。
#!/usr/bin/env bash
# ~/.claude/scripts/swiftlint-after-edit.sh
set -euo pipefail
INPUT="$(cat)"
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')"
# Swift 以外は何もしない
[[ "$FILE_PATH" == *.swift ]] || exit 0
# プロジェクトルートを推定(最も近い .swiftlint.yml を探す)
DIR="$(dirname "$FILE_PATH")"
while [[ "$DIR" != "/" ]]; do
[[ -f "$DIR/.swiftlint.yml" ]] && break
DIR="$(dirname "$DIR")"
done
# .swiftlint.yml が見つからないプロジェクトはスキップ
[[ -f "$DIR/.swiftlint.yml" ]] || exit 0
cd "$DIR"
OUTPUT="$(swiftlint --quiet --path "$FILE_PATH" 2>&1 || true)"
if [[ -n "$OUTPUT" ]]; then
# Claude Code に違反を返す(stdout は会話に流れる)
echo "SwiftLint findings for ${FILE_PATH}:"
echo "$OUTPUT"
fijq で tool_input.file_path を取り出している部分が地味に重要です。Hooks は JSON を stdin で受け取るので、ここを間違えるとファイルパスが取れずに永遠に空振りします。私も最初の数日はこれで時間を取られました。
6週間運用してわかった3つの調整
1. .swiftlint.yml をアプリ別に分けたほうがよかった
私の手元には壁紙アプリと癒し系アプリが混在していて、UI の作り方も微妙に違います。最初は1つの .swiftlint.yml を共通で使っていましたが、線種の違うコードベースを同じルールで縛ると、片方では正しい書き方が片方では違反になる場面が出てきました。
結局、各アプリのリポジトリ直下に .swiftlint.yml を置き、上のスクリプトで「最も近い .swiftlint.yml を探す」ロジックにしました。プロジェクトごとに方言を許す方が、現実の保守には合っていました。
2. --fix を Hook 側で自動実行するのはやめた
最初は swiftlint --fix --quiet --path "$FILE_PATH" で自動修正までかけていました。確かに楽なのですが、Claude Code が編集している最中に裏でファイルが書き換わると、Claude 側の差分認識がずれます。一度、自動修正と Claude の連続編集がぶつかって、同じ修正が二度かかったことがありました。
それ以来、Hook 側では検出のみを返し、修正の判断は Claude Code 自身に任せています。違反内容を stdout で返せば、Claude は次のターンで「指摘された箇所を直しましょう」と動いてくれます。
3. 違反が多すぎる初期はノイズと割り切る
導入1週目は、過去に書いた古いファイルを編集するたびに既存違反が大量に表示され、Claude が混乱したり、本来直すべきでない箇所まで触ろうとしました。
対処として .swiftlint.yml に excluded: を増やし、only_rules: で対象ルールを絞り込みました。
# .swiftlint.yml の例
only_rules:
- trailing_whitespace
- colon
- comma
- vertical_whitespace
- opening_brace
- control_statement
- identifier_name
- line_length
excluded:
- Pods
- Carthage
- Generated
- Legacy # 10年前のコードを置いてある場所「全部のルールを入れるか、最小ルールから入れるか」は、好みが分かれるところです。私は最小から始めて、信頼できるルールを少しずつ足していく方が安心でした。
6週間で実感した4つの変化
ここまでの3つの調整を経て、運用は安定しました。手元の感覚としては、次のような変化があります。
第一に、レビュー時間が体感で半分以下になりました。SwiftLint が拾える範囲のスタイル違反は、編集ターンの中でほぼ消えるようになり、私の目視レビューは「設計判断」と「テスト観点」に集中できるようになりました。
第二に、Claude Code が出力するコードのスタイルが、回を追うごとに揃ってきました。Hook が違反を返すと Claude は次のターンで直し、その学習が同じセッション内では確実に蓄積されます。「私のリポジトリでは return を省略するスタイルだ」を、私が言葉で説明しなくても、リンタの違反から逆向きに伝わっていく感覚です。
第三に、過去の古いファイルに手を入れる際の心理的な障壁が下がりました。10年前の Objective-C 混在コードでも、Hook が「直すべき箇所」を機械的に示してくれるので、Claude に「最小差分で SwiftLint 違反だけ直してください」と頼みやすくなりました。
第四に、これは少し意外だったのですが、コミットメッセージが具体的になりました。違反が明示的に出ているので、Claude が「trailing_whitespace 4件と identifier_name 2件を修正」のような具体的なメッセージを書くようになりました。
落とし穴 — 私が踏んだもの
良かった点ばかり書いても誠実ではないので、6週間で踏んだ落とし穴も書いておきます。
swiftlint の起動オーバーヘッドは、Hook で1ファイルごとに走らせると意外と重く感じることがありました。M1 Mac では1ファイル数百ミリ秒ですが、MultiEdit で10ファイル一気に編集された直後は、明らかにテンポが落ちます。--quiet を付ける、only_rules: で絞る、excluded: を広めにとる、の3点で改善しましたが、リンタを編集のたびに走らせる以上、ゼロにはなりません。
もう一つは、Hook の標準出力が長くなると、Claude Code 側のコンテキストを地味に圧迫します。ファイル1つあたりの違反は最大10件までに絞るような後処理を、スクリプトに足しました。head -20 を最後にかぶせる程度で十分です。
次に試したいこと
この仕組みは、SwiftLint を別のリンタや型チェッカーに置き換えれば、同じ構造で他言語にも展開できます。私は次に、Python 製のスクリプト群で ruff を同じパターンで組み込もうとしているところです。
離れて暮らす子どもたちに何を残せるかと考えるたび、技術を分かりやすく書き残すことの意味を再確認します。Claude Code のように頼りになる相棒ができても、最終的に「私らしいコード」を残せるかどうかは、私自身の運用設計にかかっています。Hooks のような細い仕組みでも、長く運用すればその違いが現れます。
同じように一人でアプリ事業を続けている方、複数のリポジトリを行き来している方の参考になれば幸いです。お読みいただきありがとうございました。