第8章 「実行していい?」の正体 — 許可と人間の確認(human-in-the-loop)
📖 この章のゴール:危険な道具(
write_file/run_command)を使う直前で止めて、人間に「実行していい?」と確認する仕組みを作る。安全な道具(読むだけ)は自動、危険な道具(書く・走らせる)は確認という線引きが、自分のコードで引けるようになる。あの Claude Code の「実行していい?」の正体が分かる。 ← 目次・はじめにへもどる
📱 Claude Code ではこう見える
Claude Code を使っていると、こういう瞬間に出くわします。
- ファイルを読むときや、フォルダを一覧するときは、何も聞かれず、すっと進む。
- でも、ファイルを書き換えるときや、
npm testのようなコマンドを走らせるときは、急にこう聞いてくる。
Claude wants to run: npm test
Do you want to proceed? (y/n)
ここで y(はい)を押すまで、Claude はじっと待ちます。n(いいえ)を押せば、その操作は実行されずに止まり、Claude は「分かりました、では別の方法を考えます」と引き返します。
不思議ですよね。同じAIなのに、ある操作は黙ってやるのに、ある操作はわざわざ許可を求める。この線引きは、いったい誰が、どこで決めているのでしょうか。
答えはシンプルです。この「実行していい?」は、AIが気をきかせているのではありません。エージェント本体(あなたが書くプログラム)のコードが、危険な道具の前で“わざと”止めて、人間に聞いているだけです。この章では、その仕組みをそっくり自分で作ります。
🤔 なぜ確認するの?/確認しないとどうなる(🟢 基礎)
第5〜7章で、私たちは道具をどんどん増やしてきました。読む(read_file / list_files)、書く(write_file)、コマンドを走らせる(run_command)。手が増えて便利になった反面、第6章・第7章で見たとおり、危ない手も持たせてしまいました。
ここで、両極端な2つの作りを考えてみます。
- 全部、自動で動かす(確認ゼロ) → ものすごく便利。でも、AIが勘違いして「いらないファイルを掃除します」と
rm -rfを走らせたら、止める間もなく実行されます。取り返しがつきません。 - 全部、いちいち確認する(確認だらけ) → 安全。でも、ファイルを1つ読むたびに「読んでいい?」と聞かれていたら、うるさすぎて使い物になりません。
どちらもダメ。そこで、ちょうどいい真ん中を取ります。
🔑 線引きの合言葉:安全な道具は自動、危険な道具は確認。 読むだけ(
read_file/list_files)は、何が起きても元に戻せるので自動。書く・走らせる(write_file/run_command)は、取り返しがつかないので人間に確認。
この 「自動の流れの途中に、人間の確認をはさむ」 やり方を、専門用語で human-in-the-loop(ひゅーまん・いん・ざ・るーぷ/=輪の中に人間を入れる) と呼びます。第1章の合言葉「便利さと危なさは、いつもセット」を、いよいよコードで形にする回です。
「動かせる」と「やっていい」は別もの(🟢 基礎)
ここで、前作(ChatGPTクローン)を作った人なら聞き覚えのある話とつなげます。
前作でログイン機能を作ったとき、「ログインできた=何でもやっていい、ではない」という話がありました。ログイン(=あなたが誰かを確かめること)が済んでも、他人の投稿を消す権限まで与えられたわけではない。「あなたが誰か」と「あなたに何を許すか」は別の判断だったわけです。
📚 ことばで言うと:認証(にんしょう/あなたが誰かを確かめる) と 認可(にんか/その人に何を許すか決める) は、別もの。
エージェントでも、まったく同じことが言えます。
道具を“持っている=その操作をやっていい”、ではない。 AIが
run_commandという道具を持っていても、「rm -rfを実行していいかどうか」は、また別の判断。その判断を、最後に人間がするのが、この章の確認です。
「道具を渡したこと」と「その道具を今この瞬間に使ってよいこと」を、きっぱり分けて考える。これが安全なエージェントの土台です。
🛠 こう作る(🟢 基礎)
やることは、たった3つです。
- y/n を聞く小さな関数
askPermissionを作る。 - 危険な道具の名前を1か所にまとめておく。
- 道具を実行する直前で「これは危険な道具か? なら確認する」を入れる。
順番に作っていきましょう。コードはすべて、第5〜7章までに作った agent.ts に付け足すだけです。
① y/n を聞く関数 askPermission
第2章で、ユーザーの入力を受け取るのに readline(りーどらいん)という道具を使いました。あれは「> と出して、1行入力してもらう」ものでしたね。まったく同じ仕組みを、今度は「はい/いいえ」を聞くために使い回します。
import * as readline from "node:readline/promises";
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
async function askPermission(question: string): Promise<boolean> {
const answer = await rl.question(`${question} (y/n) `);
return answer.trim().toLowerCase() === "y";
}
1行ずつ読むと:
import * as readline from "node:readline/promises";… 第2章のCLI入力ループと同じreadlineを読み込む。新しい道具は何も足していません。const rl = readline.createInterface({ ... });… 「キーボードから1行もらう」窓口を1つ作る。inputはキーボード(標準入力)、outputは画面(標準出力)。第2章の入力ループとこのrlを共有します(同じファイルなので、すでに作ってあればそれを使い回してOK)。async function askPermission(question: string): Promise<boolean> {… 「質問の文章(question)を受け取り、はい(true)/いいえ(false)を返す」関数。返り値がPromise<boolean>=待つと最後に true か false が出てくる、という意味。const answer = await rl.question(\${question} (y/n) `);… 渡された質問の後ろに(y/n)をくっつけて画面に出し、**ユーザーが1行打つまで待つ**。打った文字列がanswer` に入る。return answer.trim().toLowerCase() === "y";… 入力の前後の空白を消し(trim)、小文字にそろえて(toLowerCase)、ちょうど"y"のときだけ true。YでもyでもOKにしている。それ以外(n・空Enter・適当な文字)は全部「いいえ」扱い。——これが大事。「はっきり y と言われたときだけ実行する」=迷ったら安全側(実行しない)に倒す、という設計です。
💡 ここがポイント:「
yのときだけ true」にしておくと、うっかりEnterを押しても実行されません。空Enterや想定外の入力が「はい」になってしまうと、確認の意味が無くなります。安全弁は、迷ったら止まる側に倒すのが鉄則です。
② 危険な道具の名前を1か所にまとめる
「どの道具が危険か」を、コードのあちこちに散らさず、1か所に書いておきます。こうしておくと、後で道具を足したときに「ここだけ直せばいい」ので、直し忘れが減ります。
const DANGEROUS = new Set(["write_file", "run_command"]);
1行ずつ読むと:
const DANGEROUS = new Set([...])… 「危険な道具の名前の集まり」を作る。Set(せっと)は重複しない名前の袋で、「この名前、袋に入ってる?」をDANGEROUS.has("write_file")のように一発で聞けるのが便利。配列([...])でも書けますが、Setのほうが「入ってる?」の判定が読みやすいので使います。- 中身は
"write_file"(書く=上書きの危険/第6章)と"run_command"(コマンド=最も危険/第7章)の2つ。読むだけの道具(read_file/list_files)は、ここに入れません=自動で動く、という線引きそのものです。
🔧 応用(読み飛ばし可):将来「ファイルを削除する道具」や「ネットに送信する道具」を足したら、その名前もこの袋に入れるだけで、自動的に確認の対象になります。「危ない操作(書き込み・コマンド・削除・送信)は確認」という線引きが、この1行に集約されているわけです。
③ 道具を実行する直前で確認する
いよいよ本丸。第5章で作った道具のディスパッチャ executeTool のいちばん最初に、「危険な道具なら、まず人間に聞く」というガード(門番)を1つ足します。
async function executeTool(name: string, input: any): Promise<string> {
if (
DANGEROUS.has(name) &&
!(await askPermission(`${name} を実行していい? ${JSON.stringify(input)}`))
) {
return "ユーザーが拒否しました";
}
switch (name) {
case "read_file":
return await fs.readFile(input.path, "utf-8");
case "list_files":
return (await fs.readdir(input.dir)).join("\n");
case "write_file":
await fs.writeFile(input.path, input.content);
return `${input.path} に書き込みました`;
case "run_command":
return (await exec(input.command)).stdout;
default:
return `不明な道具: ${name}`;
}
}
1行ずつ読むと:
async function executeTool(name: string, input: any): Promise<string> {… 第5章から使っている、道具を実際に動かす係。nameが道具の名前、inputがその引数(パスやコマンド)。if ( DANGEROUS.has(name) && ...… この道具は、さっきの危険な袋に入っているか? 入っていなければ(=読むだけの安全な道具なら)このifを素通りして、下のswitchでそのまま実行されます=自動。!(await askPermission(\${name} を実行していい? ${JSON.stringify(input)}`))… 危険な道具のときだけ、ここまで来る。askPermissionで人間に聞き、その答えを!(否定)で見ています。**「許可されなかった(false)」とき**だけ、このif` の中身に入る、という意味。- 質問の文章
\${name} を実行していい? ${JSON.stringify(input)}`がこの章の心臓です。${name}(例:run_command)に加えて、JSON.stringify(input)で**引数の中身**=**実際に何を実行しようとしているか**を画面に出します。たとえばrun_command を実行していい? {“command”:”npm test”}` のように。 return "ユーザーが拒否しました";…nなどで断られたら、道具を一切実行せず、この文字列を返して終わり。ここがミソで、例外で止める(クラッシュさせる)のではなく、ふつうの“結果の文字列”として返す。こうすると、第4章のエージェントループがこの文字列をLLMに渡し、LLMは「ユーザーが拒否したのか。では別の手を考えよう」と自然に引き返せます。switch (name) { ... }以下 … 門番を通り抜けた(=安全な道具か、危険でもyで許可された)ものだけが、実際に実行されます。中身は第5〜7章で作ったものと同じです。
🔑 確認の文章には、必ず“何を実行するか”を出す。これは飾りではなく、確認の命綱です。
run_command を実行していい?とだけ聞かれても、npm testなのかrm -rf /なのか分からなければ、人間は判断できません。判断に必要な材料(パス・コマンドの中身)を見せて、はじめて確認は意味を持ちます。
実際に動かすと、こう見えます。
> このフォルダのテストを走らせて
run_command を実行していい? {"command":"npm test"} (y/n) y
(テストが走って、結果が表示される)
> ついでに古いログを掃除して
run_command を実行していい? {"command":"rm -rf logs"} (y/n) n
(実行されず、「ユーザーが拒否しました」がLLMに渡り、AIは別の提案をしてくる)
これで、あの Claude Code の「Do you want to proceed? (y/n)」と同じものが、自分のエージェントに付きました。読むだけは黙って進み、書く・走らせるは必ず人間に一声かける——安全な道具は自動、危険な道具は確認、です。
⚠️ ハマりどころ
- 全部に確認をはさんでしまう(確認だらけ)
→
read_fileまでDANGEROUSに入れると、AIがファイルを1つ読むたびにyを押す羽目になり、うるさすぎて使えません。読むだけ=元に戻せる操作は自動でOK。取り返しがつくかどうかで線を引きましょう。 - 逆に、確認を一切はさまない(全部自動)
→ 便利ですが、AIの勘違い1つで
rm -rfが止める間もなく走ります。手を持たせるほど、確認の安全弁は外せません。 - 確認の文章に“中身”を出していない
→
run_command を実行していい?とだけ聞いて、{"command": ...}を出さないと、何を許可しているのか分からないままyを押すことになります。これでは確認の意味がゼロ。必ず引数(パス・コマンド)を見せること。 - 「yes連打」で慣れてしまう(確認疲れ/かくにんづかれ)
→ これは仕組みではなく、人間側の落とし穴。確認が何度も出ると、人はだんだん中身を読まずに
yを連打するようになります。すると、せっかくの安全弁が形だけになる。対策は、そもそも確認の回数を減らすこと=危険な操作自体を減らし、AIが触れる範囲を最初から狭めておくこと。これが次章(第9章)の話につながります。 - 「確認はAIにお願いすればいい」と思ってしまう
→ これは一番やってはいけない勘違いです。システムプロンプトに「危ない操作の前は確認してね」とお願いするだけにして、コード側の門番(
executeToolのif)を作らない——これはダメです。AIは指示を忘れたり、勝手に判断して飛ばしたりします。確認は、AIの善意ではなく、あなたのコードで強制する。LLMがどう振る舞っても、executeToolを通る限り必ず確認が走る。この「コード側で止める」ことが、安全弁が安全弁である理由です。
🤖 AIに頼むなら(Vibe codingのコツ)
この「確認をはさむ」改造も、AIに手伝ってもらえます。ただし「危ない操作に確認を付けて」とだけ頼むと、システムプロンプトにお願い文を足すだけで済ませてくることがあります(前項のNGパターン)。“コード側で強制する”と明示して頼みましょう。
🗣 プロンプト例: 「いま
executeTool(name, input)で道具を実行している。危険な道具(write_fileとrun_command)だけ、実行する直前に人間へ y/n の確認をはさみたい。確認はreadlineを使ったaskPermission(question)という関数にして、第2章で作ったrlを使い回してほしい。危険な道具の名前はconst DANGEROUS = new Set([...])のように1か所にまとめて。確認の文章には、実行しようとしている引数(パスやコマンド)もJSON.stringifyで表示して。断られたら例外を投げず、"ユーザーが拒否しました"という文字列を返して、ループがLLMに渡せるようにして。この確認はシステムプロンプトのお願いではなく、executeToolのコードの中で必ず通るように作って。」
出てきたコードを見るときの確認ポイント:
- 危険な操作の直前で確認をはさんでいるか?(
write_file/run_commandの実行前に止まるか) - 確認の文章に、実行しようとしている引数(パス・コマンドの中身)が表示されているか?
- 安全な道具(
read_file/list_files)は、確認なしで自動で動くか?(読むだけまで確認していないか) - 確認は LLMへのお願いではなく、コード側(
executeToolの中)で必ず通る作りか? - 断られたとき、例外で落ちず、文字列を返してループが続けられるか?
y以外(空Enter・想定外の文字)はぜんぶ「いいえ」として扱う、安全側に倒した作りか?
📝 ことばメモ
- human-in-the-loop(ひゅーまん・いん・ざ・るーぷ):自動の流れの途中に人間の確認をはさむこと。「輪(ループ)の中に人間を入れる」の意味。危ない操作の安全弁。第1章で名前だけ出た言葉を、この章でコードにしました。
- 認可(にんか):その相手に何を許すかを決めること。「道具を持っている」ことと「その操作をやっていい」ことは別、という線引き。認証(あなたが誰かを確かめる/前作のログイン)とは別もの。エージェントでは「今この操作を実行していいか」を最後に人間が決めるのが認可にあたります。
- 許可リスト(きょかりすと):「これは確認なしでOK」または「これは確認が必要」をあらかじめ名前で決めておくリスト。この章の
DANGEROUS(危険=確認が要る道具の名前の集まり)が、その素朴な形です。読むだけの道具をここに入れないことが、「安全な道具は自動」という線引きそのもの。 - 確認疲れ(かくにんづかれ):確認が何度も出ると、人が中身を読まずに
yを連打してしまう状態。安全弁が形だけになる落とし穴。対策は、確認の回数を減らす=触れる範囲を狭めること(第9章)。
➡️ 次の章へ
これで、危ない操作の前で人間に「実行していい?」と聞く安全弁ができました。Claude Code のあの確認の正体は、エージェント本体のコードが危険な道具の前でわざと止めているだけ——もう魔法ではありませんね。
でも、確認だけでは足りません。確認は「やる直前に止める」最後の砦ですが、人間は確認疲れで y を連打してしまうし、そもそもAIがいじれる範囲が広すぎれば、確認の回数も被害の大きさも増えるばかりです。本当に守るには、もう一段手前——そもそもAIが触れる範囲を最初から狭めておくことが要ります。
次の第9章では、サンドボックス(砂場)と最小権限を作ります。作業フォルダの外には出させない、危険なコマンドは入口で弾く、そして失敗してもすぐ元に戻せる(取り消せる設計)——「確認」と「囲い込み」の二段構えで、エージェントを本当に安全にしていきます。