AWS 上で Claude Code を cron から動かす — サブスク認証のまま、API 課金なしで

AWS
B!

手元の Mac で毎日21回動かしていた Claude Code のバッチを、AWS のサーバへ移しました。
「Claude をサーバで動かす=API 従量課金」だと思われがちですが、サブスクリプションの
認証のまま動かせます

この記事は、その具体的なやり方と、移して初めて分かった落とし穴の記録です。

記事中の IP アドレス・ホスト名はすべてダミーです(RFC 5737 の文書用レンジ)。

この記事は3本シリーズの 2 本目です。
あわせて読む: CloudFormation で Lightsail を立ち上げるLinux サーバで Claude Code のアカウントを切り替える

前提:何を動かしているか

Claude Code を -p(headless)で起動し、手順書を渡して自律実行させるバッチです。

claude -p "$(cat prompt.txt)" \
  --model claude-opus-5 \
  --permission-mode auto \
  --max-budget-usd 45

1日21スロット、1回あたり数分〜数十分。これをノートPCで回し続けるのが嫌になった、
というのが移行の動機です。

認証:claude setup-token

サーバで動かすための鍵はこれ1本です。

ssh -t batch-runner 'claude setup-token'

ブラウザで承認すると、1年有効の長命トークンが発行されます。

✓ Long-lived authentication token created successfully!

Your OAuth token (valid for 1 year):

sk-ant-oat01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Store this token securely. You won't be able to see it again.
Use this token by setting: export CLAUDE_CODE_OAUTH_TOKEN=<token>

これはサブスクリプションの認証であって API キーではありません。
API の従量課金は発生せず、サーバ代(この構成では月12ドル)だけで済みます。

★トークンは「表示されるだけ」で保存されない

一番の罠がここです。setup-token値を表示するだけで、どこにも保存しません
~/.claude/.credentials.json も作られません。自分で置き場所を決める必要があります。

cron から認証をどう読ませるか

CLAUDE_CODE_OAUTH_TOKEN を export すればいいのですが、cron は .bashrc
.profile も読みません
。cron は $SHELL -c コマンド で起動するだけだからです。

素直に効く手が1つあります。BASH_ENV です。
bash は非対話・非ログインで起動したとき、$BASH_ENV が指すファイルを読み込みます。

# ~/.claude/secrets.sh (パーミッションは 600)
export CLAUDE_CODE_OAUTH_TOKEN="$(cat ~/.claude/accounts/main.token)"
SHELL=/bin/bash
HOME=/home/ubuntu
PATH=/home/ubuntu/.local/bin:/usr/local/bin:/usr/bin:/bin
BASH_ENV=/home/ubuntu/.claude/secrets.sh

45 3,16 * * * /home/ubuntu/batch/job-a/run.sh
05 6,18 * * * /home/ubuntu/batch/job-b/run.sh

crontab に書いた環境変数はジョブに引き継がれるので、BASH_ENV もそのまま効きます。

効くかどうかは実物で確かめる

「たぶん効く」で夜間バッチに任せると、朝には全滅しています。
2分後に走る使い捨てのジョブを仕込んで確かめました。

cat > /tmp/crontest.sh <<'EOS'
#!/bin/bash
{
  echo "BASH_ENV=${BASH_ENV:-未設定}"
  echo "token_len=${#CLAUDE_CODE_OAUTH_TOKEN}"   # ★値そのものは書かない
} > /tmp/crontest.txt
EOS
chmod +x /tmp/crontest.sh
{ crontab -l; echo "$NEXT * * * * /tmp/crontest.sh"; } | crontab -
BASH_ENV=/home/ubuntu/.claude/secrets.sh
token_len=108

これで「cron → bash → 環境変数 → claude」の鎖がつながったと確認できました。

★ワークスペースを信頼済みにしないと、許可設定が全部捨てられる

移行後の最初のバッチのログに、こんな行がありました。

Ignoring 285 permissions.allow entries from .claude/settings.json:
this workspace has not been trusted. Run Claude Code interactively here once and
accept the trust dialog, or set projects["/path/to/project"].hasTrustDialogAccepted: true

許可設定285件が丸ごと無視されていました。
対話なら trust ダイアログに答えれば済みますが、cron に人はいません。

たまたまその回は作業ゼロのバッチだったので通りましたが、実作業があれば
権限待ちで止まっていました。~/.claude.json に書いて解決します。

import json, pathlib
p = pathlib.Path.home() / ".claude.json"
d = json.loads(p.read_text()) if p.exists() else {}
pj = d.setdefault("projects", {})
for path in ["/home/ubuntu/works/myproject"]:
    e = pj.setdefault(path, {})
    e["hasTrustDialogAccepted"] = True
    e.setdefault("hasCompletedProjectOnboarding", True)
p.write_text(json.dumps(d, ensure_ascii=False, indent=2))

これは「動くかどうか」ではなく「静かに劣化するかどうか」の問題なので、
起動確認の項目に入れておくべきです。私はプリフライトの検査に加えました。

OUT="$(cd "$PROJECT" && claude -p 'ok と2文字だけ返して' --max-turns 1 2>&1)"
printf '%s' "$OUT" | grep -q 'has not been trusted' \
  && echo "[NG] 未信頼で許可設定が捨てられている" \
  || echo "[OK] 許可設定が効いている"

実際どれくらいのマシンが要るか

移行前に一番読めなかったのがここでした。結論から言うと、驚くほど小さくて足ります

1分ごとにメモリを記録して実測しました。

USED=$(free -m | awk 'NR==2{print $3}')
FREE=$(free -m | awk 'NR==2{print $7}')
SWAP=$(free -m | awk 'NR==3{print $3}')
read -r N MAX <<EOF
$(ps -eo rss,args --no-headers | grep 'bin/claude' | grep -v grep \
  | awk '{n++; if ($1+0 > m) m = $1+0} END {printf "%d %d\n", n+0, int(m/1024)}')
EOF

余談:最初 pgrep -fc でプロセス数を数えていたのですが、0件のとき「0」を出力した上で
終了コード1を返す
ため、|| echo 0 を付けていると 0 が二重に出て記録が壊れました。
ps から数えるほうが素直です。

結果(2GB メモリのインスタンス):

時刻使用MB空きMBswapclaude最大RSS
18:4039515120148
18:41574133301267
18:43570133701266

claude 1プロセスあたり約330MB、swap の使用はゼロ。 2GB で十分でした。
念のため 2GB のスワップファイルは張ってありますが、使われていません。

★サブエージェントの同時実行数はバージョンで変わる

移行後、あるバッチの所要時間が 約500秒 → 984秒 に伸びました。
作業量は同じ(処理件数も同一)です。

ログに理由が書いてありました。

サブエージェントの同時実行上限が20体で、21体目以降はキューされず即エラーになりました。
45体を一度に投入できないため、20体 → 15体 → 9体 + 1 のバッチ投入で回しています。

45チャンクに分割する処理が、20体ずつ3波に分かれていたのです。
手元の Mac は少し古いバージョンで、45体を一度に起動できていました。

つまりこれは「AWS が遅い」のではなく、Claude Code のバージョン差です。
CPU もメモリも余っていました。環境変数で上げられます。

CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS=45

移行で性能が落ちたように見えたら、まずリソースを疑う前に処理の並列度を確認する
のが正解でした。メモリと CPU の実測があったので誤診を避けられました。

Lambda ではダメなのか

聞かれそうなので書いておきます。このワークロードでは無理でした。

Lambda の実行時間上限は **900秒(15分)**で、引き上げられません。
実績を集計するとこうなりました。

バッチ平均最長判定
A3,403秒14,644秒
B1,036秒2,014秒
C896秒1,629秒
D165秒627秒

12本中7本が超過します。しかも「たまに超える」ではなく、A は26回中25回が超過でした。

さらに、接続先が送信元IPを許可制にしている場合、Lambda には固定IPがないので
VPC + NAT Gateway が要ります。東京リージョンで 0.062 USD/時 = 月約45 USD
バッチの実行費を1円も払わない段階で、サーバ1台(12 USD)の3.7倍です。

15分制限のない ECS Fargate でも、固定IPの問題は同じです。
実行時間が長く・頻度が高く・送信元IPの固定が要るワークロードは、
素直に小さな箱を1台常時起動させるほうが安く単純でした。

プリフライトを作る

夜中に静かに全滅するより、今ここで気づきたい。11項目の検査を作りました。

=== 1. タイムゾーン ===
  [OK] Asia/Tokyo(現在 2026-09-14 11:02)
=== 4. claude の認証 ===
  [OK] secrets.sh を読んだ
  [OK] claude が応答した
  [OK] 許可設定(settings.json の allow)が効いている
=== 7. 接続先への ssh ===
  [OK] 繋がった / PHP 8.3.x
=== 9. 形態素解析用 venv ===
  [OK] sudachipy あり
...
=== 全項目 OK。cron を入れてよい ===

タイムゾーンは特に忘れがちです。cron は OS のタイムゾーンで動くので、
UTC のままだと全バッチが9時間ずれます。

sudo timedatectl set-timezone Asia/Tokyo

まとめ

  • claude setup-tokenサブスク認証のままサーバで動く。API 課金は発生しない
  • トークンは表示されるだけで保存されない。置き場所は自分で決める
  • cron から読ませるなら BASH_ENV.bashrc.profile も読まれない
  • hasTrustDialogAccepted を立てないと許可設定が全部捨てられる
    エラーにならず静かに劣化するので、起動確認の項目に入れる
  • メモリは 1プロセス約330MB。2GB で足りた
  • 遅くなったらまず並列度を疑うCLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS
  • Lambda は15分制限で載らない。固定IPが要るなら NAT の費用がサーバ代を超える

シリーズ記事

Mac の定期バッチを AWS へ移した一連の記録です。インフラを作る → その上で Claude Code を動かす → 利用上限で止まらないようにする、の順に読むと流れがつながります。

  1. CloudFormation で Lightsail を立ち上げる
    月12ドル固定のバッチ実行基盤をコードで作る
  2. AWS 上で Claude Code を cron から動かす(この記事)
    サブスク認証のまま、API 課金なしで
  3. Linux サーバで Claude Code のアカウントを切り替える
    利用上限で止まらないバッチ基盤
B!
← 一覧へ戻る