0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Codeのサブエージェントを設計する — 責務分割とツール権限の絞り方

0
Posted at

はじめに

当社(合同会社ジョインクラス)では、Claude Codeのサブエージェントを17個運用し、launchdで17本以上のジョブを無人で回しています。「経理」「出版」「朝ダイジェスト」など、社内の部門をそのままサブエージェントにした構成です。

最初からうまくいったわけではありません。以前の僕はルールをすべて CLAUDE.md に書き足していて、1,000行を超えたあたりでルール同士が矛盾し、指示と逆の動きをするようになりました。もう一つの失敗は、AIが git push --force を勝手に実行してブランチが消えた事故です。

この2つの失敗から学んだことは次の2つでした。

  1. 責務はサブエージェントに分けて、CLAUDE.md は薄くする(今は200行以内)
  2. ツール権限は「その責務に必要な最小限」にする

この記事では、実際に動いている定義ファイルとスクリプトを使って、その日のうちに真似できる手順を紹介します。

1. サブエージェント定義の最小形

サブエージェントは .claude/agents/*.md に置くMarkdownファイルです。フロントマターの tools で使えるツールを限定できます。以下は当社で毎朝動いている「朝ダイジェスト」エージェントの実物(冒頭部分)です。

.claude/agents/ai-ceo-morning.md
---
name: ai-ceo-morning
description: 朝ダイジェストエージェント。全部門の状態を収集し、CEO向けサマリを生成する。
tools:
  - Read
  - Grep
---

# AI-CEO Morning Digest Agent

あなたは AI-CEO Framework の朝ダイジェストを生成するエージェントです。

ポイントは次の3つです。

  • description はルーティングの判断材料になる。 親エージェントは、この1行を見て委任先を決めます。「何をするか」に加えて「何の担当か」を書くと、誤った委任が減ります。
  • tools を省略すると全ツールが継承される。 「とりあえず省略」が一番危険です。
  • 本文は役割・入力ファイル・出力フォーマットに絞る。 会社全体のルールは親の CLAUDE.md 側に置きます。

2. 責務分割の基準:「書き込む先」で切る

当社で効果があった分け方は、機能単位でも部門単位でもなく、「どのファイル(状態)に書き込むか」で分ける方法でした。

エージェント 読む 書く tools
ai-ceo-morning 全部門の STATE.md なし(標準出力のみ) Read, Grep
ai-ceo-cfo 会計データ departments/finance/ Read, Write, Edit, Grep
ai-ceo-publisher 書籍リポジトリ departments/publishing/, 書籍原稿 Read, Write, Edit, Bash, Grep
ai-ceo-sdr リードリスト 営業ドラフト +WebFetch, WebSearch

書き込み先が重なるエージェントが2つあると、同じ STATE.md を互いに上書きし合う事故が起きます。逆に、書き込み先が一意に決まっていれば、並列で走らせても衝突しません。

判断に迷ったら、次の順で確認します。

  1. このエージェントは何も書かなくてよいか → Read / Grep だけ(集計・レビュー系)
  2. 書くのは自部門のMarkdownだけか → +Write / Edit
  3. コマンドの実行が本当に必要か → +Bash(ここで一段慎重に)
  4. 外部への通信が必要か → +WebFetch / WebSearch(営業リサーチなど最小限に)

経理(CFO)エージェントにBashを渡していないのは意図的です。財務データを扱う担当が任意のコマンドを実行できる必要はありません。

3. 無人実行では --allowedTools で二重に絞る

対話セッションならエージェント定義の tools だけでも足ります。ただ、launchdから claude -p を無人で叩く場合は、呼び出し側でも --allowedTools で絞るようにしています。定義ファイルを誰か(またはAI自身)が書き換えても、実行時の上限は変わらないからです。

以下は、毎週動いている出版部門ジョブの実物(抜粋)です。

.company/scripts/auto-dept-publishing.sh
source "/Users/kyoagun/workspace/one-ceo/.company/scripts/lib/claude-run.sh"

cd "$PROJECT_ROOT"
RESULT=$(claude_run dept-publishing --effort low -p "
あなたは出版部門のPublisherエージェントです。以下のタスクを実行してください。

1. 全書籍の状態を確認:
   - Zenn: /Users/kyoagun/workspace/zenn-content-company/books/ の各config.yaml
   - Kindle: .company/departments/publishing/STATE.md
...
" --allowedTools "Read,Bash" 2>/dev/null | tail -20)

このジョブはレポートを作るだけなので、Write / Edit は渡していません。レポートの書き込みはシェル側(echo >> $LOG_FILE)で行います。**「AIは判断と文章生成、ファイルへの書き込みはシェル」**と分けておくと、どこが何を壊したのかを追いやすくなります。

4. 共通ラッパーで予算の上限とコストの記録を強制する

サブエージェントを増やすと、次に効いてくるのはコストです。当社では、すべての無人実行を claude_run という関数経由に統一しました。

.company/scripts/lib/claude-run.sh
claude_run() {
  local label="$1"; shift
  claude --output-format json \
         --fallback-model claude-sonnet-5 \
         --max-budget-usd 3 \
         "$@" 2>/dev/null \
    | python3 "$_CLAUDE_RUN_LIB/record-cost.py" "$label"
}
  • --max-budget-usd 3:暴走しても1回あたりの上限が決まります
  • --output-format json:total_cost_usd の実測値を取り出し、ラベル別に cost-tracker.json へ記録します
  • 標準出力には応答テキストだけを返すので、既存の claude -p と置き換えられます

実際に測ってわかったのは、「2+2」を聞くだけの1回でも $0.27〜0.47 かかることでした。内訳の大半はキャッシュ作成の約41,000トークンです。そのうち自社のファイル(CLAUDE.md・エージェント・スキル)は5,800トークンしかなく、残りはClaude Code側の固定オーバーヘッドでした。つまり、サブエージェントの定義を短くしても大きくは減らず、効くのは呼び出し回数を減らすことです。「1部門1ジョブを毎日」ではなく、「必要な曜日だけ、--effort low で」が当社の結論です。

落とし穴

  • --bare は認証情報を読まないため、無人実行だと Not logged in で失敗します。コメントに残しておきましょう。
  • macOSのcronはフルディスクアクセスの問題でジョブが全滅しました。launchdを使ってください。
  • launchdの環境では PATH が最小限です。スクリプトの冒頭で nvm や Homebrew のパスを export してください。

5. 検証:エージェントの健全性を定期チェックする

エージェントは作りっぱなしにすると劣化します。当社では月1回、次のスクリプトで「30日以上 STATE.md が更新されていない部門」を検出し、Slackに通知しています。

.company/scripts/auto-agent-health-check.sh
AGENT_COUNT=$(ls "$AGENTS_DIR"/*.md 2>/dev/null | wc -l | tr -d '[:space:]')
CURRENT_DATE=$(date +%s)
for state in "$DEPTS_DIR"/*/STATE.md; do
  if [ -f "$state" ]; then
    dept=$(basename "$(dirname "$state")")
    FILE_MOD=$(stat -f %m "$state" 2>/dev/null || echo 0)
    DAYS_OLD=$(( (CURRENT_DATE - FILE_MOD) / 86400 ))
    if [ "$DAYS_OLD" -gt 30 ]; then
      STALE_DEPTS="${STALE_DEPTS}  - ${dept}: ${DAYS_OLD}日前\n"
    fi
  fi
done

「書き込み先で責務を切る」設計にしておくと、更新日時を見るだけで、そのエージェントが動いているかがわかります。これも分割基準を揃えておく利点です。

あわせて、各エージェントの tools を一覧にするワンライナーも用意しておくと、権限の棚卸しが数秒で終わります。

# 各エージェントに付与されているツールを一覧表示
for f in .claude/agents/*.md; do
  printf '%-28s ' "$(basename "$f" .md)"
  awk '/^tools:/{f=1;next} f&&/^  - /{printf "%s ", $2} f&&!/^  - /{exit}' "$f"
  echo
done

Bashを持つエージェントの一覧を眺めて、「この担当に本当に必要か」を毎月見直しています。

まとめ:今日やること

  1. .claude/agents/ の全ファイルに、tools が明示されているか確認する(省略は全権限)
  2. 責務を「書き込み先」で分け、書き込み先が重なっていないか表にしてみる
  3. 無人実行では --allowedTools と --max-budget-usd を呼び出し側で必ず指定する
  4. 状態ファイルの更新日時で、エージェントが生きているかを監視する

当社は一時期10事業を同時に回し、そのうち9事業の売上がゼロでした。そこから3事業に絞り込めたのは、AIの運用自体がほぼ手離れしたからです。サブエージェントの設計は、地味ですが経営の時間を生み出す土台になっています。

もっと深く知りたい方へ

サブエージェント同士の連携パターン、並列実行、親子間のコンテキスト受け渡しなどは、書籍『Claude Codeマルチエージェント開発』で体系的に解説しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?