FRL 自由研究ラボ

AIエージェントことはじめ — LAB 015

人がいない時間に、AIを働かせる(後編)

— 実行スクリプトと、夜の予約・朝の確認 —

自分が寝ているあいだにも、AI に作業を進めてもらう。便利ですが、 人がいない時間は、昼間と前提が3つ変わります。 確認を出しても答える人がいない・終わったかを誰も見ない・止まらなくても止める人がいない。 この記事では、その3つに備えた小さな夜の仕組みを、ダミーの仕事で試しながら組み立てます。 後編では、時間切れと合否の検査つきの実行スクリプトを作り、夜に予約して朝に確かめます。

約30分

全部やる場合の目安

2 STEP

手順の数

最初は週1回

夜の実行の頻度

この記事の道順

  1. 前編のおさらい
  2. STEP 4 — 時間切れと合否の検査つきの実行スクリプト
  3. STEP 5 — 夜の予約(cron)と、朝のレビュー
  4. つまずき集(症状から引く)
  5. よくある質問
  6. 次の一歩

※ LAB 005(戻れる仕組み)、 LAB 010(終了コードで合否を決める)、 LAB 012(AI に勝手をさせない壁)を終えている前提です。 特にLAB 012 の壁(設定の禁止リストとフック)が入っていることが出発点です。 壁なしで夜に任せるのは、お勧めしません。 例は Mac の Claude Code(ターミナルで動く AI エージェント)です。 なお cron(クロン)は、決めた時刻にコマンドを自動で走らせる Mac の仕組みで、STEP 5 で使います。

RECAP

前編のおさらい

前編では、人がいない時間に任せる準備として、次の3つを済ませました。

STEP 1

任せる仕事を4つの条件で選んだ

STEP 2

ダミーの場所で1回だけ試走した

STEP 3

無人で使うモードを決め、壁が効くかを確かめた

04

STEP 4 — 目安 15分 / この記事の中心

時間切れと合否の検査つきの実行スクリプト

夜の3つの前提(確認に答える人がいない・終わったか見ない・止める人がいない)に備える、小さなスクリプトを1本置きます。 やることは4つだけです。

夜のあいだに、スクリプトがひとりで回る流れ cron が 夜に起動 run.py が claude -p を呼ぶ 上限時間つきで AI が作業 成果物を 完了条件で検査 合格 ログに残して終わり 不合格 → 理由を渡して もう1回だけやり直す 合否は、AI の言葉ではなく、検査の終了コードで決める(LAB 010)。 上限時間を超えたら、子プロセスごと止める。止めるボタンを押す人の代わりです。 2回失敗したら深追いせず、不合格のまま朝を待つ。

4-1 スクリプトを置く

ターミナルに貼ってください。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

筆者のタスクキューは時間の上限を置いていません。画面に止めるボタンがあり、人が押せるからです。 cron にはそのボタンを押す人がいないので、ここでは上限時間が停止ボタンの代わりになります。

4-2 手で1回走らせて、合否を見る

/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行、ログの最初の行にモードが書かれていた。

05

STEP 5 — 目安 15分

夜の予約(cron)と、朝のレビュー

cron は、決めた時刻にコマンドを自動で走らせる Mac の仕組みです。 予約は人が自分のターミナルでやる作業にします。 筆者の環境では、AI の箱(サンドボックス)の中から crontab(予約を書き換えるコマンド)は動かず、 AI にやらせる理由もないためです。

5-1 予約を足す(毎週月曜の午前2時)

先に 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

Mac が眠っていると、cron は走りません。 caffeinate は「走っているあいだ」眠らせないだけで、眠った Mac を起こしてはくれません。 確実にするなら、人が自分で sudo pmset repeat wakeorpoweron M 01:55:00(月曜 1:55 に起こす)を打ちます。 やめるときは sudo pmset repeat cancel です。 sudo は管理者の操作なので、AI にはやらせません(LAB 012 の壁でも止めてあります)。

5-2 最初は週1回から

夜の実行も、契約している AI の使用量を消費します。 筆者のタスクキューの自動巡回は、1日1回、5時間ごとの使用量が 0% のときだけ走る作りです。 最初は週1回で動かして、ログと使用量を見てから回数を決めます。 毎日にするのは、1か月ほど様子を見てからで十分です。

5-3 朝のレビュー(3か所だけ見る)

夜に任せた仕事は、朝に人が見るところまでが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 ---
結果: 合格
  1. ログ:モードが決めたとおりか/壁に止められた回数が 0 か/結果が「合格」か
  2. 変わったファイル:git status --short に、決めたフォルダ以外の変更や、思いがけないファイルが無いか。?? summary.md は想定内で、2回目の実行からは(STEP 4-2 の手動実行も数えます) ?? summary.md.prev(前回の成果物。スクリプトがどけた控え)も出ます
  3. 成果物の中身: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

よくある質問

完了条件(check)も、AI に書かせてよいですか?

案を作らせるのは構いませんが、採用するのは人にします。 仕事をする側が、自分の合格基準を書くと、通りやすい基準になりがちだからです。 夜に走らせる前に、わざと失敗させて、不合格になることも確かめてください(LAB 010 と同じ)。

夜のうちに、公開や送信までやらせてはいけませんか?

お勧めしません。外へ出す行為は取り消せず、朝に人が見る前に起きてしまうからです。 夜は下書きまで、外へ出すのは朝に人が押すと分けると、失敗しても被害が内側で済みます。 筆者のサイトも、夜に手直しした記事を公開するのは、人がボタンを押したときだけです。

cron ではなく、launchd を使うべきですか?

launchd(Mac 標準の、常駐や予約の仕組み)も使えます。ここでは、設定が1行で済み、手で書き換えて確かめやすい cron にしました。 仕組みが違っても、「眠っていると走らない」「フルパスで書く」「保護された場所に触らない」といった注意は同じです。

仕事を増やしたくなったら?

前編 STEP 1 の4条件を、仕事ごとに通します。 仕事を増やすより、1つの仕事の完了条件を厳しくする方が先です。 週1回で1か月ほど、ログと朝のレビューで「止められた回数」と「不合格の回数」が落ち着いてから足します。

夜の仕組みが、きちんと回っているかを知るには?

この記事の朝のレビュー(3か所)が最小の形です。 「AI がどこまで自分で回り続けているか」を、毎回測り直して見る話は、LAB 016 に書きました。

NEXT

次の一歩

まず、昼のうちに、前編 STEP 2 の試走を1回だけやってみてください。 目的は夜の自動化ではなく、「止められたのに、正常終了に見える」を自分の目で1回見ることです。

  • 人がいないと、たずねるは自動で拒否になり、終わったかを見る人も、止める人もいない
  • 任せる仕事は、戻せる・合否が機械で決まる・外へ出ない・30分以内の4条件を全部満たすものだけ
  • 止められた操作はエラーにならない。permission_denials を、自分で見に行く
  • モードは小さい権限から。確認を全部飛ばす設定と、サンドボックスの自動許可は使わない
  • 合否は成果物の検査で決め、上限時間で打ち切る。朝はログ・変更・中身の3か所を見る

NEXT — LAB 016

どこまで回っているかを測る →

夜に回り始めたら、次は「AI が自分でどこまで回り続けているか」を測る話です。 測れると、増やしてよいか、減らすべきかを数字で決められます。

※ この記事は、筆者が実際に使っているタスクキューの設計と、2026-08-30〜09-09 の記録をもとにしていますが、結果を保証するものではありません。 モードの名前や、止められたときの出力は、製品やバージョンで変わります。 「人がいない前提で、通すものを絞り、成果物で合否を決め、朝に見る」という考え方の方を持ち帰ってください。