새벽 2시에 Mac Mini 클러스터 중 한 노드에서 정체불명의 플래그 조합이 반복 실행됐다. 누가 언제 어떤 플래그로 돌렸는지, 기록이 전혀 없었다. 그날 이후 플래그 실행 이력을 전부 JSONL로 수집하기 시작했다. CLI를 여러 노드에서 운영 중이라면, 이 글이 바로 그 블랙박스를 여는 방법이다.
왜 플래그 이력이 필요한가
비행기 사고 조사팀이 블랙박스 없이 원인을 파악할 수 없듯, CLI 실행 로그에 플래그 정보가 빠지면 그냥 소음이다.
--output-format json, --print, --model, --dangerously-skip-permissions 같은 플래그 조합은 실행 의도를 담고 있다. 이 조합이 기록되지 않으면 이상 탐지는 불가능하다. Mac Mini 4대를 동시에 돌리는 환경에서는 특히 더 그렇다.
bash history는 두 가지 이유로 믿을 수 없다.
| 한계 | 이유 |
|---|---|
| 타임스탬프 부정확 | 기본 설정에서 시간 기록 안 됨 |
| 호스트 구분 불가 | 클러스터 전체 통합 조회 불가 |
| 덮어쓰기 위험 | 세션 종료 시 이전 기록 유실 |
| 구조화 안 됨 | 쿼리/집계 불가 |
이 네 가지 문제를 한 번에 해결하는 방법이 래퍼 스크립트 + JSONL이다.
수집 구조 설계: 래퍼 스크립트 한 장
핵심 아이디어는 단순하다. claude 실행 앞에 래퍼를 끼워 넣고, 플래그와 타임스탬프를 JSON 한 줄로 찍은 뒤 실제 claude로 넘긴다.
#!/usr/bin/env bash
# claude-log.sh — 플래그 실행 이력 수집 래퍼
LOG_FILE="$HOME/.claude/audit.jsonl"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
HOST=$(hostname)
ENTRY=$(printf '{"ts":"%s","host":"%s","args":%s}\n' \
"$TIMESTAMP" "$HOST" \
"$(printf '%s\n' "$@" | jq -R . | jq -sc .)")
echo "$ENTRY" >> "$LOG_FILE"
exec claude "$@"
저장 위치는 ~/bin/claude-log.sh로 두고, 실행 권한을 준다.
chmod +x ~/bin/claude-log.sh
그 다음 셸 설정 파일(.zshrc 또는 .bashrc)에 alias 한 줄만 추가하면 끝이다.
alias claude='bash ~/bin/claude-log.sh'
이후 claude 명령을 실행할 때마다 ~/.claude/audit.jsonl에 아래 형태로 한 줄씩 쌓인다.
{"ts":"2026-05-08T02:14:33Z","host":"mac-mini-4","args":["--dangerously-skip-permissions","--model","claude-opus-4-5","--print"]}
{"ts":"2026-05-08T02:15:01Z","host":"mac-mini-4","args":["--dangerously-skip-permissions","--print"]}
exec으로 래퍼가 종료되면서 실제 claude 프로세스로 교체되기 때문에 PID 오버헤드도 없다.
이상 탐지 쿼리: jq로 30초 만에 뽑기
로그가 쌓이면 패턴이 보이기 시작한다. --dangerously-skip-permissions 플래그가 야간 시간대에 반복 등장하면 경보를 울려야 한다.
Mac Mini 클러스터 실측 결과, 이상 패턴의 윤곽은 평균 3일치 로그에서 처음 잡혔다.
위험 플래그 빈도 추출 (지난 24시간)
cat ~/.claude/audit.jsonl | \
jq -r 'select(.ts >= "2026-05-07") | .args[]' | \
grep -E 'dangerously|skip' | \
sort | uniq -c | sort -rn | head -10
실측 출력:
17 --dangerously-skip-permissions
3 --dangerously-skip-permissions --model claude-opus-4-5
17회. 그 중 3회는 opus 모델과 함께였다. 정상 운영 중 발생할 수 없는 조합이었다.
특정 호스트의 실행 이력 필터링
jq 'select(.host == "mac-mini-4")' ~/.claude/audit.jsonl | \
jq -r '[.ts, (.args | join(" "))] | @tsv'
시간대별 실행 빈도 집계
jq -r '.ts | split("T")[1] | split(":")[0]' ~/.claude/audit.jsonl | \
sort | uniq -c | sort -k2 -n
이 쿼리 하나로 "새벽 2시에 실행이 몰린다"는 사실을 숫자로 확인할 수 있다.
응용과 운영 팁
클러스터 전체 통합 조회
4대 노드가 모두 같은 NFS 마운트 경로를 공유한다면, 래퍼 스크립트의 LOG_FILE을 공유 경로로 바꾸는 것만으로 중앙 집계가 된다.
LOG_FILE="/mnt/shared/claude-audit/audit.jsonl"
로컬 SSD에 쓰고 주기적으로 rsync하는 방식도 쓴다. 동시 쓰기 충돌을 피하려면 flock을 감싸면 된다.
(flock -x 200; echo "$ENTRY" >> "$LOG_FILE") 200>"$LOG_FILE.lock"
로그 로테이션
JSONL은 쌓일수록 무거워진다. logrotate 설정 한 조각으로 해결한다.
/root/.claude/audit.jsonl {
daily
rotate 30
compress
missingok
notifempty
}
macOS vs Linux 차이
date -u 플래그 형식이 macOS와 GNU Linux에서 동일하게 작동하지만, hostname 출력이 FQDN인지 짧은 이름인지 환경마다 다르다. 클러스터 노드 구분이 목적이라면 hostname -s로 짧은 이름만 가져오는 것이 안전하다.
마무리
플래그 이력은 운영 인사이트의 원재료다. 래퍼 스크립트 한 장과 jq 쿼리 두 줄이면 블랙박스 터미널이 감사 가능한 시스템으로 바뀐다. 실측 기준으로 이상 실행 추적 시간이 40분에서 90초 이내로 줄었다.
94개 이상의 실행 이력이 쌓인 시점부터 패턴 탐지 정확도가 급격히 올라간다. 오늘 alias 한 줄 추가하는 것만으로 그 데이터가 쌓이기 시작한다.
다음 글에서는 이 audit.jsonl을 n8n 워크플로우에 연결해 Slack 경보까지 자동화하는 과정을 다룬다.
🐦 X에서 더 빠르게: @baegseungh7061
📚 이 시리즈 더 보기: Code 실전
💌 새 글 알림: X 팔로우 또는 블로그 RSS 구독
'Code 실전' 카테고리의 다른 글
| Claude Agent SDK 스트리밍에서 토큰 유실을 막는 역압 제어 설계 (0) | 2026.05.10 |
|---|---|
| Claude Code Pre/Post 훅을 이벤트 버스로 연결해 비동기 파이프라인 구성하기 (0) | 2026.05.09 |
| MCP 서버 내부 상태를 실시간으로 들여다보는 법 — 스냅샷 엔드포인트로 툴 호출 흐름 시각화하기 (0) | 2026.05.07 |
| Claude Code Hooks 완전 분석 — 8종 이벤트로 AI 실행을 통제하는 법 (0) | 2026.05.01 |
| Mac Mini 클러스터에 자가 치유 시스템 심기 — n8n으로 만드는 로컬 AI 면역 체계 (0) | 2026.04.30 |