Claude のデスクトップアプリで、Code タブのローカル定期タスクに Bash のコマンドを書いている人に向けた、失敗の記録です。日付を取り出すために書いた $( ) の1行で、誰も見ていない朝に実行が止まった話です。
対象と前提
- 対象は、Claude デスクトップアプリの Code タブで作るローカルの定期タスクです。クラウドで動く定期タスクは確かめていません
- 起きたことは、2026年9月の私の環境のものです。公式の資料は2026年10月24日に読んだもので、当時と今とで仕様が同じかは分かりません
何が起きたか
2026年9月5日は、SNS へ毎日1本投稿する定期タスクが、初めて自動で動いた朝でした。
7時6分に実行が始まり、最初の Bash のコマンドで4分以上止まりました。誰も画面を見ていない時間です。エラーの知らせは来ていません。
止まっていたのは、「今日はもう投稿したか」を数える1行でした。投稿の履歴を1行1件のファイルに残していて、行の頭が今日の日付なら、もう投稿済みという判定です。
grep -c "^$(date +%Y-%m-%d)" 投稿履歴.tsv
その朝の投稿が出たのは7時16分です。止まっていた実行がどうやって先へ進んだのかは、記録に残っていません。
この日はもう1つ、「常に許可」を選んだ結果、日付の入った使い捨ての許可が4本増えています。日付が入っている以上、次の日のコマンドには当たらないはずの許可です。
なぜ止まったか
公式の資料で確かめられたこと
定期タスクの資料(Schedule recurring tasks in Claude Code Desktop)に書いてあることです。
- ローカルの定期タスクは、時刻が来るたびに新しいセッションを始める
- 許可のモードはタスクごとに決める。
~/.claude/settings.jsonの allow のルールも効く - Manual のモードで許可の無い道具を使おうとすると、人が承認するまで実行が止まる
- 止まらないように、作ったあと Run now で一度動かし、出てきた許可を「常に許可」にしておく、と勧めている
許可の資料(Configure permissions)には、Bash について次のことが書いてあります。
-
&&や|でつないだコマンドは、ひとつずつ分けてルールに合うかを見る - deny や ask のルールは、コマンド置換や
forの中のコマンドにも効く - 解析しきれないコマンドは、承認を求める
$( ) の中身も調べられる、というところまでは公式に書いてあります。allow のルールが $( ) 入りのコマンドを通すかどうかは、私が読んだ範囲では書かれていませんでした。また、いまの資料では grep は承認なしで動く読み取りだけのコマンドの一覧に入っています。9月5日に止まった理由を、当時の仕様と照らし合わせることはできていません。
私の環境で起きたこと
-
grepを許可の一覧に入れていても、$( )が入った1行は自動では通らず、承認待ちになった - チャットの中で私が積み上げた承認は、定期タスクの新しいセッションには引き継がれなかった
- 承認待ちになっても、知らせは来なかった
$( ) は、動かしてみるまで中で何が走るかが決まらない書き方です。10月1日には、for や while のループ、$変数、ヒアドキュメントでも、私の環境では同じように承認待ちになることが分かりました。
人がそばにいれば、承認の画面が出たところで押せば済みます。定期タスクは、人がいない時間に動かすためのものです。そこで承認待ちになると、知らせる仕組みが別に無い限り、誰も気づきません。
直し方
日付はスクリプトの中で取る
コマンドの中で日付を作らず、日付を取る所から判定までを、小さなスクリプトにまとめました。定期タスクの指示に書くのは、それを呼ぶ1行だけです。
python3 /絶対パス/kyou_toukou.py
スクリプトの形はこうです(実物を一般化したものです)。
#!/usr/bin/env python3
"""今日の分がもう履歴にあるかを見る。定期タスクの最初に呼ぶ。
まだなら 0、済んでいれば 1 で終わる。"""
import sys
from datetime import datetime
from pathlib import Path
LOG = Path("/絶対パス/投稿履歴.tsv")
kyou = datetime.now().strftime("%Y-%m-%d")
if not LOG.exists():
print("まだ:履歴ファイルがありません。進んでよい。")
sys.exit(0)
with LOG.open(encoding="utf-8") as f:
hit = [line.rstrip("\n") for line in f if line.startswith(kyou)]
if hit:
print(f"済:{kyou} の分はもうあります。何もしないこと。")
for h in hit:
print(" " + h)
sys.exit(1)
print(f"まだ:{kyou} の分はありません。進んでよい。")
sys.exit(0)
考えたことは3つです。
1つ目は、コマンドの文字を毎回同じにすることです。日付はスクリプトの中で決まるので、定期タスクが打つ1行は今日も明日も同じ文字です。公式の資料が勧める「一度動かして常に許可」は、文字が毎回同じであってはじめて次の日にも効く、と私は理解しています。
2つ目は、読む相手に向けた出力です。結果を読むのは定期タスクの中の Claude なので、「済」「まだ」と次にどうするかを、そのまま文で出しています。
3つ目は終了コードです。ここでは「済」を1にしています。launchd や cron が直接見るものではなく、定期タスクの中で結果を読むための道具なので、1は失敗ではなく「もう済んでいる」の意味です。スケジューラーに直接つなぐなら、紛らわしいので分けたほうがよいと思います。
弱いところも1つ残っています。履歴のファイルが無いときは「まだ」と答えるので、ファイルの場所を書き間違えたり置き場を移したりすると、毎回「まだ」と答え、同じ日に2回出す方向に倒れます。見直したい所です。
指示に「書かない形」を書いておく
あわせて、定期タスクの指示の側に、使わない形を書いておくようにしました。$( ) とバッククォート、ループ、$変数、ヒアドキュメントは使わない。日付や時刻が要るならスクリプトに見させる。ファイルの場所は頭から全部書く。許可の範囲を広げるのではなく、止まる形を指示から外す向きです。
置き換えたあと、この1行で止まったという記録は私の手元にはありません。ただ、当時のアプリのログはもう残っていないので、止まらなかったことを数で示せるわけではありません。この投稿の仕事は、その後、定期タスクの外(launchd)へ移しました。
確かめ方
どれも読み取りだけです。場所は私の環境のものです。
定期タスクの指示に $( ) が残っていないかは、この1行で探しています。
grep -n -F '$(' ~/.claude/scheduled-tasks/*/SKILL.md
私の環境では、当たった行のほとんどが「$( ) を使わない」という注意書きそのものでした。当たった行は、ひとつずつ目で見て分けています。バッククォートは Markdown のコードの書き方と見分けがつかないので、機械では探していません。
ほかに見ている所です。
- ループ、
$変数、ヒアドキュメントも、指示に入っていないか - タスクの詳しい画面の Always allowed に、日付の入った使い捨ての許可が並んでいないか。並んでいるなら、毎回文字が変わるコマンドを打っているのかもしれません
- 作ったタスクを Run now で一度動かし、承認の画面が出ないかを見たか
- 承認待ちで止まったとき、誰かに知らせが届く仕組みがあるか。無いなら、出るはずのものが出たかを、別の仕組みで数えているか
定期タスクの中の1行の $( ) は、人のいる時間なら一度押せば済む話です。人のいない朝には、それだけで止まります。少なくとも私の環境ではそうでした。
この件を含む定期タスクの実測の記録は note にまとめています:https://note.com/vellum_office/n/n466306e5553b