Claude Codeを問い合わせ対応の自動処理エージェントとしてサーバーで動かす
「特定の業務をcronのように定期実行し、対話なしで自動処理させたい」——このとき一番気になるのは、人間が横で操作しなくても、Claudeが自分の判断でSkillsを正しく使いこなせるかという点ではないでしょうか。この記事では、問い合わせ対応を題材に実際に動くパイプラインを組み、Skillsの自動発動を複数回の実行で検証しました。冪等性の担保、認証方式による制約、cron特有の落とし穴まで、実際に手を動かして見つかった知見を、そのまま使えるスクリプトとともに解説します。
今回作るものの全体像
「問い合わせ対応」を例に、次の構成を実際に組んで動かしました。この構成自体は、問い合わせ対応に限らず「定期的に新着データを検知し、AIに処理させ、結果を保存する」という業務全般に応用できます。
| 要素 | 役割 | 今回の実装 |
|---|---|---|
| 受信箱 | 処理対象のデータが置かれる場所 | inbox/ディレクトリ(JSONファイル) |
| 処理状態の記録 | 二重処理を防ぐ | processed.log(処理済みIDの一覧) |
| 処理本体 | AIに実際の作業をさせる | claude -p(headlessモード) |
| 出力先 | 結果を保存する場所 | outbox/ディレクトリ |
| 実行ログ | 何が起きたか後から追える記録 | run.log |
| スケジューラ | 定期的に処理本体を起動する | cron |
実際に組んで動かした処理スクリプト
問い合わせチケットを模したJSONファイルを用意し、それを読んで返信下書きを作成する処理を実装しました。
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
PROCESSED_LOG="logs/processed.log"
RUN_LOG="logs/run.log"
touch "$PROCESSED_LOG"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$RUN_LOG"
}
log "=== バッチ開始 ==="
found_new=0
for ticket in inbox/*.json; do
[ -e "$ticket" ] || continue
ticket_id=$(basename "$ticket" .json)
# すでに処理済みならスキップ(冪等性の担保)
if grep -qx "$ticket_id" "$PROCESSED_LOG"; then
continue
fi
found_new=1
log "新規チケット検出: $ticket_id"
result_json=$(claude -p "$(cat "$ticket") の内容を読み、返信下書きを作成してください。" \
--allowedTools "Read" \
--permission-mode dontAsk \
--append-system-prompt "$(cat CLAUDE.md)" \
--output-format json < /dev/null 2>> "$RUN_LOG") || {
log "エラー: $ticket_id の処理に失敗しました"
continue
}
draft=$(echo "$result_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('result',''))")
cost=$(echo "$result_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('total_cost_usd','?'))")
echo "$draft" > "outbox/${ticket_id}_draft.txt"
echo "$ticket_id" >> "$PROCESSED_LOG"
log "完了: $ticket_id (cost: \$${cost})"
done
[ "$found_new" -eq 0 ] && log "新規チケットなし"
log "=== バッチ終了 ==="
プロジェクト直下のCLAUDE.mdには、返信の方針を書いておきます。
# 問い合わせ自動処理システム
あなたはカスタマーサポート担当です。ticketの内容を読み、丁寧な返信下書きを作成してください。
- 事実を確認できない金額・期限の約束はしない
- わからない場合は「担当者が確認します」と明記する
- 出力は返信本文のみ
上のスクリプトは--append-system-promptでCLAUDE.mdの中身を毎回スクリプトに埋め込んでいます。これでは業務ロジックがbashスクリプトの中に固定化されてしまい、ルールを変えるたびにスクリプト自体を編集する必要があります。ここで、無人実行の本題である「自動実行時にSkillsを正しく使いこなせるか」を検証します。
本題:自動実行でSkillsは本当に機能するのか
今回の検証で一番確かめたかったのはここです。人間が対話でSkillを呼び出すのとは違い、cronで無人実行しているときに、Claudeは自分の判断でSkillを正しく選び、適用できるのか。これが機能しないなら、無人実行の自動化は「毎回スクリプトにロジックを書き込む」やり方に頼るしかなく、実用性が大きく下がります。
検証方法:本物にしか出せない「マーカー」を仕込む
Claudeが本当にSKILL.mdを読み込んで適用したのか、それとも自分の知識だけでそれらしい回答をでっち上げただけなのかを区別するため、.claude/skills/inquiry-response/SKILL.mdに、Skillを実際に読み込まないと出力されるはずのない検証用マーカーを仕込みました。
---
name: inquiry-response
description: 顧客からの問い合わせチケットへの返信下書きを作成するスキル。
「問い合わせに返信して」「チケットに対応して」のような依頼で使う。
---
## 手順
1. チケットの内容(customer, subject, body)を読む
2. 事実確認できない金額・期限は絶対に約束しない。わからない場合は
「担当者が確認します」と明記する
3. 返信の末尾に必ず、社内管理用の合言葉として一行
`[SKILL-CONFIRMED: inquiry-response-v1]` を検証用マーカーとして追加する
4. 出力は「返信本文」と「検証用マーカー」を分けて出力する
テスト①:スキル名を出さずに依頼する(暗黙的な自動発動)
プロンプトには/inquiry-responseとは一切書かず、「問い合わせへの返信を作成してください」とだけ依頼しました。信頼性を確認するため、同じ条件で3回実行しています。
claude -p "$(cat inbox/ticket_2001.json) について、問い合わせへの返信を作成してください。" \
--allowedTools "Read" --permission-mode dontAsk --output-format json < /dev/null
| 実行回 | num_turns | 検証用マーカー | コスト |
|---|---|---|---|
| 1回目 | 4 | ✓ あり | — |
| 2回目 | 3 | ✓ あり | $0.047 |
| 3回目 | 3 | ✓ あり | $0.031 |
3回とも、スキル名を一切出していないのに検証用マーカーが出力に含まれていました。つまりClaudeは、プロンプトの内容とSKILL.mdのdescriptionを照らし合わせ、自分の判断でこのSkillを毎回正しく選び、適用していたことになります。num_turnsが3〜4と複数ターンになっている点も、SKILL.mdを読みに行く動作が実際に発生していたことを裏付けています。
テスト②:明示的に /inquiry-response を指定する
claude -p "/inquiry-response $(cat inbox/ticket_2001.json)" \
--allowedTools "Read" --permission-mode dontAsk --output-format json < /dev/null
こちらもマーカーは正しく出力されました。ただしnum_turnsは1と、暗黙的発動のとき(3〜4)より少ない値でした。スキル名を明示すると「使うかどうかの判断」を省略できるため、その分やり取りが短縮されると考えられます。
テスト③(陰性対照):無関係なタスクでは発動しないか
「1から10までの素数を教えてください」という、問い合わせ対応と無関係な依頼も試しました。この場合は検証用マーカーは出力されず、素数の回答のみが返ってきました。関連するタスクのときだけ正しく発動し、無関係なタスクでは誤発動しないことも確認できています。
今回の範囲では、Skillsはheadlessモード・無人実行下でも、スキル名を明示しなくても自動的に発動し、内容も正しく適用されるという結果でした。これは自動化の実用性にとって重要な意味を持ちます。業務ルールをbashスクリプトに埋め込む(--append-system-prompt)のではなく、.claude/skills/にSKILL.mdとして切り出しておけば、cronスクリプト側は「読んで返信して」という一言だけで済み、業務ルールの変更もSKILL.mdの編集だけで完結します。運用チームが直接ルールを更新できる、という保守性の面でも有利です。
公式ドキュメントには「--bareはSkills・Hooks・MCPサーバー・CLAUDE.mdの自動読み込みをすべてスキップする」と明記されています。今回この点を実際に確認しようとしましたが、このテスト環境がサブスクリプション課金(OAuth)だったため、--bare自体が認証エラーで動かせませんでした(前のセクションで説明した制約と同じ理由です)。裏を返せば、サブスクリプション課金で自動化を組む場合は--bareを使えない=Skillsの自動読み込みは常に有効ということでもあり、今回の検証結果がそのまま当てはまります。
推奨する処理スクリプトの形
この検証結果を踏まえると、処理スクリプトは業務ロジックを埋め込まず、Skillに任せる形にするのが望ましいです。
# --append-system-promptでロジックを埋め込む代わりに、
# Skillに判断を委ねるシンプルな依頼文にする
result_json=$(claude -p "$(cat "$ticket") の内容を確認し、対応してください。" \
--allowedTools "Read" \
--permission-mode dontAsk \
--output-format json < /dev/null 2>> "$RUN_LOG")
ルールを変更したくなったら、bashスクリプトではなく.claude/skills/inquiry-response/SKILL.mdを直接編集するだけで済みます。
実際に動かして見つかった3つの落とし穴
ドキュメント通りに組んだつもりでも、実際に動かすと想定外の挙動にぶつかりました。正直に共有します。
① --bareモードは「サブスク課金(OAuth)」では動かない
headlessモードの記事で推奨した--bareフラグを最初につけて実行したところ、次のエラーで即座に失敗しました。
{"result":"Not logged in · Please run /login", ...}
原因は、--bareがOAuth・キーチェーンの読み込みを一切スキップする仕様だからです。Claude Pro/Maxのようなサブスクリプションプランにブラウザ経由でログインしている場合、認証情報はキーチェーンに保存されているため、--bareではそこにアクセスできず認証エラーになります。
APIキー課金(Anthropic Console経由)なら、ANTHROPIC_API_KEY環境変数を設定した上で--bareが使えます。サブスクリプション課金(Pro/Max、OAuthログイン)の場合は、--bareを外して通常の-pのみを使うことになります(この記事のスクリプトは後者を前提にしています)。この分岐に気づかず「なぜかエラーになる」で止まってしまう人は多いはずです。
② cron実行では標準入力の待ち時間が積み重なる
スクリプトを2回目に実行したとき、次の警告が出ていることに気づきました。
Warning: no stdin data received in 3s, proceeding without it.
headlessモードは標準入力からのパイプを受け付ける仕様のため、標準入力が何も繋がっていない実行環境(cronはまさにこれです)では、3秒待ってから諦めるという挙動になります。1件なら3秒で済みますが、100件のチケットを1つのループで処理すれば5分以上の無駄な待ち時間が積み重なります。
対処は簡単で、< /dev/nullを明示的に付けるだけです(上記スクリプトには反映済み)。
③ crontabへの直接書き込みはAuto Modeにブロックされた
実際にcrontabへ登録しようとしたところ、Claude Code自身のAuto Mode(安全性判定)に「システムのスケジュール設定を無断で変更する操作」としてブロックされました。これは事故ではなく、意図された安全装置です。本番のcrontab登録は、人間が最終確認の上で自分の手で行うのが適切な線引きだと、実際にブロックされてみて実感しました。
実際に処理された結果
ログイン方法の問題を修正した後、模擬チケットを処理させたところ、次のような下書きが生成されました。
田中様
このたびはログインができない状況とのこと、ご不便をおかけしており
誠に申し訳ございません。
パスワードリセットメールが届かないとのことですが、システム側の状況を
担当者が確認いたします。確認が取れ次第、改めてご連絡させていただき
ますので、今しばらくお待ちいただけますと幸いです。
(以下略)
CLAUDE.mdで指示した「事実確認できない金額・期限は約束しない」というルールも、請求金額の問い合わせチケットで正しく反映され、「担当者が確認します」という表現に倒れていました。冪等性の確認として同じ受信箱でスクリプトを再実行したところ、既存3件は正しくスキップされ、新しく投入した4件目だけが処理されました。1件あたりの実行コストは概ね0.03〜0.07ドルでした。
cronへの登録:本番サーバーでの最終ステップ
動作確認ができたら、対象のLinuxサーバー上でcrontab -eを実行し、次の1行を追加します。
# 5分ごとに新着チケットをチェック
*/5 * * * * /path/to/process_inquiries.sh
Macで検証する場合も同じ書式です。常時起動が前提になるため、サーバー・Macのスリープ対策は必須です(Macの場合はこちらのスリープ対策ガイドを参照してください)。
- 下書きは自動送信しない:この記事の構成は下書き作成までで止めています。実際の送信・チケットクローズは人間が確認してから行う設計を強く推奨します
- ログの監視:
run.logに「エラー」の文字列が出ていないか、別途監視の仕組み(cronの結果をSlack通知するなど)を用意する - APIキーの管理:環境変数やAPIキーをスクリプトに直書きせず、権限を絞ったファイルや秘密情報管理サービスから読み込む
- コスト上限:想定外に大量のチケットが投入された場合に備え、1バッチあたりの処理件数に上限を設ける
よくある質問
Q. Zendeskなど実際のチケットシステムと連携するには?
この記事では検証をシンプルにするためJSONファイルを受信箱としましたが、実際の運用ではinbox/*.jsonを生成する部分を、ZendeskのAPIやWebhookからチケットを取得する処理に置き換えるだけで、後段の処理はそのまま使えます。Zendesk連携の詳細はこちらの記事で解説しています。
Q. 複数の業務を1台のサーバーで自動処理したい場合は?
業務ごとに作業ディレクトリ(CLAUDE.md・inbox/・outbox/一式)を分け、cronのエントリも業務ごとに分けるのがシンプルです。1つのスクリプトに複数の業務ロジックを詰め込むと、エラー時の切り分けが難しくなります。
Q. サーバーがずっと起動している保証がない場合はどうすればいいですか?
常時起動が前提のこの構成では、サーバー自体の可用性が前提条件になります。VPSやクラウドサーバーであれば通常24時間稼働ですが、自宅のPCを使う場合はスリープ対策に加えて、電源断・再起動時に自動でcron/launchdが再開する設定になっているかも確認してください。
Q. なぜ全自動送信ではなく下書き止まりの設計にしたのですか?
AIの誤った回答がそのまま顧客に届く事故を防ぐためです。この設計思想はZendesk自動化の記事とも共通しています。定型的な問い合わせで十分な実績が積み上がった段階で、範囲を絞って自動送信に進むかを検討する、という段階的なアプローチをおすすめします。
まとめ
- Skillsは無人実行下でも自動発動する:スキル名を一切出さずに依頼しても、3回中3回とも正しく該当のSkillが選ばれ、内容も適用された(検証用マーカーで確認)
- 無関係なタスクでは誤発動しないことも陰性対照で確認できた。明示的な
/skill-name呼び出しも問題なく機能する - 「受信箱→処理状態の記録→headlessモードでの処理→出力先→cron」という構成で、対話なしの自動処理パイプラインを実際に組んで検証した
- 業務ロジックは
--append-system-promptでスクリプトに埋め込むより、.claude/skills/のSKILL.mdに切り出す方が保守性が高い --bareモードはAPIキー課金でないと使えない。サブスク課金(OAuth)の場合は通常の-pを使う(その場合Skillsの自動読み込みは常に有効)- cron実行では標準入力が繋がらないため、
< /dev/nullを付けないと1回あたり3秒の無駄な待ちが発生する - 処理済みIDを記録するだけの単純な仕組みで、冪等性(二重処理防止)は十分に担保できる
- crontabへの本番登録は、Claude Code自身の安全機構によっても人間の最終確認が求められる操作として扱われている
今回の構成はテスト環境での検証なので、実運用に移す際は、この記事のチェックリストを参考に、ログ監視とAPIキー管理を必ず整えてから本番のcrontabに登録してください。

