셀프 호스팅 환경에서 Claude Code를 구동하기: 사내 Mac으로 17개 작업을 무인 운영하는 구성과 주의점
요약
본 글은 외부 클라우드 사용 없이 사내 Mac 환경에서 Claude Code를 무인으로 운영하는 구체적인 방법을 제시합니다. macOS의 launchd와 래퍼 함수, Bash 스크립트를 조합하여 자동화된 작업을 구축하며, 특히 PATH 및 HOME 변수 명시와 StandardErrorPath 지정 등 실질적인 설정 노하우를 공유합니다.
핵심 포인트
- macOS 작업 스케줄러는 cron 대신 launchd 사용이 필수적입니다.
- launchd 환경에서는 절대 경로(PATH/HOME)를 반드시 명시해야 합니다.
- 무인 실행 시 에러 추적을 위해 StandardErrorPath 지정이 중요합니다.
- API 키 관리 없이 로그인된 일반 모드(`claude -p`)로 호출하는 것이 안정적입니다.
당사(合同会社ジョインクラス)는 직원이 필자 한 명인 회사입니다. 출판, 수탁 개발, SaaS의 세 가지 사업을 집의 Mac 1대로 구동되는 Claude Code의 무인 작업 17개로 운영하고 있습니다. 클라우드의 실행 기반은 사용하지 않습니다. 이유는 간단합니다. 재무 데이터나 전략 메모를 포함하는 리포지토리를 외부에 노출하고 싶지 않았기 때문입니다.
이 글에서는 '자신의 머신에서 Claude Code를 무인으로 실행'하기 위한 구성을, 실제로 작동하는 스크립트를 그대로 담아 설명합니다. 경영 이야기는 최소화하고, 본체는 설정 파일과 커맨드로 구성했습니다. 읽고 나면 그날 안에 첫 번째 작업(job)을 구축할 수 있을 것입니다.
구성 요소는 단 3가지입니다.
- launchd: macOS의 작업 스케줄러. cron의 대안 -
- 래퍼 함수 (Wrapper function):
claude_run
claude -p를 감싸고 실제 비용을 기록하는 기능 - - 부서 스크립트 (Department script): 래퍼를 호출하여 결과를 Slack으로 전송하는 Bash 스크립트
launchd (plist)
└─ auto-dept-publishing.sh
└─ claude_run ← lib/claude-run.sh
...
처음에는 cron으로 구성했습니다. 완전히 실패했습니다. 원인은 macOS의 전체 디스크 접근(full disk access) 문제로, cron에서 시작된 프로세스가 홈 디렉토리 하위 파일을 읽을 수 없었기 때문입니다. launchd로 전면 전환하여 해결했습니다.
launchd 작업 정의는 ~/Library/LaunchAgents/에 plist를 놓기만 하면 됩니다. 아래는 실제로 매주 수요일 7시에 작동하는 출판 부서 작업의 정의입니다.
<?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">
...
핵심 포인트는 두 가지가 있습니다.
- PATH와 HOME을 명시할 것. launchd는 로그인 셸의 환경 변수를 상속받지 않습니다. nvm으로 설치한
claude는 PATH에 포함되어 있지 않으므로, plist 측에서 절대 경로를 작성해야 합니다. 이것을 잊으면 'command not found'조차 출력되지 않고, 그저 조용히 아무 일도 일어나지 않습니다. - StandardErrorPath를 반드시 지정할 것. 무인 실행에서는 에러가 보이지 않는 것이 가장 무섭습니다. 처음부터 표준 오류(standard error)를 파일에 기록해 둡니다.
등록 및 동작 확인은 다음 커맨드로 합니다.
# 등록
launchctl load ~/Library/LaunchAgents/com.joinclass.auto-dept-publishing.plist
# 스케줄을 기다리지 않고 지금 바로 한 번 작동시키기 (검증용)
...
launchctl list의 두 번째 열이 종료 코드입니다. 0이 아니면 error.log를 확인합니다.
무인 실행이라 가벼워 보이는 --bare 옵션을 시도했지만, 이것은 인증 정보를 읽지 않는 모드라 Not logged in 상태로 즉시 종료됩니다. 셀프 호스팅 환경에서는 로그인된 일반 모드로 claude -p를 호출합니다. Max 플랜으로 로그인한 상태를 그대로 사용하기 때문에 API 키 관리도 필요 없습니다.
이것이 가장 뼈아팠던 실패 경험입니다. 당초의 비용 추적은 스크립트가 자체 신고하는 고정값(0.02달러 등)을 쌓는 구조였습니다. 월 160달러에서 경고, 200달러에서 자동 정지라는 가드를 구축했었는데, 허구적인 숫자로 판별하고 있었기 때문에 한 번도 작동하지 않는 상태였던 것입니다.
실측해 보니 '2+2'를 듣는 것만으로도 0.27~0.47달러가 소요되었습니다. 내역은 캐시 생성에 약 41,000 토큰이 사용되었으며, 그중 사내 파일(CLAUDE.md, 에이전트 정의, 스킬)은 5,800토큰 정도입니다. 나머지는 Claude Code 측의 고정 오버헤드입니다. 즉 줄일 수 있는 것은 호출 횟수뿐이며, 프롬프트를 짧게 해도 큰 효과를 보기 어렵습니다.
대책으로 모든 작업을 공통 래퍼를 거치도록 교체했습니다.
#!/bin/bash
# lib/claude-run.sh — claude를 무인 실행할 때의 공통 래퍼. 실제 비용을 기록한다.
#
...
세 가지 플래그가 핵심입니다.
| 플래그 | 역할 |
|---|---|
--output-format json | 응답과 함께 total_cost_usd와 usage가 반환됩니다. 이를 기록합니다 |
--max-budget-usd 3 | 1회 실행 상한선입니다. 폭주해도 3달러에서 멈춥니다 |
--fallback-model | 상위 모델이 혼잡할 때 다운되는 대신, Sonnet으로 전환하여 완료시킵니다 |
JSON을 받아 기록하는 쪽은 이쪽입니다.
#!/usr/bin/env python3
"""claude --output-format json의 출력을 받아 응답 텍스트를 표준 출력에 흘리면서 실제 비용을 기록합니다."""
import datetime, json, os, pathlib, subprocess, sys
...
표준 출력에는 응답 텍스트만 흐르기 때문에, 기존의 claude -p
을 그대로 claude_run <레이블>
로 대체할 수 있습니다. 알림을 '임계값을 넘긴 순간에만' 설정한 것은 매번 울리면 사람이 무시하기 시작하기 때문입니다.
래퍼(Wrapper)를 사용하는 쪽은 이 정도면 충분합니다. 실제로 수요일 7시에 작동하는 것을 그대로 올립니다.
#!/bin/bash
# 출판 부문 자율 실행 스크립트
# launchd: com.joinclass.auto-dept-publishing (매주 수요일 7:00 JST)
...
주의점을 말씀드립니다.
스크립트 내에서도 PATH와 HOME을 재export하고 있습니다. plist로 지정했더라도, 수동 실행이나 다른 스케줄러에서 호출되었을 때의 보험입니다. 중복으로 작성할 가치가 있습니다. -
여기서는 사용할 도구를 제한합니다. 리포트 계열 작업에는 Write나 Edit가 필요 없습니다. 무인 실행에서는 '할 수 있는 것을 줄이는' 것이 안전한 설계입니다. `--allowedTools
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기