前回は、API がトークンから役割を判定し、Claude Code の環境の Network secrets にトークンを持たせる形を作りました。

今回は、AI への「指示書」の分け方です。Claude Code は、リポジトリの CLAUDE.md(=AI への指示を書いたファイル。セッションの始めに毎回読み込まれる)に従って動きます。当社は最初、全役割の決まりを1つの CLAUDE.md に書いていて、それが「見えすぎる」「重い」の原因の1つになっていました。
「AI に守ってほしいことを CLAUDE.md にどんどん書いていたら、長くなりすぎて、どこに何が書いてあるか分からなくなった…」
結論から言うと、CLAUDE.md は「最初に何をするか」だけを書いた入口にし、役割ごとの決まりは API が「共通+その役割」の分だけ返す 形にしました。権限と同じく、指示書も役割ごとに分け、テストで守ります。
📰 出典:How Claude remembers your project(Claude Code Docs)
最初の CLAUDE.md で困ったこと
一体型のリポジトリでは、直下に CLAUDE.md が1つありました。2026年10月27日の時点で約5,000バイト(日本語で約1,700文字)、「必ず従う共通設定」が12項目です。
12項目のうち、ブログの記事作成に関係するのは2〜3項目だけでした。残りは営業のもの(見積もりの単価、Drive へのアップロード、契約書のひな形、案件への応募、問い合わせへの返信、メールの差出人など)です。記事の作り方の本体は、ブログごとの資料(技術ブログの指示書が約33KB、マニュアルが約26KB)にありました。
| 困ったこと | 中身 |
|---|---|
| 毎回全部読む | どのセッションでも、全役割の決まりを読み込む(第1回の「使用料が増えていた見込み」の一因) |
| 場所を知ってしまう | ブログのセッションが、営業の決まりの中にあるファイル(単価など)の場所を知ってしまう |
| 1つ足すと全体に効く | 営業のための決まりを1つ足すと、ブログのセッションの動きまで変わりうる |
分け方:3つの置き場所
| 置き場所 | 中身 | 読める役割 |
|---|---|---|
リポジトリの CLAUDE.md | 入口だけ(最初に指示書を取得する、このリポジトリを直すときの決まり) | 全員 |
instructions/_common/ | 全役割の共通の決まり(最初にやること、してはいけないこと、書き方) | 全役割 |
instructions/<役割>/ | その役割の入口だけ(できること、できないこと、従う資料) | その役割だけ |
セッションの最初に指示書を取得するコマンドを実行すると、API が「共通 → 自分の役割」の順につないだ文章を返します。役割は API がトークンから判定するので、ほかの役割の指示書は読めません。
当社の社内基盤での大きさ(2026年10月31日)は、共通が約3.2KB、技術ブログの役割が約2.1KB、営業の役割が約3.4KB です。セッションが読むのは「共通+自分の役割」だけになりました。役割の指示書には中身を書かず、「記事を作るときは blogs/<サイト>/docs/指示書.md に従う」のように資料を案内するだけにしたので、資料の本体は今までのものをそのまま使えました。
CLAUDE.md を入口だけにする
サンプルの CLAUDE.md です。プロジェクトの直下に置きます。
CLAUDE.md
# このリポジトリでの作業ルール
渡された文章を WordPress に投稿する中継の API(連載「Claude CodeとAWSで作るAIエージェントの土台」のサンプル)。
## 役割のセッションとして使うとき
- 最初に `python3 tools/agent.py instructions` を実行し、返ってきた指示書(共通+自分の役割)に従う
- 役割ごとの決まりはここに書かない。`instructions/` に書き、API が役割に合うものだけを返す
## このリポジトリを直すとき
- 投稿できるサイトの決まりは `app/roles.py` の1か所にまとめる
- 役割を足したら、`instructions/<役割>/` に指示書を置く(テストで確かめる)
- 変更したら `pip install -e ".[dev]" && ruff check . && pytest -q` を通す
- トークン・パスワード・キーをコード・ファイル・会話に書かない
「役割のセッションとして使うとき」は最初に指示書を取得すること、「このリポジトリを直すとき」は開発の決まりだけを書きます。役割ごとの決まり(どのサイトにどう投稿するか)は書きません。
役割ごとの指示書を置く
mkdir -p instructions/_common instructions/blog-sample
全役割の共通の指示書です。
instructions/_common/00_共通.md
# 共通の指示(全役割)
このセッションは、AI エージェントの土台の「役割」の1つとして動いている。役割ごとにできることが決まっていて、範囲の外は API が断る。
## 最初にやること
1. `python3 tools/agent.py me` で自分の役割を確かめる
2. `python3 tools/agent.py instructions` でこの指示書(共通+役割)を読む(今読んでいるもの)
## してはいけないこと
- トークン・パスワード・キーを探さない・表示しない・ファイルに書かない(トークンは Network secrets が付ける。WordPress のパスワードは API が付ける)
- 役割の範囲の外のことを試さない。必要なら、使う人に伝える
- 使う人に頼まれていないのに公開しない(ふだんは下書きにする)
## 書き方
- 説明は日本語。技術用語は言い換えず、そのまま書いて説明を括弧で足す
役割 blog-sample の指示書です。フォルダ名は app/roles.py の役割の名前と同じにします。
instructions/blog-sample/10_blog-sample.md
# 役割:ブログ sample
サンプルのブログに記事を投稿する。
## できること
- サイト `sample` に載せる記事の本文を書き、ファイルに保存する
## できないこと
- ほかのサイトへの投稿 → そのサイトの役割のセッションで行う
投稿のコマンドは次回作るので、今は「本文を書いて保存する」までにしています。
API で「共通+自分の役割」だけを返す
app/main.py に次の変更を加えます(+ が追加、- が削除)。
--- a/app/main.py
+++ b/app/main.py
@@ -2,6 +2,7 @@
GET /healthz 起動確認(トークン不要)
GET /me 自分の役割
+ GET /instructions 共通+自分の役割の指示書(セッションの最初に読む)
起動:uvicorn --factory app.main:create_app
"""
@@ -10,6 +11,7 @@
from pathlib import Path
from fastapi import Depends, FastAPI
+from fastapi.responses import PlainTextResponse
from .auth import Principal, TokenStore, get_principal
from .settings import Settings
@@ -31,4 +33,12 @@
def me(p: Principal = Depends(get_principal)):
return {'role': p.role}
+ @app.get('/instructions', response_class=PlainTextResponse)
+ def instructions(p: Principal = Depends(get_principal)):
+ """共通 → 役割の順に、指示書を1つの文章にして返す。ほかの役割の指示書は返さない"""
+ base = Path(settings.instructions_dir)
+ files = sorted((base / '_common').glob('*.md')) + sorted((base / p.role).glob('*.md'))
+ return '\n\n'.join(f'<!-- {f.relative_to(base)} -->\n' + f.read_text(encoding='utf-8') for f in files) \
+ or f'({p.role} の指示書はまだありません)'
+
return app
ファイルの名前の順(00_、10_)に並べ、共通 → 役割の順につなぎます。それぞれの先頭に <!-- _common/00_共通.md --> のような印を付けて、どのファイルの内容かが分かるようにしています。役割のフォルダは p.role(トークンから判定した役割)だけを見るので、呼び出し側がほかの役割の名前を指定する方法はありません。
コンテナにも指示書を入れます。Dockerfile に次の変更を加えます。
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,6 +3,7 @@
COPY pyproject.toml ./
COPY app ./app
COPY config ./config
+COPY instructions ./instructions
RUN pip install --no-cache-dir . && useradd -r -u 10001 api
USER api
EXPOSE 8080
セッションから取得するコマンドにも足します。tools/agent.py に次の変更を加えます。
--- a/tools/agent.py
+++ b/tools/agent.py
@@ -2,6 +2,7 @@
"""Claude Code のセッションから API を呼ぶ小さなコマンド。
python3 tools/agent.py me 自分の役割
+ python3 tools/agent.py instructions 共通+自分の役割の指示書(セッションの最初に読む)
接続先は環境変数 TH_API_URL。トークンは Claude Code の環境の Network secrets が付ける。
手元で試すときだけ、環境変数 TH_TOKEN を使う。
@@ -30,7 +31,7 @@
def main():
ap = argparse.ArgumentParser()
- ap.add_argument('cmd', choices=['me'])
+ ap.add_argument('cmd', choices=['me', 'instructions'])
print(get('/' + ap.parse_args().cmd))
指示書をテストで確かめる
指示書は文章ですが、テストで確かめられることがあります。当社の社内基盤で確かめていることを元に、サンプルでは次の6つを確かめます。
# tests/test_instructions.py
"""役割の指示書:役割ごとにあること、ほかの役割の場所を案内しないこと、API が自分の分だけ返すこと。"""
import json
import re
from pathlib import Path
from conftest import bearer
from app.roles import ROLE_SITES
ROOT = Path(__file__).resolve().parents[1]
INSTR = ROOT / 'instructions'
SITES = {k for k in json.loads((ROOT / 'config' / 'sites.json').read_text(encoding='utf-8')) if not k.startswith('_')}
def test_every_role_has_instructions_and_nothing_else():
dirs = {p.name for p in INSTR.iterdir() if p.is_dir()}
assert dirs == set(ROLE_SITES) | {'_common'}
for d in dirs:
assert list((INSTR / d).glob('*.md')), d
def test_role_instructions_only_point_to_its_own_sites():
for role, allowed in ROLE_SITES.items():
text = '\n'.join(f.read_text(encoding='utf-8') for f in (INSTR / role).glob('*.md'))
for site in re.findall(r'--site\s+(\w+)', text):
assert site in allowed, f'{role} の指示書が投稿できないサイト {site} を案内している'
def test_common_instructions_do_not_mention_any_single_site():
text = '\n'.join(f.read_text(encoding='utf-8') for f in (INSTR / '_common').glob('*.md'))
for site in SITES:
assert f'--site {site}' not in text
def test_scripts_named_in_instructions_exist():
text = '\n'.join(f.read_text(encoding='utf-8') for f in INSTR.rglob('*.md'))
text += (ROOT / 'CLAUDE.md').read_text(encoding='utf-8')
for script in set(re.findall(r'tools/[\w./-]+\.py', text)):
assert (ROOT / script).is_file(), script
def test_api_returns_common_then_own_role_only(prod):
assert prod.get('/instructions').status_code == 401
text = prod.get('/instructions', headers=bearer('th_ok')).text
assert text.index('共通の指示') < text.index('役割:ブログ sample')
other = prod.get('/instructions', headers=bearer('th_other')).text # 指示書の無い役割
assert '共通の指示' in other and 'ブログ sample' not in other
def test_claude_md_stays_an_entrance():
"""CLAUDE.md は入口だけ。役割ごとの決まり(サイトへの投稿の手順)は書かない"""
text = (ROOT / 'CLAUDE.md').read_text(encoding='utf-8')
assert 'tools/agent.py instructions' in text
assert '--site' not in text and len(text.encode('utf-8')) < 3000
| テスト | 確かめること |
|---|---|
test_every_role_has_instructions_and_nothing_else | すべての役割に指示書のフォルダがあり、役割でないフォルダはない |
test_role_instructions_only_point_to_its_own_sites | 役割の指示書が、その役割で投稿できないサイトを案内していない |
test_common_instructions_do_not_mention_any_single_site | 共通の指示書に、特定のサイトの手順が書かれていない |
test_scripts_named_in_instructions_exist | 指示書と CLAUDE.md が案内するスクリプトが、リポジトリにある |
test_api_returns_common_then_own_role_only | API が共通 → 自分の役割の順に返し、ほかの役割の分を返さない |
test_claude_md_stays_an_entrance | CLAUDE.md が入口のまま(役割ごとの手順を書かない、大きくしすぎない) |
最後のテストは、CLAUDE.md が3,000バイトを超えたら失敗します。「便利だから」と CLAUDE.md に決まりを書き足していくと、すぐに元の一体型に戻ってしまうためです。
実際のつまずき
当社の社内基盤で、指示書を分けたあとに起きたことです。
- 共通の指示書の書き方の決まりが、新しい表記のルールと逆だった。共通の指示書に「専門用語は言い換えるか短く説明する」と書いていましたが、あとで「技術用語は言い換えず、説明を括弧で足す」というルールができました。テストでは気付けない種類の食い違いで、表記のルールを共通の指示書に入れるときに見つけて直しました
- 指示書が案内するスクリプトが、土台のリポジトリに無かった。役割の指示書が「このスクリプトで投稿する」と案内していたのに、スクリプトは古い一体型のリポジトリにしか無く、営業のセッションの AI がそのリポジトリを丸ごと取り込もうとしました(第7回で詳しく扱います)。スクリプトを移し、「案内するスクリプトがあるか」のテストを足しました
- 技術ブログの指示書が、連載のサンプルコードのために一体型のリポジトリを使うよう案内していた。サンプルコードを別のリポジトリに移し、指示書を「2つ目のリポジトリとして選ぶ」に直しました
- 共通の指示書が「表記のルールの語のリストを取得する」と案内していたのに、取得のコマンドがそのフォルダを取得しない作りだった。別のセッションが見つけて直しました。指示書の案内と、道具の実際の動きを、あわせて確かめる必要があります
表記のルールは、最初は「汎用のナレッジ」に置く案でしたが、ナレッジは技術ブログと営業しか読めません。全役割が記事や資料を書くので、共通の指示書に置きました。
残っている課題
役割のセッションは土台のリポジトリを選んで始めるので、土台のリポジトリの CLAUDE.md(開発者向けの決まり。約4.7KB・48行)も毎回読み込みます。「AWS のアクセスキーを使わない」「本番のタグを作らない」など、役割の作業には関係の薄い内容です。今は小さいので問題にしていませんが、大きくなったら、開発者向けの決まりを別の場所に移すことを考えます。
動作確認
pip install -e ".[dev]"
ruff check .
pytest -q
テストが10件通れば、指示書の決まりが守られています。続いて、API から指示書を取得します(前回と同じく、手元でトークンを発行して起動します)。
python3 tools/issue_token.py issue --local .tokens.local.json --role blog-sample
STAGE=staging uvicorn --factory app.main:create_app --port 8080
別のターミナルで、<トークン> を発行したものに置き換えて実行します。
TH_API_URL=http://localhost:8080 TH_TOKEN=<トークン> python3 tools/agent.py instructions
<!-- _common/00_共通.md -->
# 共通の指示(全役割)
…
<!-- blog-sample/10_blog-sample.md -->
# 役割:ブログ sample
…
試しに instructions/blog-sample/10_blog-sample.md に --site other と書いて pytest -q を実行すると、「投稿できないサイト other を案内している」と失敗します。確かめたら元に戻してください。
つまずきやすい点・セキュリティ上の注意
- 指示書は「お願い」であって、守りではない:指示書に「営業の資料を読まない」と書いても、読める場所にあれば読めてしまいます。守りは API の権限(第1回・第5回)で行い、指示書はその範囲の中での進め方を書くものです
- 役割の指示書に、ほかの役割の場所を書かない:「できないこと」の説明でも、ほかの役割の資料の場所は書かずに「そのセッションで行う」とだけ書きます
- CLAUDE.md を大きくしない:決まりを足したくなったら、まず「どの役割に必要か」を考え、共通か役割の指示書に置きます
発注者向けメモ
AI のコーディングツールや AI エージェントを使う開発会社・社内のチームには、次を確認すると運用が安定します。
- 「CLAUDE.md(や、それに当たる AI への指示書)には何を書いていますか。大きさはどのくらいですか」
- 「役割や担当ごとに、AI が読む指示書を分けていますか。ほかの担当の情報が見えませんか」
- 「AI に守らせたいことのうち、指示書だけに頼っているものはどれですか。権限で止めているものはどれですか」
- 「指示書が案内する場所やスクリプトが実在するかを、確かめる仕組みはありますか」
役割の数が多い、指示書を社外の人も直す、指示書の変更に承認を挟む、といった条件があると、工数が増えやすくなります。
まとめと次回予告
- CLAUDE.md は入口だけにし、役割ごとの決まりは API が「共通+その役割」の分だけ返す
- 役割の指示書には中身を書かず、従う資料を案内する。資料の本体は今までのものを使える
- 指示書もテストで守る(役割ごとにあるか、ほかの役割の場所を案内していないか、案内するスクリプトがあるか)
次回は、既存のリポジトリからの移行 です。移す前の秘密の値の検査、値の置き場の決め方、そして WordPress への投稿の中継を作ります。
この連載の記事一覧
この記事は連載「Claude CodeとAWSで作るAIエージェントの土台」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【Claude CodeとAWSで作るAIエージェントの土台 第1回】なぜリポジトリをやめて、役割ごとの API にしたのか
- 【Claude CodeとAWSで作るAIエージェントの土台 第2回】AWS を4つのアカウントに分け、Google Workspace でログインする
- 【Claude CodeとAWSで作るAIエージェントの土台 第3回】GitHub Actions から AWS へ入る(OIDC)と、sub が番号付きになる落とし穴
- 【Claude CodeとAWSで作るAIエージェントの土台 第4回】ECS Fargate と ALB を NAT なしで小さく始める:証明書・DNS の落とし穴
- 【Claude CodeとAWSで作るAIエージェントの土台 第5回】トークンで役割を判定し、Claude Code の環境に持たせる(Network secrets)
- 【Claude CodeとAWSで作るAIエージェントの土台 第6回】CLAUDE.md は入口だけにする:役割ごとの指示書を API で配る(この記事)
- 【Claude CodeとAWSで作るAIエージェントの土台 第7回】既存のリポジトリから移す:秘密の値の検査と、WordPress への投稿の中継
- 【Claude CodeとAWSで作るAIエージェントの土台 第8回】守りと費用:401 の監視、WAF の回数制限、確認用はふだん止める









