TechFeedTechFeed
DevOps

macOS launchd 자동화 디버깅 2026 | 로그·권한·PATH

macOS launchd 잡이 안 돌 때 list·print, StandardOut/Error, 권한, 비대화형 PATH·HOME, 스케줄 순 디버깅 체크리스트와 plist 예시를 정리한다.

by

한 줄: macOS launchd 자동화가 안 돌면 로그 파일, 실행 권한, 비대화형 PATH·HOME을 이 순서로 보면 원인 대부분이 갈린다.


cron보다 launchd를 쓰는 Mac 미니·개발 맥에서 새벽 잡이 “아무 일도 없이” 끝나는 경우가 많다. plist는 로드됐는데 바이너리를 못 찾고, 표준 출력은 버려지고, GUI 세션에서만 되는 환경 변수를 가정한 스크립트가 실패한다. 아래 체크리스트로 로드 상태 → 로그 → 권한 → PATH → 의존 서비스(인증·네트워크) 순으로 닫는다. Claude 쪽 401이면 Claude Code 인증 복구를 병행한다.


로드 상태 확인 | list·print로 등록 여부 먼저

스크립트를 고치기 전에 “에이전트가 실제로 로드됐는지”를 본다. 사용자 LaunchAgents와 시스템 LaunchDaemons 위치가 다르다.


# 사용자 에이전트 (로그인 사용자 기준)
launchctl list | grep -i ambit || true
launchctl print gui/$(id -u)/com.example.myjob 2>&1 | head -40

# plist 문법·로드
plutil -lint ~/Library/LaunchAgents/com.example.myjob.plist
launchctl bootout gui/$(id -u)/com.example.myjob 2>/dev/null || true
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.myjob.plist
launchctl enable gui/$(id -u)/com.example.myjob
launchctl kickstart -k gui/$(id -u)/com.example.myjob

macOS 버전마다 load/unload 대신 bootstrap/bootout이 권장된다. print 출력의 state, last exit code, path를 보면 “한 번도 안 뜸”과 “떴다가 즉시 죽음”이 구분된다. cron 대비 스케줄 설계는 Claude Code cron 스케줄 글과 겹친다.


로그를 남기는 법 | StandardOut·StandardError 필수

launchd는 기본적으로 잡 출력을 삼킨다. plist에 로그 경로를 안 쓰면 “실패했는지”조차 안 보인다.


<key>StandardOutPath</key>
<string>/Users/you/logs/myjob.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/you/logs/myjob.err.log</string>
<key>WorkingDirectory</key>
<string>/Users/you/project</string>
  • 로그 디렉터리를 미리 mkdir -p 해 둔다. 없으면 생성이 실패하거나 출력이 유실된다.
  • 스크립트 첫 줄에 타임스탬프·uid·PATH를 echo 한다. “파일이 갱신되는가”가 1차 신호다.
  • tail -f로 kickstart 직후 스트림을 본다.
  • exit code 0이어도 비즈니스 로직 실패일 수 있다. 앱 로그(예: content-agent)를 같이 본다.

mkdir -p ~/logs
# 스크립트 상단 예시
echo "[$(date '+%Y-%m-%d %H:%M:%S')] start uid=$(id -u) PATH=$PATH" >> ~/logs/myjob.out.log

권한·PATH·HOME | 대화형 셸과 다른 환경

터미널에서는 되고 launchd만 안 되면 환경 차이일 확률이 높다.


증상 원인 후보 조치
command not found PATH에 Homebrew 없음 ProgramArguments에 절대 경로
permission denied 스크립트 +x 없음, 다른 사용자 소유 chmod +x, 소유자 일치
설정·토큰 파일 없음 HOME이 다르거나 상대 경로 EnvironmentVariables에 HOME, 절대 경로
키체인·OAuth 실패 GUI 세션과 자격 증명 분리 같은 uid 로그인 사용자로 Agent 실행
잡이 안 뜸 (전원·잠금) Mac sleep, StartCalendarInterval 전원 어댑터·caffeinate·서버용 맥

<key>ProgramArguments</key>
<array>
  <string>/bin/bash</string>
  <string>/Users/you/project/scripts/daily.sh</string>
</array>
<key>EnvironmentVariables</key>
<dict>
  <key>PATH</key>
  <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
  <key>HOME</key>
  <string>/Users/you</string>
  <key>LANG</key>
  <string>en_US.UTF-8</string>
</dict>

which node·which claude 결과를 대화형에서 확인한 뒤 그 절대 경로를 plist에 박는다. alias·direnv에 의존하면 비대화형에서 깨진다. headless 권한 플래그는 skip-permissions 자동화와 함께 점검한다.


스케줄·스로틀 | 달력 트리거와 재시작 루프 방지

StartCalendarInterval·StartInterval 설정이 틀리면 “등록은 됐는데 안 도는” 착시가 난다.


  • StartCalendarInterval의 Hour/Minute는 로컬 타임존 기준이다. 서버 UTC 습관과 혼동하지 않는다.
  • 짧은 간격 + 긴 실행 시간이 겹치면 동시 실행이 생길 수 있다. 스크립트에 flock·pid 파일 락을 둔다.
  • 즉시 재시작 루프가 반복되면 launchd가 잡을 잠시 보류한다. err 로그에 반복 exit를 남기고 원인을 고친다.
  • RunAtLoad는 로드 직후 1회 실행. 디버깅용으로 켠 뒤 운영에서는 스케줄만 남길지 결정한다.

# 단일 실행 락 예시
LOCK=/tmp/myjob.lock
exec 9>"$LOCK"
flock -n 9 || { echo "already running"; exit 0; }
# ... 본 작업 ...

리밋으로 본문이 0건인 경우와 스케줄 미실행을 헷갈리지 않는다. 인증·쿼터는 리밋과 재개, 인증 만료는 앞의 401 글을 본다.


디버깅 체크리스트 | 10분 안에 닫는 순서

현장용 순서다. 위에서 막히면 아래를 보지 않는다.


  1. plutil -lint 통과 여부
  2. launchctl print에 라벨이 보이는지, last exit status
  3. StandardOut/Error 파일이 kickstart 후 mtime 갱신되는지
  4. err 로그 첫 에러 한 줄 (not found / permission / python path)
  5. ProgramArguments 0번이 절대 경로인지, 스크립트 chmod +x
  6. EnvironmentVariables PATH·HOME·토큰 필요 변수
  7. WorkingDirectory에 상대 경로 파일이 실제로 있는지
  8. 수동: /bin/bash /path/to/script.sh 를 같은 사용자로 실행
  9. 의존 CLI 인증(예: claude ping) 성공 여부
  10. Mac sleep·전원·네트워크(VPN) 상태

# 한 번에 훑기
plutil -lint ~/Library/LaunchAgents/com.example.myjob.plist
launchctl print gui/$(id -u)/com.example.myjob 2>&1 | head -50
ls -la ~/logs/myjob.*.log
tail -n 50 ~/logs/myjob.err.log
/bin/bash -x /Users/you/project/scripts/daily.sh

프리빌트 배포 잡을 launchd에 묶은 경우 업로드 실패와 스케줄 실패를 분리한다. 배포 쪽은 Vercel prebuilt 업로드 글을 보면 된다.


※ launchctl 서브커맨드는 macOS 메이저 버전마다 문구가 조금 다르다. man launchctl 과 print 출력을 우선한다.


보안·운영 메모 | 토큰과 로그 위치

자동화 plist에 시크릿을 평문으로 넣지 않는 편이 낫다. 스크립트가 .env.local을 source 하되, 파일 권한은 600, 로그에 토큰이 echo 되지 않게 한다.


  • 로그 디렉터리를 홈 아래 전용 경로로 두고 백업·공유 폴더에 두지 않는다.
  • 팀 공용 맥이면 LaunchDaemon(root)과 사용자 Agent를 섞지 않는다. 권한 상승 필요 시에만 Daemon.
  • 실패 알림(메일·슬랙 웹훅)은 err 로그 tail 또는 exit code 훅으로 붙인다.

프로덕션 배포 공통 점검은 배포 체크리스트를 참고한다.


launchctl 동작은 macOS 버전에 따라 다를 수 있습니다. print 출력과 공식 man 페이지를 기준으로 맞추면 됩니다.


macOSlaunchd자동화PATH디버깅

함께 보면 좋은 문제 해결

EXPLORE / DevOps

이어서 읽어보기

전체 토픽 둘러보기