무인 에이전트에게 셸을 주지 않았다 — 스케줄 위 LLM을 돈에서 떼어놓고 살려두기
뉴스 수집을 매매 루프 밖으로 옮긴 이유
이 자동매매 프로젝트에는 뉴스와 투자 판단의 근거를 모으는 지식층이 있다. 사람이 매번 뉴스를 훑는 부담을 줄이려고, 달라진 내용을 정리하는 작업을 언어 모델(LLM)에 맡겼다. 여기서 자동화하려는 것은 자료 수집이다. 수집한 내용을 정식 지식에 반영하는 판단은 사람이 맡는다.
무인 실행에서는 두 가지를 따로 확인해야 했다. 하나는 에이전트가 사용할 수 있는 도구와 파일의 범위다. 프로젝트에 주문 코드와 인증 정보가 있으므로, 뉴스 수집에 필요하지 않은 접근을 줄여야 한다. 다른 하나는 실패를 발견하는 방법이다. 작업이 멈춰도 터미널을 보고 있는 사람이 없기 때문이다.
자동매매 1편의 원칙에 따라 뉴스 수집도 매매 루프 밖에 뒀다. 엔진이 LLM 응답을 기다리는 대신, launchd가 run_delta.sh를 호출하고 스크립트가 Claude Code CLI를 실행한다. 뉴스 수집 실패가 매매 루프의 직접적인 호출 실패로 이어지지 않게 한 구조다.
별도 프로세스로 나누면 주소 공간과 실행 수명이 분리된다. 그러나 같은 사용자로 실행하면 파일 접근 권한과 환경을 공유할 수 있다. 프로세스 분리만으로 주문 코드나 키에 접근할 수 없게 되는 것은 아니다.
도구 제한과 OS 격리를 구분한다
실행 시에는 대화형 승인을 기다리지 않도록 dontAsk를 쓰고, 내장 도구인 Bash와 Edit를 금지했다. 다음은 현재 스크립트의 CLI 호출에서 리다이렉션과 백그라운드 실행을 생략한 형태다. PROMPT_TEXT에는 작업 지시가 들어 있다.
claude -p "$PROMPT_TEXT" \
--permission-mode dontAsk \
--disallowedTools "Bash,Edit"
dontAsk는 승인이 필요한 호출을 묻지 않고 거부하는 모드다. 작업 디렉토리 안의 파일 읽기처럼 원래 승인이 필요 없는 동작과 미리 허용한 도구는 실행될 수 있다. 따라서 “허용 목록에 없는 모든 도구와 경로를 거부한다”는 설명은 정확하지 않다. 거부 규칙은 허용 규칙보다 먼저 평가되므로, 도구 전체를 거부하면서 특정 경로만 허용하는 예외를 만들 수도 없다. Claude Code 권한 규칙
이 규칙을 집행하는 주체는 Claude Code다. OS 샌드박스가 아니다. 공식 샌드박스는 셸 명령과 그 자식 프로세스의 파일·네트워크 접근을 제한하는 별도 계층이며, 모든 MCP 서버와 도구를 자동으로 격리하는 설정도 아니다. 원문의 “OS 레이어에서 통로를 원천 차단했다”는 주장은 이 둘을 혼동했다. 권한과 샌드박스의 차이
설정 파일의 존재와 적용 여부는 다르다
현재 파일을 대조하니 의도와 실행 배선 사이에도 차이가 있었다.
| 확인한 대상 | 현재 상태 | 해석할 때의 한계 |
|---|---|---|
| 무인 실행용 설정 | 일부 위키 쓰기와 검색 허용, Bash·키 경로 거부 규칙 존재 | 스크립트에서 --settings로 지정하지 않음 |
| 프로젝트 설정 | Read, Write, Agent, 검색 도구 등을 넓게 허용 |
드래프트 경로만 허용한 설정으로 볼 수 없음 |
| CLI 플래그 | Bash,Edit 금지 |
셸 도구와 부분 편집 도구를 제한하며, 모든 실행 경로를 격리하지는 않음 |
| 출력 지시 | 날짜·시각별 *-auto.md에 쓰도록 프롬프트에 명시 |
경로를 기술적으로 강제하는 검사는 없음 |
전용 설정 파일을 만들어 뒀다는 사실만으로 실행에 적용됐다고 판단할 수는 없다. 다른 위치의 설정을 지정하려면 해당 파일을 불러오는 배선이 필요하다. 설정의 허용 목록은 여러 출처에서 합쳐질 수 있으므로 좁은 규칙을 추가하는 것만으로 기존의 넓은 허용이 없어지는 것도 아니다. CLI 설정 옵션, 설정 파일과 병합 규칙
전용 파일에는 현재 공식 문법과 어긋나는 단일 / 시작의 절대 경로 표기도 있었다. 현재 문서에서 파일 시스템 루트의 절대 경로는 //, 홈 기준 경로는 ~/로 표현한다. 이번 검수는 현재 파일과 공식 문서의 대조이며, 당시 CLI 버전의 적용 결과를 복원한 것은 아니다. 파일 경로 규칙
Edit 금지도 새 파일 전용 권한을 뜻하지 않는다. Write는 파일 생성과 기존 파일 전체 덮어쓰기를 모두 수행한다. 날짜별 이름은 충돌 가능성을 줄이는 운영 규칙일 뿐, 덮어쓰기 방지는 아니다. 특히 현재 스크립트는 재시도 전에 출력 경로를 한 번 정하므로 모든 시도가 같은 경로를 사용한다. Write 도구의 동작
사람이 검토한 뒤 병합한다는 운영 방식은 유지할 가치가 있다. 이를 강제하려면 승인된 지식의 읽기 전용 배치, 별도 출력 디렉토리, 파일이 이미 있으면 쓰기를 거부하는 처리, 산출물 검증 같은 통제가 더 필요하다. 키를 받지 않는 실행 환경과 필요한 MCP만 연결하는 구성도 별도로 검증해야 한다. 이들은 현재 구현이 보장하는 성질이 아니라 보완할 항목이다.
잠자기 이후의 실행과 네트워크 대기
평일 작업은 하루 세 시각에 예약하고 스크립트에서 주말 실행을 건너뛴다. 토요일의 전체 수집은 별도 plist로 예약했다. StartCalendarInterval 작업은 잠자기 중 실행 시각을 지나면 깨어난 뒤 실행될 수 있다. 전원이 꺼진 동안 놓친 실행까지 같은 방식으로 보충하지는 않는다. Apple의 예약 작업 설명
깨어난 직후 연결 실패에 대비해 CLI 실행 전에 다음 대기를 넣었다. 현재 스크립트에서 로그 출력을 생략한 예제다.
NET_OK=0
for _ in $(seq 1 30); do
if curl -s -m 5 -o /dev/null https://api.anthropic.com; then
NET_OK=1
break
fi
sleep 10
done
[ "$NET_OK" = 1 ] || exit 1
원문은 최대 5분이라고 했지만, 실패한 요청도 매번 최대 5초를 쓴다. 전부 실패하면 요청 시간과 10초 대기를 합쳐 약 30 × (5 + 10) = 450초, 즉 7분 30초의 예산이 된다. 프로세스 스케줄링이나 시스템 잠자기까지 포함한 엄격한 실제 경과 시간 상한은 아니다.
이 검사는 전송 가능성을 살피는 사전 확인이다. curl에 --fail이 없으므로 HTTP 401이나 500을 받아도 종료 코드가 0일 수 있다. 인증 성공, 모델 호출 성공, 검색 MCP의 연결까지 검증하지 않는다. 사전 확인을 통과해도 실제 작업은 실패할 수 있다. curl의 시간 제한과 HTTP 오류 처리
종료 타이머와 재시도의 범위
한 시도가 오래 멈추는 상황에 대비해 900초 뒤 종료 신호를 보내는 타이머도 넣었다. 다음은 현재 함수의 핵심이다. CLAUDE는 실행 파일 경로이고, LOG는 작업 로그 경로다.
run_once() {
caffeinate -i "$CLAUDE" -p "$PROMPT_TEXT" \
--permission-mode dontAsk --disallowedTools "Bash,Edit" >>"$LOG" 2>&1 &
local pid=$!
( sleep 900; kill "$pid" 2>/dev/null ) &
local wd=$!
wait "$pid"; local rc=$?
kill "$wd" 2>/dev/null
return $rc
}
여기서 kill은 기본적으로 SIGTERM을 보낸다. 대상이 이를 무시하면 wait는 계속 기다릴 수 있고, PID 하나에 보낸 신호가 모든 자식 프로세스의 종료를 보장하지도 않는다. 따라서 이 코드는 “15분 뒤 종료 요청”이며, 프로세스 트리를 반드시 정리하는 강제 시간 제한은 아니다. 로컬 재현에서도 종료 신호를 무시하는 시험 프로세스는 타이머 뒤에 남았다.
caffeinate -i는 실행 중 유휴 상태로 인한 시스템 잠자기를 억제한다. 사용자가 직접 유도한 모든 잠자기나 키체인 잠금을 막는다는 뜻은 아니다. 또 현재 배치에서는 CLI 시도에만 적용돼 사전 네트워크 대기와 시도 사이 대기는 포함하지 않는다. Apple의 caffeinate 구현
시도는 최초 실행을 포함해 최대 세 번이며, 실패 사이에 60초를 기다린다. 현재 구현은 종료 코드가 0이 아닌 모든 실패를 같은 방식으로 재시도한다. 일시적 장애에는 도움이 될 수 있지만, 자격 증명 만료가 대기만으로 해결되는 것은 아니다. 네트워크 사전 확인이 실패하면 이 반복에 진입하지도 않는다.
인증 경로는 비용에도 영향을 준다. 구독 인증을 사용한다고 추가 과금이 항상 없는 것은 아니다. 현재 공식 안내는 구독 사용량과 API 사용량을 구분하며, API 키 환경변수나 별도 사용량 결제 설정에 따라 비용 경로가 달라질 수 있다고 설명한다. 이 글에서는 실제 인증 정보나 과금 설정을 조사하지 않았다. Claude Code 구독과 과금 안내
실행 기록과 산출물을 따로 확인한다
작성 당시 원문에는 401, ENOTFOUND, 종료 코드 0과 143을 관찰한 기록이 있었다. 이 코드만으로 원인까지 확정할 수는 없다. 401은 토큰 만료 외의 인증 문제도 가능하고, ENOTFOUND만으로 잠자기가 원인이라고 단정할 수 없다. 143 역시 Bash에서 SIGTERM 종료와 부합하지만, 누가 신호를 보냈는지나 실제로 멈춰 있었는지까지 증명하지 않는다.
2026-09-22 검수에서는 두 launchd 작업이 등록돼 있음을 읽기 전용으로 확인했다. 당시 둘 다 실행 중이 아니었고 마지막 종료 코드는 1이었다. 스케줄 등록과 최근 성공은 다른 사실이다. 과거 운영 로그나 인증 정보는 다시 열지 않았으며, 원문의 기록으로 성공률 개선 수치를 계산하지 않았다.
종료 코드 0도 올바른 드래프트가 생성됐다는 증거로는 부족하다. 현재 스크립트는 종료 코드를 기록하지만 산출물의 존재·형식·출처·누락 여부를 검사하지 않는다. 실패를 알아차리려면 실행 로그와 함께 마지막 정상 산출물의 시각을 확인하는 절차가 필요하다.
결과와 학습
이번 검수에서는 스크립트와 plist, 프로젝트·전용 설정을 대조하고 16개 검사를 통과했다. 외부 모델 호출 없이 준비한 응답과 짧게 축소한 타이머로 네트워크 대기, 재시도, 종료 신호 처리를 확인했다. 운영 작업을 실행하거나 설정을 바꾸지는 않았다.
구현의 성과는 뉴스 수집을 매매 루프 밖으로 옮기고, 무인 실행에 대기·재시도·로그를 연결한 데 있다. 사람이 결과를 검토하는 단계도 명시했다. 다만 도구 금지, 출력 지시, OS 접근 제한은 각각 집행하는 주체가 다르므로 하나의 격리 보장으로 묶어 설명해서는 안 된다.
앞으로 같은 자동화를 만들 때는 세 가지를 완료 조건으로 삼으려 한다. 첫째, 설정 파일의 내용뿐 아니라 실제로 불러오는 설정과 도구를 확인한다. 둘째, 재시도 횟수와 종료 요청을 작업 완료의 보장으로 해석하지 않는다. 셋째, 프로세스의 성공과 산출물의 유효성을 따로 검증한다. 무인 작업에서는 실행을 예약하는 것만큼 실패와 결과를 확인할 경로가 중요하다.