1
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 -pを叩く時のハマりどころ — 権限・PATH・タイムアウト

1
Posted at

はじめに

当社(合同会社ジョインクラス)では、記事のネタ補充、リード収集、部門ごとの状態チェックなどを claude -p を叩くシェルスクリプトにして、macOS の launchd で17本以上のジョブとして毎日回しています。

ターミナルで手で叩くと動くのに、スケジュール実行にすると動かない。このパターンに何度もハマりました。原因はだいたい次の3つです。

  1. PATH: claude や node が見つからない
  2. 権限: ツール許可プロンプトで止まる。または許可しすぎる
  3. タイムアウト・暴走: 終わらない、または気づかないうちにコストが膨らむ

この記事では、当社で実際に動いているスクリプトをもとに、それぞれの対処を書きます。

1. PATH: 「ターミナルでは動くのに」の正体

症状

launchd や cron から起動すると、ログに claude: command not found や env: node: No such file or directory が出ます。

原因

launchd や cron は ~/.zshrc を読みません。PATH は /usr/bin:/bin:/usr/sbin:/sbin くらいしかなく、nvm で入れた Node も Homebrew も見えません。claude は Node 製の CLI なので、claude 本体と node の両方が PATH に必要です。

対処: スクリプトの先頭で PATH を明示する

当社のスクリプトは、すべて先頭で PATH を固定しています(共通部分は common.sh にまとめています)。

common.sh(抜粋)
#!/bin/bash
export PATH="/Users/kyoagun/.nvm/versions/node/v22.17.0/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

ポイントは2つです。

  • nvm のバージョン入りの絶対パスを書く。nvm use はシェル関数なので、非対話シェルでは使えません
  • Node をアップデートしたら、このパスも直す。古いバージョンを消すと全ジョブがまとめて止まります

実際、当社でも一部のスクリプトだけ /opt/homebrew/bin が抜けていて、jq や gh を使う後半の処理だけ失敗していたことがありました。claude は動くので、気づくのが遅れます。

検証方法

launchd に近い最小限の環境で手動実行してみると、すぐに再現できます。

env -i HOME="$HOME" /bin/bash -c 'source ./common.sh; which claude node jq; claude --version'

env -i で環境変数を空にして起動すれば、「自分のシェルでは動く」状態をなくせます。

補足: そもそも cron が動かなかった件

もう一つ、macOS の cron はフルディスクアクセスの制約に引っかかりやすく、当社では cron ジョブが全滅しました。最終的に launchd へ全面移行して解決しています。macOS で定期実行するなら、最初から launchd を使うことをおすすめします。

2. 権限: 止まらせず、許可しすぎない

症状

  • 非対話の -p 実行でツール許可が必要になり、何もせずに終わる
  • 面倒になって --dangerously-skip-permissions を付けたくなる

対処: --allowedTools でジョブごとに最小限だけ許可する

当社では、ジョブごとに必要なツールだけを列挙しています。実際の呼び出しはこうです。

auto-replenish-articles.sh(抜粋)
# ネタ調査フェーズ:読む・調べるだけ
claude_run replenish --effort low --allowedTools "Read,WebSearch,WebFetch" -p "..."

# 執筆フェーズ:ファイル書き込みだけ(Bash は渡さない)
claude_run replenish --effort low --allowedTools "Write,Read,Edit" -p "..."
note/like-notes-api.sh(抜粋)
# MCP ツールは個別名で絞る
claude --effort low \
  --allowedTools "mcp__note-api__search-notes,mcp__note-api__like-note" -p "..."

運用してみて決めたルールは次のとおりです。

方針 理由
フェーズごとに許可を分ける 調査フェーズに Write を渡さない。プロンプトインジェクション対策にもなる
MCP はワイルドカードより個別名 mcp__note-api* だと削除系 API まで許可してしまう
Bash は本当に必要なジョブだけ 開発・営業部門のチェックだけに限定している
--dangerously-skip-permissions は使わない 無人実行では、事故が起きても止める人がいない

もう一つのハマりどころ: --bare で認証が消える

起動を速くしようと --bare を付けたところ、Not logged in で全ジョブが失敗しました。--bare は設定や認証情報の読み込みを省くため、ログイン済みのサブスクリプション認証も読まれません。当社のラッパーには、二度とやらないようにコメントで残しています。

3. タイムアウトと暴走: 時間とお金の両方に上限を付ける

症状

  • Web 検索を繰り返して何十分も終わらず、次のジョブと重なる
  • 気づいたらコストが想定の数倍になっている

対処: 共通ラッパーで予算の上限とフォールバックを強制する

当社では claude を直接叩かず、すべて次のラッパーを通しています。

lib/claude-run.sh
#!/bin/bash
# 注意: --bare は認証を読まないため "Not logged in" で失敗する。使わない。
_CLAUDE_RUN_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

claude_run() {
  local label="$1"; shift
  claude --output-format json \
         --fallback-model claude-sonnet-5 \
         --max-budget-usd 3 \
         "$@" 2>/dev/null \
    | python3 "$_CLAUDE_RUN_LIB/record-cost.py" "$label"
}
  • --max-budget-usd 3: 1回あたりの上限です。暴走しても $3 で止まります
  • --fallback-model: メインのモデルが混雑していても、ジョブを落とさずに続けます
  • --output-format json: 結果と一緒に実コストが返ってくるので、record-cost.py でラベル別に cost-tracker.json へ積み上げます。標準出力には応答テキストだけを返すので、$(claude -p ...) とそのまま置き換えられます
  • --effort low: 定型ジョブには深い思考がいらないので、呼び出し側で指定します

実際に計測してわかったのは、「2+2」を聞くだけでも1回 $0.27〜0.47 かかることです。内訳の大半は約41,000トークンのキャッシュ作成で、そのうち自社の CLAUDE.md・エージェント・スキルは5,800トークンしかありませんでした。残りは Claude Code 側の固定コストなので、削れるのは呼び出し回数だけです。小さな claude -p をループの中で何回も呼ぶ設計は避け、1回にまとめるようにしました。

時間の上限: 予算とは別に wall-clock でも切る

予算の上限だけでは、待ち時間が長い場合(API の応答待ちなど)に止まりません。macOS には GNU timeout が標準で入っていないので、brew install coreutils で入る gtimeout を使うのが手軽です。

# 15分で SIGTERM、さらに30秒後に SIGKILL
gtimeout --kill-after=30 900 bash -c 'source lib/claude-run.sh; claude_run lead-collect --effort low -p "..."'
rc=$?
[ $rc -eq 124 ] && echo "$(date) lead-collect timeout" >> logs/error.log

終了コード 124 がタイムアウトです。ここをログに残しておかないと、「何も起きていない」のか「毎回途中で殺されている」のか区別できません。

二重起動を防ぐ

長いジョブが次の起動時刻とぶつかると、同じファイルを2つのプロセスが書き換えます。当社では Zenn の「1日1コンテンツ」制限をロックファイルで守っていますが、考え方は同じです。

LOCK="/tmp/$(basename "$0").lock"
mkdir "$LOCK" 2>/dev/null || { echo "already running"; exit 0; }
trap 'rmdir "$LOCK"' EXIT

mkdir は排他的に作成されるので、macOS に flock がなくても使えます。

4. 見落としがちな細かい点

  • 2>/dev/null の副作用: ラッパーで stderr を捨てているため、認証エラーも見えなくなります。導入直後や不調のときは、一時的に外してログに流してください
  • 作業ディレクトリ: launchd の初期ディレクトリは / です。CLAUDE.md を読ませたいなら、スクリプトの先頭で cd してください
  • プロンプト中のクォート: -p "..." に変数を埋め込むと、" や $ で壊れます。長いプロンプトはヒアドキュメントで変数に入れてから渡すのが安全です

チェックリスト

  • スクリプトの先頭で、node と claude を含む PATH を絶対パスで export しているか
  • env -i の最小環境で動作確認したか
  • --allowedTools をフェーズ・ジョブごとに最小限にしているか
  • --bare / --dangerously-skip-permissions を使っていないか
  • --max-budget-usd と gtimeout で、お金と時間の両方に上限があるか
  • 二重起動の防止とタイムアウトのログがあるか

おわりに

当社は少人数の会社で、17本以上の定期ジョブを AI に任せています。それが回っているのは、モデルが賢いからというより、止まり方と失敗のしかたを設計しているからだと思っています。PATH・権限・タイムアウトの3点を押さえておけば、「夜中に止まっていた」はかなり減ります。

launchd の設定や Hooks との組み合わせ、ジョブ全体の設計については、書籍『Claude Code 全自動化バイブル』にまとめています。権限設計をさらに深く知りたい方には『企業のためのClaude Codeセキュリティガイド』もあわせておすすめします。

書籍一覧: https://zenn.dev/joinclass?tab=books

1
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
1
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?