AI Agent를 백그라운드에서 돌리기 — cron · launchd · Task Scheduler 정리
Claude Code 같은 AI 에이전트를 사람 없이 주기적으로 실행하는 스케줄링 패턴. macOS launchd · Linux cron · Windows Task Scheduler 비교.
Claude Code 같은 AI 에이전트는 사람이 옆에 있을 때만 쓰는 도구가 아니다. 매일 새벽에 PR 리뷰, 이슈 분류, README 동기화, 로그 요약 — 사람 손이 안 닿는 시간에 자율 실행하면 가치가 배가된다.
이 가이드는 백그라운드 자율 실행의 3 OS별 표준 도구(macOS launchd, Linux cron, Windows Task Scheduler)를 같은 시나리오로 비교하고, AI 에이전트 특유의 함정(API 키 보안·로그 회수·실패 알림)을 짚는다.
TL;DR
| OS | 도구 | 정의 위치 | 로그 |
|---|---|---|---|
| macOS | launchd (LaunchAgent) | ~/Library/LaunchAgents/<label>.plist | StandardOutPath/StandardErrorPath |
| Linux | cron | crontab -e 또는 /etc/cron.d/<name> | MTA 또는 redirect >> log.txt 2>&1 |
| Windows | Task Scheduler | GUI 또는 PowerShell Register-ScheduledTask | Event Viewer 또는 파일 redirect |
공통 함정 5가지:
- PATH가 비어있어서 명령어를 못 찾음
- API 키를 평문으로 plist/crontab에 박음 (✗)
- 같은 작업이 겹쳐 실행되어 race
- 실패해도 조용히 죽음 (알림 없음)
- 컴퓨터가 슬립 중이면 실행 안 됨
본문 §1~§3에서 OS별 패턴, §4에서 함정 대응.
사전 조건
- 실행할 AI 에이전트 1개 (예: Claude Code, Cursor CLI, 자체 스크립트)
- 환경변수 또는 시크릿 매니저로 API 키 관리
- 셸 스크립트 1개 (예:
~/agents/daily-pr-review.sh)
1. macOS — launchd LaunchAgent — 10분
cron은 macOS에도 있지만 Apple은 launchd 권장. 슬립·재로그인 후 보정 실행도 launchd가 더 안정.
1.1 plist 작성
~/Library/LaunchAgents/dev.local.daily-pr-review.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>dev.local.daily-pr-review</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-lc</string>
<string>$HOME/agents/daily-pr-review.sh</string>
</array>
<!-- 환경변수: PATH는 비어있을 수 있으니 명시 -->
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<!-- ANTHROPIC_API_KEY는 plist에 박지 말 것 — §4.2 -->
</dict>
<!-- 매일 09:00 실행 -->
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>9</integer>
<key>Minute</key><integer>0</integer>
</dict>
<!-- 슬립 후 보정 실행 -->
<key>RunAtLoad</key><false/>
<key>StartOnMount</key><false/>
<!-- 로그 -->
<key>StandardOutPath</key>
<string>/tmp/daily-pr-review.out.log</string>
<key>StandardErrorPath</key>
<string>/tmp/daily-pr-review.err.log</string>
</dict>
</plist>1.2 등록 + 실행
# 등록
launchctl load ~/Library/LaunchAgents/dev.local.daily-pr-review.plist
# 즉시 실행 테스트 (스케줄 무시하고 1회)
launchctl start dev.local.daily-pr-review
# 상태 확인
launchctl list | grep daily-pr-review
# 해제
launchctl unload ~/Library/LaunchAgents/dev.local.daily-pr-review.plist1.3 슬립 후 보정 실행
기본 cron은 슬립 중인 시각의 작업을 건너뛴다. launchd는 다음 깨어났을 때 한 번 실행 (단, StartCalendarInterval은 보정 1회).
여러 시각 (매시간 등) 보정이 필요하면:
<key>StartInterval</key>
<integer>3600</integer> <!-- 1시간마다 -->→ 슬립에서 깨어난 직후 한 번만 실행. cron과 다른 점.
2. Linux — cron — 5분
2.1 crontab 편집
crontab -e예시:
# m h dom mon dow command
0 9 * * * /home/user/agents/daily-pr-review.sh >> /home/user/logs/pr-review.log 2>&1| 필드 | 값 | 의미 |
|---|---|---|
0 | 0 | 정각 |
9 | 9 | 9시 |
* * * | 모든 | 매일 |
매일 09:00 실행 + stdout/stderr를 로그 파일로 추가.
2.2 PATH 명시
cron의 PATH는 매우 빈약. 스크립트 또는 crontab 상단에 명시:
PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/sbin
SHELL=/bin/bash
0 9 * * * $HOME/agents/daily-pr-review.sh >> $HOME/logs/pr-review.log 2>&12.3 시스템 cron (/etc/cron.d/)
서버에서 root 권한으로 등록:
sudo tee /etc/cron.d/daily-pr-review > /dev/null <<'EOF'
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
0 9 * * * appuser /home/appuser/agents/daily-pr-review.sh >> /var/log/agent.log 2>&1
EOFappuser 필드 — 어떤 사용자로 실행할지 명시. 보안에 중요.
2.4 systemd timer (현대적 대안)
cron 대안. 단위 파일로 재시작·실패 처리·로깅 통합.
# /etc/systemd/system/pr-review.service
[Unit]
Description=Daily PR review agent
[Service]
Type=oneshot
User=appuser
ExecStart=/home/appuser/agents/daily-pr-review.sh
StandardOutput=journal
StandardError=journal# /etc/systemd/system/pr-review.timer
[Unit]
Description=Run daily PR review at 09:00
[Timer]
OnCalendar=*-*-* 09:00:00
Persistent=true # 시스템 종료 중 시각 지나갔으면 부팅 후 보정 실행
[Install]
WantedBy=timers.targetsudo systemctl enable --now pr-review.timer
journalctl -u pr-review.service # 로그Persistent=true가 cron 대비 큰 장점 — 서버 다운타임 후 자동 보정.
3. Windows — Task Scheduler — 10분
GUI도 가능하지만 PowerShell로 재현 가능하게 만드는 게 정석.
3.1 PowerShell로 등록
# 작업 정의
$action = New-ScheduledTaskAction `
-Execute "wsl.exe" `
-Argument "-d Ubuntu -e bash -lc '~/agents/daily-pr-review.sh'"
# 트리거: 매일 09:00
$trigger = New-ScheduledTaskTrigger -Daily -At 9am
# 설정: 사용자 로그인 안 해있어도 실행, 슬립에서 깨어나서 실행
$settings = New-ScheduledTaskSettingsSet `
-StartWhenAvailable `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-ExecutionTimeLimit (New-TimeSpan -Hours 1) `
-RestartCount 2 `
-RestartInterval (New-TimeSpan -Minutes 10)
# 등록
Register-ScheduledTask `
-TaskName "DailyPRReview" `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-Description "Run Claude Code daily PR review in WSL"3.2 GUI에서 확인
Win + R → taskschd.msc → Task Scheduler Library → "DailyPRReview".
3.3 즉시 실행 테스트
Start-ScheduledTask -TaskName "DailyPRReview"
Get-ScheduledTaskInfo -TaskName "DailyPRReview" | Select LastRunTime, LastTaskResultLastTaskResult: 0 = 성공.
3.4 WSL 안 스크립트 호출
위 예시처럼 wsl.exe -d Ubuntu -e bash -lc '...'로 WSL 안 셸 스크립트 호출. 환경변수·API 키 모두 WSL ~/.bashrc 또는 별도 env 파일에서 로드.
3.5 로그 회수
Task Scheduler 자체 로그는 부족. 스크립트 안에서 직접 redirect:
# ~/agents/daily-pr-review.sh
#!/usr/bin/env bash
set -euo pipefail
LOG="$HOME/logs/pr-review-$(date +%Y%m%d).log"
mkdir -p "$(dirname "$LOG")"
exec >> "$LOG" 2>&1
echo "[$(date)] starting..."
# 실제 에이전트 실행
claude-code --resume --task pr-review --quiet
echo "[$(date)] done."4. AI 에이전트 특유의 함정
4.1 PATH가 비어있어서 명령어 못 찾음 — 가장 흔한 1번 원인
대화형 셸 PATH ≠ launchd/cron PATH. 스크립트 첫 줄에 명시:
#!/usr/bin/env bash
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"또는 plist/crontab의 EnvironmentVariables / PATH= 헤더 사용.
검증:
launchctl getenv PATH # macOS
sudo -u appuser env | grep PATH # Linux cron user4.2 API 키를 plist/crontab에 박는 실수 — 절대 금지
ANTHROPIC_API_KEY=sk-ant-...를 plist EnvironmentVariables에 박으면:
ls -la에 노출- Time Machine·iCloud Backup에 평문 저장
- 다른 사용자가 같은 머신에 있으면
cat ~/Library/LaunchAgents/*.plist로 노출
대신:
- macOS: Keychain에 저장 + 스크립트가
security find-generic-password -s anthropic-api -w로 읽기 - Linux: 별도
~/.config/agent/env파일 +chmod 600+ 스크립트source ~/.config/agent/env - Windows:
Get-Credential또는 Credential Manager
스크립트 예시 (macOS):
#!/usr/bin/env bash
set -euo pipefail
export ANTHROPIC_API_KEY="$(security find-generic-password -s anthropic-api -w 2>/dev/null)"
[ -z "$ANTHROPIC_API_KEY" ] && { echo "missing API key in Keychain"; exit 1; }
# ... 에이전트 실행Keychain에 등록 한 번:
security add-generic-password -s anthropic-api -a "$USER" -w 'sk-ant-...'4.3 동시 실행 방지 — flock / 단일 인스턴스
같은 작업이 두 번 겹쳐 돌면 API 비용 2배 + race. flock으로 락:
#!/usr/bin/env bash
exec 200>/tmp/daily-pr-review.lock
flock -n 200 || { echo "already running"; exit 0; }
# ... 에이전트 실행flock -n = non-blocking. 이미 락이 있으면 즉시 종료.
4.4 실패 알림 — 조용히 죽지 않게
기본은 stderr가 로그 파일로 가고 끝. 실패해도 모름. trap으로 알림:
#!/usr/bin/env bash
set -euo pipefail
notify_failure() {
local code=$?
if [ $code -ne 0 ]; then
# macOS — terminal-notifier 또는 osascript
osascript -e "display notification \"PR review failed (exit $code)\" with title \"Agent\""
# 또는 Slack webhook / email / Sentry
# curl -X POST -H 'Content-Type: application/json' \
# -d "{\"text\":\"Agent failed: exit $code\"}" "$SLACK_WEBHOOK_URL"
fi
}
trap notify_failure EXIT
# 에이전트 실행4.5 슬립 / 부팅 직후 보정
- macOS launchd:
StartCalendarInterval은 슬립 중 시각 지나가면 다음 깨어남에 1회 보정 - Linux systemd timer:
Persistent=true옵션 활용 - Windows: Task Scheduler의 Run task as soon as possible after a scheduled start is missed 옵션 ON
cron만 슬립 보정 안 함 — 노트북에서 cron 안 쓰는 이유.
5. 실전 시나리오 — Claude Code 매일 PR 리뷰
~/agents/daily-pr-review.sh:
#!/usr/bin/env bash
set -euo pipefail
# 1. PATH + 시크릿
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
export ANTHROPIC_API_KEY="$(security find-generic-password -s anthropic-api -w)"
export GITHUB_TOKEN="$(security find-generic-password -s github-token -w)"
# 2. 락
exec 200>/tmp/daily-pr-review.lock
flock -n 200 || exit 0
# 3. 로그
LOG="$HOME/logs/pr-review-$(date +%Y%m%d).log"
mkdir -p "$(dirname "$LOG")"
exec >> "$LOG" 2>&1
echo "[$(date)] starting daily PR review..."
# 4. 작업
cd "$HOME/code/my-repo"
git fetch --quiet origin
gh pr list --state open --json number,title,author --jq '.[] | select(.author.login != "me")' | \
while read -r pr; do
num=$(echo "$pr" | jq -r .number)
echo "Reviewing PR #$num..."
claude-code --task "PR #$num 리뷰 — 보안·성능·테스트 누락 위주" --quiet
done
echo "[$(date)] done."LaunchAgent에 등록 → 매일 09:00 자동 실행.
6. 트러블슈팅
launchd "Service exited with abnormal code: 127"
PATH 안 잡힘 또는 스크립트 미실행 권한. chmod +x ~/agents/*.sh + plist에 PATH 명시.
cron이 안 돌고 로그도 없음
- crontab 문법 오류:
crontab -l로 다시 확인 - MTA(mailutils) 없으면 stderr가 사라짐 →
2>&1 >> log명시 journalctl -u cron(또는 cronie)로 실행 시도 흔적 확인
Task Scheduler "Last Task Result: 0x1"
스크립트가 1로 종료. WSL 안에서 직접 실행해서 디버그. $LASTEXITCODE 확인.
claude-code 명령어 못 찾음
스크립트의 PATH에 ~/.npm-global/bin 또는 npm global bin 누락. which claude-code 결과 디렉토리를 PATH에 추가.
다음 단계
- Claude Code 셋업 — 본 가이드 전제
- Claude Code hooks — 자동화 이벤트 패턴
- Multi-agent 워크플로 — 여러 에이전트 협업
참고 링크
변경 이력
- 2026-05-12 — 초안 (devAlice M2 시드 확장)