はじめに
対象読者は、OpenCode を「読み取り専用で回す運用」に組み込みたい開発者です。
plan エージェントの説明文には Plan mode. Disallows all edit tools.(編集ツールをすべて禁止する)と書かれています。この一文を素直に読むと、plan では編集系のツールがそもそも読み込まれていない、と考えたくなります。ところが解決済みの設定を出力してみると、plan の編集ツールは build とまったく同じ状態で有効のままでした。では何が書き込みを止めているのでしょうか。
3つのエージェントの権限を並べる
OpenCode 1.18.18 で opencode debug agent <name> を実行し、解決済みの設定を比較した結果です。数値はすべて筆者が取得した出力から数えています。
| 項目 | build | plan | explore |
|---|---|---|---|
| mode | primary | primary | subagent |
| 無効化されたツール | なし | なし | invalid / question / edit / write / task / todowrite / skill |
| permission ルール総数 | 22 | 27 | 38 |
うち external_directory(環境依存) |
11 | 12 | 21 |
| 実質のルール数(総数から上を引いた数) | 11 | 15 | 17 |
通常ファイルへの edit
|
allow | deny | deny |
| 例外的に書けるパス | なし |
.opencode/plans/*.md ほか計2件 |
なし |
bash の個別ルール |
なし(先頭の * → allow が適用) |
なし(同左) |
bash * → allow を明示 |
plan_enter / plan_exit
|
allow / deny | deny / allow | deny / deny |
同じ「書けないエージェント」でも、plan と explore では止め方の層がまったく違います。ルール数は opencode debug agent <name> | jq '.permission | length' と、external_directory だけを数えた差分から出しています。external_directory は実行環境のパス許可(一時ディレクトリやスキル配置先)が並ぶ枠で、環境ごとに件数が変わるため本題からは切り離しました。
検証環境
Linux コンテナ(Node.js v22)に npm から導入しました。
npm install -g opencode-ai
opencode --version
# 1.18.18
npm レジストリの公開情報では、opencode-ai の週間ダウンロード数は 2,138,728(2026-08-03〜2026-08-09 の集計・npm registry API)、ライセンスは MIT でした。CLI の導入からバージョン確認まで、外部の LLM プロバイダー認証は不要です。以降の測定はすべて opencode debug agent のみで完結しており、モデルへのリクエストは発生しません。
plan は編集ツールを外していない
まず plan の tools マップを見ます。
opencode debug agent plan | jq -c .tools
{"invalid":true,"question":true,"bash":true,"read":true,"glob":true,
"grep":true,"edit":true,"write":true,"task":true,"webfetch":true,
"todowrite":true,"skill":true}
edit も write も true です。build 側で同じ出力を取ると、キーの並びも値も完全に一致していました。つまり plan の read-only は、ツールの有無ではなく permission ルールで作られています。
該当部分だけ抜き出すと次のとおりです(plan の external_directory 12 件を除外し、1 行 1 ルールに整形しています)。なお opencode debug agent は公式ドキュメントのコマンド一覧では見つけられませんでしたが、opencode debug --help に show agent configuration details として掲載されています。
* | * -> allow
doom_loop | * -> ask
question | * -> deny
plan_enter | * -> deny
plan_exit | * -> deny
read | * -> allow
read | *.env -> ask
read | *.env.* -> ask
read | *.env.example -> allow
question | * -> allow
plan_exit | * -> allow
task | general -> deny
edit | * -> deny
edit | .opencode/plans/*.md -> allow
edit | root/.local/share/opencode/plans/*.md -> allow
plan_enter / plan_exit は公式ドキュメントの permission キー一覧には載っていない、組み込みエージェント定義側のキーです。公式ドキュメントによれば、複数のルールが一致した場合は 最後に一致したルールが勝ちます。また edit キーは編集系ツールをまとめて指し、edit / write / patch の 3 つを覆います(OpenCode Permissions)。この 2 点を踏まえると、plan で src/index.ts を書き換えようとしたときに最後に一致するのは edit * -> deny で、write ツールを選んでも同じ deny に落ちます。
plan にだけ空いている書き込み口
上の一覧の末尾 2 行が効いてきます。edit * -> deny の 後ろ に .opencode/plans/*.md -> allow が置かれているため、最後に一致するルールは allow になります。plan モードは「何も書けない」のではなく、計画ファイルの置き場だけ書ける 設計でした。
この配置は理にかなっています。計画を立てる役なのに計画そのものを保存できなければ、成果はセッションが切れた時点で消えます。ただし運用側から見ると、「plan だから CI の作業ツリーは絶対に汚れない」とは言い切れません。少なくとも .opencode/plans/ 配下には新しいファイルが生えます。
bash には既定ルールが無い
もうひとつ、表に出しておきたい測定結果があります。plan の 15 件のルールを bash で絞り込むと、ヒットは 0 件でした。
opencode debug agent plan | jq '[.permission[] | select(.permission=="bash")] | length'
# 0
一致するのは先頭の * | * -> allow だけです。編集ツールは deny されているのに、シェルは既定で通ります。読み取り専用を厳密に要求する用途(他人のリポジトリの調査、CI での差分レビューなど)では、plan を選んだだけでは足りず、bash にも自分で deny を書く必要があります。
explore は二層で締めている
サブエージェントの explore は、同じ read-only でも作りが違いました。tools の側で edit と write を false にして機能自体を落とし、そのうえで permission を「いったん全部 deny してから必要なものだけ allow する」許可リスト方式に切り替えています。
* | * -> allow ← 先頭の既定
...
* | * -> deny ← ここで全部落とす
grep | * -> allow
glob | * -> allow
list | * -> allow
bash | * -> allow
webfetch | * -> allow
websearch | * -> allow
read | * -> allow
* -> deny を途中に挟み、その後ろに必要な操作だけを並べる構造です。plan が「禁止したいものを列挙する」拒否リスト方式なのに対し、explore は許可リスト方式になっています。同じ製品の中に 2 つの流儀が同居しているのは、plan が人間と対話する主エージェント、explore が呼び出されて結果だけ返すサブエージェント、という役割差から来ていると読めます。
優先順位を対照実験で確かめる
設定ファイルからどう上書きされるのかを、条件を変えて 2 回測りました。1 回目は設定なし、2 回目は次の opencode.json を置いた状態です。
{
"$schema": "https://opencode.ai/config.json",
"permission": { "bash": { "*": "deny", "git status": "allow" } },
"agent": {
"plan": { "permission": { "edit": { "*": "allow" } } }
}
}
2 回目の plan で edit と bash のルールだけを取り出すと、並び順はこうなりました。
edit | * -> deny ← 組み込み
edit | .opencode/plans/*.md -> allow ← 組み込み
bash | * -> deny ← グローバル設定
bash | git status -> allow ← グローバル設定
edit | * -> allow ← エージェント個別設定
組み込み → グローバル → エージェント個別、の順に連結されています。最後に一致したルールが勝つ仕組みなので、エージェント個別設定に edit: {"*": "allow"} を 1 行足すだけで、plan の read-only は丸ごと外れます。
逆に言えば、締めたい側もこの順番を使えます。agent.plan.permission に bash: {"*": "deny"} を書けば、組み込みでは素通りだったシェルを plan だけ塞げます。グローバルに書くと build まで巻き込むので、エージェント個別に置くほうが事故が少ないはずです。
3つを並べて見えたこと
筆者がこの 3 つを同じ手順で測って一番意外だったのは、説明文と実装レイヤーがずれていた ことです。plan の Disallows all edit tools という説明から想像するのはツールの取り外しですが、実際に外していたのは explore のほうでした。plan は同じツールを持ったまま、権限で押さえています。
この差は運用の判断を変えます。ツールを外す方式は、設定を書き換えても false のツールがそもそも呼べないため、事故の下限が固定されます。権限方式は 1 行の上書きで全開になる代わりに、.opencode/plans/*.md のような例外を後ろに置くだけで柔軟に穴を開けられます。どちらが安全かではなく、どちらの外れ方をするか が違うわけです。
外部から受け取った設定ファイルを検証せずに使う運用では、agent.plan.permission の 1 行が最後に効くという事実が効いてきます。設定を配布する側なら、plan に対する edit の上書きが混ざっていないかを、レビューの観点に足しておく価値があります。
確認しなかったこと
今回測ったのは、解決済みの権限テーブルの中身までです。モデルを呼ぶセッションを実際に走らせて「plan で書き込みツールを叩いたら本当に拒否されるか」までは検証していません。ルール表の読み取りは公式ドキュメントの「最後に一致したルールが勝つ」「edit は edit / write / patch を覆う」という記述に基づく解釈です。
まとめ
- OpenCode 1.18.18 の plan は編集ツールを無効化しておらず、build と
toolsマップが完全一致していました。read-only の実体は permission ルールです。 - plan には
.opencode/plans/*.mdへの書き込みが許可として最後に置かれており、完全な読み取り専用ではありません。 - plan の
bashには個別ルールが無く、先頭の* -> allowが適用されます。シェル経由の書き込みを止めたいなら自分で deny を書きます。 - explore はツール無効化と許可リストの二層で締めており、同じ製品内で plan とは別の流儀になっています。
- 権限は 組み込み → グローバル → エージェント個別 の順に連結され、最後に一致したルールが勝ちます。エージェント個別の 1 行で read-only は外れます。