はじめに
2026年7月9日、Codex CLI 0.144.0がリリースされた1。リリースノートの目玉のひとつが「writes app-approval mode」だ。公式の説明はこうなっている。
Added a
writesapp-approval mode that allows declared read-only actions while prompting for writes.
— Release 0.144.0(openai/codex)(2026-07-09)
「read-only と宣言されたアクションは確認なしで実行し、書き込みだけプロンプトする」という説明を読む限り、MCPツールを多用するワークフローでは承認プロンプトの数がかなり減りそうに見える。筆者は普段からCodex CLIに自作MCPサーバーを繋いで検索・参照系のツールを使っているので、「これで検索ツールの確認が消えるなら地味に嬉しい」と期待して試した。
結論から言うと、期待は外れた。「read-only と宣言された」の主語はMCPサーバー側であり、サーバーがその宣言を怠っているツールは、writesモードでも普通に書き込み扱いされる。 しかも非対話実行(codex exec)では、承認待ちのツール呼び出しは自動でキャンセルされるため、プロンプトが「増える」どころか黙って失敗する。
この記事で学べること
-
writes承認モードの正しい設定キー(apps.*.default_tools_approval_mode)と、--strict-configを使った検証手順 - MCP仕様の
readOnlyHintアノテーションを宣言したツール/していないツールで、実際に承認結果が変わる実測ログ -
readOnlyHintのデフォルト値がfalse(=書き込み扱い)であるという、見落としがちなMCP仕様の落とし穴
前提環境
- OS: Linux(Ubuntu 24.04)
- Node.js: v22.x
- Codex CLI:
@openai/codexv0.144.1(npm) - 認証:
OPENAI_API_KEYをcodex login --with-api-keyで登録 - 自作した最小限のstdio MCPサーバー(Node.js、外部依存なし)
TL;DR
-
writesモードはapps._default.default_tools_approval_mode = "writes"(またはapps.<id>...)で設定する2。--strict-configフラグでこのキーが有効な設定として認識されることを確認済み。 - 自作MCPサーバーに
readOnlyHint: trueを明記したツールは、writesモードで 確認なしに実行された。 - 同じサーバーの、アノテーションを一切付けていないツールは、writesモードでも 承認待ちのまま
codex execにキャンセルされた(非対話実行のため承認者がおらず自動失敗)。 - さらに検証すると、この「宣言なしツールが失敗する」挙動は
writesモードを指定してもしなくても まったく同じ だった。つまりwritesモードの恩恵は、サーバー側がreadOnlyHintを明示している場合にしか発生しない。 - MCP仕様上
readOnlyHintのデフォルト値はfalse(=書き込みとみなす)3。自作・サードパーティ問わず、多くのMCPサーバーはこのアノテーションを付け忘れているため、「writesモードにすれば承認が減る」と期待して導入すると裏切られるケースが多いはずだ。
検証環境のセットアップ
まずCLIをインストールし、APIキーでログインする。
npm install -g @openai/codex
codex --version
# => codex-cli 0.144.1
codex login --with-api-key <<< "$OPENAI_API_KEY"
codex login status
# => Logged in using an API key - sk-proj-***
次に、承認モードの検証専用に最小限のstdio MCPサーバーを自作する。ツールを2つ用意した。
-
read_hinted:annotations: { readOnlyHint: true }を明記 -
read_no_hint: アノテーションを一切付けない(MCP仕様のデフォルトに委ねる)
// mini-mcp-server.js(抜粋)
if (method === 'tools/list') {
send({ jsonrpc: '2.0', id, result: { tools: [
{
name: 'read_no_hint',
description: 'Returns a fixed string. No annotations declared.',
inputSchema: { type: 'object', properties: {} }
},
{
name: 'read_hinted',
description: 'Returns a fixed string. Explicitly read-only.',
inputSchema: { type: 'object', properties: {} },
annotations: { readOnlyHint: true, title: 'Read Hinted' }
}
]}});
}
どちらのツールも実際にやることは同じ(固定文字列を返すだけ)で、実害のある書き込みは一切行わない。違いは MCPサーバーがCodexにそれを申告しているかどうか だけだ。これをグローバル設定として登録する。
codex mcp add minitest -- node /tmp/claude/codex-test/mini-mcp-server.js
codex mcp get minitest
# => minitest
# enabled: true
# transport: stdio
# command: node
# args: /tmp/claude/codex-test/mini-mcp-server.js
writes モードの設定キーを確認する
公式のconfig reference2には apps._default.default_tools_approval_mode(全アプリ共通)と apps.<id>.default_tools_approval_mode(アプリ個別)、apps.<id>.tools.<tool>.approval_mode(ツール個別)の3階層が載っている。値は auto | prompt | writes | approve の4種類だ。
このキーが実在するかは --strict-config(未知の設定キーがあるとエラーで止まる検証専用フラグ)で裏取りできる。
# 正しいキー: エラーにならず実行できる
codex exec --strict-config \
-c apps._default.default_tools_approval_mode=\"writes\" \
"respond with just OK, do not call any tools" \
--skip-git-repo-check
# => OK(正常終了)
# 対照実験: 存在しないキーだとこうなる
codex exec --strict-config -c totally_bogus_key_xyz=\"writes\" "respond OK" --skip-git-repo-check
# => Error loading config.toml: unknown configuration field `totally_bogus_key_xyz` in -c/--config override
apps._default.default_tools_approval_mode はエラーにならず通った。対照実験で存在しないキーが即座にエラーになることも確認できたので、これは有効な設定パスだと判断してよい。
実測: readOnlyHintの有無で結果が変わるか
writes モードを指定した状態で、read_hinted(宣言あり)と read_no_hint(宣言なし)をそれぞれ1回ずつ呼ばせてみる。--json でイベントログを取得した。
宣言ありツール(read_hinted):
codex exec --json \
-c apps._default.default_tools_approval_mode=\"writes\" \
"Call the MCP tool named read_hinted exactly once, then report what it returned." \
--skip-git-repo-check
{"type":"item.completed","item":{"id":"item_0","type":"mcp_tool_call","server":"minitest","tool":"read_hinted","result":{"content":[{"type":"text","text":"called read_hinted"}]},"error":null,"status":"completed"}}
問題なく completed している。プロンプトなしで即実行された。
宣言なしツール(read_no_hint):
codex exec --json \
-c apps._default.default_tools_approval_mode=\"writes\" \
"Call the MCP tool named read_no_hint exactly once, then report what it returned." \
--skip-git-repo-check
{"type":"item.completed","item":{"id":"item_0","type":"mcp_tool_call","server":"minitest","tool":"read_no_hint","result":null,"error":{"message":"user cancelled MCP tool call"},"status":"failed"}}
status: "failed"、エラーは "user cancelled MCP tool call"。ツール自体は同じ「固定文字列を返すだけ」の処理なのに、アノテーションの有無だけで明暗が分かれた。codex exec は非対話実行(このセッションのデフォルト表示では approval: never)なので、承認が必要と判定された呼び出しはユーザー入力を待てず、その場でキャンセル扱いになる。
writesモードを外しても結果は同じだった
念のため、writes モードの指定自体を外した素の状態でも read_no_hint を試した。
codex exec --json \
"Call the MCP tool named read_no_hint exactly once, then report what it returned." \
--skip-git-repo-check
{"type":"item.completed","item":{"id":"item_0","type":"mcp_tool_call","server":"minitest","tool":"read_no_hint","result":null,"error":{"message":"user cancelled MCP tool call"}}}
結果は同一だった。つまり writes モードを設定しても、readOnlyHint を宣言していないツールに対しては 何も変わらない。「writesモードにすれば承認が減る」という期待は、サーバー側の実装が伴って初めて成立する条件付きの話だったことになる。
なぜこうなるのか
原因はCodex側の実装バグではなく、MCP仕様そのものの設計にある。MCPのツールアノテーション仕様では、readOnlyHint を含む全てのヒントに 保守的なデフォルト値 が定義されている3。
| アノテーション | デフォルト値 | 意味 |
|---|---|---|
readOnlyHint |
false |
宣言がなければ「書き込みする」とみなす |
destructiveHint |
true |
宣言がなければ「破壊的操作」とみなす |
idempotentHint |
false |
宣言がなければ「毎回副作用がある」とみなす |
つまりMCP仕様は「性善説」ではなく「性悪説」で設計されている。ホスト(この場合はCodex)は、サーバーが明示的に安全だと申告しない限り、そのツールを潜在的な書き込み操作として扱う。これは安全側に倒した妥当な設計だが、裏を返せば 書いた本人が「これは読み取り専用ツールだ」と思っていても、annotations フィールドにそう書かなければCodexには伝わらない ということだ。
社内向けに雑に書いたMCPサーバーや、readOnlyHint が整備される前の古いサーバーは軒並みこの対象になる。writesモードを導入する前に、実際に繋いでいるMCPサーバーの tools/list レスポンスを確認し、annotations.readOnlyHint が付いているツールがどれだけあるかを見ておく方がよい。
まとめ
-
writes承認モードはapps._default.default_tools_approval_mode = "writes"で有効化できる(--strict-configで存在確認済み)。 - 効果があるのは、MCPサーバーが
readOnlyHint: trueを明示しているツールだけ。 - 宣言していないツールは、writesモードの有無にかかわらず承認待ちになり、非対話実行(
codex exec)では黙って失敗する。 - 原因はMCP仕様の
readOnlyHintデフォルト値がfalse(書き込みとみなす)であるため。自作MCPサーバーを使っている場合は、アノテーションの付与状況を先に棚卸しすることを勧める。
著者視点の発見ポイント
最初は「writesモードを設定すればCodexが賢く判定してくれる」くらいの感覚で試そうとしていた。ところが自作の最小サーバーで検証を始めると、同じ「固定文字列を返すだけ」の処理なのに一方は通り一方は失敗するという結果に直面し、「モードの実装が壊れているのでは」と疑った。だが writes を外した対照実験で同じ失敗が再現したことで、原因はCodex側ではなくMCP仕様の readOnlyHint デフォルト値にあると特定できた。公式のリリースノート一文だけを読んで「プロンプトが減る機能」と早合点していたら、実際に自分のMCPサーバー群でも同じ落とし穴を踏んでいたはずだ。設定キーの存在を --strict-config で裏取りしてから動作検証に進んだことで、「キーが間違っているのか」「挙動が仕様通りなのか」を切り分けられたのも今回の収穫だった。
参考リンク
-
Release 0.144.0(openai/codex) —
writesapp-approval mode の追加告知(2026-07-09) -
Configuration Reference(OpenAI Developers) —
apps.*.default_tools_approval_mode等の設定キー一覧 -
Tool Annotations as Risk Vocabulary(MCP Blog) —
readOnlyHint等のデフォルト値と設計思想の解説 -
Schema Reference(Model Context Protocol) —
ToolAnnotationsの型定義とデフォルト値