rm -rf는 안 물어보고 .env는 물어본다 — Claude Code 훅 실측

rm -rf는 안 물어보고 .env는 물어본다 — Claude Code 훅 실측

rm -rf는 안 물어보고 .env는 물어본다 — Claude Code 훅 실측

두 달 전에 Claude Code 훅을 전수 점검했다. 보안 훅 하나가 필드명 오타 때문에 한 번도 안 돌고 있었다. 고쳤고, 그 과정을 글로 적었다.

8월 2일에 같은 설정을 다시 쟀다. 이번엔 성격이 다른 게 나왔다.

세 줄 요약. 6월 감사가 잡은 건 “선언은 됐는데 작동은 안 한다”였다. 이번에 나온 건 “작동은 하는데 그 범주를 안 본다”다. 파괴적 명령을 검사하는 훅은 원격 세션 전용이라 로컬에서는 네 번째 줄에 빠져나가고, 같은 자리의 시크릿 훅은 로컬에서도 확인을 요구한다. 남은 방어선인 permissions.deny 46개는 무해한 명령으로 세 건을 시험했는데, 목록이 실제로 잡은 건 한 건이었다.

여기서 하네스는 모델을 감싸는 설정·훅·권한 묶음을 뜻한다.

설정 파일에 훅을 등록해두고 그 뒤로 다시 안 열어본 적이 있다.

위험해 보이는 명령을 넘기면서 “이건 어딘가 걸리겠지”라고 넘어간 적이 있다.

에이전트가 서버를 날렸다는 글을 보고 잠깐 내 쪽을 생각하다 그냥 닫았다.

다시 재보게 만든 것

8월 2일 수집 데이터에 r/ClaudeAI 글이 올라왔다. 제목은 “Fable 5 ultracode deleted 2.2M files on my server”였다.

같은 날 한국어권에도 Claude 보안 사고를 다룬 글이 세 건 잡혔다.

같은 커뮤니티에 반대편 스레드도 같이 있었다.

“Genuine question for the ‘Claude deleted my server’ crowd: What are you actually asking it to do?”

사용자가 뭘 시켰는지부터 보자는 주장이다.

수집된 건 제목뿐이고 본문·삭제 규모·원인은 확인하지 않았다. 사건의 진위도 책임 소재도 판단할 자료가 없다. 그걸 보고 설정 파일을 열었다. 확인한 건 그게 전부다.

로컬을 보는 훅과 안 보는 훅

~/.claude/settings.jsonPreToolUse, matcher Bashremote-command-guard.sh가 걸려 있다.

스크립트 주석은 차단 범주를 여섯 개로 선언한다. 파괴적 삭제, 경로 순회, 외부 통신, 권한 변경, 프로세스 종료, 명령 주입.

그 아래 18행부터가 조기 종료다.

# 원격 세션이 아니면 검사 건너뜀
if [[ -z "${OPENCLAW_SESSION_ID:-}" ]]; then
    exit 0
fi

현재 세션에 OPENCLAW_SESSION_ID는 없다. 원격 세션에서만 세워지는 변수라 로컬 터미널에는 존재하지 않는다.

Claude Code가 기본으로 넣는 훅이 아니라는 점은 짚어둔다. 직접 만들어 등록한 것이고, 조기 종료 조건도 직접 넣은 것이다.

위 여섯 범주는 전부 검사되지 않는다.

스크립트에 JSON을 직접 파이프해 10건을 넣어봤다. rm -rf /를 포함해 전부 통과로 나왔다. 실제 명령을 실행한 건 아니고 훅 스크립트만 호출한 결과다.

바로 옆 훅은 다르다. secret-output-guard.sh는 같은 PreToolUse에 matcher Bash|Read로 걸려 있고, 판정은 한 줄이다.

verdict = "deny" if remote else "ask"

원격이면 차단, 로컬이면 확인을 요구한다. 이 하네스는 .env를 읽으려 하면 로컬에서도 물어보고, rm -rf는 로컬에서 안 물어본다.

훅 이름이 remote-command-guard다. 원격 전용은 설계고, 주석 3행에도 그렇게 적혀 있다. 고장 난 게 아니다. 정작 확인하지 않은 건 그 범위였다. 그러고도 “파괴적 명령을 막는 훅이 있다”로 두 달을 세어왔다.

남은 방어선, deny 목록 46개

훅이 안 보면 방어는 아래로 내려간다. 이 환경에서 명령을 거를 수 있는 자리는 셋이다. 훅, 권한 목록, auto 모드가 부르는 분류기. 훅이 빠졌으니 남은 둘을 봐야 한다.

permissions.deny에 46개 패턴이 들어 있다. rm 계열은 넷이고, 전부 -rf 형태다.

Bash(rm -rf /)*   Bash(rm -rf ~)*   Bash(rm -rf .)*   Bash(rm -rf *)*

git 계열은 열둘이다. force push 여덟 개, git clean -f*, git checkout -- ., git restore ., git reset --hard origin/*.

마지막 것은 origin/ 형태만 있다.

그 위에 permissions.allow에는 Bash(*)가 들어 있고, defaultModeauto, skipDangerousModePermissionPrompttrue다.

목록에 없으면 묻지 않고 실행된다.

목록에 없는 형태를 대조했다.

형태 deny 목록
rm -r (f 없이) 없음
find . -delete 없음
rsync --delete 없음
git reset --hard HEAD~5 없음 (origin/* 형태만 있음)
bash script.sh (스크립트 안에 rm) 없음 (bash -c *만 차단)
python3 script.py 없음 (python3 -c *import os*만 차단)

여기까지는 목록을 눈으로 대조한 결과다. 목록에 있는 것이 실제로 막히는지는 다른 질문이라 직접 시도했다. 파괴적인 건 빼고 무해한 명령만 골랐다.

무해한 명령 세 개로 시험

임시 파일에 chmod 777을 걸었다.

Permission to use Bash with command chmod 777 … has been denied

막혔다. 목록의 Bash(chmod 777:*)가 잡았다.

xargs --version을 실행했다. 목록에 Bash(xargs *)*가 있다. 파이프 중간이 아니라 명령 맨 앞이다.

그대로 실행됐다.

마지막으로 넣은 건 node -e 'console.log("probe-node")'다. 목록에 Bash(node -e *)*가 있다. 이건 막혔는데 메시지가 달랐다.

Blocked by classifier

deny 규칙이 아니라 auto 모드 분류기가 잡았다. 목록에 있는 명령인데 차단 메시지는 목록에서 나오지 않았다.

두 층이 같이 걸렸는데 분류기 메시지가 먼저 뜬 것일 수도 있다. 순서까지는 확인하지 못했다. 메시지가 달랐다. 거기까지 관측했다.

세 결과를 놓고 46개를 형태별로 다시 셌다.

콜론 형태  Bash(...:*)    2개   sudo, chmod 777
별표 형태  Bash(...)*    40개   rm -rf 4개 · git 12개 · eval · bash -c · xargs 등
기타                      4개   WebFetch 계열, 리다이렉트 2개

차단이 확인된 chmod는 콜론 형태다. 통과한 xargs는 별표 형태다. rm 계열 넷과 git 계열 열둘은 전부 별표 형태에 있다.

시험한 건 세 개고 나머지 마흔셋은 세어만 뒀다. rm -rf는 파괴적이라 시도하지 않았다. 같은 형태에 들어 있다는 것만 사실이고, 안 막힌다고 말할 근거는 없다.

세 개를 시험해서 목록이 잡는 걸 본 건 하나다. 46이라는 숫자를 방어선의 크기로 읽어온 게 그동안의 착각이었다.

응답 메시지도 세 종류로 갈렸다. 목록이 잡으면 실행하려던 명령이 그대로 찍히고, 분류기가 잡으면 “Blocked by classifier”가 뜨고, 훅은 로컬에서 아무 말도 하지 않는다.

앞에서 .env는 물어보고 rm -rf는 안 물어본다고 적었다. 그건 훅 층의 얘기였다.

아래 두 층까지 놓고 보면 질문이 달라진다. “무엇이 막히나”에서 “방금 막은 게 어느 층인가”로 옮겨간다.

뒤쪽은 메시지를 보고서야 안다.

목록 밖에서 난 사고 셋

목록이 얼마나 잡는지와 별개로, 목록이 애초에 못 보는 자리가 있다. 이 하네스에서 실제로 뭔가 사라진 적이 세 번 있는데 셋 다 그쪽이었다.

Blogger API의 useTrash=True는 휴지통이 아니라 즉시 영구 삭제였다. 이름만 보고 되돌릴 수 있다고 읽은 게 원인이다.

git stash push --include-untracked <pathspec>은 빈 stash를 만들고 작업 중인 파일을 남겨뒀다. 그 파일들은 이후 사라졌다.

지금은 금지 패턴으로 명문화돼 있다.

6월 11일 훅·발행 스크립트 감사에서는 심각 3건이 나왔다. 시크릿 필터가 무력화돼 있었고, 버퍼 파일이 20.9MB까지 무한 증가했고, 발행 후 결과를 검증하는 단계가 없었다.

셋 다 명령 문자열 자체는 위험해 보이지 않았다. 위험은 도구의 기본값과 API 의미론에 있었다. 패턴 목록의 시야 밖이다.

막힌 것, 안 막힌 것, 확인 못 한 것

구분 항목
막힌다 (실측) chmod 777 — deny 목록이 잡음 · node -e — 분류기가 잡음 · 시크릿 읽기는 로컬에서 확인을 거침
안 막힌다 (실측) xargs … — 세 겹 다 통과 · 로컬 세션의 rm·권한 변경·프로세스 종료 등 6범주는 훅이 검사하지 않음
확인 못 했다 deny 목록 나머지 37개 · rm -rf 계열(파괴적이라 시도 안 함) · 목록에 없는 삭제 형태 · 220만 파일 사건의 원인과 규모

두 달 전 글은 “고쳐서 돌게 했다”로 끝났다. 돌게 됐다고 덮이는 범주가 있는지는 안 봤다.

그 글을 읽고 오타를 고친 사람도 지금 이 상태 그대로일 것이다.

다음 수를 목록에서 찾지 않기로 했다. 목록에 없으면 못 보고, 있어도 잡는다는 보장이 없다는 게 오늘 나온 결과다.

되돌릴 수 있게 만드는 쪽을 대신 재볼 생각이다. 백업, 진짜 휴지통, 드라이런. 아직 안 해봤으니 이건 가설이다.

Q&A

Q. 훅을 켜두면 안전한가.
A. 훅은 matcher와 실행 조건에 걸린 것만 본다. 이 하네스의 파괴적 명령 훅은 OPENCLAW_SESSION_ID가 없으면 검사 없이 빠져나간다. 등록 여부와 적용 범위는 다른 질문이다.

Q. 220만 파일 사건은 모델 잘못인가.
A. 모른다. 원문 본문을 확인하지 않았고, 같은 커뮤니티에 사용자 지시를 문제 삼는 스레드가 나란히 올라와 있다.

Q. deny 목록을 늘리면 해결되나.
A. 부분적이다. rm -rf를 넷으로 막아도 rm -r, find -delete, 스크립트 안의 삭제는 다른 문자열이다. 게다가 목록에 넣는 것과 그게 실제로 잡는 것은 별개였다. xargs는 목록에 있는데 그대로 실행됐다.

Q. 내 설정은 어떻게 확인하나.
A. 볼 곳은 셋이다. ~/.claude/settings.jsonpermissions.deny 길이, PreToolUse 훅 스크립트의 첫 20행, 그리고 목록에 든 명령 하나를 실제로 실행해보는 것. 셋째가 제일 빠르다.

시험할 명령은 이렇게 고른다. 자기 deny 목록에서 파일을 지우지도 보내지도 않는 항목을 찾는다.

--version이나 --help를 붙일 수 있는 것이면 더 안전하다. 여기서는 xargs --version을 썼다.

막히면 메시지에 명령이 그대로 찍히고, 안 막히면 아무 일도 일어나지 않는다.

Q. 파괴적 명령으로 직접 시험해도 되나.
A. 하지 마라. 이 감사도 chmod 777, xargs --version, node -e 같은 무해한 것만 썼다. rm -rf 계열은 시도하지 않았고, 그래서 그 넷이 막히는지는 여전히 모른다.

관련 글


글쓴이 — 프론트엔드 9년, 블록체인 8년. Claude Code 하네스를 직접 굴리며 나온 수치를 기록한다. 이 글의 측정은 2026년 8월 2일 로컬 세션 기준이다.

태그: #ClaudeCode #AI에이전트 #개발환경 #보안 #PreToolUse #설정감사 #AI코딩

댓글

이 블로그의 인기 게시물

Opus 5는 더 잘 쓰고, 더 AI답게 쓴다 — 세 모델 첫날 실측

AI 코딩 스킬, 많을수록 좋을까 — 자산이 부채로 바뀌는 손익분기

맥 스튜디오 M4 Max 128GB 로컬 LLM 4개 속도 비교 — gemma4·llama3.3·qwen3 실측