Yyatmita

【第3回】エージェントの設定を理解する——Write vs Edit の罠と権限モデルの全貌

Claude Code カスタムエージェントの frontmatter 設定項目、権限モデルの罠(Write vs Edit)、同期/非同期の挙動差、パス記法、ホットリロードを実験で検証した記録

Claude Codeサブエージェント検証#claude-code#subagent#ai#automation
← 前の記事: 【第2回】エージェントの性質を検証する——戻り値・並列・同期非同期・ネストの実験記録

前回は、エージェントの「性質」を実験で確かめました。戻り値はテキストのみ、並列実行はツールコールを並べるだけでほぼ完全並列、バックグラウンドでは権限が厳しくなる、ネスト呼び出しは不可(追実験ではハングせず即エラーになるよう改善)——そしてファイル出力パターン(結果をファイルに書き出してパスだけ返す)という実用的な手法を見つけました。

あのパターンを実運用に使おうとした瞬間、壁が来ました。

エージェントに /tmp/ へ書き出させようとしたら、権限拒否で失敗。原因を調べようとしたら settings.local.json のルール名の話になり、同期と非同期の挙動が違うという話になり、気づいたら丸一日が溶けていた。

今回はこの体験をそのまま書きます。設定ファイルの各項目を一つずつ確認して、権限モデルの全体像を明らかにします。第1回・第2回を読んでいない方でも「Write vs Edit の罠」だけは持って帰ってもらえるよう書きました。

カスタムエージェント定義の設定項目

前回の実験中に、設定項目について聞きました。

私: agent の md での設定項目は?

Claude Code が全フィールドの一覧を出してくれました。実際の neta-research-agent.md を例に、私が使った項目を見ていきます。

---
name: neta-research-agent
description: yatmita の記事ネタを Obsidian ナレッジベースから探す
tools: Bash, Read, Glob, Grep, Write
maxTurns: 20
---

name

Agent ツールの subagent_type に指定する識別名です。ここで指定した名前が、呼び出し時のキーになります。

description

私: ところでサブエージェントの発火はスキルみたいに description に依存するの?

Claude Code: いいえ。サブエージェントは自動発火しません。ただし description を見て Claude が「このタスクはあのエージェントに委譲すべき」と判断する可能性はあります。確実に使わせたいなら、スキルのプロンプト内で Agent ツールを呼ぶよう指示するのが正解です。

単なる説明文ではなく、Claude が委譲判断の材料にする——第4回で紹介するスキル→エージェント委任パターンの自動トリガーにもなります。

tools

このエージェントが使えるツールのリスト。上限を絞る方向のみ機能します。許可を広げることはできません。

私: そのsettingsはセッション全体じゃないの?

Claude Code: はい、その通りです。settings.local.jsonpermissions.allow もセッション全体に適用されます。サブエージェント単位で権限を制御する仕組みはありません。エージェント YAML の tools: は「使えるツールの上限」を絞るだけで、許可を広げる方向には使えない。

ReadGrep だけにすれば読み取り専用エージェントになります。* と書くと「全ツール」ですが、Agent ツールだけは常に除外されます——これは第2回のネスト実験で痛感しました。

model

inherit(親と同じモデルを使う)か、claude-haiku-4-5-20251001 などで明示指定できます。Explore エージェントが Haiku で動くのはスピード重視の設計判断。コスト削減や速度優先のサブタスクに明示指定するのも一つの戦略です。

maxTurns

ターン上限。第2回で1時間ハングした後に追加した安全弁です。

私: なるほどね、設定しときましょう。

Claude Code: maxTurns: 20 を追加しました。前回の実験でツール使用が15〜17回だったので、20あれば十分余裕がありつつ、暴走も防げます。

設定しないと理論上永遠に動き続けます。複雑なタスクほど余裕を持った値に設定を。

ここで紹介したのは私が実際に使った項目だけです。公式ドキュメントの Configure subagents には、permissionMode(権限モード)、hooks(ライフサイクルフック)、memory(永続メモリ)、isolation(worktree 分離)など、さらに多くの設定項目が記載されています。

Write vs Edit の罠——丸一日ハマった話

権限ルール名と実際のツールの対応——Edit ルールが Write ツールもカバーする。Write(...) は無効なルール名

ここが今回の核心です。

発端は、第2回で紹介したファイル出力パターンを非同期で使おうとしたことでした。

neta-research-agent を run_in_background: true で起動して、結果を /tmp/ に書き出させる。前回の実験で同期実行ではうまくいっていたので、非同期でも同じはずだと思っていました。

非同期で失敗

汎用エージェントとカスタムエージェントを並列で非同期起動し、Write ツールで /tmp/ に結果を書き出す指示をしました。

両方とも権限拒否で失敗。

Claude Code: 両方とも完了しましたが、どちらもファイル書き出しで権限拒否されました。カスタムエージェントの tools: Write は「使えるツール一覧」であって「自動承認」ではない。

同期では成功

私: それほんとに? サブエージェントが書き込みできないならできることがすごく限られるんだけど。

Claude Code が同期で Write テストをしてくれました。成功しました。 しかも承認プロンプトが表示されて、手動で「許可する」を選んだら動いた。

「同期なら動く、非同期なら動かない」——この謎が最初の手がかりになりました。

settings.local.json に Write 権限を追加

私: skip-dangerous なんとかを渡せないの?

Claude Code: --dangerously-skip-permissions はセッション起動時の CLI フラグです。サブエージェント単位では渡せません。サブエージェントで Write を通したいなら、settings.local.json にパターンを追加するのが現実的です。

「非同期では事前許可が必要なはず」と考えて、settings.local.json に以下を追加しました。

{
  "permissions": {
    "allow": [
      "Bash(kbsearch:*)",
      "Write(//tmp/neta-*)"
    ]
  }
}

セッションを再起動して、非同期で再実行。

また失敗。 権限を追加したのに効かない。

ここで本格的に混乱しました。設定は書いた。再起動もした。なぜ動かないのか。

Write → Edit に変更して解決

何度か再起動を繰り返して試行錯誤した後、Claude Code がドキュメントを読み直して一行の記述を見つけました。

Claude Code: 見つけました。WriteEdit と同じカテゴリで、Edit ルールの説明に「Edit rules apply to all built-in tools that edit files」とあります。つまり WriteEdit ルールでカバーされる。Write(...) というルール名は存在しない。

公式ドキュメントでもパーミッションルール名は Bash, Read, Edit, WebFetch, MCP, Agent の6つだけが定義されており、Write は含まれていません。

設定を変更しました。

{
  "permissions": {
    "allow": [
      "Bash(kbsearch:*)",
      "Edit(//tmp/neta-*)"
    ]
  }
}

セッション中にファイルを更新して、即座に非同期でエージェントを起動しました。

成功。

Claude Code: 完璧です。Write(//tmp/neta-*) は失敗、Edit(//tmp/neta-*) は成功。公式ドキュメントの通り「Edit rules apply to all built-in tools that edit files」。しかもホットリロードも効いた。

3つの実験を経て、ようやく原因が確定しました。Write(...) は無効なルール名。ファイル書き込み権限は Edit(...) で設定する。シンプルすぎて、かえって見えなかった。

同期と非同期で権限の挙動が変わる理由

「なぜ同期では動いたのか」の種明かしをします。

同期実行では、エージェントが未許可のツールを使おうとすると、ユーザーに承認プロンプトが表示されます。「このツールの使用を許可しますか?」と聞いてくる。そこで「はい」と答えれば、たとえ settings.local.json の設定が間違っていても動く。

非同期実行では、バックグラウンドで動くため、ユーザーに承認プロンプトを出すことができません。事前に settings.local.json で許可されているツールしか使えない。未許可のツールは自動拒否されます。

同期で「成功した」のは、手動承認のおかげでした。権限設定が正しかったわけではない——設定は最初から間違っていた。同期の対話的承認がエラーを隠していた。

「テストでは動いたのに、本番(非同期)では動かない」という罠がここにあります。

同期で動作確認をして「OK」と判断して、非同期に切り替えた途端に失敗する。原因は権限設定の誤りですが、同期テストの時点では問題が表れない。この罠は知っていないと、原因にたどり着くまで時間がかかります。

パス記法のもう一つの罠

settings.local.json のパスパターンには、もう一つ罠があります。スラッシュの数です。

Claude Code: 現在の Write(/tmp/neta-*)/tmp/neta-* をプロジェクトルート相対として解釈してしまっています。正しくは Write(//tmp/neta-*) です。

  • //path → 絶対パス(例: Edit(//tmp/scratch.txt)/tmp/scratch.txt
  • /path → プロジェクトルート相対(例: Edit(/tmp/**)<project>/tmp/**

スラッシュ1つと2つで、マッチする場所が完全に変わります。

最終的に採用した Edit(/tmp/**) は、プロジェクト相対です。<project>/tmp/ 以下にマッチします。

私: 揮発の問題があるから永続化してる projects/tmp とかでどう?

Claude Code: いいですね。プロジェクト内なら gitignore に入れておけば安全で、かつ揮発しない。

理由は「永続性」です。システムの /tmp はOSの再起動で消えます。プロジェクト内の tmp/ は消えない。.gitignoretmp/ を追加しておけば、バージョン管理もしない。エージェント専用のスクラッチ領域として、揮発せず、リポジトリを汚さず、権限設定もシンプルになります。

<project>/
  tmp/              # gitignore 済み、エージェントが自由に使える
  .gitignore        # tmp/ を追加
  .claude/
    settings.local.json   # Edit(/tmp/**) を常時許可
    agents/
      neta-research-agent.md

Edit(/tmp/**) の1行が、エージェント専用のスクラッチ領域を安全に開放します。

ホットリロードは正常に機能する

私: ホットリロードってウェイト必要?

Claude Code がパターンを追加して即座に非同期テストを走らせました。

ウェイト不要。即時反映されました。

Claude Code: つまり settings.local.json のホットリロードは正常に機能する。今までの失敗は全て Write(...) という無効なルール名が原因だった。

ネット上に「ホットリロードが効かない」という報告がいくつかあります。同じように悩んでいた形跡があった。でも今回の経験からすると、ほぼ間違いなく Write(...) という無効ルール名を使っていたことが原因だと思います。設定ファイル自体は即時反映されている。ただし内容が間違っているから「効いていない」ように見えていた。

第1回で「セッション中に作成したエージェントは再起動が必要」と書きました。これはエージェント定義(.claude/agents/<name>.md)の認識に再起動が要るということです。権限設定(settings.local.json)の変更はホットリロードが効く。

この違いは実用上の意味があります。「コマンドで Edit(/tmp/**) を追加 → 即座にエージェント起動」のパターンが成立する。設定変更のたびにセッションを再起動する必要はありません。

追実験: CLAUDE.md はエージェントに継承される(2026-03-09 追記)

権限モデルの話と密接に関わる重要な発見がありました。

当初、エージェントは「独立したコンテキスト」で動くため、CLAUDE.md のプロジェクトルールは参照できないと考えていました。権限の事前許可を settings.local.json に書く必要があるのと同様に、プロジェクトルールもエージェント定義に手動で書き込む必要がある——そう思い込んでいました。

これは誤りでした。 tools: [](ツールなし)のテスト用エージェントで検証したところ、以下がすべてエージェントのコンテキストに自動注入されていることが確認できました:

  • ~/.claude/CLAUDE.md(グローバル設定)
  • <project>/CLAUDE.md(プロジェクトルール)
  • MEMORY.md(自動メモリ)
  • git status スナップショット

つまり、エージェント定義の中にプロジェクトの文体ガイドラインや法的表現の規約を書き写す必要はありません。CLAUDE.md に書いてあるルールは、エージェントも最初から持っています。

この発見は、次回(第4回)で触れる agent-creator の問題——「CLAUDE.md のルールまでエージェント定義に書き込もうとする」——の解決にもつながります。agent-creator が過剰に書き込む内容の多くは、そもそも CLAUDE.md から自動で継承されているため不要なのです。

一方、settings.local.json の権限設定(permissions.allow)はツール実行時の動的チェックなので、これは引き続き明示的に設定する必要があります。ルール(CLAUDE.md)は継承される。権限(settings.local.json)は事前設定が必要。 この区別を押さえておくと、エージェント定義をシンプルに保てます。

まとめ

今回の実験で確定した事実をまとめます。

  • Write(...) は無効なルール名。ファイル書き込み権限は Edit(...) で設定する
  • 同期実行では手動承認で動くが、非同期では settings.local.json の事前許可が必須
  • 「同期で動いた」は権限が正しいことの証明にならない
  • パスはスラッシュ1つ(プロジェクト相対)と2つ(絶対パス)で意味が変わる
  • <project>/tmp/ + Edit(/tmp/**) + gitignore がエージェント用スクラッチ領域の標準パターン
  • settings.local.json のホットリロードは正常に機能する
  • CLAUDE.md・MEMORY.md はエージェントに継承される。 ルールの二重管理は不要

frontmatter の各フィールドも整理できました。description が自動トリガーの条件になること、tools は上限を絞るだけで広げられないこと、Agent ツールはネスト防止で常に除外されること。

設定ファイルの全体像が見えてきました。

次回: エージェントを構築する

第4回ではこれらの知識を使って、スキル→エージェント委任パターンを組み立てます。

スキルが「いつ・どう呼ぶか」のルールを持ち、エージェントが「何をするか」の専門知識を持つ——この関心の分離が、実運用で力を発揮します。設定の理解が、ようやく構築に変わる回です。