Claude Code에 SlopScan 연결하기 — 기술(Skill), 훅(Hook), 그리고 출시할 뻔했던 버그
요약
LLM의 패키지 이름 환각 현상과 이를 악용한 'slopsquatting' 공격을 방지하기 위해 SlopScan을 Claude Code에 연결하는 방법을 설명합니다. 모델의 판단에 의존하는 '스킬(skill)' 방식과 시스템 차원에서 강제하는 '훅(hook)' 방식의 차이를 통해 보안 안전망 구축 전략을 제시합니다.
핵심 포인트
- LLM이 생성한 코드의 약 20%가 존재하지 않는 패키지를 참조함
- SlopScan을 통해 패키지 신뢰도를 점수화하여 보안 위험을 사전 차단 가능
- 모델의 판단에 맡기는 '스킬'보다 시스템 이벤트 기반의 '훅'이 더 확실한 안전망임
- Claude Code의 라이프사이클 이벤트를 활용한 자동화된 보안 검증 구현
SlopScan은 npm/PyPI 패키지 이름을 설치하기 전에 실제 존재하는지 확인하는 작은 오픈 소스 API입니다. 이 도구가 해결하려는 문제는 구체적이면서도 점점 더 흔해지고 있습니다. 바로 LLM(대규모 언어 모델)이 패키지 이름을 환각(hallucinate)하는 현상입니다. 연구에 따르면 AI가 생성한 코드의 약 20%가 존재하지 않는 패키지를 참조하고 있으며, 공격자들은 이러한 환각된 이름을 실제 레지스트리에 악성 페이로드(malicious payloads)와 함께 미리 등록하기 시작했습니다. 이를 _slopsquatting_이라고 부르는데, 더 고약한 점은 환각된 이름의 약 43%가 동일한 모델의 실행 시마다 일관되게 나타난다는 것입니다. 이는 공격자가 체계적으로 예측하여 선점(squatting)할 가치가 있게 만듭니다.
SlopScan 자체는 간단합니다. 패키지 이름을 입력하면 레지스트리 생성 시기, 다운로드 수, GitHub 신호(signal) 및 기타 몇 가지 요소를 기반으로 신뢰도를 점수화하여 SAFE / CAUTION / SUSPICIOUS / DANGEROUS를 반환합니다. 이 포스트의 실제 주제는 이를 Claude Code에 연결하여 설치가 실행되기 전에 자동으로 확인하도록 만드는 방법, 그리고 자동화 부분을 구축하면서 마주친 정말 흥미로운 버그에 관한 것입니다.
서로 다른 역할을 수행하는 두 가지 요소
저는 이것을 **기술 (skill)**과 **훅 (hook)**으로 구축했으며, 이 둘은 중복되지 않습니다. 서로 다른 문제를 해결하기 때문입니다.
A **기술 (skill)**은 Claude가 읽고 실행할 수 있는 문서입니다. 이는 "필요할 때 X를 수행하는 방법"을 알려주는 데 매우 유용하지만, 모델이 이를 호출해야 한다는 사실을 기억할 때만 작동합니다. 스스로에게 솔직하게 물어보십시오. 모든 세션에서, 예외 없이, 패키지를 설치하기 전에 항상 확인하는 것을 비서에게 전적으로 신뢰할 수 있겠습니까? 저라면 그렇게 하지 않을 것이며, 저 또한 매일 이 작업을 수행합니다.
**훅 (Hook)**은 성격이 다릅니다. 이는 Claude Code 하네스(harness)가 지정된 라이프사이클 이벤트(PreToolUse, PostToolUse 등)에서 결정론적으로 실행하는 실제 셸 명령(shell command)이며, 실제로 동작을 차단할 수 있는 결정을 반환할 수 있습니다. "모델이 확인하기로 결정했다"가 아니라, 시스템이 예외 없이 매번 사용자의 스크립트를 실행하는 것입니다. "모델이 이번 한 번만 깜빡했다"라는 상황이 실제 비용(risk)을 초래하는 모든 경우에 있어, 이 차이가 핵심입니다.
즉, 스킬 (skill)은 필요할 때 수행하는 수동 점검을 위한 것이고, 훅 (hook)은 누군가가 무언가를 기억해야 하는 여부에 의존하지 않는 안전망을 위한 것입니다.
스킬 (The skill)
거창한 것은 없습니다. SlopScan의 API 규약(contract)과 결과 해석 방법을 설명하는 SKILL.md 파일뿐입니다:
---
name: slopscan-check
description: "설치하기 전에 npm 또는 PyPI 패키지 이름을 SlopScan으로 확인합니다. npm install/pip install/uv add를 수행하기 전, 또는 익숙하지 않거나 LLM이 제안한 의존성(dependency)을 추가하기 전에 사용하세요."
...
이것만으로도 진정으로 유용합니다. Claude는 확신이 없는 무언가를 설치하려 할 때 이 스킬을 호출할 것입니다. 하지만 "확신이 없다"는 것은 판단의 영역이며, 판단의 영역이야말로 바로 "이번 한 번만 깜빡했다"라는 상황이 스며드는 지점입니다.
훅 (The hook)
이것은 Bash 도구에 적용되는 PreToolUse 훅입니다. 이 훅은 세 가지 일을 수행해야 합니다. 여러 패키지 매니저에 걸쳐 설치 명령을 인식하고, 실제 패키지 이름만을 추출하며(플래그, 버전 고정, 로컬 경로, URL 등은 모두 필터링해야 함), SlopScan의 판결을 실제 권한 결정으로 변환하는 것입니다.
#!/usr/bin/env python3
"""PreToolUse/Bash 훅: 명령이 실행되기 전 npm/pip/uv 패키지 설치를 SlopScan으로 확인합니다."""
import json, re, shlex, subprocess, sys, urllib.request
...
settings.json에 연결된 모습:
{
"hooks": {
"PreToolUse": [
...
판결(Verdicts)은 실제 결정으로 이어집니다: DANGEROUS이거나 레지스트리(registry)에 아예 존재하지 않는 패키지인 경우 → 거부(deny), 설치가 실행되지 않습니다. SUSPICIOUS인 경우 → 질문(ask), 일반적인 권한 요청이 뜨며 사용자가 결정합니다. 그 외의 모든 경우 — 또는 SlopScan에 접근할 수 없는 경우 —에는 조용히 '실패 시 허용(fail open)'됩니다. 이 마지막 부분이 중요합니다. 네트워크가 불안정할 때마다 워크플로우를 중단시키는 보안 체크는 일주일 안에 비활성화되기 마련입니다. 인프라 문제에는 '실패 시 허용(fail open)'하고, 실제 판결에 대해서는 '실패 시 차단(fail closed)'하십시오.
버그: 셸 리다이렉션(shell redirect)이 가짜 패키지 이름이 되다
여기부터가 이 글을 읽을 가치가 있는 부분입니다. 저는 이것을 만들고, pip install requests를 통해 테스트했으며, 가짜 패키지 이름을 올바르게 허용하고 올바르게 거부하는 것을 확인한 뒤 완료했다고 생각했습니다.
몇 주 후, pip install "qrcode[pil]" cairosvg 2>&1 | tail -10을 실행했습니다. stderr를 stdout으로 리다이렉션하고 tail로 파이프를 연결하는 아주 평범한 셸 관용구(shell idiom)였습니다. 그런데 훅(hook)이 설치 전체를 거부했습니다. 에러 메시지는 다음과 같았습니다: BLOCKED — dangerous/nonexistent package(s): 2. 패키지 이름이 "2"라고요? qrcode도 cairosvg도 이름이 "2"가 아닙니다.
첫 번째 본능은 SlopScan 서비스에 일시적인 장애가 발생했을 것이라는 생각이었습니다. 저는 이전에 이런 종류의 버그를 본 적이 있습니다(레지스트리 타임아웃이 확인된 404 에러와 동일하게 점수가 매겨지는 경우인데, 이는 그 자체로 교훈을 줍니다: "확인할 수 없음"을 "확인했으나 나쁨"과 동일하게 취급하지 마십시오" — 이 둘은 신뢰 수준이 완전히 다르며 서로 다른 처리가 필요합니다). 하지만 qrcode와 cairosvg에 대해 SlopScan에 직접 수동으로 쿼리를 날려본 결과, 두 번 모두 문제가 없는 SAFE 결과가 반환되었습니다. 서비스는 정상적이었습니다. _정확히 동일한 JSON 페이로드(JSON payload)_를 훅 스크립트에 직접 파이핑(piping)했을 때도 잘 작동했습니다. 오직 실제 라이브 Bash-tool 호출만 실패했습니다. 이는 실제 명령어가 제가 수동으로 재현한 것과 무언가 다르다는 것을 의미했습니다.
저는 훅 내부에 임시 디버그 로깅을 추가하고(원시 명령어, 추출된 대상, SlopScan 결과를 임시 파일에 기록), 실제로 다시 실행했습니다. 로그를 확인하자마자 즉시 명확해졌습니다:
TARGETS: [{'ecosystem': 'pypi', 'name': 'qrcode'}, {'ecosystem': 'pypi', 'name': 'cairosvg'}, {'ecosystem': 'pypi', 'name': '2'}]
세 번째 유령 타겟인 "2"가 나타났습니다. 이를 생성한 연쇄 과정은 다음과 같습니다. 제가 만든 세그먼트 분할기(segment-splitter)는 파이프라인(|)을 기준으로 연결된 명령어를 분할하여(파이프라인의 각 명령어를 개별적으로 포착하기 위해) 처리합니다. 따라서 ... 2>&1 | tail -10 명령에서 shlex.split을 거친 후 2>&1이 별도의 토큰으로 남게 되었습니다. shlex는 쉘 리다이렉션 (shell redirection) 구문을 전혀 이해하지 못합니다. shlex는 토크나이저 (tokenizer)이지 쉘 파서 (shell parser)가 아니기 때문에, 2>&1은 단순히 일반 문자열로 통과됩니다. 그 후, 패키지 이름에서 버전 고정 (version pin)을 제거하기 위해 ==, >=, <=, >, <, [를 기준으로 분할하는 저의 버전 제거 정규식 (version-stripping regex)이 2>&1에 포함된 >를 발견했고, 이를 버전 구분자로 기쁘게 취급하여 그 앞에 있는 것만 남겨두었습니다: "2".
이는 일시적인 오류 (flake)가 아닌 완전히 결정론적인 (deterministic) 버그였습니다. 2>&1 | anything을 통해 파이프로 전달되는 모든 설치 명령은 이 버그에 걸리게 되며, 이는 설치 출력을 캡처하려는 사람들에게 매우 흔한 패턴입니다.
해결책은 단 한 줄입니다. 버전 제거 로직에 도달하기 전에 쉘 리다이렉션처럼 보이는 모든 것을 필터링하는 것입니다.
if re.match(r"^&?\d*(>>?|<)", tok):
continue # 쉘 리다이렉션 (예: "2>&1", "2>/dev/null", ">out.log") -- 패키지 이름이 아님
이 패턴은 2>&1, 2>/dev/null, >out.log, 1>&2, &>file 등 선택적인 &, 선택적인 숫자, 그 뒤에 리다이렉션 연산자가 오는 모든 경우를 포착합니다. 숫자로 시작하는 실제 패키지 이름에 대해 오탐 (false-positive)이 발생하지 않는지도 확인했습니다 (2to3-pkg의 경우 2 뒤에 리다이렉션 연산자와 일치하는 것이 없으므로 정상적으로 통과됩니다).
실제 교훈
저는 이 훅(hook)을 깨끗하게 직접 타이핑한 예시들과 몇 가지 적대적 예시(가짜 패키지 이름)를 대상으로 테스트했습니다. 하지만 제가 테스트하지 않았던 것은 실제적인(realistic) 명령 형태, 즉 파이프(pipe)와 리다이렉션(redirect)이 뒤섞여 실제 셸(shell)에서 실제로 실행되는 지저분한 것들이었습니다. shlex.split은 토크나이저(tokenizer)이지 셸(shell)이 아닙니다. 깨끗한 인자 토큰(argument tokens)을 가정하는 그 하위의 모든 요소는 결국 설계되지 않은 리다이렉션, 서브셸(subshell), 또는 히어 도큐먼트(here-doc)를 마주하게 될 것입니다.
만약 여러분이 셸 명령을 파싱하는 훅을 구축하고 있다면(단순히 이것뿐만 아니라, 무엇을 할지 결정하기 전에 tool_input.command를 검사하는 모든 것), 실질적인 교훈은 다음과 같습니다: 단순히 깨끗한 교과서적인 버전이 아니라, 파이프 출력 등을 포함하여 사람들이 실제로 입력하는 명령 형태를 대상으로 테스트하십시오. 그리고 검사가 여러분의 수동 재현 방식과 일치하지 않는 방식으로 실패한다면, 여러분의 수동 재현을 믿지 마십시오. 상대측 서비스에 결함이 있다고 결론을 내리기 전에, 훅이 받는 정확한 JSON을 파이프로 넘기거나 일회성 디버그 로깅(debug logging)을 추가하십시오.
직접 해보기
SlopScan은 GitHub에 공개되어 있으며, Apache 2.0 라이선스를 따르고, 약 두 개의 명령어로 셀프 호스팅(self-host)이 가능합니다. 이미 Claude Code를 사용 중이라면, 위에서 설명한 스킬(skill) + 훅(hook) 조합은 진정한 "한 번 연결해 두면 다시는 신경 쓸 필요 없는" 추가 기능이 될 것입니다. 에이전트가 여러분이 완전히 인지하지 못한 무언가를 설치하라고 제안하는 상황을 한 번이라도 겪어본 적이 있다면, 5분 정도의 시간을 투자할 가치가 있습니다.
— Cor, Skyblue Soft
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기