リリース前の確認を分担させていて、レビュー役の返事が妙に短くなったことがありました。
以前は「ここは根拠が示されていません」と食い下がってきた相手が、その日は「先に確認されたとおり問題ありません」と返してきたのです。指摘が減ったのではなく、指摘する立場でなくなっていた、と気づくまでに少し時間がかかりました。
原因は私の書き方ではなく、委譲の既定が変わっていたことでした。
先に結論を書きます。継承させてよいのは作業で、継承させてはいけないのは判定です
サブエージェントに親の会話を引き継がせると、前提の説明を省けます。同じファイルを読み直させる無駄も減ります。作業を任せるだけなら、これは素直に速くなります。
一方で、レビュー・再検証・監査のように「親が出した結論が正しいかを問う」仕事は、親の会話を引き継いだ瞬間に成立しなくなります。相手はもう第三者ではなく、同じ結論に至った当事者だからです。
ですので、線はエージェントの賢さではなく 役割 で引きます。
| 任せる内容 | 親の文脈 | 理由 |
| 列挙・収集・変換(アセットの棚卸し、文言の突き合わせ) | 継承してよい | 結論の独立性を必要としない。前提の再説明が純粋に無駄になる |
| 手順の実行(ビルド設定の確認、番号の整合) | 継承してよい | 判断の余地が小さく、文脈があるほど手戻りが減る |
| レビュー・批評 | 継承させない | 親の理屈を知っていると、その理屈の内側でしか読めなくなる |
| 合否の再検証 | 継承させない | 「合格と報告された」という情報自体が、検証の答えを先に渡してしまう |
8月14日に変わったのは、無名の委譲の行き先でした
Claude Code 2.1.232 で、サブエージェントのフォークが既定で有効になりました。subagent_type: "fork" のサブエージェントは、親セッションの会話全体とプロンプトキャッシュを引き継ぎます。対話セッションでの起動は、既定でバックグラウンド実行になります。
ここで見落としやすいのが、フォークが発火する条件 です。フォークになるのは subagent_type を省いて委譲したときであり、名前付きのサブエージェントを指名した呼び出しは、これまでどおり空の文脈から始まります。サブエージェントの公式ドキュメントにあるとおり、名前付きエージェントは親の会話履歴も、親が読んだファイルも見ていません。
つまり「既定が変わったので、自分のエージェントは全部継承するようになった」という理解は、実際とは逆です。影響を受けたのは、名前を指定せずに「ちょっと見ておいて」と投げていた委譲のほうでした。
私のレビュー役が追認に回ったのも、まさにその経路です。定義ファイルは置いてあったのに、実際の呼び出しでは名前を省いていました。エージェント定義を書いた時点で安心してしまい、呼び出し側が定義を経由しているかを確かめていなかった わけです。
切り替えの向きは環境変数で宣言できます。
| 設定 | 効果 |
CLAUDE_CODE_FORK_SUBAGENT=1 | 非対話実行や Agent SDK でもフォークを有効にする |
CLAUDE_CODE_FORK_SUBAGENT=0 | すべての種類のセッションでフォークを無効にする |
Agent(fork) を deny | フォーク自体は有効のまま、無名の委譲がフォークへ落ちるのを止める |
3行目が、この記事でいちばん使いどころの多い設定です。
継承させると指摘が減るのは、モデルが手を抜くからではありません
追認に変わった返答を読み返して、腑に落ちたことがあります。レビュー役は怠けていたのではなく、与えられた文脈のなかでは正しく振る舞っていた のです。
親の会話には、私が「この方針で行く」と決めた経緯と、その根拠として並べた材料がすべて入っています。その履歴を受け取った側から見れば、結論はすでに検討済みの既定事実です。そこから「この判断は誤りではないか」と切り出すには、渡された文脈そのものを疑う必要があります。それは、文脈を信頼して作業させるという継承の目的と正面から衝突します。
だから、独立性は指示文では取り戻せません。「前提を疑ってください」と書き足しても、疑うべき前提が会話として与えられている限り、出発点は動きません。
独立性を取り戻す方法は、渡さないことだけです。
継承の可否は、次の4つの問いで決めています
エージェントを1本追加するたびに、この順で自問しています。
- この相手の出力は、親の結論を否定しうるか。 否定しうるなら継承させません。レビュー、再検証、リリース可否の最終確認が該当します。
- 前提の再説明にかかる往復は、実際に何回か。 1〜2回で済むなら、継承の利点はほぼありません。素直に名前付きで呼びます。
- 並列で何本走らせるか。 子が2本以上に増えるなら、キャッシュ前置の共有で入力コストが効いてきます。1本だけの委譲では、この利点はほとんど現れません。
- その相手に、親の会話に含まれる情報をすべて見せてよいか。 会話には試行錯誤の断片や、別件の調査結果も混ざります。狭い仕事に広い文脈を渡す理由はありません。
3番目は判断を分けやすい問いなので、少し補足します。フォークで入力コストが下がるのは、親のプロンプト前置を子どうしが共有するからです。効きが大きいのは2本目以降で、直列に1本ずつ投げる使い方では、恩恵は小さいままです。「安くなるから常にフォーク」という単純化は、構成によっては成り立ちません。
境界は各自の環境変数ではなく、リポジトリに書きます
ここまでの線引きを、頭のなかだけに置いておくと必ず崩れます。私の場合は、追認されたレビューを読むまで崩れていることに気づけませんでした。
そこで、フォークの姿勢をリポジトリ側に宣言しています。
{
"env": {
"CLAUDE_CODE_FORK_SUBAGENT": "1"
},
"permissions": {
"deny": ["Agent(fork)"]
}
}
一見すると矛盾した書き方に見えるかもしれません。意図はこうです。
CLAUDE_CODE_FORK_SUBAGENT を 1 にして、非対話実行でもフォークを使える状態にしておく(並列のファンアウトでコストを下げたいため)
Agent(fork) を deny して、名前を省いた委譲が自動でフォークへ落ちる経路だけ をふさぐ
こうしておくと、継承したいときは明示的にフォークを選ぶ必要が出てきます。うっかり継承する経路が消え、意図した継承だけが残ります。
フォークを一切使わない構成なら CLAUDE_CODE_FORK_SUBAGENT を 0 にするほうが単純ですが、その場合は並列時のコスト面の利点も一緒に手放すことになります。私は並列のアセット走査でこの利点を使いたかったので、上の形に落ち着きました。
既定を反転させた直後に踏んだ落とし穴
Agent(fork) を deny した翌日、作業が何度か止まりました。
止まったのは、名前付きの定義を用意していない委譲です。細かい調べ物を「ちょっと調べておいて」と無名で投げる癖が残っていて、その経路がフォークへ落ちる代わりに拒否されるようになったためです。deny 規則は独立性を守るためのものですが、同時に 受け皿のない委譲をすべて締め出します。
対処は2つあります。
- 汎用の受け皿になる名前付きエージェントを1本置き、無名で投げていた仕事をそこへ寄せる
- 一時的に allow へ戻し、後から定義を整える
私は前者をお勧めします。受け皿を1本作ると、無名で投げていた仕事の内容が定義ファイルとして残り、次に線を引き直すときの材料になるからです。後者は当座の作業は進みますが、なぜ止まったのかという情報がどこにも残りません。
移行そのものを楽にする回避策もあります。deny を入れる前に1日だけ通常どおり作業して、無名の委譲が何件出るかを数えておくことです。数が多ければ受け皿を先に用意し、少なければそのまま deny して構いません。私の場合は思っていたより多く、先に受け皿を作る判断をしました。
本番運用の手順(提出前の確認)に組み込むのは、この数を把握したあとにしています。確認手順の途中で委譲が拒否されると、確認が失敗したのか設定が拒否したのかの区別がつきにくく、原因の切り分けに余計な時間がかかるためです。
定義と姿勢のずれを、機械に見つけさせます
宣言しても、エージェント定義が増えれば線引きは再びずれます。そこで、定義ファイルを走査して仕分けるスクリプトを書きました。役割を示す語(レビュー・再検証・監査・第三者など)を手がかりに、継承させてはいけない側を拾い上げます。
#!/usr/bin/env python3
"""fork_scope_audit.py — フォーク既定下で、文脈継承の可否をエージェント単位に仕分ける。
使い方:
python3 fork_scope_audit.py [PROJECT_ROOT]
"""
from __future__ import annotations
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
# 独立性が要る仕事を示す語。description と本文の両方を見る
ISOLATE_MARKERS = (
"レビュー", "再検証", "検証し直", "監査", "第三者", "独立に", "見落とし",
"review", "verify", "audit", "critique", "double-check", "second opinion",
)
# 継承を前提にした書き方(親の文脈がないと成立しない指示)
INHERIT_HINTS = ("前提", "続き", "先ほど", "この会話", "上記の", "follow up", "continue")
FM = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)$", re.S)
@dataclass
class Agent:
path: Path
name: str
description: str
body: str
@property
def haystack(self) -> str:
return f"{self.name}\n{self.description}\n{self.body}".lower()
def isolate_reasons(self) -> list[str]:
return [m for m in ISOLATE_MARKERS if m.lower() in self.haystack]
def inherit_reasons(self) -> list[str]:
return [m for m in INHERIT_HINTS if m.lower() in self.haystack]
def parse_agent(path: Path) -> Agent | None:
m = FM.match(path.read_text(encoding="utf-8"))
if not m:
return None
front, body = m.group(1), m.group(2)
fields: dict[str, str] = {}
for line in front.splitlines():
# ネストしたキーは拾わない(トップレベルのみ)
if ":" in line and not line.startswith(" "):
k, _, v = line.partition(":")
fields[k.strip()] = v.strip().strip('"').strip("'")
return Agent(path, fields.get("name", path.stem), fields.get("description", ""), body)
def read_posture(root: Path) -> dict[str, object]:
"""settings.json から、フォークに関する現在の姿勢を読む。"""
posture: dict[str, object] = {"env": None, "deny_fork": False}
# local が後勝ちになるよう、この順で読む
for rel in (".claude/settings.json", ".claude/settings.local.json"):
p = root / rel
if not p.exists():
continue
data = json.loads(p.read_text(encoding="utf-8") or "{}")
env = (data.get("env") or {}).get("CLAUDE_CODE_FORK_SUBAGENT")
if env is not None:
posture["env"] = str(env)
deny = (data.get("permissions") or {}).get("deny") or []
if any(str(r).replace(" ", "").lower() == "agent(fork)" for r in deny):
posture["deny_fork"] = True
return posture
def main(argv: list[str]) -> int:
root = Path(argv[1] if len(argv) > 1 else ".").resolve()
agent_dir = root / ".claude" / "agents"
if not agent_dir.is_dir():
print(f"エージェント定義が見つかりません: {agent_dir}")
return 2
posture = read_posture(root)
agents = [a for a in (parse_agent(p) for p in sorted(agent_dir.glob("*.md"))) if a]
isolate = [(a, r) for a in agents if (r := a.isolate_reasons())]
inherit = [a for a in agents if not a.isolate_reasons()]
env = posture["env"]
effective = "off" if env == "0" else "on"
declared = "宣言あり" if env is not None else "宣言なし(既定に従う)"
print(f"対象: {root}")
print(f"エージェント定義: {len(agents)} 本")
print(f"CLAUDE_CODE_FORK_SUBAGENT: {declared} → 実効 fork={effective}")
print(f"Agent(fork) の deny 規則: {'あり' if posture['deny_fork'] else 'なし'}")
print()
print(f"[継承させない] {len(isolate)} 本 — 名前付きで呼び、親の文脈を渡さない")
for a, reasons in isolate:
print(f" - {a.name:<20} 根拠: {', '.join(reasons[:3])}")
print()
print(f"[継承してよい] {len(inherit)} 本 — フォークでも成立する")
for a in inherit:
hints = a.inherit_reasons()
tail = f" (継承前提の記述: {', '.join(hints[:2])})" if hints else ""
print(f" - {a.name}{tail}")
print()
problems = 0
if isolate and effective == "on" and not posture["deny_fork"]:
problems += 1
print("⚠️ 独立性が要るエージェントがあるのに、無名の委譲がフォークへ落ちる状態です。")
print(" 対処: これらは必ず名前付きで呼ぶ。無名の委譲を止めるなら Agent(fork) を deny する。")
if env is None:
problems += 1
print("⚠️ フォークの可否がリポジトリに宣言されていません(各自の環境変数に依存します)。")
print(" 対処: .claude/settings.json の env に CLAUDE_CODE_FORK_SUBAGENT を明示する。")
if not problems:
print("✅ 継承の境界が宣言と一致しています。")
return 1 if problems else 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
ISOLATE_MARKERS を description だけでなく本文まで見ているのは意図的です。名前と description は短くなりがちで、「独立にレビューする」という性格は本文側にしか書いていないことが多いためです。
宣言前の状態に対して走らせると、こう出ます。
対象: /path/to/project
エージェント定義: 5 本
CLAUDE_CODE_FORK_SUBAGENT: 宣言なし(既定に従う) → 実効 fork=on
Agent(fork) の deny 規則: なし
[継承させない] 2 本 — 名前付きで呼び、親の文脈を渡さない
- change-review 根拠: レビュー, 第三者, 独立に
- gate-verify 根拠: 再検証, 検証し直, verify
[継承してよい] 3 本 — フォークでも成立する
- asset-inventory
- release-preflight
- store-copy-i18n
⚠️ 独立性が要るエージェントがあるのに、無名の委譲がフォークへ落ちる状態です。
対処: これらは必ず名前付きで呼ぶ。無名の委譲を止めるなら Agent(fork) を deny する。
⚠️ フォークの可否がリポジトリに宣言されていません(各自の環境変数に依存します)。
対処: .claude/settings.json の env に CLAUDE_CODE_FORK_SUBAGENT を明示する。
先ほどの settings.json を置いてから同じスクリプトを走らせると、警告が消えて終了コードが 0 になります。
CLAUDE_CODE_FORK_SUBAGENT: 宣言あり → 実効 fork=on
Agent(fork) の deny 規則: あり
...
✅ 継承の境界が宣言と一致しています。
終了コードを返すようにしてあるので、リリース前の確認手順にそのまま挟めます。継承の境界は、目視で守るには細かすぎる種類の約束事だと感じています。
個人開発の並行運用にどう落としたか
私は複数の壁紙アプリを個人で並行して運用していて、提出前の確認をいくつかに分けています。アセットの棚卸し、日本語と英語のストア文言の突き合わせ、バージョン番号と署名まわりの確認。どれも親の文脈があったほうが速く終わる作業です。ここは継承させています。
残る1つ、「提出してよいか」の最終確認だけは、名前で呼んでいます。渡すのは対象と判定基準だけで、そこに至るまでの会話は渡しません。
分け方を変えてから、最終確認の返答が長くなりました。長くなった分の多くは、私がすでに直したつもりでいた箇所への問い直しです。読むのは少し面倒ですが、面倒でない確認は確認になっていないのだと思います。
コストの面でも、この形は無駄が少ないと感じています。数の多い走査は並列で回してキャッシュ前置を共有させ、本数が1本しかない最終確認だけを独立させる。継承の利点が効く場所と、独立性が要る場所が、そもそも重なっていませんでした。
次に確かめること
まず、手元の .claude/agents/ を開いて、レビューや検証を担当しているエージェントを1本だけ選んでください。そのうえで、直近の作業ログで そのエージェントを名前で指名していたか を確かめます。名前を省いていたなら、その相手はすでに追認側へ回っている可能性があります。
線を引き直すのは、そこからで間に合います。
私自身、既定の変更に気づくまでレビューを信頼しきっていました。同じ取りこぼしを未然に避けられる方がいれば嬉しく思います。お読みいただきありがとうございました。