この記事は、個人開発の体験をもとに要点を書き出し、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-sources は user, 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 = `
この画像の内容を説明して`;
claude -p は Base64 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システムを構築する際のポイント:
-
-pでプログラムから呼び出し可能 — SDKやAPIキー管理不要 -
spawn+ stdinパイプ — 日本語プロンプトを安全に送る唯一の方法 -
--output-format json— 構造化レスポンスでエラーハンドリングも楽に -
画像は
--allowedTools Read+ ファイルパス — data URLは罠、Readツール経由が正解 -
画像認識は
--model opus— Haikuでは精度が出ない -
CLAUDECODE環境変数を削除 — Claude Code上で開発してる人は要注意 -
--setting-sources— CLAUDE.mdの読み込み制御 - JSONパースは多段フォールバック — CLIの出力は「ほぼJSON」であって純粋なJSONではない
Claude Codeがインストール済みなら、claude -p 一つでローカルのアプリにAI機能を組み込めます。API SDKのセットアップなしに使い始められるので、個人開発やプロトタイピングでは特に手軽です。