MMax's Develop Story

Claude Code security-guidance 플러그인: 코드가 PR에 닿기 전에 보안 리뷰를 세션 안으로

Max··29분 읽기
Claude CodeSecurity플러그인Hooks코드 리뷰회고

AI가 코드를 쓰는 속도는 사람이 리뷰하는 속도를 이미 넘었다. 그렇다면 리뷰도 코드가 만들어지는 그 자리에서 시작되어야 한다. security-guidance 플러그인의 명제는 단순하다: 취약점은 PR에서 잡는 것보다 에디터에서 잡는 게 싸다. 그리고 그 검토를, 코드를 쓴 모델이 아닌 다른 눈에게 맡긴다.

Claude Code로 개발하는 비중이 올라가면서 팀에서 반복적으로 나오는 걱정이 하나 있다. "생성되는 코드의 양이 늘어나는 만큼, 보안 리뷰의 병목도 커지는 것 아닌가." 맞는 걱정이다. 사람의 리뷰 용량은 늘지 않는데 diff의 양은 늘어난다. 이 문제를 리뷰어를 늘려서 푸는 건 답이 아니고, 리뷰가 필요한 코드의 양 자체를 상류에서 줄이는 것 이 답이다. security-guidance 플러그인은 정확히 그 지점을 겨냥한다: Claude가 작업하는 동안 자신의 변경 사항을 일반적인 취약점(인젝션, 안전하지 않은 역직렬화, 안전하지 않은 DOM API 등)에 대해 검토받고, 발견된 문제를 같은 세션에서 수정한다.

이 글은 공식 문서를 기반으로 플러그인의 구조를 뜯어본 기록이다. 재미있게도 이 플러그인은 세미나 3주차에서 다룬 Hooks와 5주차에서 다룬 격리된 검토의 원리가 실전에서 어떻게 조립되는지 보여주는, 지금까지 본 것 중 가장 좋은 레퍼런스 구현이기도 하다.

1. 무엇인가: 세션 안에서 도는 보안 검토 동반자

플러그인의 위상부터 정확히 하자. Claude Code의 보안 도구는 하나가 아니라 스택이고, 이 플러그인은 그 스택의 가장 이른 지점 을 맡는다.

이 플러그인은 PR에 도달하는 것을 줄인다. Code Review는 도달한 것을 포착한다.

설치하면 자동으로 실행된다. 호출할 명령도, 기억할 것도 없다. 이 "존재감 없음"이 설계의 핵심인데, 보안 도구는 개발자가 의식적으로 호출해야 하는 순간부터 지켜지지 않기 때문이다. 훅(Hook) 기반으로 Claude의 라이프사이클에 붙어서, 코드가 만들어지는 흐름 안에 검토를 심는다.

필수 조건은 세 가지다: Claude Code CLI 2.1.144 이상, PATH의 Python 3.8 이상(python3pythonpy -3 순서로 탐색), 그리고 git 저장소. 턴 끝·커밋 검토는 git 상태에 대해 diff하므로 저장소 밖에서는 자동으로 건너뛰고, 편집당 패턴 확인만 어디서나 동작한다. 첫 실행 시 ~/.claude/security/ 아래에 가상 환경을 만들고 Claude Agent SDK를 설치하는데, 이 설치가 실패하면 커밋 검토가 에이전트 검토 대신 단일 샷 검토로 폴백된다. 실패 시 기능을 끄는 게 아니라 얕은 모드로 강등한다: 폴백 설계의 교과서적 처리다.

2. 세 지점, 세 깊이: 검토 계층의 해부

플러그인은 Claude의 작업을 세 지점에서 검토하고, 각 지점의 검토 깊이가 다르다. 이 구조가 이 플러그인에서 가장 배울 게 많은 부분이다.

security-guidance의 세 가지 검토 지점

① 각 파일 편집 시: 결정론적 패턴 매칭. Claude가 파일에 쓸 때마다 새 콘텐츠를 알려진 위험 패턴과 대조한다. 모델 호출이 없는 순수 문자열 매칭이라 추가 비용이 0 이다. 기본 패턴의 범주를 보면 노리는 지점이 명확하다.

  • 동적 코드 실행: eval(, new Function, os.system, child_process.exec
  • 안전하지 않은 역직렬화: pickle
  • DOM 주입: dangerouslySetInnerHTML, .innerHTML =, document.write
  • 워크플로우 파일: .github/workflows/ 아래의 편집(저장소 수준 권한을 부여할 수 있는 파일)

확인은 편집이 적용된 에 실행되고, 경고가 Claude의 다음 스텝 컨텍스트에 주입된다. 경고는 세션당·파일당·패턴당 한 번만 발생해서 반복 매칭이 대화를 범람시키지 않는다. 이런 디테일에서 실전 운영을 해본 티가 난다. 알림 시스템에서 중복 억제(deduplication) 없는 경고는 곧 무시되는 경고라는 걸 우리는 안다.

② 각 턴의 끝에서: diff 전체의 모델 리뷰. 턴이 끝날 때마다 그 턴 동안 작업 트리에서 변경된 모든 것(편집 도구·Bash 명령·서브에이전트의 변경분 포함)의 git diff를 계산해, 보안에 집중된 별도의 Claude 검토 로 보낸다. 백그라운드에서 돌기 때문에 Claude의 응답이 지연되지 않는다. 이 계층이 잡는 것은 문자열 매칭이 원리적으로 잡을 수 없는 것들이다: 인증 우회, 안전하지 않은 직접 객체 참조(IDOR), 인젝션, 서버 측 요청 위조(SSRF), 약한 암호화. 검토에서 문제가 나오면 Claude가 발견 사항으로 다시 프롬프트되어 후속 조치로 해결한다. 상한도 명시되어 있다: 턴당 최대 30개 파일, 연속 최대 3회 후에는 사용자에게 양보.

③ 커밋·푸시 시: 주변 코드를 읽는 에이전트 리뷰. Claude가 Bash 도구로 git commit이나 git push를 실행하면, 백그라운드에서 더 깊은 에이전트 검토가 돈다. 이 검토는 diff만 보는 게 아니라 호출자, 새니타이저(Sanitizer), 관련 파일까지 직접 읽고 발견이 실제인지 판단한 뒤에 보고한다. 격리해서 보면 위험해 보이지만 코드베이스 맥락에서는 안전한 패턴, 즉 거짓 양성(False Positive)을 낮추기 위한 설계다. 이 계층은 Claude가 Bash로 수행한 커밋·푸시에만 발동하고, 내가 내 셸에서 직접 실행한 커밋(세션 내 ! 이스케이프 포함)은 검토되지 않는다. 롤링 시간당 20회로 제한되며, 턴 끝 검토와 중복된 발견이면 다시 프롬프트하지 않아 깨끗한 커밋은 아무 출력도 만들지 않는다.

세 계층을 관통하는 트레이드오프 축이 보이는가. 왼쪽으로 갈수록 빠르고 싸고 얕으며, 오른쪽으로 갈수록 느리고 비싸고 깊다. 방화벽 룰(패턴 매칭) → IDS(diff 리뷰) → 포렌식 분석(에이전트 리뷰)의 배치와 같은 그림이다. 어떤 단일 계층도 완전하지 않지만, 깊이가 다른 계층들이 서로의 빈틈을 메운다.

3. 검토 독립성: 쓴 손에게 채점을 맡기지 않는다

이 플러그인에서 가장 마음에 드는 설계 결정이다.

플러그인은 코드를 작성한 동일한 Claude 인스턴스에 자신을 평가하도록 요청하지 않는다. 턴 끝·커밋 검토는 새로운 컨텍스트와 보안 중심 프롬프트를 사용하는 별도의 Claude 호출 로 실행된다. 검토자는 diff에서 시작하고, 원래 접근 방식에 투자한 바가 없으며, 문제를 찾도록만 지시받는다.

"자기가 쓴 코드는 자기가 리뷰하지 않는다"는 4-eyes 원칙이 LLM에도 그대로 적용된 것이다. 같은 컨텍스트 안에서 "방금 쓴 코드 검토해줘"라고 하면, 모델은 자신의 구현 결정을 정당화해 온 컨텍스트를 그대로 물려받는다. 확증 편향은 사람만의 문제가 아니다. 별도 호출·새 컨텍스트·"문제를 찾아라"라는 단일 목적. 이 세 가지가 검토의 독립성을 만든다.

동시에 한계도 문서가 직접 못 박는다. 어떤 계층도 쓰기나 커밋을 차단하지 않는다. 발견 사항은 쓰기 Claude에게 지시로 전달되고 대화 안에서 해결되며, 검토 모델은 문제를 놓칠 수 있다. 플러그인은 완전한 보안 솔루션이 아니라 심층 방어(Defense in Depth)의 한 계층이다. 이 솔직함이 오히려 신뢰를 준다. "우리가 다 잡아줍니다"라고 말하는 보안 제품보다, 자기가 못 잡는 것을 명시하는 제품이 실무에서 쓸 만하다.

4. 설치와 활성화: 개인 머신부터 조직 전체까지

설치는 공식 Anthropic 마켓플레이스에서 두 줄이다.

/plugin install security-guidance@claude-plugins-official
/reload-plugins

설치 시 범위를 묻는데, 사용자 범위를 선택하면 이 머신의 모든 새 세션에 로드된다. 마켓플레이스를 못 찾는다고 하면 /plugin marketplace add anthropics/claude-plugins-official을 먼저 실행한다.

주의할 지점이 하나 있다. 사용자 범위 플러그인은 웹의 Claude Code(클라우드 세션)로 전달되지 않는다. 세션이 내 머신이 아니라 Anthropic 인프라에서 돌기 때문이다. 클라우드 세션에서 켜거나, 저장소를 클론하는 팀원 전체에게 적용하려면 체크인된 프로젝트 설정에 선언한다.

// .claude/settings.json
{
  "enabledPlugins": {
    "security-guidance@claude-plugins-official": true
  }
}

조직 전체 적용은 관리자가 관리 설정의 enabledPlugins로 강제할 수 있다. 개인 실험 → 프로젝트 체크인 → 조직 강제의 3단 승격 경로가 마련되어 있는 셈이고, 보안 도구의 도입 순서로 이 경로는 정석이다. 팀에 물리려면 개인 설치가 아니라 .claude/settings.json 체크인이 정답이다. 개인 설치는 "내 머신에서만 지켜지는 규칙"이고, 그건 팀 관점에서 규칙이 아니다.

5. 나만의 규칙 추가: 위협 모델을 코드베이스에 태운다

기본 체크는 범용 취약점이다. 하지만 실제 사고는 대부분 도메인 규칙 위반에서 난다: "이 로그에는 이 필드를 찍으면 안 된다", "이 경로는 이 권한 체크를 반드시 거쳐야 한다" 같은 것들. 플러그인은 두 가지 확장 지점을 제공한다.

모델 검토용 지침: .claude/claude-security-guidance.md. 자연어로 위협 모델과 리뷰 체크리스트를 쓰면, 턴 끝·커밋 검토가 기본 체크리스트에 추가 컨텍스트로 얹어 읽는다.

# 이 저장소에 대한 보안 지침
 
- INFO 수준 이상에서 `customer_id` 또는 `account_number`를 로깅하지 마십시오.
- `/admin` 아래의 모든 경로는 데이터베이스 읽기 전에 `require_role("admin")`을 호출해야 합니다.
- `===` 대신 토큰 비교를 위해 `crypto.timingSafeEqual`을 사용합니다.

편집당 패턴: .claude/security-patterns.yaml. 정규식 또는 부분 문자열 규칙을 추가하면 기본 패턴과 함께 결정론적으로 매칭된다.

patterns:
  - rule_name: internal_api_key
    substrings: ["sk_live_", "AKIA"]
    reminder: "하드코딩된 API 키 접두사. 비밀 관리자에서 자격 증명을 로드합니다."
  - rule_name: tenant_unfiltered_query
    regex: "\\.objects\\.all\\(\\)"
    paths: ["**/src/tenants/**"]
    reminder: "다중 테넌트 코드는 org_id로 필터링해야 합니다."

paths/exclude_paths glob은 전체 파일 경로와 매칭되므로 프로젝트 상대 패턴 앞에 **/를 붙여야 한다는 것, reminder는 1KB 제한, 사용자 정의 규칙은 최대 50개, 그리고 재앙적 백트래킹(Catastrophic Backtracking)이 의심되는 정규식은 플러그인이 알아서 건너뛴다 는 디테일까지 문서화되어 있다. YAML 파싱은 PyYAML이 필요하고 플러그인이 대신 설치해 주지 않으므로, 의존성을 최소화하려면 같은 스키마의 security-patterns.json을 쓰면 된다.

규칙 파일의 조회 위치는 세 곳이고, 존재하는 것을 모두 로드해 연결한다(지침 파일 합산 8KB 상한).

범위경로용도
사용자~/.claude/claude-security-guidance.md머신의 모든 프로젝트
프로젝트.claude/claude-security-guidance.md저장소에 체크인, 팀 공유
프로젝트 로컬.claude/claude-security-guidance.local.mdgitignore 대상, 개인 재정의

그리고 확장 설계에서 가장 중요한 한 줄이 이것이다.

두 확장 지점 모두 추가적(additive) 이다. 확인을 추가할 수는 있지만, 이 파일들로 기본 제공 확인을 비활성화할 수는 없다. 특정 취약점 클래스를 무시하라는 규칙은 해당 발견을 억제하지 않는다.

지침 파일이 검토를 약화시키는 통로가 될 수 없다는 뜻이다. 저장소에 악의적이거나 부주의한 지침이 들어와도 기본 검토는 그대로 돈다. 확장 지점을 열어주되 무력화 지점은 닫는다: 플러그인 아키텍처를 설계해 본 사람이라면 이 경계선을 긋는 일이 얼마나 어려운지 알 것이다.

단, 문서가 분명히 하듯 이 지침은 검토자를 위한 힌트지 결정론적 보호 장치가 아니다. 위반이 무조건 잡힌다는 보장은 없다. 강한 강제가 필요한 규칙이라면 편집을 차단하는 훅이나 CI 확인과 쌍으로 구성하라 는 것이 공식 권고다.

6. 비용과 제어: 계층별 스위치

비용 구조는 계층과 정확히 대응한다. 편집당 패턴 확인은 모델 호출이 없으니 0원이다. 턴 끝·커밋 검토는 별도 Claude 요청으로 사용량에 합산된다. 대략 파일을 변경하는 턴당 검토 호출 1회, 커밋당 더 깊은 검토 1회를 예상하면 되고, 커밋 검토는 에이전트라서 커밋당 여러 모델 턴을 쓸 수 있다(시간당 20회 상한). 두 모델 검토의 기본 모델은 Claude Opus 4.7이고, SECURITY_REVIEW_MODEL(턴 끝)과 SG_AGENTIC_MODEL(커밋)로 각각 바꿀 수 있다.

제어도 계층 단위다. 기본 체크를 개별적으로 뺄 수는 없지만, 계층은 독립적으로 끌 수 있다.

환경 변수효과
ENABLE_PATTERN_RULES=0편집당 패턴 확인 끄기
ENABLE_STOP_REVIEW=0턴 끝 diff 검토 끄기
ENABLE_COMMIT_REVIEW=0커밋·푸시 검토 끄기
ENABLE_CODE_SECURITY_REVIEW=0모든 모델 검토 한 번에 끄기
SECURITY_GUIDANCE_DISABLE=1제거 없이 플러그인 전체 비활성화

팀 운영 관점에서 눈여겨볼 디테일: 프로젝트 .claude/settings.json으로 활성화된 플러그인을 개인이 /plugin에서 비활성화하면, 체크인된 파일을 건드리지 않고 .claude/settings.local.json에 재정의를 기록 한다. 나에게만 꺼지고 팀원은 영향받지 않는다. 관리 설정으로 강제된 경우에는 관리자만 끌 수 있다. 공유 설정과 개인 재정의의 계층화가 끝까지 일관되어 있다.

문제가 생기면 ~/.claude/security/log.txt부터 본다. 검토가 소리 없이 건너뛰어지는 흔한 이유 세 가지도 문서에 있다: git 저장소가 아님(턴 끝·커밋 검토 불가), 세션에 Anthropic 인증이 없음(패턴 확인만 동작), PyYAML을 가져올 수 없음(YAML 파일 무시, JSON 사용 권장).

7. 구현을 들여다보면: Hooks의 레퍼런스 구현

플러그인은 전적으로 훅 위에 만들어져 있다. 등록된 훅 다섯 개를 보면 전체 그림이 한눈에 들어온다.

훅 이벤트목적
SessionStart플러그인의 Python 환경 부트스트랩
UserPromptSubmit턴 끝 검토가 diff할 작업 트리 기준선 캡처
PostToolUse (Edit·Write·NotebookEdit)편집당 패턴 매칭
Stop턴 끝 diff 검토, 백그라운드 실행
PostToolUse (Bash, git commit·git push 필터)커밋·푸시 검토, 백그라운드 실행

세미나 3주차에서 훅 라이프사이클을 다뤘을 때, "이 이벤트 지점들로 뭘 만들 수 있는가"라는 질문에 대한 가장 좋은 답이 바로 이 플러그인이다. UserPromptSubmit에서 기준선을 찍고 Stop에서 diff를 뜨는 조합, PostToolUse를 도구별로 다르게 거는 방식, 훅에서 별도 모델 호출을 실행해 결과를 세션으로 되먹이는 패턴까지. 플러그인 소스가 공개되어 있으니, 자기만의 훅 기반 자동화를 만들 계획이 있다면 이 소스를 정독하는 것이 문서 열 편보다 낫다.

8. 심층 방어 스택에서의 위치: 대체가 아니라 감량

마지막으로 이 플러그인이 무엇이 아닌지 를 분명히 하자.

심층 방어 스택에서 security-guidance의 위치

단계도구잡는 것
세션 내security-guidance 플러그인Claude가 쓴 코드의 일반 취약점, 같은 세션에서 수정
온디맨드/security-review현재 브랜치에 대한 일회성 보안 패스
풀 요청 시Code Review (Team·Enterprise)전체 코드베이스 컨텍스트의 다중 에이전트 검토
CI기존 SAST·의존성 스캐너언어별 규칙, 공급망 확인, 정책 적용

플러그인은 CI의 정적 분석을 대체하지 않고, 공급망 검사를 시도하지도 않는다. 각 이후 단계는 이전 단계가 놓친 것을 잡는다. 플러그인의 가치는 다운스트림에 도달하는 양을 줄이는 것이지, 다운스트림의 필요성을 제거하는 것이 아니다. 이 문장을 팀에 도입할 때의 커뮤니케이션에 그대로 쓰면 된다. "이거 깔면 보안 리뷰 안 해도 되나요?"라는 질문은 반드시 나오고, 답은 항상 "아니요, 리뷰가 한가해질 뿐입니다"여야 한다.

9. 시니어의 시선으로 정리하는 인사이트

① Shift-Left의 극한은 "코드가 태어나는 자리"다

버그 수정 비용은 발견 시점이 늦을수록 지수적으로 커진다는 오래된 곡선이 있다. 이 플러그인은 그 곡선의 가장 왼쪽 끝, 코드가 아직 에디터 안에 있고 작성 컨텍스트가 살아 있는 시점에 검토를 심는다. 발견과 수정 사이의 거리가 0이다: 수정할 사람(Claude)이 방금 그 코드를 쓴 맥락을 그대로 들고 있기 때문이다. PR 리뷰에서 "이거 왜 이렇게 하셨어요?"라고 묻고 답을 기다리는 왕복이 통째로 사라진다.

② 작성자와 검토자의 분리는 LLM에도 유효하다

새 컨텍스트, 다른 프롬프트, "문제를 찾아라"는 단일 목적. 사람 조직의 4-eyes 원칙이 모델 호출 구조로 번역된 것이다. 5주차에서 sub-agent의 격리 컨텍스트를 "외주 전문가"에 비유했는데, 이 플러그인의 검토자는 정확히 그 외주 전문가다. 자기 확신에 오염되지 않은 눈은, 사람이든 모델이든, 별도의 컨텍스트에서만 나온다.

③ 결정론적 계층과 확률적 계층은 서로를 대체하지 못한다

패턴 매칭은 빠르고 공짜고 확실하지만 맥락을 모르고, 모델 리뷰는 맥락을 알지만 느리고 비싸고 확률적이다. 이 플러그인은 하나를 고르지 않고 둘을 쌓았다. 방화벽 룰과 이상 탐지(Anomaly Detection)를 함께 쓰는 이유, 스키마 검증과 비즈니스 검증을 나누는 이유와 같은 논리다. "AI 리뷰가 있으니 정적 규칙은 필요 없다"는 주장과 "정적 규칙이 있으니 AI 리뷰는 사치다"라는 주장은 같은 실수의 양면이다.

④ 차단하지 않는 가드레일에는 분명한 철학이 있다

어떤 계층도 쓰기·커밋을 막지 않는다는 결정은 약점처럼 보이지만, 개발 흐름을 멈추는 보안 도구가 어떤 운명을 맞는지 생각하면 이해된다: 개발자들이 우회로를 찾고, 결국 꺼진다. 플러그인은 가드레일(지시와 수정)을 맡고, 게이트(차단)는 훅과 CI에 위임한다. 어디를 부드럽게 막고 어디를 단단하게 막을지의 분리, 이것이 보안 도구가 조직에 뿌리내리는 조건이다.

⑤ 확장은 열고 무력화는 닫는 경계선이 신뢰를 만든다

사용자 규칙은 추가만 가능하고, "이 취약점 클래스는 무시하라"는 지침은 먹히지 않는다. 지침 파일이 저장소에 체크인되는 파일이라는 점을 생각하면 이 결정의 무게가 보인다. 악의적 PR 하나가 팀 전체의 보안 검토를 무력화할 수 있는 통로를 원천적으로 막은 것이다. 확장 지점을 설계할 때 "무엇을 할 수 있게 할까"만큼 "무엇을 할 수 없게 유지할까" 를 고민해야 한다는 것: 플러그인 아키텍처의 오래된 교훈이다.

마무리: 리뷰어를 늘리지 말고 상류를 정리하라

한 문장으로 줄이면 이렇다. security-guidance는 보안 리뷰를 대체하는 도구가 아니라, 보안 리뷰어에게 도달하는 잡음을 상류에서 걸러내는 필터다. 설치 두 줄로 시작해서, 팀의 위협 모델을 지침 파일로 얹고, 프로젝트 설정에 체크인해서 팀 전체의 기본값으로 만드는 순간부터 가치가 복리로 쌓인다.

다음 액션은 정해뒀다. 첫째, 우리 서비스 저장소에 .claude/claude-security-guidance.md를 만들어 로깅 금지 필드와 권한 게이트 규칙부터 문서화한다. 둘째, 하드코딩된 자격 증명 접두사와 멀티테넌트 쿼리 규칙을 security-patterns.json으로 박는다. 셋째, .claude/settings.json에 플러그인을 체크인해서 "내 머신의 규칙"이 아니라 "저장소의 규칙"으로 만든다. 위협 모델을 문서로 쓰는 일은 늘 미뤄지는 숙제였는데, 이제 그 문서를 읽어주는 독자가 세션마다 생겼다. 미룰 이유가 하나 줄었다.