コーディング規約を「読まれるドキュメント」にする — AIエージェントのSkill/hook化 📏
はじめに
「コーディング規約をちゃんと書いたのに、AIに実装させると平気で破ってくる」——AIコーディングをチームに導入して、一度はこう思った人は多いのではないでしょうか。
規約ドキュメントを整備すること自体はゴールではありません。
実装の瞬間に参照されて初めて意味がある。
ところが現実には、規約は「リポジトリのどこかにある長い Markdown」になりがちで、人間も AI も実装直前には読まない。
この記事では、397行あるコーディング規約を AI(Claude Code)に守られやすくするために、
- 規約本体(SSoT)を一箇所に集約する
- その入口となる薄い Skill を置く
- ファイル編集の直前に規約サマリを差し込む PreToolUse hook を仕込む
という3層構成にたどり着いた過程と、設計上ハマった点・避けた失敗を共有します。
考え方を伝えるためのフック用シェルスクリプトの例も載せます。
対象読者
- Claude Code(あるいは類似のAIコーディングエージェント)をチーム開発に導入している/したい人
- コーディング規約やレビュー観点を整備したのに「守られない」課題を抱えている人
- Agent Skill や hook で、AIの振る舞いをプロジェクト固有にチューニングしたい人
前提知識
- Claude Code の Skill(
SKILL.md)と hook(settings.jsonのhooks)の存在をなんとなく知っている - Markdown とシェルスクリプトが読める
環境
- Claude Code(Skill / PreToolUse hook 機能)
- Biome(Linter/Formatter。ESLint/Prettier でも考え方は同じ)
- TypeScript / React のプロジェクト
背景・課題 🤔
きっかけは、Biome で機械的に強制できない設計面のルールを1枚に集約した規約ドキュメントを整備したことでした。
内容はたとえば次のようなものです。
- 関数引数はオブジェクトで受ける(1引数でも原則オブジェクト化。例外はReactハンドラ等の外部シグネチャのみ)
-
asキャストは限定用途のみ(型エラー回避目的のキャストは禁止) - コメントは WHY を書き、WHAT は書かない
-
React 依存の有無で
hooks/とhelpers/を使い分ける(pure 関数をhooks/に置かない)
ここで方針として明確にしたのが、**「Biome で表現できるルールはドキュメントに書かず Biome に強制させ、ドキュメントには Biome で表現できないルールだけを残す」**という線引きです。
any 禁止や non-null assertion 禁止、マジックナンバーの定数化などは Biome ルールへの参照だけ残し、本文からは外しました。ドキュメントは「人間とAIが判断を要するルール」に絞る、という割り切りです。
問題はここからでした。
プロジェクトの CLAUDE.md(Claude Code がセッション開始時に読むファイル)からは、この規約はドキュメント一覧表の1行リンクとして案内されているだけ。
「実装直前に必ず読む」という強さでは全く案内されていません。
結果、Claude は規約の存在を知ってはいても、実装のたびに本文を開いて確認することはほとんどありませんでした。
規約は「存在する」だけでは守られない。実装の動線上に置かれて初めて守られる。
これは人間のオンボーディングと同じ構図です。
「wikiに書いてあるから読んでおいて」で守られるなら苦労しません🤷♀️
解決策 💡
アプローチ:規約を「実装の動線」に3層で差し込む
最終的に、強制力の異なる3つのレイヤーを重ねる構成にしました。
-
レイヤー1: SSoT(規約本体) — ルールの正は常に
コーディング規約.md1箇所だけ。コード例・例外規定・詳細はすべてここにある。 - レイヤー2: Skill(自発的な参照) — Claude が TS/TSX を触るとき、自分で「規約を読もう」と判断するための入口。
- レイヤー3: hook(強制的なリマインダー) — Claude の自発性に頼らず、編集の直前に機械的に規約サマリを差し込む。
👉 「Skill か hook か」の二択ではなく、性質が違うので両方使うのがポイントです。Skill はトリガーが緩い分カバー範囲が広く、hook は確実だが発火条件が機械的。重ねることで穴を塞ぎます。
実装1:Skill は「中身を持たない、SSoTへのポインタ」にする 🔑
最初に踏みかけた地雷がこれです。
Skill にコーディング規約の中身(具体例や例外規定)を書き写したくなる。
しかしそれをやると、規約本体と Skill の二重管理になり、必ず drift(内容のズレ)が起きます。
規約を1行直すたびに2箇所直す羽目になり、いずれ片方が腐る。
そこで Skill は 「規約名 + SSoTへのアンカーリンク」だけを持つインデックスに徹する設計にしました。
冒頭で「実装・レビューの前に必ず規約本体を読むこと」と明示し、中身は一切複製しません。
---
name: coding-rules
description: TypeScript/React のコーディング規約。*.ts / *.tsx ファイルの作成・編集・レビュー・リファクタリング時に必ず参照する。必須ルール(引数オブジェクト化、asキャスト制限、WHYコメント、hooks/helpers配置)と推奨ルール(Top-down定義順、ファイル内宣言順序、早期return)を規定。
---
# TypeScript / React コーディング規約
このファイルは規約のインデックスです。
**実装・レビューの前に必ず [docs/guidelines/コーディング規約.md](../../../docs/guidelines/コーディング規約.md) を読むこと**。
コード例・例外規定・詳細はすべて規約本体にあります。本ファイルでは重複させません。
## 必須ルール(違反は 🔴 Critical)
- **関数引数はオブジェクトで受ける** — [規約本体へのアンカーリンク](.../コーディング規約.md#引数はオブジェクトで受ける必須)
- **`as` キャストは限定用途のみ** — [規約本体へのアンカーリンク](.../コーディング規約.md#as-キャストの使用基準必須)
- **コメントは WHY を書き、WHAT は書かない** — [...]
- **React 依存の有無で hooks / helpers を使い分ける** — [...]
## 推奨ルール(違反は 🟡 Important)
- **関数の定義順序: Top-down** — [...]
- **ファイル内の宣言順序** — [...]
- **早期 return / guard clause** — [...]
ここでの設計上の工夫が2つあります。
(a) description をトリガー設計として書く。 Skill は description のマッチで起動します。だから description には「何のSkillか」ではなく「いつ起動してほしいか」を書く。*.ts / *.tsx ファイルの作成・編集・レビュー・リファクタリング時に必ず参照する と編集トリガーを明示し、さらに必須/推奨ルールのキーワードを列挙して、関連タスクでヒットしやすくしています。
(b) レビュー時の重要度判定とメタルールを埋め込む。 Skill 末尾に「必須違反は🔴Critical / 推奨違反は🟡Important」という判定基準と、
ファイル内の既存パターンが規約違反でも、新規追加コードに対しては規約を優先する(既存違反を新規違反の根拠にしない)
というメタルールを入れました。これは AI も人間も陥りがちな「周りがこう書いてあるから合わせました」を防ぐためです。規約の中身は SSoT に置きつつ、「規約をどう適用するか」という運用ルールだけは Skill 側に持たせる、という切り分けにしています。
実装2:PreToolUse hook で編集直前に強制リマインド 🪝
Skill は強力ですが、起動するかどうかは最終的に Claude の判断に委ねられます。「確実に毎回」を担保したいルールには物足りない。そこで Write / Edit / MultiEdit の直前に発火する PreToolUse hook を用意し、TS/TSX を編集するときだけ規約サマリを system-reminder として注入するようにしました。
settings.json への登録はこれだけです。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "/path/to/coding-rules-reminder.sh"
}
]
}
]
}
}
スクリプト本体は、ノイズを出さないためのフィルタリングが肝です。考え方を示すための一例として、最小構成を載せます(パスや文言は環境に合わせて調整する前提です)。
#!/bin/bash
# TS/TSX ソースを編集する直前に、コーディング規約の必須ルールを
# system-reminder として注入する PreToolUse hook。
#
# - 対象プロジェクト配下でのみ発火
# - .ts/.tsx 以外(docs, json, scss など)はスキップ
# - テスト/Storybook ファイルはスキップ(規約適用優先度が低いため)
set -euo pipefail
# 1. 対象プロジェクト配下でのみ発火(他プロジェクトに無影響にする)
case "$PWD" in
*/your-project*) ;;
*) exit 0 ;;
esac
# 2. stdin から tool_input.file_path を取得(jq に依存しないよう python3 を使用)
file_path=$(/usr/bin/python3 -c 'import json,sys; d=json.load(sys.stdin); print((d.get("tool_input") or {}).get("file_path") or "")')
# 3. .ts / .tsx 以外はスキップ
case "$file_path" in
*.ts|*.tsx) ;;
*) exit 0 ;;
esac
# 4. テスト/Storybook はスキップ
case "$file_path" in
*.spec.ts|*.spec.tsx|*.test.ts|*.test.tsx|*.stories.ts|*.stories.tsx) exit 0 ;;
esac
# 5. additionalContext を JSON で返す
cat <<'JSON'
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": "【コーディング規約リマインダー】TS/TSX を編集します。以下を満たすこと:\n- 関数引数はオブジェクトで受ける(1引数でも必須。例外はフレームワークコールバック / state setter / 型ガードのみ)\n- as キャストは限定用途のみ\n- コメントは WHY を書き、WHAT は書かない\n- React API 依存なし → helpers/ / React API 依存あり → hooks/\n- ファイル内の宣言順は import → 型 → 定数 → メイン export → 内部 helper(Top-down)\n既存ファイル内のパターンが規約違反でも、新規追加コードは規約を優先すること。"
}
}
JSON
PreToolUse hook は stdin で tool_input(編集対象の file_path など)を JSON で受け取り、hookSpecificOutput.additionalContext に文字列を返すと、それが system-reminder として Claude のコンテキストに差し込まれます。ツール実行をブロックはしない(規約違反を機械判定しているわけではない)が、編集の直前に必ず規約が目に入る、という位置づけです。
ポイント:hook 実装でハマった/気をつけた点 ⚠️
実際に運用してみて効いたフィルタリングの工夫です。
-
jqに依存しない。 stdin の JSON パースにjqを使いたくなりますが、jqは環境によって未インストールです(実際この記事を書く過程の別作業でもjq: command not foundを踏みました)。多くの環境にあるpython3でtool_input.file_pathを取り出すほうが移植性が高い。 -
対象プロジェクトの外では即
exit 0。 ユーザーグローバル(~/.claude/)に置くと全プロジェクトで発火してしまうので、$PWDがプロジェクトパスを含むときだけ動かす。 -
テスト/Storybook は除外。
*.spec.ts/*.test.ts/*.stories.ts(x)は規約適用の優先度が低く、毎回リマインドするとノイズになる。対象を「設計判断が要るプロダクションコード」に絞ることでリマインダーの価値を保ちます。 -
段階的に展開する。 いきなりリポジトリの
.claude/settings.jsonにコミットしてチーム全員に配るのではなく、まずユーザーグローバルの個人運用で「ノイズ過多にならないか」「本当に守られるようになるか」を検証中です。効果が確認できたら、リポジトリ tracked な$CLAUDE_PROJECT_DIR/.claude/hooks/に移植してチーム共有に進める段取りにしています。
hook はブロック型にもできますが、設計ルールのように「機械判定が難しく、文脈で例外もある」ものは**リマインダー型(非ブロック)**から始めるのが現実的でした。
any禁止のような白黒つくものは Biome(CI)に任せ、役割を分担します。
なぜこの3層なのか(他の選択肢との比較)🎯
検討したが採らなかった案と、その理由です。
| 案 | 採らなかった理由 |
|---|---|
CLAUDE.md に規約を全部ベタ書き |
セッション開始時に常にコンテキストを食う。TS/TSXを触らないタスクでも乗ってくる。規約本体とdriftする |
| Skill に規約の中身もコピー | SSoTが2つになり二重管理。更新漏れで腐る |
| hook だけで完結 | 発火が機械的(拡張子ベース)で、レビューや設計相談など「ファイルを編集しない場面」をカバーできない |
| Skill だけで完結 | 起動はAIの自発性頼み。「確実に毎回」を担保できない |
結局、正は1つ(SSoT)に保ちつつ、参照経路を性質の違う2系統(自発のSkill+強制のhook)で重ねるのが、管理コストと網羅性のバランス点でした。
結果 📊
- 規約本体(SSoT)は変更せず、入口(Skill)とリマインダー(hook)を足すだけで、Claude が TS/TSX 実装時に規約を踏まえる頻度が体感で大きく上がりました。
- Skill は「中身ゼロのポインタ」なので、規約を更新しても Skill 側のメンテはほぼ不要。drift しません(hook のサマリ文言だけは、規約本体を直したら追従させる運用にしています)。
- 副次的に、Skill に書いた重要度判定(🔴Critical / 🟡Important)が、AIによるコードレビューの指摘の粒度を揃えるのにも効きました。
定量評価はこれからですが、「規約が存在するのに守られない」という最初の課題に対しては、動線に置くというアプローチが明確に効いています。
まとめ 📝
この記事の要点を4つに整理します。
- 👉 ドキュメントは正の置き場所、Skill/hook は参照経路。 役割を分けると、SSoTを汚さずに参照性だけ強化できる。
- 👉 Skill は中身を持たせず SSoTへのポインタにする。
descriptionは「何のSkillか」ではなく「いつ起動してほしいか」で書く。 - 👉 hook は確実だがノイズになりやすい。 対象を絞り(拡張子・プロジェクト・テスト除外)、まずは非ブロックのリマインダー型・個人運用から段階展開する。
- 👉 機械判定できるルールは Linter/CI に、判断が要るルールは Skill/hook に。 線引きを決めると、どちらも軽くなる。
「AIが規約を守らない」のは、多くの場合 AI の問題ではなく規約が動線上に無い問題なのだと思います。
規約を書いたら、次は「実装の瞬間にどう目に入れるか」を設計する番です。
お使いのプロジェクトでも、まずは薄いポインタSkillと非ブロックのリマインダーhookから試してみるのもおすすめです🙌
おわりに ✨
AIコーディングが当たり前になるほど、「ルールをどう書くか」だけでなく「AIにどう届けるか」の設計が効いてくる、というのが今回の実感でした。
同じように「規約はあるのに守られない」と感じている方の参考になればうれしいです。
弊社ではこうした開発体験の改善にも一緒に取り組んでくれる仲間を募集しています。
興味があればぜひのぞいてみてください🙌
参考 🔗
※本記事のコード例はプロジェクト名などを一般化しています。
additionalContextの文言や除外パターンは、ご自分のプロジェクトの規約に合わせて調整してください。
Discussion