어느날 부턴가 MacBook의 Cron이 안돌기 시작합니다. 얼마전 MacOS업데이트 이후로 그런거 같습니다.
Crontab에 단순명령을 입력해보아도 돌지 않아요. 아래는 1분마다 date를 실행하는 job입니다.
$ crontab -l
* * * * * /bin/date >> /tmp/cron_test.txt 2>&1
크론탭 데몬이 살아있는지 확인해보면 살아있는데
$ ps aux | grep cron
slim 1155 0.0 0.0 34129352 724 s000 S+ 9:09AM 0:00.00 grep cron
root 1051 0.0 0.0 33753552 2548 ?? Ss 8:58AM 0:00.02 /usr/sbin/cron
Cron 로그를 확인해도 아무것도 안뜹니다.
$ log show --predicate 'process == "cron"' --last 10m
Filtering the log data using "process == "cron""
Skipping info and debug messages, pass --info and/or --debug to include.
Timestamp Thread Type Activity PID TTL
--------------------------------------------------------------------------------------------------------------------
Log - Default: 0, Info: 0, Debug: 0, Error: 0, Fault: 0
Activity - Create: 0, Transition: 0, Actions: 0
Launchd에서 cron상태를 확인했을때 정상적으로 com.vix.cron이 떠있습니다.
09:10 ~ $ sudo launchctl list | grep cron
Password:
1051 -15 com.vix.cron
Cron을 다시 로드해보지만 실패합니다.
$ sudo launchctl unload /System/Library/LaunchDaemons/com.vix.cron.plist
Unload failed: 5: Input/output error
Try running `launchctl bootout` as root for richer errors.
$ sudo launchctl load /System/Library/LaunchDaemons/com.vix.cron.plist
Load failed: 5: Input/output error
Try running `launchctl bootstrap` as root for richer errors.
시간이 지나도 Cron은 여전히 돌지 않습니다.
$ ls -al /tmp/cron_test.txt
ls: /tmp/cron_test.txt: No such file or directory
아예 실행조차 되지 않는것 같았습니다.
$ log show --predicate 'process == "cron"' --last 15m
Filtering the log data using "process == "cron""
Skipping info and debug messages, pass --info and/or --debug to include.
Timestamp Thread Type Activity PID TTL
--------------------------------------------------------------------------------------------------------------------
Log - Default: 0, Info: 0, Debug: 0, Error: 0, Fault: 0
Activity - Create: 0, Transition: 0, Actions: 0
권한 문제가 있는것 같았습니다.
$ stat -f "%Sm %N" /usr/lib/cron/tabs /usr/lib/cron/tabs/slim
Mar 15 08:25:50 2026 /usr/lib/cron/tabs stat: /usr/lib/cron/tabs/slim: stat: Permission denied
권한 상태를 살펴보았습니다.
$ sudo ls -ld /usr/lib/cron/tabs
Password:
drwxr-xr-x@ 3 root wheel 96 Mar 15 09:17 /usr/lib/cron/tabs
$ sudo ls -l /usr/lib/cron/tabs
total 8
-rw-------@ 1 root wheel 226 Mar 15 09:17 slim
권한을 맞춰주었습니다.
$ sudo chmod 755 /usr/lib/cron/tabs
$ sudo chown root:wheel /usr/lib/cron/tabs
$ sudo chmod 600 /usr/lib/cron/tabs/slim
$ sudo chown root:wheel /usr/lib/cron/tabs/slim
$ sudo ls -ld /usr/lib/cron/tabs
drwxr-xr-x@ 3 root wheel 96 Mar 15 09:17 /usr/lib/cron/tabs
Cron 데몬을 Restart시켜보려는데 에러가 납니다.
$ sudo launchctl kickstart -k system/com.vix.cron
Could not kickstart service "com.vix.cron": 150: Operation not permitted while System Integrity Protection is engaged
크론파일을 삭제하고 다시 넣어봅니다. 여전히 안돌아갑니다.
$ sudo rm /usr/lib/cron/tabs/slim
$ crontab -e
* * * * * /bin/date >> /tmp/cron_test.txt 2>&1
$ ls -al /tmp/cron_test.txt
ls: /tmp/cron_test.txt: No such file or directory
뒤에 붙은 @ 표시가 중요합니다. 이건 extended attributes (xattr) 가 붙어 있다는 뜻입니다. macOS에서 /usr/lib/cron/tabs/* 파일에 quarantine / provenance 같은 xattr이 붙으면 cron이 실행을 무시하는 경우가 있습니다. @을 제거해보도록 하겠습니다. 하지만 제거가 원활하게 되지 않습니다.
$ sudo ls -l /usr/lib/cron/tabs
total 8
-rw-------@ 1 root wheel 226 Mar 15 09:16 slim
$ sudo xattr -l /usr/lib/cron/tabs/slim
com.apple.provenance:
$ sudo xattr -c /usr/lib/cron/tabs/slim
$ sudo ls -l /usr/lib/cron/tabs
total 8
-rw-------@ 1 root wheel 226 Mar 15 09:17 slim
MacBook을 재부팅해봤습니다. 여전히 Cron이 안돌아 갑니다. 로그를 열어놓고 기다려봤지만 안돌아 갑니다.
$ sudo log stream --predicate 'process == "cron"' --info
Filtering the log data using "process == "cron""
cron 대신 macOS 기본 스케줄러(launchd) 로 같은 작업을 실행해 보면 cron 문제인지 바로 알 수 있습니다.
09:29 ~ $ launchctl submit -l testcron -- /bin/date
그리고 확인. 시스템은 정상이고 cron만 문제입니다.
09:33 ~ $ log show --last 1m | grep testcron
2026-03-15 09:33:53.612271-0400 0xb2f6 Default 0x0 1 0 launchd: [testcron:] This service is defined to be constantly running and is inherently inefficient.
2026-03-15 09:33:53.612293-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron:] internal event: WILL_SPAWN, code = 0
2026-03-15 09:33:53.612309-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron:] service state: spawn scheduled
2026-03-15 09:33:53.612311-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron:] service state: spawning
2026-03-15 09:33:53.612338-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron:] launching: speculative
2026-03-15 09:33:53.612871-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron [1261]:] xpcproxy spawned with pid 1261
2026-03-15 09:33:53.612887-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron [1261]:] internal event: SPAWNED, code = 0
2026-03-15 09:33:53.612890-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron [1261]:] service state: xpcproxy
2026-03-15 09:33:53.612974-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron [1261]:] internal event: SOURCE_ATTACH, code = 0
2026-03-15 09:33:53.620899-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron [1261]:] service state: running
2026-03-15 09:33:53.620908-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron [1261]:] internal event: INIT, code = 0
2026-03-15 09:33:53.620916-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron [1261]:] Successfully spawned date[1261] because speculative
2026-03-15 09:33:53.624623-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron [1261]:] exited due to exit(0), ran for 12ms
2026-03-15 09:33:53.624631-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron [1261]:] service state: exited
2026-03-15 09:33:53.624635-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron [1261]:] internal event: EXITED, code = 0
2026-03-15 09:33:53.624638-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501 [100002]:] service inactive: testcron
2026-03-15 09:33:53.624640-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron [1261]:] service state: not running
2026-03-15 09:33:53.624643-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron:] Service only ran for 0 seconds. Pushing respawn out by 10 seconds.
2026-03-15 09:33:53.624677-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron:] internal event: WILL_SPAWN, code = 0
2026-03-15 09:33:53.624681-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron:] service state: spawn scheduled
2026-03-15 09:33:53.624682-0400 0xb2f6 Default 0x0 1 0 launchd: [gui/501/testcron:] service spawn deferred by 10 seconds due to throttle
크론을 죽였다 살려봅니다. 여전히 안됩니다.
$ sudo pkill cron
$ ps aux | grep cron
$ ls -al /tmp/cron_test.txt
ls: /tmp/cron_test.txt: No such file or directory
그렇다면 내 계정의 cron만 무시하는건지 root로 실행해도 안되는건지 테스트해봅니다. root로 해도 안됩니다.
$ sudo nano /etc/crontab
* * * * * root /bin/date >> /tmp/cron_test.txt 2>&1
$ ls -al /tmp/cron_test.txt
ls: /tmp/cron_test.txt: No such file or directory
cron daemon은 떠 있지만 cron 스케줄러가 아예 돌지 않습니다. 이 패턴은 macOS에서 가끔 생기는 cron service 자체가 launchd에서 제대로 작동하지 않는 상태입니다. 그래서 해결 방법은 cron 설정이 아니라 cron launch daemon을 다시 로드하는 것입니다. 이건 cron 서비스를 완전히 unload 한뒤 다시 load 하는 것입니다. 그런데 에러가납니다.
$ sudo launchctl bootout system /System/Library/LaunchDaemons/com.vix.cron.plist
Boot-out failed: 5: Input/output error
$ sudo launchctl bootstrap system /System/Library/LaunchDaemons/com.vix.cron.plist
Bootstrap failed: 5: Input/output error
정상적인 cron 디버깅은 거의 다 했습니다. 그래서 이제 결론은 하나입니다.
지금 상태는 cron이 launchd에서 깨진 상태입니다.
증거들을 정리하면:
- cron daemon 실행됨
- crontab 정상
/etc/crontab도 실행 안됨- cron 로그 0
- kill / reboot / permission 수정 → 효과 없음
launchctl bootout/bootstrap→ I/O error
이 패턴은 macOS에서 launch daemon registry가 꼬였을 때 나타납니다.
cron을 다시 살리는 것보다 macOS 기본 스케줄러 launchd로 바꾸는 것이 훨씬 안정적입니다. Apple도 그렇게 권장합니다.
그래서 같은 테스트를 launchd job으로 만들어 보겠습니다. 임시테스트 파일을 하나 만들겠습니다.
nano ~/Library/LaunchAgents/test.cron.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>test.cron</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>/bin/date >> /tmp/cron_test.txt</string>
</array>
<key>StartInterval</key>
<integer>60</integer>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
해당 파일을 LaunchD에 등록합니다.
launchctl load ~/Library/LaunchAgents/test.cron.plist
1분뒤에 확인합니다. 잘되네요.
$ cat /tmp/cron_test.txt
Sun Mar 15 09:04:29 EDT 2026
임시로 올렸던 스케줄파일과 테스트파일들 삭제하세요.
launchctl unload ~/Library/LaunchAgents/test.cron.plist
rm ~/Library/LaunchAgents/test.cron.plist
rm /tmp/cron_test.txt
크론에서 만약에 아래와 같은 파일을 실행하고 있었다면
0 5 * * * /Users/slim/git/mijutoday.com/rundailynews.sh > /Users/slim/git/mijutoday.com/output/output_rundailynews.txt
아래 폴더에 해당 스케줄을 넣을 파일을 하나 생성하고 (현재 사용자 이름이 slim인경우입니다.)
$ nano ~/Library/LaunchAgents/com.slim.rundailynews.plist
아래와 같은 XML파일을 저장합니다.
<?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>com.slim.rundailynews</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>/Users/slim/git/mijutoday.com/rundailynews.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>5</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/Users/slim/git/mijutoday.com/output/output_rundailynews.txt</string>
<key>StandardErrorPath</key>
<string>/Users/slim/git/mijutoday.com/output/output_rundailynews_error.txt</string>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>
문법이 맞는지 확인합니다.
plutil -lint ~/Library/LaunchAgents/com.slim.rundailynews.plist
/Users/slim/Library/LaunchAgents/com.slim.rundailynews.plist: OK
$ plutil -p ~/Library/LaunchAgents/com.slim.rundailynews.plist
{
"EnvironmentVariables" => {
"PATH" => "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"
"PYTHONPATH" => "/Users/slim/git/mijutoday.com"
}
"Label" => "com.slim.rundailynews"
"ProgramArguments" => [
0 => "/bin/zsh"
1 => "/Users/slim/git/mijutoday.com/rundailynews.sh"
]
"RunAtLoad" => 0
"StandardErrorPath" => "/Users/slim/git/mijutoday.com/output/output_rundailynews_error.txt"
"StandardOutPath" => "/Users/slim/git/mijutoday.com/output/output_rundailynews.txt"
"StartCalendarInterval" => {
"Hour" => 5
"Minute" => 0
}
"WorkingDirectory" => "/Users/slim/git/mijutoday.com"
}
실행권한을 줍니다.
$ chmod 644 ~/Library/LaunchAgents/com.slim.rundailynews.plist
$ ls -l ~/Library/LaunchAgents/com.slim.rundailynews.plist
-rw-r--r--@ 1 slim staff 1200 Mar 15 09:53 /Users/slim/Library/LaunchAgents/com.slim.rundailynews.plist
그리고 설정한 파일을 스케줄러에 등록합니다.
$ launchctl load ~/Library/LaunchAgents/com.slim.rundailynews.plist
혹시 이미 등록이 되어 있어서 에러가 난다면 내리고 다시 올려야합니다. 아래와 같이등록여부를 확인합니다.
$ launchctl list | grep rundailynews
- 0 com.slim.rundailynews
이미 등록이 되어 있다면 어떻게 등록이 되어있는지 확인합니다.
$ launchctl print gui/$(id -u)/com.slim.rundailynews
gui/501/com.slim.rundailynews = {
active count = 0
path = /Users/slim/Library/LaunchAgents/com.slim.rundailynews.plist
type = LaunchAgent
state = not running
program = /bin/zsh
arguments = {
/bin/zsh
/Users/slim/git/mijutoday.com/rundailynews.sh
}
working directory = /Users/slim/git/mijutoday.com
stdout path = /Users/slim/git/mijutoday.com/output/output_rundailynews.txt
stderr path = /Users/slim/git/mijutoday.com/output/output_rundailynews_error.txt
inherited environment = {
SSH_AUTH_SOCK => /private/tmp/com.apple.launchd.tmkWVan7Yd/Listeners
}
default environment = {
PATH => /usr/bin:/bin:/usr/sbin:/sbin
}
environment = {
PATH => /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin
PYTHONPATH => /Users/slim/git/mijutoday.com
XPC_SERVICE_NAME => com.slim.rundailynews
}
domain = gui/501 [100002]
asid = 100002
minimum runtime = 10
exit timeout = 5
runs = 0
last exit code = (never exited)
event triggers = {
com.slim.rundailynews.268435470 => {
keepalive = 0
service = com.slim.rundailynews
stream = com.apple.launchd.calendarinterval
monitor = com.apple.UserEventAgent-Aqua
descriptor = {
"Minute" => 17
"Hour" => 4
}
}
}
event channels = {
"com.apple.launchd.calendarinterval" = {
port = 0x0
active = 0
managed = 1
reset = 0
hide = 0
watching = 1
}
}
spawn type = daemon (3)
jetsam priority = 40
jetsam memory limit (active) = (unlimited)
jetsam memory limit (inactive) = (unlimited)
jetsamproperties category = daemon
jetsam thread limit = 32
cpumon = default
probabilistic guard malloc policy = {
activation rate = 1/1000
sample rate = 1/0
}
properties = inferred program | needs LWCR update | managed LWCR
}
만약 plist파일을 수정했고 다시 반영하고 싶다면 bootout/bootstrap으로 재런칭을 합니다.
$ launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.slim.rundailynews.plist
$ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.slim.rundailynews.plist
새로 반영된 스케줄을 확인합니다.
$ launchctl print gui/$(id -u)/com.slim.rundailynews
gui/501/com.slim.rundailynews = {
active count = 0
path = /Users/slim/Library/LaunchAgents/com.slim.rundailynews.plist
type = LaunchAgent
state = not running
program = /bin/zsh
arguments = {
/bin/zsh
/Users/slim/git/mijutoday.com/rundailynews.sh
}
working directory = /Users/slim/git/mijutoday.com
stdout path = /Users/slim/git/mijutoday.com/output/output_rundailynews.txt
stderr path = /Users/slim/git/mijutoday.com/output/output_rundailynews_error.txt
inherited environment = {
SSH_AUTH_SOCK => /private/tmp/com.apple.launchd.tmkWVan7Yd/Listeners
}
default environment = {
PATH => /usr/bin:/bin:/usr/sbin:/sbin
}
environment = {
PATH => /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin
PYTHONPATH => /Users/slim/git/mijutoday.com
XPC_SERVICE_NAME => com.slim.rundailynews
}
domain = gui/501 [100002]
asid = 100002
minimum runtime = 10
exit timeout = 5
runs = 0
last exit code = (never exited)
event triggers = {
com.slim.rundailynews.268435471 => {
keepalive = 0
service = com.slim.rundailynews
stream = com.apple.launchd.calendarinterval
monitor = com.apple.UserEventAgent-Aqua
descriptor = {
"Minute" => 0
"Hour" => 5
}
}
}
event channels = {
"com.apple.launchd.calendarinterval" = {
port = 0x0
active = 0
managed = 1
reset = 0
hide = 0
watching = 1
}
}
spawn type = daemon (3)
jetsam priority = 40
jetsam memory limit (active) = (unlimited)
jetsam memory limit (inactive) = (unlimited)
jetsamproperties category = daemon
jetsam thread limit = 32
cpumon = default
probabilistic guard malloc policy = {
activation rate = 1/1000
sample rate = 1/0
}
properties = inferred program
}
등록된 스케줄을 확인합니다.
이제 Cron은 전부 지우세요
$ crontab -l





































