GitHubのトレンドを眺めていたら、anomalyco/opencode が3位にいました。説明は「The open source coding agent.」の一文だけ。確認した時点でスターは 215,926 です。
ターミナルで動くコーディングエージェントは、CI に組み込んで使う話もよく聞くようになりました。自分が気になったのは賢さよりも「どこまで勝手にやるのか」です。README には、読み取り専用の plan エージェントについて「Denies file edits by default」「Asks permission before running bash commands」と書いてあります。公式にはこう書いてあるんですが、実際に動かすとどうなるのか。そこを確かめたくて触ってみました。
APIキーは使っていません。コンテナの中に OpenAI 互換のモックサーバを自分で立てて、opencode からはそれが LLM に見えるようにしました。モックなら「何が送られたか」を全部保存できますし、モデルに好きなツール呼び出しをさせられます。指示を守らないモデルの役もできる、というのが今回のミソです。
試した環境
- 使い捨ての Docker コンテナ(
node:22-bookworm、Debian 12.14、Node v22.23.1)。メモリ4GB・CPU2の制限つき - opencode v2.0.32(README の
npm i -g @opencode/cli@latestで導入) - ソースの確認用に
anomalyco/opencodeを浅く clone(HEAD214fb7fd、2026-10-18 時点) - LLM 役: コンテナ内
127.0.0.1:4000の自作モック。台本どおりに、文章かツール呼び出しを1つだけ返す
最初の小さな驚きはインストールでした。README の npm コマンドで入るのは @opencode/cli の 2.0.32 です。一方、もう一つの opencode-ai パッケージの最新は 1.19.3 で、GitHub の最新リリース(v1.19.3)もこちらと同じでした。README どおりに入れると v2 系、リリースページを見ると v1 系、という状態だったりします。以下の結果はすべて v2.0.32 のものです。
# 導入(README の手順そのまま)
$ npm i -g @opencode/cli@latest
added 5 packages in 6s # 実測 6,431ms
$ opencode --version
opencode v2.0.32
$ ls -la /usr/local/lib/node_modules/@opencode/cli/bin/
-rwxr-xr-x 2 root root 211842528 Oct 18 12:06 opencode.exe # 中身は Linux の ELF。名前だけ .exe
# 「こんにちは」を1回だけ送る
$ opencode run "こんにちは"
こんにちは。モックです。
# rc=0 elapsed_ms=1050
# モックが受け取った要求(自作モックのアクセスログ)
{"n":1,"url":"/v1/chat/completions","bytes":2376,"stream":true,"model":"mock-1","n_tools":0,"n_msgs":2,"last_role":"user","action":"text"}
{"n":2,"url":"/v1/chat/completions","bytes":22163,"stream":true,"model":"mock-1","n_tools":12,"n_msgs":2,"last_role":"user","action":"text"}
モックへの向け先は、設定ファイルで provider を1つ足しただけです。
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"mock": {
"npm": "@ai-sdk/openai-compatible",
"name": "Mock",
"options": { "baseURL": "http://127.0.0.1:4000/v1", "apiKey": "dummy-not-a-key" },
"models": { "mock-1": { "name": "Mock 1" } }
}
},
"model": "mock/mock-1"
}
正直なところ、ここは自分の使い方次第で詰め切れていない部分があります。今回は ~/.config/opencode/opencode.json に provider を書いただけで、権限(permission)の設定は一切足していません。後で書く plan の挙動は「何も設定しない既定のまま」の結果です。設定で plan の shell にも規則を足せば止まるはずだと思っていますが、書き方も含めて今回は試していません。
「こんにちは」1回で、要求は2回・24,539バイト
1回目の要求はツールが0個で、システムプロンプトの書き出しは「You are a title generator.」。会話に題名を付けるための要求でした。本体は2回目で、こちらに12個のツール定義が付いています。中身をバイト数で分けると次のとおりです。
| 要求 | 中身 | バイト数 |
|---|---|---|
| 1回目(題名づけ) | システムプロンプト 2,119 + 入力「こんにちは」15 ほか | 2,376 |
| 2回目(本体) | システムプロンプト | 8,216 |
| 2回目(本体) | ツール定義12個(edit / glob / grep / question / read / shell / skill / subagent / webfetch / websearch / write / execute) | 13,521 |
| 2回目(本体) | 入力「こんにちは」 | 15 |
| 2回目(本体) | 合計(max_completion_tokens: 32000 などのパラメータを含む) | 22,163 |
ツール定義だけで本体の約6割を占めます。いちばん大きいのは subagent の 2,696 バイトで、edit の 1,550、shell の 1,529 と続きます。README の plan の説明は「bash commands」ですが、v2.0.32 のツール名は shell でした。
システムプロンプトには作業ディレクトリ(/work/proj)、git リポジトリかどうか、Platform: linux、Today's date: Sun Oct 18 2026 が入っていました。プロジェクトのファイル一覧は入っておらず、置いておいた math.js の名前はどこにも出てきません。中身は必要になったらツールで読みに行く作りですね。
想定外その1: run が終わっても常駐プロセスが残る
opencode run は答えを返して終了コード 0 で終わったのに、ps を見ると opencode.exe serve --service が動き続けていました。次のケースを始める前(56秒後)にもまだ残っていたので、以降はケースごとに自分で止めています。
今思えば、opencode --help の最初に「—standalone Run with a private server instead of the background service」と書いてあったんですよね。v2 の run は、既定で裏の常駐サービスに乗る作りです。--standalone を付けた回は、終了後に何も残りませんでした。opencode service には start / status / stop などのサブコマンドもありますが、stop は今回試していません。CI のジョブや使い捨てのコンテナで回すなら、--standalone を付けておくのが無難だと思います。
本題: 同じツール呼び出しを build と plan に投げる
モックに返させるツール呼び出しは3種類です。write で note.txt を作る、shell で echo from-shell > /work/proj/shell.txt && echo wrote を実行する、read で .env を読む。これをエージェント(既定の build か plan か)と --auto の有無の組み合わせで、1ケース1プロセスで回しました。--auto は、ヘルプによると「明示的に拒否されていない許可を自動で承認する」フラグです。プロジェクトは毎回作り直しています。
| エージェント | —auto | モックが返したツール呼び出し | 結果 | 終了コード |
|---|---|---|---|---|
| build(既定) | なし | write で note.txt を作る | 確認なしで作成された | 0 |
| plan | なし | write で note.txt を作る | 拒否(Plan directory の外は不可) | 0 |
| plan | あり | write で note.txt を作る | 拒否(同じメッセージ) | 0 |
| plan | なし | shell で shell.txt を作る | 確認なしで実行され、ファイルができた | 0 |
| plan | あり | shell で shell.txt を作る | 同上 | 0 |
| build | なし | read で .env を読む | 自動で拒否。中身はモックに届かない | 0 |
| build | あり | read で .env を読む | 承認され、中身がモックへの要求に入った | 0 |
plan で write が止まるのは README のとおりです。気になったのは shell のほうで、README の「Asks permission before running bash commands」とは違い、--auto なしでも確認なしに実行されて shell.txt ができました。
理由は opencode debug agents で見えました。plan の権限は8行で、build と同じ規則に「edit は全部拒否、ただし Plan directory の中だけ許可」を足したものです。shell についての規則は無く、先頭の * に対する allow がそのまま効きます。clone したソースでも、packages/core/src/plugin/plan.ts が足しているのは question の allow と edit の deny、Plan directory の allow の3つでした。
# plan + write(--auto の有無に関係なく同じ)
✗ Write note.txt failed
Error: Cannot use write to modify files outside the Plan directory: /root/.opencode/plan
# plan + shell(--auto なしでも)
$ echo from-shell > /work/proj/shell.txt && echo wrote
wrote
# opencode debug agents で見た plan の権限(8行すべて)
{"action":"*","resource":"*","effect":"allow"}
{"action":"read","resource":"*.env","effect":"ask"}
{"action":"read","resource":"*.env.*","effect":"ask"}
{"action":"read","resource":"*.env.example","effect":"allow"}
{"action":"question","resource":"*","effect":"allow"}
{"action":"edit","resource":"*","effect":"deny"}
{"action":"edit","resource":"/root/.opencode/plan/*","effect":"allow"}
{"action":"browser","resource":"*","effect":"deny"}
じゃあ plan では shell の書き込みを何が止めているのか。plan のときだけ、要求に 460 バイトの system-reminder が1通増えていました(本体の要求は 22,173 から 22,669 バイトに増加)。その中に「Do not modify any other files or ask a subagent to do so.」という一文があります。つまり shell については、権限ではなく「モデルがこの指示を守るかどうか」に預けられている作りです。
ここは公平に書いておきたいところです。今回のモックは、わざと指示を無視するように作っています。実際のモデルなら、plan の指示に従って shell で書き込まないことがほとんどだと思います(本物のモデルでは試していません)。それでも「plan だから何も変わらない」と言い切れるのは edit 系のツールまでで、shell は柵の外にある。これは知っておいて損はないと思います。
想定外その2: 「ask」は run では自動拒否、—auto で全部承認
既定の権限で ask になっているのは .env と .env.* の read だけでした。対話できない opencode run でここに当たると、「この実行ではユーザーに許可を求められないので拒否した」というエラーがツールの結果としてモデルに返ります。人に聞けないなら断る、という安全側の作りで、ここは好感が持てました。
ただし --auto を付けると話が変わります。同じ read が承認され、試しに置いておいたダミーの値が、モックへの3回目の要求の tool メッセージにそのまま入っていました。ぶっちゃけ当たり前の挙動なんですが、--auto は「ask を全部 yes にする」スイッチだと覚えておいたほうがよさそうです。
# build + read .env(--auto なし)
! permission requested: read (.env); auto-rejecting
✗ Read .env failed
Error: This non-interactive run cannot ask the user for permission, so the request was rejected. Continue without this action.
# rc=0
# 同じケースを --format json で実行し、tool_use の行だけ取り出したもの
tool= read status= error error= "This non-interactive run cannot ask the user for permission, so the request was rejected. Continue without this action."
# build + read .env(--auto あり): モックが受け取った3回目の要求の tool メッセージ
msg[3] role=tool bytes=73 head="Read file /work/proj/.env, lines 1-1\n1: DUMMY_TOKEN=not-a-real-secret-123"
もう一つ、表の終了コードの列を見てください。拒否された回も含めて全部 0 です。スクリプトから「ツールが拒否されたか」を知りたいなら、終了コードではなく --format json の出力で tool_use の status を見る必要があります。
試してみての所感
コンテナを立ててから捨てるまで308秒。モデル抜きでも、ここまで中身が見えるとは思っていませんでした。
- 「こんにちは」1回で要求は2回・24,539バイト。システムプロンプトより、ツール定義12個(13,521バイト)のほうが重い
- v2 の
runは既定で常駐サービスに乗り、終了後もプロセスが残る。使い捨ての環境なら--standalone - plan が権限で止めるのは edit 系(write を含む)まで。shell は既定の権限では止まらず、プロンプトの指示頼み
- run では ask が自動で拒否される。
--autoはそれを全部承認に変える - 拒否されても終了コードは 0。見るなら
--format json
自分が CI で使うなら、--standalone で回して --auto は付けず、作業ディレクトリに .env を置かない形にします。plan は「うっかり編集を防ぐ柵」くらいに考えて、shell まで閉じたいときは、今回のようなコンテナを外側にもう一枚かぶせるつもりです。README と v2 の挙動のずれは、そのうち追いつくのかもしれません。もっと良い方法があったら教えてください。