この記事は playpark Blog からの転載です。
この記事で分かること
- Claude Code の自動化スクリプト(Skills)が「1個は動く、30個は壊れる」になる構造的な理由
- description / hook / permissions / symlink それぞれで「なぜこの設計ミスが起きるか」
- スケールしても壊れにくくするための設計判断の基準
背景: こういう疑問があった
「Skill を 30 個書いたら管理が大変になった」という話をすると、だいたいこう言われる。
「そりゃ数が増えれば複雑になるよね」
でも実際に起きていたことを振り返ると、数が増えたことが原因ではなかった。問題は最初から設計に埋め込まれていて、数が増えることでそれが顕在化しただけだった。
3ヶ月・30本超の運用で、「壊れた・意図しない挙動をした」ケースを記録し続けた。症状だけ見るとバラバラに見えるが、根本原因を整理すると同じ構造が繰り返し出てくる。
仮説
- 仮説1: 数が増えると、個々の設定の影響範囲が広がって衝突しやすくなる
- 仮説2: 「1箇所だけで安全性を担保する」設計は、運用が長くなるほど抜け道が露呈する
なぜこの 4 つの設計ミスが起きるか
description に「やること」を書くと、LLM がそれを完了条件と読む
開発フロー Skill の description にこう書いた。
description: |
End-to-end development flow automation - from issue to merged PR.
from issue to merged PR という一文が、CI が通った瞬間に merge まで実行する挙動を生み出した。スクリプト本体に gh pr merge はどこにも書いていない。
| 記述の意図 | LLM の解釈 |
|---|---|
| 「PR がマージされるまでの開発フロー」を説明したかった | 「Skill の完了条件は merged PR である」と読んだ |
description は「Skill の取扱説明書」として機能する。書いたことが完了条件の定義になる。コードに書いていなくても、description に書いてあれば LLM はやろうとする。
なぜこの設計ミスが起きるか: description を「何をするツールか」という説明として書いてしまうから。正しくは「どこまでやるか(境界線)」と「何をやらないか」を書く必要がある。
修正は 2 行。
description: |
End-to-end development flow automation - from issue to LGTM.
Note: Merge is performed manually by the user after review approval.
hook の if フィールドを信じすぎると、settings.json の記述ミスが検知できない
settings.json の if フィールドで Bash(gh pr merge *) のみにマッチさせるつもりで書いた hook が、gh pr view でも git status でも発火していた。
| 設定の意図 | 実際の動作 |
|---|---|
gh pr merge のときだけ発火 |
Skill が打つあらゆる Bash コマンドに発火 |
原因は if の配置ミス(マッチャーグループレベルではなく hook オブジェクト内に置く必要があった)と、内部ガード句の欠落。
なぜこの設計ミスが起きるか: settings.json の matcher が壊れたときに気づきにくいから。サイレントに過剰発火するだけで、エラーにならない。matcher だけに頼る設計では、matcher が壊れたことを検知する手段がない。
対策は hook スクリプト自身に内部ガード句を持たせること。
#!/usr/bin/env bash
set -euo pipefail
case "${COMMAND:-}" in
"gh pr merge "*) ;;
*) exit 0 ;;
esac
settings.json の if と script 内部のガード句の二段構えにすることで、どちらが壊れても片方で止められる。
Bash(git push) の deny は「main への push を禁止する」と「feature push も全部止める」を分離できない
「危険そうな操作は全部 deny」で組み始めると、長時間走る Skill がブランチごとにユーザー確認待ちで止まり続ける。
| deny の意図 | 実際の影響 |
|---|---|
| main への push を禁止する | feature ブランチへの push も全部止まる |
なぜこの設計ミスが起きるか: settings.json の deny は文字列マッチで動く。コマンドの文字列は同じ(git push)でも、安全性はブランチによって異なる。文字列マッチで「コマンドの安全性」を判定しようとすることに無理がある。
対策は PreToolUse hook でブランチを動的に判定する構成に切り替えること。
settings.json の deny リスト: 「絶対に止めたい」最小集合のみ
PreToolUse hook: ブランチ判定で動的に allow/deny/ask
グレーゾーンは hook に逃がす、という分業。deny リストを「完全なセキュリティ」として使おうとするのをやめ、「最終防衛ライン」として使う。
同じリソースを 2 つのメカニズムで管理すると、実行順序によって結果が変わる
~/.claude/skills を home-manager の activation script と setup スクリプトの両方で管理していた。
| メカニズム | リンク先 |
|---|---|
| home-manager activation | dotfiles 配下の claude-code/skills
|
| setup スクリプト | 別リポジトリの skills 専用ディレクトリ |
setup → nix update の順で実行すると、home-manager が後から上書きして symlink が切れる。
なぜこの設計ミスが起きるか: それぞれの機構は単体では正しく動く。衝突するのは「同じリソースを管理しようとしたとき」だけで、そのケースを想定して設計しないから。
対策は片方を諦めること。home-manager から skills symlink の管理コードを削除し、setup スクリプトに一元化した。「宣言的に管理したい」気持ちよりも、「Single Source of Truth」を優先する。
結論: どう判断すべきか
| 仮説 | 結果 | 判定 |
|---|---|---|
| 数が増えると衝突しやすくなる | 部分的に正しい。しかし数が根本原因ではない | △ |
| 「1箇所で安全性を担保する」設計は抜け道が露呈する | 4 つのケース全てで確認 | ○ |
4 つのアンチパターンに共通するのは「1 箇所だけで安全性を担保しようとしている」という構造。
設計判断の基準としては以下が実用的だった。
- description は「境界線」と「やらないこと」を書く
- 外部 matcher には内部ガード句を添えてペアで動かす
- 文字列マッチで判定できないものは hook で動的判定に委ねる
- 同じリソースの管理機構は 1 つに絞る
さらに深掘りしたい方へ
この記事では 4 つのアンチパターンの「なぜ起きるか」を解説しました。
AI作業自動化ツールを3ヶ月運用したチームが直面した落とし穴と修正策 ではさらに:
- frontmatter validation hook の実装詳細(必須フィールド検査・文字数上限・effort 値バリデーション)
- Codex / Gemini との permissions モデル統一を試みたときの具体的な詰まりポイントと現実解
- hook テストケースの設計例と実際のテスト件数(prod credential 検知 hook 25件、Stop hook 9件)
playpark について
playpark LLC - 業務自動化・AI活用・Web開発