Claude Code 훅이 안 막는 이유 — 로컬 적용 범위와 deny 점검

Claude Code 훅이 등록돼 있는데 왜 로컬 명령을 못 막을까?

등록 여부, matcher, 스크립트의 조기 종료 조건을 따로 봐야 한다. 이 글은 직접 만든 원격 전용 훅이 로컬에서는 검사를 건너뛴 사례다. Claude Code 전체가 삭제 명령을 허용한다는 뜻도, 모든 deny 규칙이 무효라는 뜻도 아니다.

작성자: 닥터조지. 측정 시점: 2026-08-02 로컬 세션 / 편집 보완: 2026-09-23. 당시 CLI 버전은 이 글에 기록되어 있지 않다. 새 버전에서 재실행한 결과가 아니며, 아래 절차는 독자의 환경을 따로 확인하기 위한 점검 순서다.

훅 미작동을 구분하는 점검 순서

  1. CLI 버전, 로컬·원격 여부, 실제로 읽힌 설정 파일을 기록한다. 사용자 설정과 프로젝트 설정을 혼동하지 않는다.
  2. PreToolUse의 matcher가 검사하려는 도구와 일치하는지, 실행할 스크립트 경로가 맞는지 확인한다.
  3. 스크립트의 앞부분과 모든 조기 종료 조건을 읽는다. 원격 전용 환경변수가 없을 때 exit 0이면 등록돼 있어도 해당 경로를 검사하지 않는다.
  4. 삭제·외부 전송·실제 비밀 읽기를 하지 않는 확인 명령을 골라, 평소 사용하는 Claude Code 세션에서 한 건만 요청한다. 일반 터미널에서 명령만 실행하는 것은 Claude Code 권한 검사 시험이 아니다.
  5. 요청한 도구·명령, 훅 로그, 차단 메시지, 종료 상태를 함께 남긴다. 훅 단독 호출 결과와 Claude Code 전체 경로의 결과를 다른 칸에 기록한다. 차단 주체를 구분할 수 없으면 미확인으로 남긴다.

기대 결과는 단순히 “막힘”이 아니라 어느 설정·어느 훅이 그 요청을 처리했는지 추적 가능한 기록이다. xargs --version 지원 여부는 운영체제의 구현에 따라 다르다. 옵션 오류가 나더라도 권한 차단과 실행 후 오류를 구분해야 한다. 과거 시험의 chmod 777을 실제 작업 파일에 따라 하지 않는다.

현재 설정 형식의 기준은 Claude Code 공식 Hooks reference다(2026-09-23 문서 조회). 문서 확인은 아래 과거 측정을 새 버전에서 재현했다는 뜻이 아니다.

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

2026년 8월 2일 로컬 훅 점검 기록

두 달 전에 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.json의 PreToolUse, matcher Bash에 remote-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(*)가 들어 있고, defaultMode는 auto, skipDangerousModePermissionPrompt는 true다.

이 허용 목록만 보고 목록 밖 명령이 모두 실행된다고 단정할 수는 없다. 아래 시험처럼 auto 모드 분류기가 개입할 수 있어 실제 경로를 구분해야 한다.

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

형태 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 목록 중 시험하지 않은 43개(46개 중 3개 시험) · rm -rf 계열(파괴적이라 시도 안 함) · 목록에 없는 삭제 형태 · 220만 파일 사건의 원인과 규모

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

다른 사람의 환경도 같다는 뜻은 아니다. 오타 수정 뒤에도 자신의 훅이 어느 세션과 도구에 적용되는지 별도로 확인해야 한다.

다음 수를 deny 목록만에서 찾지 않기로 했다. 그 목록의 패턴에 해당하지 않는 요청은 deny 목록 층의 보장 범위 밖이다. 훅과 분류기의 판정은 별도로 확인해야 하며, 목록에 적었다는 사실만으로 실제 차단을 보장할 수도 없다.

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

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.json의 permissions.deny 길이, PreToolUse 훅 스크립트의 첫 20행, 그리고 목록에 든 명령 하나를 실제로 실행해보는 것. 셋째가 제일 빠르다.

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

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

권한 차단이 없다면 명령 실행 단계로 진행하므로 결과와 오류를 확인한다. 차단됐다면 당시 메시지와 훅 로그를 함께 기록하고, deny 목록·훅·분류기 중 어느 층의 판정인지 구분한다. 메시지 형식은 버전에 따라 달라질 수 있다.

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

관련 글


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

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

다음에 읽을 글

처음 시작 · 문제 해결 · 비용·실측 비교로 돌아가기 · 글쓴이 소개

댓글

이 블로그의 인기 게시물

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

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

M4 Max 128GB 로컬 LLM 4종 속도 비교 — 실측 기록과 한계