AIエージェントことはじめ — LAB 015
— 実行スクリプトと、夜の予約・朝の確認 —
自分が寝ているあいだにも、AI に作業を進めてもらう。便利ですが、 人がいない時間は、昼間と前提が3つ変わります。 確認を出しても答える人がいない・終わったかを誰も見ない・止まらなくても止める人がいない。 この記事では、その3つに備えた小さな夜の仕組みを、ダミーの仕事で試しながら組み立てます。 後編では、時間切れと合否の検査つきの実行スクリプトを作り、夜に予約して朝に確かめます。
約30分
全部やる場合の目安
2 STEP
手順の数
最初は週1回
夜の実行の頻度
RECAP
前編では、人がいない時間に任せる準備として、次の3つを済ませました。
STEP 1
任せる仕事を4つの条件で選んだ
STEP 2
ダミーの場所で1回だけ試走した
STEP 3
無人で使うモードを決め、壁が効くかを確かめた
STEP 4 — 目安 15分 / この記事の中心
夜の3つの前提(確認に答える人がいない・終わったか見ない・止める人がいない)に備える、小さなスクリプトを1本置きます。 やることは4つだけです。
~/night/workspace)
ターミナルに貼ってください。Python は Mac 付属の /usr/bin/python3 を使います
(LAB 012 のフックと同じ理由で、自分で入れた環境に依存させないため)。
cat > ~/night/run.py <<'EOF'
#!/usr/bin/python3
import json
import os
import signal
import subprocess
import sys
import time
HOME = os.path.expanduser("~")
WORK = os.path.join(HOME, "night", "workspace") # AI に触らせるのは、このフォルダだけ
LOGDIR = os.path.join(HOME, "night", "log")
CLAUDE = os.environ.get("NIGHT_CLAUDE", os.path.join(HOME, ".local", "bin", "claude")) # which claude の結果に直す
MODE = "acceptEdits" # 前編 STEP 3 で決めたモード
LIMIT = int(os.environ.get("NIGHT_LIMIT", "1800")) # 1回の上限(秒)
PROMPT = (
"memo.txt を読んで、要点をちょうど3行にまとめ、summary.md に書いてください。"
"他のファイルは作らず、このフォルダの外には触れないでください。"
"壁に止められたら、別の方法で回り込まず、理由を報告して止まってください。"
)
def run_claude(prompt):
cmd = [CLAUDE, "-p", prompt, "--permission-mode", MODE, "--output-format", "json"]
p = subprocess.Popen(cmd, cwd=WORK, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, start_new_session=True)
try:
out, err = p.communicate(timeout=LIMIT)
return out, err
except subprocess.TimeoutExpired:
os.killpg(p.pid, signal.SIGKILL) # 子プロセスごと止める
out, err = p.communicate()
return out, err + "\n[時間切れで止めました]"
def denials(out):
try:
return len(json.loads(out).get("permission_denials", []))
except ValueError:
return -1 # 読み取れなかった
def check(): # 完了条件:成果物を検査する
path = os.path.join(WORK, "summary.md")
if not os.path.exists(path):
return "summary.md が作られていません"
with open(path, encoding="utf-8") as f:
lines = [x for x in f.read().splitlines() if x.strip()]
if len(lines) != 3:
return "summary.md が3行ではありません(%d行)" % len(lines)
return ""
def main():
os.makedirs(LOGDIR, exist_ok=True)
old = os.path.join(WORK, "summary.md")
if os.path.exists(old):
os.replace(old, old + ".prev") # 前回の成果物で合格しないように、どかす
logpath = os.path.join(LOGDIR, "run-" + time.strftime("%Y%m%d-%H%M%S") + ".log")
problem = ""
with open(logpath, "w", encoding="utf-8") as log:
log.write("モード: %s / 上限: %d秒\n" % (MODE, LIMIT))
for attempt in (1, 2):
prompt = PROMPT
if problem:
prompt += "\n前回の不合格の理由:" + problem
out, err = run_claude(prompt)
problem = check()
log.write("--- %d回目 / 壁に止められた回数: %d ---\n%s\n%s\n" % (attempt, denials(out), out, err))
if not problem:
break
log.write("結果: %s\n" % ("合格" if not problem else "不合格 " + problem))
print("合格" if not problem else "不合格:" + problem)
print("ログ:", logpath)
return 1 if problem else 0
if __name__ == "__main__":
sys.exit(main())
EOF
start_new_session=True と os.killpg の組は、AI が呼んだ子プロセス(AI が裏で起動した別のプログラム)ごと止めるためです。親だけ止めても、子が夜通し残ることがあります(Mac には timeout コマンドが無いので、Python で書いています)check() が完了条件です。AI の返事は読まず、summary.md が「あって、3行」であることだけを見ます。条件を甘くして通すのは禁物です(LAB 010)CLAUDE の場所は、which claude で出た場所に直してください(cron は PATH、つまりコマンドを探す場所が短く、名前だけでは見つからないため。STEP 5)筆者のタスクキューは時間の上限を置いていません。画面に止めるボタンがあり、人が押せるからです。 cron にはそのボタンを押す人がいないので、ここでは上限時間が停止ボタンの代わりになります。
/usr/bin/python3 ~/night/run.py; echo $?
cat ~/night/workspace/summary.md
「合格」とログの場所が出て、最後が 0 になり、summary.md に3行が入っていれば成功です。
「できた」という文章ではなく、番号と成果物で確かめるのが、LAB 010 から続く約束です。
合格のときの画面は、次のような形です(ログの場所は人によって違います)。
合格 ログ: /Users/(あなたのユーザー名)/night/log/run-20260101-020000.log 0
不合格のときは、1行目が「不合格:」に続けて理由(「summary.md が作られていません」など)になり、最後の番号が 1 になります。 理由はログにも残ります。
✓ ここまでの確認:
手で走らせて、最後が 0、summary.md が3行、ログの最初の行にモードが書かれていた。
STEP 5 — 目安 15分
cron は、決めた時刻にコマンドを自動で走らせる Mac の仕組みです。
予約は人が自分のターミナルでやる作業にします。
筆者の環境では、AI の箱(サンドボックス)の中から crontab(予約を書き換えるコマンド)は動かず、
AI にやらせる理由もないためです。
先に which claude で場所を確かめ、STEP 4 の CLAUDE を直しておきます。
そのうえで、次を貼ります。
which claude
crontab -l
(crontab -l 2>/dev/null; echo '0 2 * * 1 /usr/bin/caffeinate -i /usr/bin/python3 $HOME/night/run.py >> $HOME/night/log/cron.log 2>&1') | crontab -
crontab -l
0 2 * * 1 は「分 時 日 月 曜日」の順で、毎週月曜の 2:00 の意味ですcrontab -l で、すでにある予約を先に見ます。3行目は、今ある予約に1行足す形です(crontab - だけで流し込むと、前の予約が丸ごと消えます)caffeinate -i は、実行しているあいだ Mac が眠らないようにするコマンドです>> …/cron.log 2>&1 は、夜は画面を見る人がいないので、本来画面に出る文字(エラーも)を cron.log に書き足す指定です。つまずき集の「cron.log も見る」は、このファイルのことです% を書かないでください。cron が特別な意味で読み、途中で切れます
Mac が眠っていると、cron は走りません。
caffeinate は「走っているあいだ」眠らせないだけで、眠った Mac を起こしてはくれません。
確実にするなら、人が自分で sudo pmset repeat wakeorpoweron M 01:55:00(月曜 1:55 に起こす)を打ちます。
やめるときは sudo pmset repeat cancel です。
sudo は管理者の操作なので、AI にはやらせません(LAB 012 の壁でも止めてあります)。
夜の実行も、契約している AI の使用量を消費します。 筆者のタスクキューの自動巡回は、1日1回、5時間ごとの使用量が 0% のときだけ走る作りです。 最初は週1回で動かして、ログと使用量を見てから回数を決めます。 毎日にするのは、1か月ほど様子を見てからで十分です。
夜に任せた仕事は、朝に人が見るところまでが1セットです。見る場所は3つだけに絞ります。
grep -h "^モード\|^---\|^結果" ~/night/log/run-*.log | tail -n 6
cd ~/night/workspace
git status --short
cat summary.md
1行目は、ログの中から行の先頭が「モード」「---」「結果」の行だけを抜き出して、後ろ6行を見せます (AI の返事そのものはとても長い1行になるので、先頭の文字で絞っています)。合格した夜は、こう見えます。
モード: acceptEdits / 上限: 1800秒 --- 1回目 / 壁に止められた回数: 0 --- 結果: 合格
git status --short に、決めたフォルダ以外の変更や、思いがけないファイルが無いか。?? summary.md は想定内で、2回目の実行からは(STEP 4-2 の手動実行も数えます) ?? summary.md.prev(前回の成果物。スクリプトがどけた控え)も出ますsummary.md を自分の目で読む。合格は「条件を満たした」だけで、「良い出来」ではありません。
気に入らなければ、git checkout -- . で記録済みのファイルの変更を戻し、新しくできた summary.md を消して、
依頼文か完了条件を直します(元に戻せる、という条件①が効く場面です)。
壁に止められた回数が 0 でないときは、夜に何かが止まっています。ログを読んで、止まった理由を確かめてください。
✓ ここまでの確認:
crontab -l に1行が入っている。
予約の時刻を過ぎたあと、ログが増え、5-3 の3か所を自分の目で見た(時刻まで待てなければ、ここは翌日の確認で構いません)。
TROUBLESHOOTING
| 起きていること | 原因 | どうするか |
|---|---|---|
| 夜に走ったのに、仕事が終わっていない。エラーも出ていない | 許可が出ない操作(たずねる場面)が、自動で拒否された。実行は「正常終了」の形になる | ログの「壁に止められた回数」を見る。必要な操作だけを通す方向で、モードか設定を見直す(全部通す設定にはしない) |
| 予約の時刻を過ぎたのに、ログが増えていない | Mac が眠っていた/予約が入っていない | crontab -l で予約を確認。眠りは 5-1 の pmset(人が打つ)で起こす。cron.log も見る |
cron.log に command not found や、No such file が出る |
cron は PATH が短い。claude の場所が、名前だけで指定されている |
which claude の結果を、スクリプトの CLAUDE にフルパスで書く |
予約の行を足したのに、動かない。crontab -l では途中で切れている |
予約の行に % を書いた |
% を使わない書き方にする(日付は、スクリプトの中で作る) |
Operation not permitted が出る |
cron から、書類・デスクトップ・iCloud Drive など、Mac が保護している場所に触ろうとした | 夜に触る場所は、ホーム直下の ~/night に置く。保護された場所を、夜の作業場にしない |
| 毎回、上限時間いっぱいで打ち切られる | ログインが切れている/Mac がロックされていて、認証情報が読めず固まっている/仕事が大きすぎる | 昼に claude を開いて、ログインし直す。それでも続くなら、仕事を小さく分ける。筆者の記録にも、無人実行で認証情報が読めず固まる問題への備えがあります。cron からログイン情報が読めるかは環境で違うので、予約を入れたら、夜を待たず昼のうちに数分後の時刻で1回走らせて確かめます |
| 2回やっても「不合格」のまま | 依頼文があいまい/完了条件が、AI に見えない形になっている | 完了条件を甘くして通さない。依頼文に「何をどう作ると合格か」を足す。条件が厳しすぎるなら、人が見直す |
| AI の使用量が、朝には空になっている | 夜の実行が、使用量を使い切った | 週1回に戻す。仕事を小さくする。claude --help に、費用の上限を決める項目があれば使う(版で変わります) |
| 予約を足したら、前の予約が消えた | 今ある予約を読まずに、新しい1行だけで書き換えた | 5-1 の形(crontab -l に1行足して流し込む)を使う。先に crontab -l で控えておく |
FAQ
案を作らせるのは構いませんが、採用するのは人にします。 仕事をする側が、自分の合格基準を書くと、通りやすい基準になりがちだからです。 夜に走らせる前に、わざと失敗させて、不合格になることも確かめてください(LAB 010 と同じ)。
お勧めしません。外へ出す行為は取り消せず、朝に人が見る前に起きてしまうからです。 夜は下書きまで、外へ出すのは朝に人が押すと分けると、失敗しても被害が内側で済みます。 筆者のサイトも、夜に手直しした記事を公開するのは、人がボタンを押したときだけです。
launchd(Mac 標準の、常駐や予約の仕組み)も使えます。ここでは、設定が1行で済み、手で書き換えて確かめやすい cron にしました。 仕組みが違っても、「眠っていると走らない」「フルパスで書く」「保護された場所に触らない」といった注意は同じです。
前編 STEP 1 の4条件を、仕事ごとに通します。 仕事を増やすより、1つの仕事の完了条件を厳しくする方が先です。 週1回で1か月ほど、ログと朝のレビューで「止められた回数」と「不合格の回数」が落ち着いてから足します。
この記事の朝のレビュー(3か所)が最小の形です。 「AI がどこまで自分で回り続けているか」を、毎回測り直して見る話は、LAB 016 に書きました。
NEXT
まず、昼のうちに、前編 STEP 2 の試走を1回だけやってみてください。 目的は夜の自動化ではなく、「止められたのに、正常終了に見える」を自分の目で1回見ることです。
permission_denials を、自分で見に行くNEXT — LAB 016
夜に回り始めたら、次は「AI が自分でどこまで回り続けているか」を測る話です。 測れると、増やしてよいか、減らすべきかを数字で決められます。
※ この記事は、筆者が実際に使っているタスクキューの設計と、2026-08-30〜09-09 の記録をもとにしていますが、結果を保証するものではありません。 モードの名前や、止められたときの出力は、製品やバージョンで変わります。 「人がいない前提で、通すものを絞り、成果物で合否を決め、朝に見る」という考え方の方を持ち帰ってください。