第7章 道具をつくる③ コマンドを走らせる(一番強くて一番危険)

📖 この章のゴール:ターミナルのコマンドを走らせる道具 run_command を作る。これで「テストを走らせる」「ビルドする」までエージェントにできるようになり、一気に本物っぽくなります。でも同時に、これは今までで一番危険な道具です。だからこの章ではわざと確認をまだ付けず、「なぜ確認が要るのか」を体で感じてもらいます(確認の作り方は次の第8章)。 ← 目次・はじめにへもどる


📱 Claude Code ではこう見える

第1章で見た「テスト直して」の場面を、もう一度思い出してください。Claude Code は途中でこう言いました。

「テストを走らせて確認します。npm test を実行していいですか?」

あなたが許可すると、Claude Code は本当に npm test走らせます。そして——

  1. コマンドの結果(成功か失敗か、エラーメッセージ)を読む
  2. まだ失敗していたら「ここが原因のようです」と直す場所を見つける
  3. もう一度 npm test を走らせて、直るまでくり返す

ここで Claude Code がやっているのは、「ターミナルにコマンドを打ち込んで、その出力を読む」ことです。あなたが手で npm test と打つのと、まったく同じこと。それをAIの代わりにあなたのコードがやってあげる——それが今回作る run_command という道具です。

これまで作った「読む(第5章)」「書く(第6章)」に、この「コマンドを走らせる」が加わると、エージェントは急に自分でテストやビルドを回せるようになります。一気に強くなる瞬間です。


🤔 なぜ?/やらないとどうなる

コマンドは「何でもできる」道具(🟢 基礎)

なぜ run_command がそんなに強いのか。答えはシンプルです。

コマンドを走らせられる、ということは「パソコンでできること全部」ができる、ということだから。

npm testnpm run build も、ls(一覧)も git commit(記録)も、ぜんぶコマンドです。つまりこの道具を1つ渡すだけで、エージェントはファイルを読む・書く以外のありとあらゆる操作ができるようになります。第5章・第6章では「読む道具」「書く道具」と1つずつ能力を足してきましたが、run_commandそれらをまとめて飛び越える万能の道具なのです。

🍳 たとえ:これまで見習い(あなたのコード)に渡したのは、包丁(読む)や鍋(書く)といった個別の道具でした。run_command は、それらと違って「厨房にある道具を全部、何でも使っていいよ」と渡すようなもの。便利さは段違いです。

でも「何でもできる」は「一番危険」の裏返し(🟢 基礎)

ここで合言葉を思い出してください。第1章で出てきた、「便利さと危なさは、いつもセット」です。

「何でもできる」ということは、裏を返せば何でもやってしまえるということ。たとえば——

  • rm -rf ~(ホームフォルダをまるごと全消し
  • ネットにこっそりファイルを送信するコマンド
  • パソコンの設定を勝手に書き換えるコマンド

read_file(読むだけ)は何があっても安全でした。write_file(書く)は第6章で「上書きで消える」という危険が初めて登場しました。そして run_command は、その危険をさらに飛び越えて——取り返しのつかないことを、一瞬でやれてしまう。今まででダントツに危険な道具です。

⚠️ とくに rm -rf(あーるえむ・まいなすあーるえふ)は、フォルダの中身を確認なしで全部消すコマンドです。打ち間違いや、AIの勘違い一発で、大事なファイルが消えて戻ってきません。これが「コマンド実行の怖さ」の象徴です。

サボると何が起きる?(🔧 応用)

もし「便利だから」と、確認なしで run_command をエージェントに渡しっぱなしにしたら、どうなるでしょう。

たとえば AI が「不要なファイルを片付けます」と判断して、よかれと思って rm -rf を実行する——これは実際に起こりうる事故です。AI は悪気なく、ただ目的(片付け)に向かって動いただけ。でも結果は取り返しがつきません。あるいは第6章で触れた間接プロンプトインジェクション(読んだファイルに罠の指示が混ざっている)と組み合わさると、「ファイルの中の文章に書いてあった通りにコマンドを実行してしまう」という、もっと怖いことも起こりえます。

💡 だから本物のエージェントは、コマンドを走らせる前に必ず人間に確認します。Claude Code が毎回「npm test を実行していいですか?」と聞いてくるのは、過保護なのではなく、この危険を知っているからです。確認の仕組みは次の第8章でしっかり作ります。この章では、あえてまだ確認を付けません——まず「確認なしのコマンド実行がどれだけ強くて、どれだけ無防備か」を、自分の手で体験してほしいからです。

そして大事な節目として——この run_command が完成すると、道具が3種類そろいます

道具 できること 危険度
📖 read_file / list_files 読む・一覧する 安全 第5章
✍️ write_file 書く・直す 危険(上書き) 第6章
run_command コマンドを走らせる 最も危険 この章

「読む・書く・コマンド」——これでコーディングエージェントの基本の手はそろいました。ここから先(第8章以降)は、この強い手をいかに安全に使わせるかの話になります。


🛠 こう作る

やることは第5章・第6章と同じパターンです。① 道具を実装する関数 → ② tools に道具メニューを足す → ③ executeTool に呼び出しを足す。コマンドを走らせる部分だけ、Node.js の child_process(ちゃいるど・ぷろせす=子プロセス)という機能を使います。

① コマンドを走らせる関数(🟢 基礎)

まず、Node.js でコマンドを走らせる準備をします。

import { exec as execCb } from "node:child_process";
import { promisify } from "node:util";

const exec = promisify(execCb);

1行ずつ読むと:

  • 1行目:node:child_process から exec という関数を取り込みます。exec は「ターミナルでコマンドを打つ」のと同じことをやってくれる機能です。ここでは名前を execCb に変えて取り込んでいます(理由は次の行)。
  • 2行目:node:util から promisify(ぷろみすふぁい)という変換ツールを取り込みます。
  • 4行目:exec は本来、結果を await で受け取れない古いスタイル(コールバックという形)です。それを promisify で包んで、await で待てる今どきの形に変換し、改めて exec という名前を付けています。これで const result = await exec(...) と書けるようになります。

🔧 補足:なぜわざわざ execCbexec と変換するの? → 本書はずっと async/await(待てる書き方)で統一しているからです。promisify で包むと、read_filewrite_file と同じ await のスタイルでコマンドを書けて、コードがそろって読みやすくなります。

次に、道具の本体です。

async function run_command(command: string): Promise<string> {
  const { stdout, stderr } = await exec(command);
  return stdout + stderr;
}

1行ずつ読むと:

  • 1行目:run_command という関数を定義します。command(走らせたいコマンドの文字列、例:"npm test")を受け取り、その結果の文字列を返します。
  • 2行目:await exec(command) でコマンドを実際に走らせます。結果は2つに分かれて返ってきます。stdout(えすてぃーでぃーあうと=標準出力=ふつうの出力)と stderr(えすてぃーでぃーいーあーる=標準エラー出力=エラーやワーニングの出力)です。{ stdout, stderr } = ... で、その2つを同時に取り出しています。
  • 3行目:stdoutstderr をくっつけて、まとめて返します。エラーの方も捨てずに返すのがポイント(理由は ⚠️ で説明します)。これがそのまま LLM への「コマンドの結果」になります。

💡 たったこれだけで「コマンドを走らせて結果を読む」道具ができます。あなたがターミナルで npm test と打って、画面に出た文字を読む——その一連を、関数1つに閉じ込めただけです。

② 道具メニューに足す(🟢 基礎)

第3章で決めた形のとおり、tools 配列に run_command の説明を1つ足します。これは LLM(頭脳)に「こんな道具が使えるよ」と伝えるメニューです。

const tools: Anthropic.Tool[] = [
  // ... read_file, list_files, write_file はそのまま ...
  {
    name: "run_command",
    description: "ターミナルでコマンドを実行し、その出力(標準出力+エラー出力)を返す。例:npm test",
    input_schema: {
      type: "object",
      properties: {
        command: { type: "string", description: "実行するコマンド。例: 'npm test'" },
      },
      required: ["command"],
    },
  },
];

1行ずつ読むと:

  • name:道具の名前。実装した関数と同じ run_command にします。LLM はこの名前を指定して「この道具を使いたい」と言ってきます。
  • description何ができる道具かを LLM に伝える説明。ここが分かりやすいほど、LLM は道具を正しく使ってくれます。「出力(標準出力+エラー出力)を返す」と書くことで、結果が文字で返ることを伝えています。
  • input_schema:道具に渡す引数の決まりごと。ここでは command(文字列)を1つ受け取り、それは必須(required)だ、と書いています。
  • required: ["command"]command は省略できない、という指定です。

③ ディスパッチャに足す(🟢 基礎)

最後に、第5章で作った executeTool(道具を名前で振り分ける係)に、run_command の場合の処理を1行足します。

async function executeTool(name: string, input: any): Promise<string> {
  try {
    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 run_command(input.command);
      default: return `不明な道具: ${name}`;
    }
  } catch (e) {
    return `エラー: ${e instanceof Error ? e.message : String(e)}`;
  }
}

1行ずつ読むと:

  • 全体:name(道具の名前)で switch で振り分け、input(LLM が決めた引数)を渡して実行します。
  • case "run_command" の行:LLM が run_command を呼んできたら、input.command(LLM が決めたコマンド文字列)を、さっき作った run_command 関数に渡して走らせ、その結果を返します。追加したのはこの1行だけです。
  • try ... catch で全体を包んでいるのが大事です。コマンドが失敗すると execエラーを投げて処理を止めようとしますが、それを catch で受け止め、エラーの内容を文字列にして返します。こうすると、失敗もクラッシュではなく「コマンドの結果」として LLM に渡り、LLM が「失敗したな、直そう」と次の手を考えられます(背骨🔁:結果を戻して、また考えさせる)。

🔧 第6章でも触れたとおり、エラーは投げっぱなしにせず、文字列にして返すのが本書のルールです。LLM にとっては「成功の出力」も「失敗のエラー」も、どちらも読んで判断する材料だからです。

動かしてみる:「node -v を実行して」(🟢 基礎)

agent.ts を起動して、こう頼んでみましょう。

> node -v を実行して

エージェントはこう動きます。

  1. LLM が「run_commandnode -v を走らせよう」と判断(node -v は Node.js のバージョンを表示する、無害なコマンド)
  2. あなたのコードが run_command("node -v") を実行
  3. 結果(例:v22.3.0)が LLM に戻る
  4. LLM が「Node.js のバージョンは v22.3.0 です」と答えて終了

ここで一度立ち止まってください。いま起きたことは、LLM が決めたコマンドを、あなたのコードが確認なしでそのまま実行したということです。今回は node -v だったから平和でした。でも、もし LLM が(あるいは罠の混ざったファイルが)rm -rf のような危険なコマンドを指定してきたら——同じように、確認なしで実行してしまいます

🛑 だから確認は次章で必ず付けます。本物のエージェントは、run_command のような危険な道具を実行する直前で必ず止まって、人間に「これ、走らせていい?」と聞きます。この章ではあえてそれを外して、確認なしのコマンド実行の強さと無防備さを体験してもらいました。第8章で、この穴をふさぎます。


⚠️ ハマりどころ

  • 「何でも実行できる」=攻撃されたら何でもされる、という怖さ run_command は、これまでの道具と危険度の桁が違います。write_file は「特定のファイルを上書き」できるだけでしたが、run_commandシステム全体に手が届きます。確認なしで本番のフォルダに置かないこと。練習は消えても困らない捨てフォルダで。

  • シェルに文字列を丸ごと渡す危険(コマンドインジェクション)(🔧 応用) exec(command) は、渡した文字列をシェル(ターミナルの言語)として丸ごと解釈します。だから ;&&| などの記号で複数のコマンドをつなげられてしまいます。たとえば LLM が(または罠のファイルが)ls; rm -rf . のような文字列を渡すと、ls の後ろの全消しまで実行されます。これをコマンドインジェクション(差し込み攻撃)と呼びます。本書ではまず動かすことを優先して exec を使いますが、本番では「許可したコマンドだけ通す」「引数を1つずつ安全に渡す(execFile などで配列で渡す)」といった守りが必要だ、と覚えておいてください。

  • stderr や「終了コード」も見る(🔧 応用) コマンドが失敗したときの情報は、stdout(ふつうの出力)ではなく stderr(エラー出力)に出ることが多いです。だから本章のコードは両方くっつけて返しています。さらに、コマンドには終了コードという「成功(0)か失敗(0以外)か」を表す数字もあります(exec は失敗時に catch へ飛ぶので、本章ではそれで失敗を拾えます)。「出力が空だから成功」と早合点しないこと。

  • 長い出力でトークンが爆発する(🔧 応用) npm install や大きなビルドのログは、何千行にもなることがあります。その全部を結果として LLM に渡すと、コンテキスト(AIが一度に見る情報)がパンパンになり、トークン(=料金)も一気に増えます。対策は「出力の末尾だけ返す」「長すぎたら切り詰める」など。コンテキストとコストの話は第14章でくわしく扱います。

  • 確認をはさまずに使い続ける この章のコードにはまだ確認がありません。学習のためにわざと外しています。第8章で確認を付け、第9章で「触れる範囲(フォルダ)を狭める」(最小権限・サンドボックス)まで作って、はじめて安心して使える道具になります。それまでは「危ない実験中」と思って扱ってください。


🤖 AIに頼むなら(Vibe codingのコツ)

run_command のような危険な道具こそ、AI に丸投げで「コマンドを実行できる道具を作って」と頼むと、確認なし・出力切り詰めなしの無防備なコードが出てきがちです。危険を分かったうえで頼むと、安全な作りに誘導できます。

🗣 プロンプト例: 「TypeScript(Node.js, ESM)の AIエージェントに、コマンドを実行する道具 run_command(command) を足したい。node:child_processexecpromisify で包んで await で使う形で。標準出力とエラー出力の両方を文字列で返し、失敗は try/catch でエラー文字列にして返すこと。実行の直前に人間へ確認をはさめるよう、確認を入れる場所をコメントで示して。さらに、出力が長すぎるとき末尾だけ返すようにもしたい。」

出てきたコードを見るときの確認ポイント:

  • コマンドを走らせる前に、人間に確認する場所があるか?(この章ではまだ無くてOK。次章で必ず付ける)
  • rm -rf のような危険なコマンドを弾く/止める余地があるか?(許可リストや確認。第8章以降で実装)
  • 失敗が try/catch で文字列として返り、クラッシュしないか?
  • stdout だけでなく stderr(エラー出力)も捨てずに返しているか?
  • 長い出力をそのまま全部 LLM に渡していないか?(トークン爆発・コスト対策)
  • 渡す文字列に;&& で別コマンドを差し込まれないよう意識できているか?(コマンドインジェクション)

📝 ことばメモ

  • child_process(ちゃいるど・ぷろせす):Node.js から別のプログラム(コマンド)を起動するためのしくみ。「子プロセス」=自分(エージェント)から呼び出した、別の小さなプログラムのこと
  • exec(いぐぜっく)child_process の機能の1つ。コマンドの文字列を丸ごとシェルに渡して実行し、出力を受け取る。手軽な反面、文字列をそのまま解釈するのでインジェクションに注意
  • stdout(えすてぃーでぃーあうと)/ stderr(えすてぃーでぃーいーあーる):コマンドの2種類の出力。stdoutふつうの出力(結果)、stderrエラーやワーニングの出力。失敗の情報は stderr に出ることが多いので、本章では両方を返す
  • コマンドインジェクション:コマンドの文字列に ;&& などをこっそり差し込んで、意図しない別のコマンドを実行させる攻撃。exec に外部由来の文字列を渡すときの代表的なリスク
  • (予告)サンドボックス:道具が触れる範囲を、安全な区画の中に閉じ込めるしくみ(砂場=外に砂がこぼれない)。「最小権限」「取り消せる設計」とセットで、第9章で作ります

➡️ 次の章へ

これで道具が3つそろいました——読む(第5章)・書く(第6章)・コマンドを走らせる(この章)。エージェントの「手」は出そろい、テストもビルドも回せる、一気に本物っぽい存在になりました。

でも同時に、いちばん危険な道具を、確認なしのまま持たせてしまっています。ここからは「安全に動かす」編です。次の第8章では、Claude Code が毎回聞いてくる 「実行していい?」の正体——つまり許可と人間の確認(human-in-the-loop)を、実際に askPermission として作り、run_command の直前にはさみます。この章でわざと開けておいた穴を、いよいよふさぎましょう。

次の第8章へ →

← 目次・はじめにへもどる