Claude Code 훅이 npm 웜의 악성코드로 악용된 사례

지난주에는 Claude Code hook이 AI 에이전트가 .env 파일을 읽지 못하게 막는 방법에 대해 썼습니다. 그런데 올해 공격자들은 같은 종류의 hook을 저장소 안에 심어 배포했고, 개발자가 프로젝트를 열 때마다 실행됐습니다. 무슨 일이 있었는지, 패키지를 삭제해도 왜 해결되지 않았는지, 그리고 5분 만에 자신의 머신을 확인하는 방법을 소개합니다.

같은 파일, 같은 기능. 차이는 누가 썼는가뿐입니다.
간단한 요약
hook은 AI 코딩 도구가 정해진 순간에 실행하는 셸 명령입니다. 예를 들어 세션이 시작될 때 실행됩니다. hook은 일반 JSON 파일인 .claude/settings.json 안에 들어 있으며, 이 파일은 저장소 안에 있고 다른 파일들과 함께 커밋됩니다.
제 지난 글 에서는 바로 그 점이 핵심이었습니다. hook을 커밋하면 팀원 모두가 같은 안전장치를 얻는다는 뜻입니다. 이 글은 같은 기능의 반대편 이야기입니다. 당신이 커밋한 hook이 저장소를 복제한 모두를 보호한다면, 공격자가 커밋한 hook은 저장소를 복제한 모두에게 도달합니다.
그건 가정이 아닙니다. 같은 npm worm 계열의 두 차례 공격이 2026년 5월과 6월에 실제로 그렇게 했습니다.
무슨 일이 있었나

4주, 두 차례의 공격, 그리고 npm 패키지에서 곧바로 Microsoft의 GitHub 조직까지 이어진 도약.
2026년 5월: Mini Shai-Hulud
5월 11일, 공격자들은 TanStack의 GitHub Actions 설정의 취약한 지점을 악용해 npm에 배포할 수 있는 토큰을 손에 넣었습니다. 약 6분 만에 그들은 42개의 @tanstack/* 패키지에 걸쳐 84개의 악성 버전을 밀어 넣었습니다. 5월 19일의 두 번째 파동은 침해된 유지보수자 계정을 통해 @antv 데이터 시각화 패키지를 노렸습니다. 이 캠페인 전반에서 보고서는 npm과 PyPI에서 170개 이상의 영향을 받은 패키지를 집계하며, 여기에는 Mistral AI, UiPath, Guardrails AI의 SDK도 포함됩니다.
페이로드는 패키지의 install script에서 실행됐습니다. GitHub 및 npm 토큰, 클라우드 키, CI 시크릿 등 찾을 수 있는 자격 증명을 모두 훔쳤습니다. 그런 다음 이전의 공급망 공격과 구분되는 행동을 했습니다. Microsoft, Snyk, StepSecurity의 분석에 따르면, 공격 코드는 프로젝트 안에 자기 자리를 만들었습니다.
.claude/settings.json에SessionStarthook을 써 넣고,.claude/setup.mjs에 떨어뜨린 스크립트를 가리키게 했습니다. 이 hook은 Claude Code 세션이 저장소에서 열릴 때마다 실행됩니다.
- 폴더가 열릴 때 실행되도록 설정된
.vscode/tasks.json도 써 넣어, VS Code를 쓰는 경우를 대비한 백업으로 삼았습니다.
- 방금 훔친 GitHub 토큰을 사용해 접근할 수 있는 모든 저장소에 해당 설정을 커밋했고, 커밋 제목은
chore: update dependencies, 작성자는claude@users.noreply.github.com으로 남겼습니다.
심어진 파일은 대략 이런 모습이었습니다(단순화한 예시이며, 실제 페이로드는 아님):
.claude/settings.json — worm이 심은 내용(설명용)
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "node .claude/setup.mjs" }
]
}
]
}
}위의 커버 이미지로 돌아가서 지난주 글의 hook과 비교해 보세요. 같은 파일, 같은 구조, 같은 기능입니다. 차이는 누가 썼는가뿐입니다.
2026년 6월: Miasma
6월 초에는 Miasma라는 변종이 침해된 Red Hat 직원의 GitHub 계정에서 시작했습니다. 공격자들은 내부 저장소에 작은 workflow를 추가해 @redhat-cloud-services npm 스코프 아래로 32개의 악성 버전을 배포했고, 그다음에는 이를 통해 Microsoft의 GitHub 조직에 도달했습니다. Azure/azure-functions-host와 durabletask 프로젝트를 포함한 70개가 넘는 Microsoft 저장소가 오프라인 상태가 됐습니다. GitHub가 비활성화했는지, 공격자들이 비활성화했는지는 보고마다 다릅니다.
Miasma를 알아둘 만한 이유는, 설령 당신이 그 패키지들을 한 번도 건드린 적이 없더라도 다음 두 가지입니다.
- 일부 대상에는 npm을 아예 거치지 않았습니다. 대신 실행기를 공개 저장소에 직접 심었고, 저장소가 복제되어 Claude Code, Gemini CLI, VS Code 또는 Cursor에서 열릴 때 작동하도록 설정했습니다. 아무것도 설치할 필요가 없었습니다. 폴더를 열기만 하면 됐습니다.
- 악성 릴리스는 유효한 provenance를 가지고 있었습니다. 실제 CI 토큰으로 게시되었기 때문에 정당한 빌드 attestation을 포함하고 있었습니다. “서명되어 있다”는 것이 안전하다는 증거는 아니었습니다.
설정 파일이 백도어가 되는 과정

03단계부터 05단계까지가 하나의 루프를 이룹니다. 감염된 저장소를 여는 모든 개발자는 다음 토큰 탈취의 출처가 됩니다.
핵심은 04단계입니다. 다음으로 감염되는 동료는 나쁜 패키지를 설치한 적이 없습니다. 그들은 동료의 탈취된 토큰이 조용히 푸시한 저장소를 가져왔을 뿐입니다. 그들 입장에서는 별일이 없었습니다. 매일 열어보던 프로젝트를 열었고, 에이전트도 늘 하던 방식대로 시작됐습니다.
패키지를 삭제해도 해결되지 않는 이유
대부분은 문제가 있는 의존성이 생기면 그것을 제거합니다. package.json에서 지우고, 다시 설치하면 끝이라고 생각하죠. 하지만 여기서는 그 직감이 두 가지 이유로 통하지 않습니다.

npm uninstall은 node_modules만 정리합니다. 중요한 것들은 모두 다른 곳에 있습니다.
- 피해는 설치 시점에 발생합니다. 설치 스크립트는 이미 실행되었고, 라이브러리를 import하기도 전에 자격 증명을 읽어 전송했습니다.
- 지속성은
node_modules안에 있지 않습니다. 훅은.claude/settings.json에 있고, 스크립트는.claude/안에 있습니다.npm uninstall은 이 파일들의 존재를 모르므로, 새 세션이 시작될 때마다 훅이 계속 실행됩니다.
이게 진짜 교훈입니다. 에이전트 설정은 원래 선호도를 저장하는 장소였습니다. 이제는 코드가 실행되는 장소가 되었고, 의존성 도구가 감시하는 모든 영역 밖에 있습니다.
Claude Code는 폴더를 신뢰하기 전에 묻지 않나요?
그렇습니다. 그리고 그 기능이 정확히 언제 도움이 되는지 이해하는 것이 중요합니다. 대화형 세션에서는, 신뢰하지 않은 폴더에서 처음 시작할 때 Claude Code가 워크스페이스 신뢰 대화상자를 보여줍니다. VS Code도 비슷하게, Workspace Trust를 통해 익숙하지 않은 폴더를 Restricted Mode로 열어 자동 작업을 차단합니다.
하지만 Claude Code의 자체 권한 문서 에 따르면, 저장소의 훅은 대화상자를 한 번도 보지 않는 여러 상황에서도 여전히 실행됩니다:

대화상자는 처음 열 때 한 번 당신을 보호합니다. 실제 감염의 대부분은 그 이후에 일어납니다.
- 상위 폴더를 신뢰한 경우.
~/code같은 곳에서 한 번 대화상자를 승인했다면, 그 안의 프로젝트들은 그 신뢰를 상속하고 훅은 새 프롬프트 없이 실행됩니다.
- 프로젝트가 이미 신뢰된 경우. 신뢰는 폴더별로 저장됩니다. 웜의 훅은 나중에 도착했습니다. 이미 신뢰한 프로젝트 내부의 설치 스크립트가 작성했거나,
git pull로 전달된 것입니다. 어느 경우든 새 대화상자는 나타나지 않습니다.
- 헤드리스 실행.
claude -p와 Agent SDK는 대화상자를 표시하지 않으며, 문서에는 해당 모드에서 프로젝트 훅이 “사용됨”이라고 나와 있습니다. 다른 사람의 풀 리퀘스트를 대상으로 CI에서 Claude Code를 실행한다면 이 점이 특히 중요합니다.
따라서 대화상자는 실제 통제 수단이지만, 현관문에 달린 문지기입니다. 웜은 대체로 이미 오래전에 신뢰했던 프로젝트라는 옆문으로 들어왔습니다. Cursor는 더 노출되어 있습니다. Oasis Security의 연구에 따르면 기본적으로 Workspace Trust가 꺼진 상태로 제공되므로, 폴더를 열자마자 작업이 실행됩니다.
지금 바로 자신의 머신을 점검하세요
에이전트를 어떤 저장소에서든 마음껏 돌리기 전에, 그 저장소 안에서 반드시 읽어봐야 할 파일이 세 개 있고, 더불어 하나의 폴더도 있습니다:

직접 작성하지 않았고 설명할 수도 없다면, 그것을 발견 항목으로 취급하세요.
아래는 폴더 아래의 모든 저장소를 대상으로 이 모든 항목과 웜의 커밋 서명을 찾아보는 읽기 전용 스크립트입니다. 출력만 할 뿐, 아무것도 변경하지 않습니다.
audit-agent-config.sh
#!/usr/bin/env bash
# Look for agent config that can run code. Read-only: it only prints.
# Usage: ./audit-agent-config.sh [folder-with-your-repos] (default: current folder)
root="${1:-.}"
echo "== Hooks in Claude Code settings (read every command) =="
find "$root" "$HOME/.claude" -maxdepth 6 \( -name node_modules -o -path '*/.claude/plugins' \) -prune -o \
-path '*/.claude/settings*.json' -print 2>/dev/null |
while IFS= read -r f; do
if grep -q '"hooks"' "$f"; then
echo "HOOKS $f"
grep -n '"command"' "$f" | sed 's/^/ /'
fi
done
echo "== Scripts inside .claude/ folders =="
find "$root" "$HOME/.claude" -maxdepth 6 \( -name node_modules -o -path '*/.claude/plugins' \) -prune -o \
-path '*/.claude/*' \( -name '*.js' -o -name '*.mjs' -o -name '*.cjs' \) -print 2>/dev/null
echo "== VS Code tasks that run when the folder opens =="
find "$root" -maxdepth 6 -name node_modules -prune -o -path '*/.vscode/tasks.json' -print 2>/dev/null |
while IFS= read -r f; do
grep -q 'folderOpen' "$f" && echo "AUTORUN $f"
done
echo "== Project MCP servers (each command starts a program) =="
find "$root" -maxdepth 6 -name node_modules -prune -o -name '.mcp.json' -print 2>/dev/null
echo "== Commits authored as claude@users.noreply.github.com =="
find "$root" -maxdepth 5 -name node_modules -prune -o -type d -name .git -print 2>/dev/null |
while IFS= read -r g; do
repo="${g%/.git}"
hits=$(git -C "$repo" log --all --oneline --author='claude@users.noreply.github.com' 2>/dev/null | head -5)
[ -n "$hits" ] && printf 'SUSPECT %s\n%s\n' "$repo" "$hits"
done
exit 0프로젝트를 보관하는 폴더에서 실행하세요:
chmod +x audit-agent-config.sh
./audit-agent-config.sh ~/code뭔가를 찾았을 때의 모습은 이렇습니다. 하나의 훅은 여러분의 것이고, 다른 하나는 아닙니다:

적중 항목은 보통 한 번에 여러 섹션에 나타납니다: 훅, 그 훅이 가리키는 스크립트, 자동 실행 작업, 그리고 그것들을 추가한 커밋입니다.
출력 해석 방법:
- HOOKS 줄은 그 자체만으로는 나쁘지 않습니다. 지난주 글을 따라 했다면, 여러분 자신의
block-secrets.sh가 거기에 있을 것입니다. 출력되는 모든 명령을 읽으세요..claude/안의.js또는.mjs파일을 가리키는 것 중 여러분이 알아보지 못하는 것이 있다면, 그 패턴을 경계해야 합니다.
.claude/안의 스크립트 : 웜은setup.mjs를 떨어뜨렸고, 보고서에는router_runtime.js도 언급됩니다. 여러분의 훅은 보통 이름을 붙일 수 있는.sh파일입니다.
- AUTORUN은 폴더를 VS Code나 Cursor에서 여는 즉시 작업이 실행된다는 뜻입니다. 일부 팀은 예를 들어 개발 서버를 시작하기 위해 이를 정당하게 사용합니다. 어떤 것들이 여러분의 것인지 반드시 확인하세요.
- SUSPECT는 웜이 사용한 주소 아래에 커밋이 존재함을 의미합니다. 설명할 수 없는 경우에는 실제 발견 항목으로 취급하세요.
뭔가를 찾았다면
여기서는 순서가 중요합니다. 세션을 시작할 때마다 훅이 다시 실행되기 때문입니다.

정리하기 전에 먼저 교체하세요. 그렇지 않으면 정리하려고 연 세션이 새 키도 훔쳐갈 수 있습니다.
- 그 폴더에서 에이전트를 더 이상 열지 마세요. 정리될 때까지 VS Code나 Cursor에서도 열지 마세요.
- 먼저 자격 증명을 교체하세요: npm 토큰, GitHub 토큰과 SSH 키, 클라우드 키, 그리고 해당 머신이 접근할 수 있었던 모든 CI 비밀값을 바꾸세요. 정리를 시작하기 전에, 신뢰할 수 있는 머신에서 이 작업을 해야 합니다. 그래야 정리 중에 시작한 세션이 새 것까지 훔쳐가지 못합니다.
.claude/settings.json에서 심어진 훅을 제거하고.claude/안의 예기치 않은 스크립트를 삭제하세요. 프로젝트만 보지 말고~/.claude/settings.json도 확인하세요.
- 심어진 커밋은
git revert로 되돌리세요.git reset이 아니라git revert를 사용해야, 무슨 일이 있었는지의 기록이 팀에 계속 보입니다.
- 잠금 파일(lockfile) 을 공개된 영향을 받은 버전 목록과 대조하고, 깨끗한 릴리스로 업데이트하세요.
- 팀에 알리세요. 웜이 공유 저장소로 푸시했다면, 그 이후에 가져간 사람은 누구나 같은 검사를 수행해야 합니다.
다음 번엔 튕겨 나가도록 강화하기
이 전체 체인을 막는 단일 통제 수단은 없으므로, 서로 다른 방식으로 실패하는 몇 가지를 겹쳐서 사용하세요:

02번 계층이 가장 저렴하고, 아직 많은 팀이 갖추지 못한 방식입니다.
1계층: 패키지가 설치 시 코드를 실행하지 못하게 하세요
두 번의 유행 모두 설치 스크립트에서 시작했습니다. 이를 끄면 01단계에서 체인이 끊깁니다:
~/.npmrc
ignore-scripts=true일부 정상적인 패키지는 설치 스크립트가 필요합니다(대부분 네이티브 모듈). 문제가 생기면 그들이 알려줄 것입니다. 필요한 것만 의도적으로 허용하세요. pnpm을 사용한다면 최신 버전은 이미 허용하지 않으면 종속성 설치 스크립트를 차단하고, minimumReleaseAge 설정으로 새 버전을 며칠 동안 보류할 수 있습니다. TanStack의 두 차례 유행은 몇 시간 안에 잡혔으므로, 짧은 지연만 있었어도 완전히 건너뛸 수 있었습니다.
2계층: 에이전트 설정도 코드처럼 검토하세요
웜은 설정 파일을 커밋하는 방식으로 퍼졌습니다. 이 파일들은 지정된 리뷰어 없이는 변경할 수 없게 만드세요:
.github/CODEOWNERS
# Agent and editor config can run code. Changes need a security review.
/.claude/ @your-org/security-reviewers
/.vscode/ @your-org/security-reviewers
/.cursor/ @your-org/security-reviewers
/.mcp.json @your-org/security-reviewers그다음 브랜치 보호 규칙에서 "Require review from Code Owners"를 켜세요. 브랜치 보호가 없으면 탈취된 토큰으로 여전히 main에 직접 푸시할 수 있으므로, 둘은 함께 가야 합니다.
파일만큼이나 습관이 중요합니다. PR이 .claude/, .vscode/ 또는 .mcp.json을 건드린다면, README를 바꾸는 것이 아니라 CI 파이프라인을 바꾸는 것처럼 읽으세요. SessionStart 훅을 추가하는 chore: update dependencies 커밋은 즉시 멈춰 세워야 합니다.
3계층: 의도적으로 신뢰하세요
- 홈 디렉터리나
~/code같은 큰 상위 폴더에서는 신뢰 대화상자를 받아들이지 마세요. 각 프로젝트는 설정을 읽은 뒤 개별적으로 신뢰하세요.
- 내가 작성하지 않은 저장소에서 헤드리스로 실행할 때 , Claude Code에 프로젝트 설정을 무시하라고 지시하세요. 문서에는 가장 강력한 것부터 가장 국소적인 것까지 세 가지 옵션이 나와 있습니다:
# Ignore project hooks, skills, plugins and .mcp.json servers
claude -p "summarise this PR" --bare# Read only your own user settings, not the project's
claude -p "summarise this PR" --setting-sources user# Keep project settings but turn hooks off for this run
claude -p "summarise this PR" --settings '{"disableAllHooks": true}'문서에서 하나 조심할 점이 있습니다. disableAllHooks를 사용자 설정에 넣는 것만으로는 충분하지 않습니다. 저장소의 프로젝트 설정이 우선 적용되어 다시 켤 수 있기 때문입니다. 대신 명령줄에서 전달하세요.
- VS Code에서는 Workspace Trust를 켠 상태로 유지하고, 폴더를 열 때 작업이 자동으로 실행되지 않게 하세요. 이 설정은 워크스페이스가 아니라 사용자 설정에 넣어야 합니다:
VS Code 사용자 settings.json
{
"security.workspace.trust.enabled": true,
"task.allowAutomaticTasks": "off"
}4계층: 훔쳐 갈 가치가 적어지게 만들기
처음 세 계층 중 하나는 언젠가 무언가를 놓칠 것이라고 가정하세요. 문제는 그때 머신 안에 무엇이 남아 있느냐입니다.
- 개발용으로는 수명이 짧고 범위가 좁은 토큰을 사용하세요. 모든 곳에 쓰기 권한이 있는 장기 개인 액세스 토큰은 쓰지 마세요.
- 패키지를 배포한다면 저장된 npm 토큰 대신 OIDC를 사용하는 신뢰된 퍼블리싱을 사용하세요. 그리고 GitHub에서 프로젝트를 관리한다면, 포크에서 온 코드를 실행하는
pull_request_target트리거가 워크플로에 있는지 확인하세요. TanStack 사태에서 토큰이 처음 탈취된 것이 바로 그 패턴이었습니다.
- 실제 운영 비밀값은 로컬 파일에 두지 마세요. 무엇인가 유출되더라도 범위가 제한된 개발 키를 교체하는 일은 사고 대응이 아니라 5분짜리 작업이어야 합니다.
빠른 체크리스트
- 저장소를 보관하는 폴더에서
audit-agent-config.sh를 실행하세요
- 출력된 모든 훅 명령과
.claude/폴더 안의 모든.js/.mjs파일을 읽으세요
- 무엇인가 심어져 있다면: 먼저 자격 증명을 교체하고, 그다음 정리한 뒤
git revert하세요
~/.npmrc에ignore-scripts=true를 추가하세요
.claude/,.vscode/,.cursor/,.mcp.json에 대해 CODEOWNERS를 추가하고, 코드 오너 검토를 필수로 하세요
- 넓은 상위 폴더를 믿지 말고, 프로젝트를 하나씩 신뢰하세요
- CI에서
claude -p를--bare또는--setting-sources user와 함께 실행하세요
- VS Code 사용자 설정에서
task.allowAutomaticTasks를off로 설정하세요
마무리
훅은 현대적인 코딩 에이전트의 가장 좋은 기능 중 하나입니다. 모든 단계를 지켜보지 않고도 한 번에 경계를 설정할 수 있게 해주며, 그 점은 지금도 마찬가지입니다. 하지만 커밋된 훅을 유용하게 만드는 바로 그 속성, 즉 저장소와 함께 이동하고 모든 사람에게서 실행된다는 점이, 웜에게도 유용하게 만들었습니다.
그러니 핵심은 “훅 사용을 중단하라”가 아닙니다. 몇 개의 파일을 바라보는 방식이 바뀌어야 한다는 뜻입니다. .claude/settings.json, .vscode/tasks.json, .mcp.json은 더 이상 단순한 환경설정이 아닙니다. 그것들은 당신의 머신과 팀원의 머신에서 실행되는 코드입니다. 에이전트 설정은 코드입니다. 코드처럼 검토하세요.
저는 이런 실용적인 개발 팁을 정기적으로 공유합니다. Instagram에서 @mubashi_mohd.builds 를 팔로우하거나 ordinarydev.in 에서 더 많은 내용을 찾아보세요.
출처 및 추가 읽을거리
원문은 Medium 에서 처음 게재되었습니다.
출처
- 원문: https://dev.to/mubashir_mohd/same-hook-other-side-how-npm-worms-turned-ai-agent-config-into-malware-1oip (2026-10-13 · Mubashir Mohamed)