スケジュールで起動するエージェントには、許可ダイアログを押してくれる人がいません。
私自身、個人開発で運営している Dolice Labs では、複数のサイトに対して毎日決まった時刻に Claude Code ベースのエージェントを走らせています。最初のころ、その無人エージェントが「本来触れる必要のないツール」にまで手を伸ばし、待ち受けるはずの許可ダイアログでそのまま固まって朝まで止まっていた、という事故を何度か経験しました。
対話セッションなら、危ないツール呼び出しは画面に出てきて自分が止められます。けれど無人運用では、その最後の砦が存在しません。だからこそ「何が起きたら止めるか」より前に、「そもそも何に手を伸ばせるか」を構成で絞っておく必要があります。本稿は、MCP サーバーとそのツールを deny-by-default で締め、必要なものだけを許可リストで渡すための設計をまとめたものです。
無人エージェントでは「届く範囲」がそのまま事故の範囲になる
対話運用と無人運用では、同じ権限設定でも意味がまったく違います。
対話では、permissions の ask モードが事実上のセーフティネットになります。判断に迷うツール呼び出しはユーザーに尋ねてくれるので、許可リストが多少ゆるくても人間が最終的に止められます。
ところが無人運用では、尋ねる相手がいません。許可されていないツールに当たれば止まり、許可されていればそのまま実行されます。中間がないのです。つまり無人エージェントにとっては、許可リストの広さが、そのまま「無断で起こりうることの広さ」になります。
私の場合、痛感したのは MCP サーバー経由のツールでした。記事を生成して push するだけのエージェントに、ファイルシステム全体を触れる MCP サーバーや、ブラウザ操作の MCP まで見えている状態だったのです。記事生成という目的に対して、明らかに届く範囲が広すぎました。
allowedTools と「ポリシー強制」はレイヤーが違う
ここで混同しやすいのが、プロジェクト単位の許可設定と、上位レイヤーで効くポリシー強制の違いです。
settings.json の permissions.allow / permissions.deny は、そのプロジェクトやユーザーの設定です。便利な一方で、同じ階層に書ける以上、別の設定で上書きされる余地があります。自動運用では「このエージェントの設定ファイルが何かの拍子に書き換わったら」を考えておく価値があります。
これに対して、管理者レイヤーの設定(managed settings)は、下位のローカル設定では上書きできない最低ラインを敷くためのものです。Claude Code は設定をいくつかの階層で読み、おおまかに次の優先順位で解決します。
優先度 レイヤー 典型的な置き場所 下位から上書き
高 管理者ポリシー(managed settings) OS のシステム領域 不可
中 コマンドライン引数 起動時の -- オプション —
中 プロジェクト設定 .claude/settings.json可
低 ユーザー設定 ~/.claude/settings.json可
無人エージェントの「最低ライン」は、この一番上のレイヤーに置くのが要点です。ローカルの設定をどう触られても、deny だけは生き残るからです。
deny-by-default を管理者レイヤーに置く
まず、考え方を「許可されていないものは拒否」に倒します。具体的には、危険度の高いカテゴリを管理者ポリシーで広く deny し、そのうえで本当に必要なものを次のステップで戻していきます。
macOS なら管理者ポリシーは次の場所に置きます(環境により異なります)。
# macOS の管理者ポリシー設置場所(要管理者権限)
/Library/Application Support/ClaudeCode/managed-settings.json
中身は、まず広く締めるところから始めます。
{
"permissions" : {
"defaultMode" : "deny" ,
"deny" : [
"mcp__filesystem" ,
"mcp__puppeteer" ,
"mcp__chrome" ,
"Bash(rm:*)" ,
"Bash(git push --force:*)" ,
"WebFetch"
]
}
}
ポイントは2つあります。第一に、defaultMode を deny にすることで、明示的に許可していないツールは原則として通らなくなります。対話なら ask が妥当ですが、無人運用では尋ねる相手がいないので deny が出発点として安全です。第二に、MCP サーバーは mcp__<サーバー名> という単位で丸ごと deny できます。「このエージェントにファイルシステム MCP は一切見せない」を、サーバー名ひとつで宣言できるわけです。
この時点では、ほぼ何もできないエージェントになります。それで構いません。次に、目的に必要なものだけを戻していきます。
MCP サーバーは「サーバーごと」と「ツールごと」の二段で絞る
許可リストは、MCP サーバーの有効化と、ツール単位の許可の二段で考えると整理しやすくなります。
まず、プロジェクトに同梱された .mcp.json のサーバーを全部自動で立ち上げてしまわないようにします。無人運用では、ここを明示するだけで「知らないサーバーが勝手に増える」事故が減ります。
{
"enableAllProjectMcpServers" : false ,
"enabledMcpjsonServers" : [ "github" ]
}
enableAllProjectMcpServers を false にすると、プロジェクトの .mcp.json にあるサーバーは原則オフになり、enabledMcpjsonServers に挙げたものだけが立ち上がります。記事生成と push が目的なら、必要なのはせいぜいリポジトリ操作系のサーバーひとつ、という具合に絞れます。
次に、立ち上げたサーバーの中でも、使ってよいツールを名前で許可します。MCP ツールは mcp__<サーバー名>__<ツール名> という形で個別に指定できます。
{
"permissions" : {
"defaultMode" : "deny" ,
"allow" : [
"mcp__github__search_repositories" ,
"mcp__github__get_file_contents" ,
"mcp__github__create_or_update_file" ,
"Bash(git add:*)" ,
"Bash(git commit:*)" ,
"Bash(git push:*)" ,
"Read" ,
"Write"
],
"deny" : [
"mcp__filesystem" ,
"mcp__puppeteer" ,
"mcp__chrome" ,
"WebFetch"
]
}
}
こうすると、同じ GitHub MCP サーバーでも「ファイルを読む・作る」は通り、別の破壊的なツールが仮にあっても許可リストにない限り通りません。サーバー単位で締めてから、ツール単位で必要分だけ開ける。この二段構えが、無人運用では効きます。
Before / After:許可ダイアログ頼みから、構成で締める運用へ
私が最初に組んでいた無人エージェントの起動は、こういう形でした。
# Before — 許可をその場で広く付与してしまう起動
claude -p "記事を生成して push して" \
--dangerously-skip-permissions
--dangerously-skip-permissions は対話での確認を飛ばすぶん手軽ですが、無人運用でこれを使うと「届く範囲を構成で考える」工程をまるごと省いてしまいます。実際これで、目的と無関係なツールに手が伸びて止まる事故が起きました。
deny-by-default に切り替えてからは、起動側はむしろ素朴になりました。締めるのは構成ファイル側の役目だからです。
# After — 管理者ポリシーで締めた前提で、必要なものだけを明示
claude -p "記事を生成して push して" \
--allowed-tools "mcp__github__create_or_update_file,Bash(git push:*),Read,Write"
起動コマンドに書いた --allowed-tools は、あくまで「このセッションで使いたいもの」の宣言です。管理者ポリシーの deny を超えて何かを開けるわけではありません。つまり、起動側でうっかり広く書いても、最低ラインは管理者レイヤーが守ってくれます。この「上書きできない床」があるかどうかが、Before と After の本質的な差でした。
ポリシーが効いているかを起動時に確認する
設定を書いただけで安心しないことを、私は失敗から学びました。書いたつもりのポリシーが、優先順位の都合で実は効いていなかった、というのがいちばん怖いからです。
無人運用に乗せる前に、対話で一度起動して次を確認します。
# 1. 設定の読み込み状況と階層を確認する
claude --debug 2>&1 | grep -i "settings\|permission\|mcp"
# 2. どの MCP サーバーが実際に立ち上がっているかを確認する
claude mcp list
# 3. 環境の健康診断(壊れた設定の隔離状況も含む)
/doctor
claude mcp list で、enabledMcpjsonServers に挙げたサーバーだけが出ていれば、サーバー単位の絞り込みは効いています。逆に、deny したはずのサーバーが一覧に残っていたら、優先順位かファイルの置き場所を疑います。/doctor は設定の健全性を点検でき、壊れた構成が隔離されているかどうかも見られます。
確認の自動化まで踏み込むなら、無人実行の前段に次の順でゲートを置くのをお勧めします。
起動直後に MCP サーバー一覧を取得する
想定したサーバー名の集合と突き合わせる
差分があれば本処理に入らず、ログだけ残して終了する
私の運用では、想定外のサーバーが見えたらその回はスキップしてログだけ残すようにしています。この前段ゲートを入れてからは、構成が静かにずれていく事故を早い段階で拾えるようになりました。
つまずきやすい点:優先順位とフォールバック
本番の無人運用で何度かつまずいた落とし穴は、設定が「効かない」のではなく「別の階層に負けている」ケースでした。ここを避けるための注意点と、起きたときの対処を順に挙げます。
いちばん多かったのは、ローカルのプロジェクト設定に書いた allow が、管理者ポリシーの deny に阻まれて通らない、というものです。これは仕様どおりの正しい挙動です。deny は上位で勝つので、ローカルで allow を足しても開きません。開けたいなら管理者ポリシー側を直す必要があります。無人運用では、むしろこの「ローカルでは開けられない」性質こそが安全装置です。
もうひとつは、MCP サーバーの起動失敗を「拒否」と取り違えるケースです。サーバーのコマンドが見つからない・環境変数が足りないといった理由で立ち上がらないと、ツールが使えないという結果だけは deny と同じに見えます。claude mcp list の状態表示と起動ログを分けて読むことで、ポリシーで止めたのか、起動に失敗したのかを切り分けられます。
最後に、フォールバックの設計です。あるツールが deny で塞がれているとき、エージェントが別の手段(たとえば Bash で同じことをやろうとする)に流れないか、という点です。だからこそ deny は「特定のMCPツール」だけでなく、Bash(rm:*) のように回り道になりがちなコマンドも併せて締めておくのが安全です。回り道になりやすいコマンドまで含めて締めておくことを推奨します。穴は、塞いだつもりの隣に開きます。
小さく始めるための一歩
いきなり全タスクを deny-by-default にする必要はありません。
まずは、いちばん無人で長く走らせているエージェントひとつを選び、そのセッションで claude mcp list を取ってみてください。目的に対して見えているサーバーが多すぎないか、それを眺めるだけで、締めるべき場所の見当がつきます。今日できる最初の一手は、その一覧を見て「このエージェントには要らない」と即答できるサーバーを、ひとつだけ deny に足すことです。
無人で動くものほど、起こりうることを事前に狭めておく。私自身、ここを構成で引き受けてからは、朝に止まっているエージェントを見つける回数が目に見えて減りました。同じように夜間バッチを回している方の、運用設計の足がかりになれば嬉しいです。