MENU

問い合わせ


    【Claude CodeとAWSで作るAIエージェントの土台 第6回】CLAUDE.md は入口だけにする:役割ごとの指示書を API で配る

    前回は、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_onlyAPI が共通 → 自分の役割の順に返し、ほかの役割の分を返さない
    test_claude_md_stays_an_entranceCLAUDE.md が入口のまま(役割ごとの手順を書かない、大きくしすぎない)

    最後のテストは、CLAUDE.md が3,000バイトを超えたら失敗します。「便利だから」と CLAUDE.md に決まりを書き足していくと、すぐに元の一体型に戻ってしまうためです。

    実際のつまずき

    当社の社内基盤で、指示書を分けたあとに起きたことです。

    1. 共通の指示書の書き方の決まりが、新しい表記のルールと逆だった。共通の指示書に「専門用語は言い換えるか短く説明する」と書いていましたが、あとで「技術用語は言い換えず、説明を括弧で足す」というルールができました。テストでは気付けない種類の食い違いで、表記のルールを共通の指示書に入れるときに見つけて直しました
    2. 指示書が案内するスクリプトが、土台のリポジトリに無かった。役割の指示書が「このスクリプトで投稿する」と案内していたのに、スクリプトは古い一体型のリポジトリにしか無く、営業のセッションの AI がそのリポジトリを丸ごと取り込もうとしました(第7回で詳しく扱います)。スクリプトを移し、「案内するスクリプトがあるか」のテストを足しました
    3. 技術ブログの指示書が、連載のサンプルコードのために一体型のリポジトリを使うよう案内していた。サンプルコードを別のリポジトリに移し、指示書を「2つ目のリポジトリとして選ぶ」に直しました
    4. 共通の指示書が「表記のルールの語のリストを取得する」と案内していたのに、取得のコマンドがそのフォルダを取得しない作りだった。別のセッションが見つけて直しました。指示書の案内と、道具の実際の動きを、あわせて確かめる必要があります

    表記のルールは、最初は「汎用のナレッジ」に置く案でしたが、ナレッジは技術ブログと営業しか読めません。全役割が記事や資料を書くので、共通の指示書に置きました。

    残っている課題

    役割のセッションは土台のリポジトリを選んで始めるので、土台のリポジトリの 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回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      目次