devAlice
← AI Agents

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도구정의 위치로그
macOSlaunchd (LaunchAgent)~/Library/LaunchAgents/<label>.plistStandardOutPath/StandardErrorPath
Linuxcroncrontab -e 또는 /etc/cron.d/<name>MTA 또는 redirect >> log.txt 2>&1
WindowsTask SchedulerGUI 또는 PowerShell Register-ScheduledTaskEvent Viewer 또는 파일 redirect

공통 함정 5가지:

  1. PATH가 비어있어서 명령어를 못 찾음
  2. API 키를 평문으로 plist/crontab에 박음 (✗)
  3. 같은 작업이 겹쳐 실행되어 race
  4. 실패해도 조용히 죽음 (알림 없음)
  5. 컴퓨터가 슬립 중이면 실행 안 됨

본문 §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.plist

1.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
필드의미
00정각
999시
* * *모든매일

매일 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>&1

2.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
EOF

appuser 필드 — 어떤 사용자로 실행할지 명시. 보안에 중요.

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.target
sudo 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 + Rtaskschd.msc → Task Scheduler Library → "DailyPRReview".

3.3 즉시 실행 테스트

Start-ScheduledTask -TaskName "DailyPRReview"
Get-ScheduledTaskInfo -TaskName "DailyPRReview" | Select LastRunTime, LastTaskResult

LastTaskResult: 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 user

4.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에 추가.


다음 단계

참고 링크

변경 이력

  • 2026-05-12 — 초안 (devAlice M2 시드 확장)

댓글