0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

claude コマンド(Claude Code CLI)でローカルAIシステムを構築する

0
Posted at

この記事は、個人開発の体験をもとに要点を書き出し、Claude Codeを利用して整形しています。

はじめに

Claude Code CLIの claude コマンドは、対話的にコードを書くだけのツールではありません。-p オプションを使えば、プログラムから呼び出せるローカルAIエンジンとして機能します。

APIキーの管理やSDKの導入なしに、claude -p 一発でテキスト生成・画像認識・構造化データ抽出ができる。自分はこれを使って、Next.jsアプリのバックエンドからClaude CLIを呼び出し、複数のAI機能をローカルで動くシステムとして構築しました。

この記事では、実際にハマったポイントと、CLIオプションの全体像を紹介します。

前提条件

  • Claude Pro(月$20)またはMax(月$100/$200)プランが必要です
    • Claude Code自体がこれらのサブスクリプションに含まれる機能です
    • Freeプランでは利用できません
  • あるいは、Anthropic APIキーを設定して従量課金で使うことも可能です
  • claude コマンドがインストール済みで、claude で対話モードが起動できる状態を前提とします

基本:-p オプションで非インタラクティブ実行

Claude Code CLIには対話モード(REPL)もありますが、プログラムから呼ぶなら -p--print)オプション一択です。

# 対話モード(人間用)
claude

# 非インタラクティブモード(プログラム用)
claude -p "このコードを説明して"

-p をつけると、プロンプトを処理して結果を標準出力に出したらすぐ終了します。これだけで「ローカルで動くAI API」が手に入ります。

実際に使ったオプション

--output-format json:構造化レスポンス

claude -p --output-format json "Hello"

テキストの代わりにJSONエンベロープで返ってきます:

{
  "type": "result",
  "subtype": "success",
  "result": "Hello! How can I help you?",
  "session_id": "...",
  "is_error": false
}

result フィールドにレスポンス本体が入っています。エラー時は is_error: true になるので、プログラムからのハンドリングが楽です。

--model:モデル指定

モデルはエイリアス(短縮名)とフルネームの2通りで指定できます。

# エイリアス(短縮名)→ 常にそのファミリーの最新モデルに解決される
claude -p --model haiku "軽い処理"
claude -p --model sonnet "バランス型の処理"
claude -p --model opus "画像認識など重い処理"

# フルネーム → 特定バージョンに固定
claude -p --model claude-haiku-4-5-20251001 "軽い処理"
claude -p --model claude-opus-4-6 "重い処理"

エイリアスを使うのがおすすめです。 新しいモデルがリリースされたとき、エイリアスなら自動的に最新版に切り替わります。フルネームだと古いバージョンに固定されたままになるので、意図的にバージョン固定したい場合以外はエイリアスを使いましょう。

使い分けの指針:

エイリアス 用途 特徴
haiku テキスト生成、軽い変換 安い・速い
sonnet バランス型 中間
opus 画像認識、複雑な判断 高精度・遅い

自分のシステムでは、テキスト生成にはHaiku、画像内テキスト読み取りにはOpusと使い分けています。

--allowedTools:ツール許可

Claude CLIにはファイル読み込みなどの「ツール」があり、-p モードではデフォルトで無効になっています。

# Readツールを許可(画像ファイルを読ませたい時)
claude -p --allowedTools Read "画像ファイル /tmp/sheet.jpg を読み取って内容を説明して"

なぜ必要か? → 後述の「画像をCLIに渡す方法」で詳しく説明します。

-(ハイフン):stdinから入力

プロンプトを引数ではなくstdinから渡す指定です。

echo "こんにちは" | claude -p -

日本語や改行を含む長いプロンプトを渡す場合、引数経由だとエスケープの問題が起きるため、stdinパイプが安全です。

--setting-sources:CLAUDE.mdの読み込み制御

Claude CLIは通常、プロジェクトの CLAUDE.md やグローバルの ~/.claude/CLAUDE.md を自動で読み込みます。

これが問題になるのが、claude -p をボットやアシスタントの応答エンジンとして使う場合です。例えば、SlackボットやAlexaスキルのバックエンドで claude -p を使い、ユーザーの発話に対してそのまま返答させるケースを考えます(参考:claude -p で自然言語買い物リストを作った話)。

もし CLAUDE.md に口調や性格の設定(「語尾に〜をつけて」「絵文字を多用して」など)を書いていると、ボットの返答にまでその性格が反映されてしまいます。開発中の対話用にカスタマイズしたCLAUDE.mdの設定が、意図せずプロダクトの出力に漏れ出すわけです。

# CLAUDE.mdを一切読み込まない
claude -p --setting-sources "" "プロンプト"

# ユーザー設定のみ(プロジェクトのCLAUDE.mdはスキップ)
claude -p --setting-sources user "プロンプト"

# プロジェクト設定のみ(ユーザーのCLAUDE.mdはスキップ)
claude -p --setting-sources project "プロンプト"

--setting-sourcesuser, project, local をカンマ区切りで指定します。

Node.jsからの呼び出しパターン

基本ラッパー関数

import { spawn } from "child_process";

const CLAUDE_TIMEOUT = 120000; // 2分

interface RunClaudeCLIOptions {
  model?: string;
  allowedTools?: string[];
}

export function runClaudeCLI(
  prompt: string,
  options?: RunClaudeCLIOptions
): Promise<string> {
  return new Promise((resolve, reject) => {
    // 引数を組み立て
    const args = ["-p", "--output-format", "json"];
    if (options?.model) {
      args.push("--model", options.model);
    }
    if (options?.allowedTools) {
      for (const tool of options.allowedTools) {
        args.push("--allowedTools", tool);
      }
    }
    args.push("-"); // stdinから入力

    // ★ CLAUDECODE環境変数を削除(後述)
    const env = { ...process.env };
    delete env.CLAUDECODE;

    const child = spawn("claude", args, {
      stdio: ["pipe", "pipe", "pipe"],
      env,
    });

    let stdout = "";
    let stderr = "";

    child.stdout.on("data", (data: Buffer) => {
      stdout += data.toString();
    });
    child.stderr.on("data", (data: Buffer) => {
      stderr += data.toString();
    });

    // タイムアウト処理
    const timer = setTimeout(() => {
      child.kill("SIGTERM");
      reject(new Error(`タイムアウト(${CLAUDE_TIMEOUT / 1000}秒)`));
    }, CLAUDE_TIMEOUT);

    child.on("close", (code) => {
      clearTimeout(timer);
      if (code === 0) {
        try {
          const envelope = JSON.parse(stdout);
          if (envelope.is_error) {
            reject(new Error(`Claude CLI エラー: ${envelope.result}`));
            return;
          }
          resolve(envelope.result ?? "");
        } catch {
          resolve(stdout); // フォールバック
        }
      } else {
        reject(new Error(`終了コード ${code}: ${stderr || stdout}`));
      }
    });

    // ★ stdinにプロンプトを書き込んで閉じる
    child.stdin.write(prompt);
    child.stdin.end();
  });
}

呼び出し例:

// テキスト生成(Haiku・速い)
const result = await runClaudeCLI("面白い挨拶を考えて", { model: "haiku" });

// 画像認識(Opus + Readツール許可)
const result = await runClaudeCLI(
  "画像ファイル /tmp/photo.jpg を読み取って内容を説明して",
  { model: "opus", allowedTools: ["Read"] }
);

なぜ exec / execFile ではなく spawn なのか

日本語 + 改行 + JSON を含むプロンプトは exec 系だと壊れます。

// ❌ NG: シェルエスケープで日本語や改行が壊れる
exec(`claude -p "${prompt}"`);
execFile("claude", ["-p", prompt]); // 長いと引数制限に引っかかる

// ✅ OK: stdinパイプなら何でも安全に渡せる
const child = spawn("claude", ["-p", "-"]);
child.stdin.write(prompt);
child.stdin.end();

これは最初に一番ハマったポイントです。プロンプトが英語の短文なら exec でも動きますが、日本語の長文やJSON構造を含むと高確率で壊れます。

CLAUDECODE環境変数の削除

Claude Code上で開発しているとき(つまりClaude Codeのセッション内で npm run dev 等を実行しているとき)、CLAUDECODE という環境変数が子プロセスに引き継がれます。Claude CLIはこれを検知すると「ネストセッション」と判断してエラーになります。

const env = { ...process.env };
delete env.CLAUDECODE;

開発環境特有の問題ですが、知らないとデバッグに時間を食います。

画像をCLIに渡す方法(Claude君、最大のハマりポイント)

画像を読み取って、そのキャラクター特徴を踏まえてテキスト生成してもらうケースで、初期段階でClaudeCodeが実装したのが、Base64形式で画像を読み取らせる手法でしたが、全然うまく行ってませんでした。

❌ 方法1:Base64 data URLを埋め込む

// これは動かない!!
const prompt = `![image](data:image/jpeg;base64,${base64data})
この画像の内容を説明して`;

claude -pBase64 data URLを画像として認識しません。テキストとして扱われ、AIは画像の内容を見ずに「それっぽい回答」を返します。しかもそれっぽいので気づきにくいのが厄介です。

❌ 方法2:画像を個別にBase64で送る

複数画像を個別のBase64で送ろうとすると "Prompt is too long" エラーになります。

✅ 正解:ファイル保存 + --allowedTools Read

画像をファイルに保存し、--allowedTools Read でReadツールを有効化。プロンプト内でファイルパスを指示すると、CLIが画像を正しく認識してくれます。

const result = await runClaudeCLI(
  "画像ファイル /tmp/photo.jpg を読み取って、写っているものを説明してください",
  {
    model: "opus", // 画像認識はOpus推奨
    allowedTools: ["Read"],    // Readツールを許可
  }
);

Readツールは画像ファイル(JPEG, PNG等)をサポートしているため、画像として適切に処理されます。

複数画像を渡したい場合:コンタクトシート方式

複数の画像を1枚のグリッド画像にまとめてから渡すと効率的です。自分のシステムでは、sharpで画像を5列グリッドに配置して1枚のJPEGにまとめ、番号ラベルを振ってClaude CLIに渡しています。

// sharpで複数画像をグリッド配置
const sheet = await sharp({
  create: {
    width: columns * cellWidth,
    height: rows * cellHeight,
    channels: 3,
    background: { r: 255, g: 255, b: 255 },
  },
})
  .composite(images.map((img, i) => ({
    input: img.buffer,
    left: (i % columns) * cellWidth,
    top: Math.floor(i / columns) * cellHeight,
  })))
  .jpeg({ quality: 75 })
  .toFile("/tmp/contact-sheet.jpg");

画像認識にはOpusが必要

画像内の日本語テキスト読み取りは、少なくとも自分の用途だとHaikuやSonnetでは思ったような精度が出ませんでした。--model opus を指定しましょう。

CLIレスポンスのJSON解析

--output-format json--json-schema の違い

ここ紛らわしいので整理します。

--output-format json は、CLIのレスポンス全体を JSONエンベロープで包んでくれるオプションです:

{
  "type": "result",
  "result": "ここにClaudeの応答テキストが入る",
  "is_error": false
}

エンベロープ自体は確実にJSONですが、result の中身はClaudeの自由記述テキストです。「JSONで返して」とプロンプトに書いても、result の中にマークダウンのコードブロックで囲まれたJSONが入ったり、前後に説明文がついたりします。

一方 --json-schema を併用すると、レスポンスに structured_output フィールドが追加され、スキーマに沿ったバリデーション済みJSONが入ります。こちらは確実にパースできます。

claude -p --output-format json \
  --json-schema '{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}' \
  "名前を返して"
エンベロープ result の中身 structured_output
--output-format json のみ 確実にJSON 自由記述(パース不安定) なし
--output-format json + --json-schema 確実にJSON 自由記述 スキーマ準拠のJSON

--json-schema を使わない場合の自前パーサー

自分のシステムでは --json-schema を使わずに開発したため、result の中身を堅牢にパースする関数を書きました。3段階のフォールバックで対応しています:

function parseJsonResponse<T>(text: string, type: "array" | "object"): T {
  // Step 1: 直接パース(そのままJSONの場合)
  try {
    return JSON.parse(text) as T;
  } catch { /* continue */ }

  // Step 2: マークダウンコードブロック除去して再試行
  const stripped = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/)?.[1]?.trim() ?? text.trim();
  try {
    return JSON.parse(stripped) as T;
  } catch { /* continue */ }

  // Step 3: ブラケット深度カウント方式で抽出
  //   テキストの中から [ ... ] や { ... } を見つけ出す
  const extracted = type === "array" ? extractJsonArray(text) : extractJsonObject(text);
  if (extracted) {
    return JSON.parse(extracted) as T;
  }

  throw new Error("JSONのパースに失敗しました");
}

Step 3の深度カウント方式では、文字列リテラル内のブラケット([ ] { })を正しくスキップするのがポイントです。「はい、結果はこちらです: [{"name": ...}]」のようなレスポンスでも確実に配列部分だけ取り出せます。

今から作るなら --json-schema を使ったほうが楽だと思います。

知っておくと便利なオプション

実際には使わなかったけど、知っておくと役立つオプションです。

--max-turns:ツール使用回数の制限

claude -p --max-turns 3 --allowedTools Read "ファイルを読んで分析して"

エージェントが無限にツール呼び出しを繰り返すのを防げます。

--max-budget-usd:コスト上限

claude -p --max-budget-usd 0.50 "大量のファイルを分析して"

暴走防止に便利。

--append-system-prompt:システムプロンプトの追加

claude -p --append-system-prompt "必ずJSON形式で回答してください" "質問"

デフォルトのシステムプロンプトを保持しつつ、追加の指示を入れられます。--system-prompt だとデフォルトが完全に上書きされるので注意。

--continue / --resume:会話の継続

# 最初のリクエスト
claude -p "このコードを分析して"

# 直前の会話を継続
claude -p -c "もっと詳しく"

# セッションIDで再開
claude -p --resume "session-id-here" "続き"

--effort:思考の深さ調整

claude -p --effort low "簡単な変換"    # 軽い・速い
claude -p --effort high "複雑な分析"    # 深い・遅い

--dangerously-skip-permissions:全権限スキップ

# ⚠️ CI/CDなど信頼できる環境でのみ
claude -p --dangerously-skip-permissions "テストを実行して修正して"

すべてのパーミッション確認をスキップ。名前の通り危険なので限定的に。

CLIオプション全一覧(リファレンス)

セッション管理

フラグ 説明
-p / --print 非インタラクティブモード
-c / --continue 直前の会話を継続
-r / --resume <id> セッションIDで再開
--from-pr <NUMBER> GitHub PRのセッションを再開
--session-id <UUID> セッションID指定
--no-session-persistence セッション保存しない(-p時のみ)

モデル・出力

フラグ 説明
--model <NAME> モデル指定(haiku, sonnet, opus 等のエイリアスも可)
--fallback-model <NAME> 過負荷時のフォールバック先
--output-format <FORMAT> 出力形式:text, json, stream-json
--json-schema <SCHEMA> 構造化出力のスキーマ
--effort <LEVEL> 思考の深さ:low, medium, high
--verbose 詳細ログ

権限・セキュリティ

フラグ 説明
--allowedTools <TOOLS> 許可するツール
--disallowedTools <TOOLS> 禁止するツール
--tools <TOOLS> 使用可能ツールの限定("" で全無効)
--dangerously-skip-permissions 全権限スキップ
--permission-mode <MODE> default, plan, bypassPermissions
--max-turns <N> 最大ターン数
--max-budget-usd <N> コスト上限

システムプロンプト・設定

フラグ 説明
--system-prompt <TEXT> システムプロンプトを完全置換
--system-prompt-file <PATH> ファイルからシステムプロンプトを読み込み(置換)
--append-system-prompt <TEXT> システムプロンプトに追加(推奨)
--append-system-prompt-file <PATH> ファイルからシステムプロンプトに追加
--setting-sources <LIST> 設定ソース制御(user,project,local

エージェント

フラグ 説明
--agent <NAME> エージェント指定
--agents <JSON> カスタムサブエージェントをJSON定義

MCP・プラグイン

フラグ 説明
--mcp-config <PATH|JSON> MCPサーバー設定
--strict-mcp-config 指定MCPのみ使用

その他

フラグ 説明
--add-dir <PATH> 追加ディレクトリ
--worktree / -w git worktreeで隔離実行
--debug [filter] デバッグモード
--input-format <FORMAT> 入力形式:text, stream-json

実践Tips

Tip 1:タイムアウトは自前で実装する

Claude CLIにはビルトインのタイムアウトがないので、setTimeout + SIGTERM で実装します。

const timer = setTimeout(() => {
  child.kill("SIGTERM");
  reject(new Error("タイムアウト"));
}, 120000);

Tip 2:--output-format json を常に使う

text 形式だとエラー判定が難しいですが、json 形式なら is_error フィールドで明確に判定できます。プログラムからの呼び出しなら常に json を使いましょう。

Tip 3:画像が多いならバッチ分割

コンタクトシートに画像を詰め込みすぎると認識精度が落ちます。20枚程度でバッチ分割して Promise.all で並列処理するのがおすすめです。

Tip 4:AIの座標は信用しない

画像からクロップ座標を返させる場合、AIが返す座標は画像の範囲外になることがよくあります。必ず実際の画像サイズでクランプしましょう。

const meta = await sharp(imagePath).metadata();
const x = Math.max(0, Math.min(aiCrop.x, meta.width! - 1));
const y = Math.max(0, Math.min(aiCrop.y, meta.height! - 1));

まとめ

Claude Code CLIをプログラムから呼び出してローカルAIシステムを構築する際のポイント:

  1. -p でプログラムから呼び出し可能 — SDKやAPIキー管理不要
  2. spawn + stdinパイプ — 日本語プロンプトを安全に送る唯一の方法
  3. --output-format json — 構造化レスポンスでエラーハンドリングも楽に
  4. 画像は --allowedTools Read + ファイルパス — data URLは罠、Readツール経由が正解
  5. 画像認識は --model opus — Haikuでは精度が出ない
  6. CLAUDECODE 環境変数を削除 — Claude Code上で開発してる人は要注意
  7. --setting-sources — CLAUDE.mdの読み込み制御
  8. JSONパースは多段フォールバック — CLIの出力は「ほぼJSON」であって純粋なJSONではない

Claude Codeがインストール済みなら、claude -p 一つでローカルのアプリにAI機能を組み込めます。API SDKのセットアップなしに使い始められるので、個人開発やプロトタイピングでは特に手軽です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?