당사(주식회사 JoinClass)에서는 아티클 소재 보충, 리드 수집, 부서별 상태 확인 등을 claude -p를 호출하는 셸 스크립트로 만들어 macOS의 launchd에서 17개 이상의 작업으로 매일 실행하고 있습니다.
터미널에서 직접 실행하면 작동하는데, 스케줄링 실행을 하면 작동하지 않는 패턴에 여러 번 빠졌습니다. 원인은 대개 다음 세 가지입니다.
PATH: claude나 node를 찾지 못함 -
권한: 도구 허용 프롬프트에서 멈추거나, 너무 많이 허용하는 경우 -
타임아웃/폭주: 끝나지 않거나, 모르는 사이에 비용이 증가함
본문에서는 당사에서 실제로 작동하고 있는 스크립트를 기반으로 각각의 대처 방법을 작성합니다.
launchd나 cron에서 실행하면 로그에 claude: command not found 또는 env: node: No such file or directory가 출력됩니다.
launchd와 cron은 ~/.zshrc를 읽지 않습니다. PATH는 /usr/bin:/bin:/usr/sbin:/sbin 정도만 있고, nvm으로 설치한 Node도 Homebrew도 보이지 않습니다. claude는 Node 기반의 CLI이므로, claude 본체와 node 모두가 PATH에 필요합니다.
당사의 스크립트는 모두 맨 앞에서 PATH를 고정하고 있습니다(공통 부분은 common.sh로 통합했습니다).
#!/bin/bash
export PATH="/Users/kyoagun/.nvm/versions/node/v22.17.0/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
포인트는 두 가지입니다.
nvm의 버전이 포함된 절대 경로를 작성할 것. nvm use는 셸 함수이기 때문에 비대화형(non-interactive) 셸에서는 사용할 수 없습니다 -
Node를 업데이트하면 이 경로도 수정할 것. 오래된 버전을 삭제하면 모든 작업이 한꺼번에 멈춥니다
실제로 당사에서도 일부 스크립트에서 /opt/homebrew/bin가 빠져 있어, jq나 gh를 사용하는 후반 처리 부분만 실패한 적이 있었습니다. claude는 작동하기 때문에 인지하는 것이 느립니다.
launchd와 유사한 최소 환경으로 수동 실행해 보면 바로 재현할 수 있습니다.
env -i HOME="$HOME" /bin/bash -c 'source ./common.sh; which claude node jq; claude --version'
env -i로 환경 변수를 비우고 실행하면, “내 셸에서는 작동한다”는 상태를 없앨 수 있습니다.
또 하나, macOS의 cron은 전체 디스크 접근 제약에 걸리기 쉬워서, 당사에서는 cron 작업들이 모두 실패했습니다. 결국 launchd로 전면 이행하여 해결했습니다. macOS에서 주기적으로 실행할 때는 처음부터 launchd를 사용하는 것을 추천합니다.
비대화형 -p 실행 시 도구 허가가 필요해 아무것도 하지 않고 끝나는 경우 — 번거로워져서 --dangerously-skip-permissions을 붙이고 싶어지는 경우
당사에서는 작업별로 필요한 도구만 나열합니다. 실제 호출은 다음과 같습니다.
# 소재 조사 단계: 읽기/조사만 할 때
claude_run replenish --effort low --allowedTools "Read,WebSearch,WebFetch" -p "..."
# 집필 단계: 파일 쓰기만 할 때 (Bash는 전달하지 않음)
...
# MCP 도구는 개별 이름으로 좁힐 것
claude --effort low \
--allowedTools "mcp__note-api__search-notes,mcp__note-api__like-note" -p "..."
운영해 보면서 정한 규칙은 다음과 같습니다.
| 정책 | 이유 |
|---|
| 단계별로 권한 분리 | 조사 단계에 Write를 전달하지 않음. 프롬프트 인젝션 방지에도 도움이 됨 |
| MCP는 와일드카드보다 개별 이름 사용 | mcp__note-api*처럼 하면 삭제 관련 API까지 허용하게 됨 |
Bash는 정말 필요한 작업만 실행 | 개발/영업 부서의 검토에만 한정함 |
--dangerously-skip-permissions는 사용하지 않음 | 무인 실행 시 사고가 발생해도 막을 사람이 없음 |
빠르게 시작하기 위해 --bare를 추가했을 때, Not logged in 메시지와 함께 모든 작업이 실패했습니다. --bare는 설정이나 인증 정보를 읽는 과정을 생략하기 때문에, 로그인된 구독 기반 인증 정보도 읽지 못합니다. 저희의 래퍼(wrapper)에는 이 실수를 다시 저지르지 않도록 주석으로 남겨두었습니다.
- 웹 검색을 반복하다가 몇십 분이 지나도 끝나지 않고, 다음 작업과 중첩됨
- 어느새 비용이 예상했던 수 배로 증가해 있음
저희는 claude를 직접 호출하지 않고 모든 것을 다음 래퍼(wrapper)를 통해 처리합니다.
#!/bin/bash
_CLAUDE_RUN_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
...
--max-budget-usd 3: 1회당 상한선입니다. 폭주하더라도 $3에서 멈춥니다.
--fallback-model: 메인 모델이 혼잡해도 작업을 중단하지 않고 계속 진행합니다.
--output-format json: 결과와 함께 실제 비용이 반환되므로, record-cost.py로 레이블별 cost-tracker.json에 누적시킵니다. 표준 출력에는 응답 텍스트만 반환하므로, $(claude -p ...)로 그대로 대체할 수 있습니다.
--effort low: 정형화된 작업에는 깊은 사고가 필요하지 않으므로 호출하는 쪽에서 지정합니다.
실제로 측정해 보니, "2+2"를 묻는 것만으로도 한 번에 $0.27~$0.47이 소요되는 것을 알게 되었습니다. 비용의 대부분은 약 41,000 토큰의 캐시 생성에 사용되었으며, 이 중 저희 회사의 CLAUDE.md 및 에이전트/스킬은 5,800 토큰밖에 되지 않았습니다. 나머지는 Claude Code 측의 고정 비용이므로, 줄일 수 있는 것은 호출 횟수뿐입니다. 작은 claude -p를 루프 안에서 여러 번 호출하는 설계는 피하고 한 번에 모아서 처리하도록 했습니다.
예산 상한선만으로는 대기 시간이 긴 경우(API 응답 대기 등)에 멈추지 않습니다. macOS에는 GNU timeout이 기본적으로 포함되어 있지 않으므로, brew install coreutils로 설치할 수 있는 gtimeout을 사용하는 것이 간편합니다.
gtimeout --kill-after=30 900 bash -c 'source lib/claude-run.sh; claude_run lead-collect --effort low -p "..."'
rc=$?
...
종료 코드 124가 타임아웃을 의미합니다. 이 코드를 로그에 남기지 않으면, "아무 일도 일어나지 않은 것인지" 아니면 "매번 중간에 강제 종료된 것인지" 구분할 수 없습니다.
긴 작업이 다음 시작 시간과 겹치게 되면, 같은 파일을 두 프로세스가 동시에 수정하게 됩니다. 저희는 Zenn의 "하루 한 콘텐츠" 제한을 잠금 파일(lock file)로 지키고 있지만, 원리는 같습니다.
LOCK="/tmp/$(basename "$0").lock"
mkdir "$LOCK" 2>/dev/null || { echo "already running"; exit 0; }
trap 'rmdir "$LOCK"' EXIT
mkdir는 배타적으로 생성되므로, macOS에 flock이 없어도 사용할 수 있습니다.
: 래퍼에서 stderr를 버리기 때문에 인증 오류도 보이지 않습니다. 도입 직후나 문제가 있을 때는 일시적으로 제거하여 로그로 흘려보내세요 (2>/dev/null)의 부작용 - **작업 디렉토리**: launchd의 초기 디렉토리는 /입니다. CLAUDE.md를 읽게 하려면, 스크립트 시작 부분에서 cd` 명령을 사용해야 합니다.
변수에 값을 삽입할 때 주의사항입니다.
프롬프트 내 따옴표: -p "..."
여기에 변수를 삽입하면 "나 $ 같은 문자로 인해 오류가 발생할 수 있습니다. 긴 프롬프트의 경우 히어독(heredoc)을 사용하여 변수에 저장한 후 전달하는 것이 안전합니다.
다음 사항들을 점검해야 합니다:
- 스크립트 시작 부분에서 node와 claude를 포함하는 PATH를 절대 경로로 export 했는지
env -i의 최소 환경에서 동작 확인을 완료했는지
--allowedTools 설정을 페이즈(phase)별/작업(job)별로 최소화했는지
--bare 또는 --dangerously-skip-permissions를 사용하지 않았는지
--max-budget-usd와 gtimeout을 사용하여 비용과 시간 모두에 상한선을 두었는지 (이중 실행 방지 및 타임아웃 로그 확보)
저희 회사는 소규모 회사이며, 17개 이상의 정기 작업을 AI에 맡기고 있습니다. 이것이 돌아가는 이유는 모델 자체가 똑똑해서라기보다, 어떻게 멈추고 어떻게 실패할지를 설계했기 때문이라고 생각합니다. PATH, 권한(Permission), 타임아웃 이 세 가지 포인트를 확보해 두면 '밤중에 멈춰버리는' 상황을 상당히 줄일 수 있습니다.
launchd 설정이나 Hooks와의 조합, 그리고 전체 작업 흐름의 설계에 대해서는 서적 『Claude Code 완전 자동화 바이블』에 정리했습니다. 권한 설계에 대해 더 깊이 알고 싶은 분들께는 『기업을 위한 Claude Code 보안 가이드』도 함께 추천드립니다.