규칙을 기억에서 코드로 — Claude Code Mods로 블로그 규칙을 강제할 수 있을까#
2026-10-11

지켜지지 않는 규칙#
이 블로그에는 글을 쓸 때 지켜야 하는 규칙이 꽤 많습니다. 1인칭은 “저는"이 아니라 “필자는"으로 쓴다, 외부 링크는 새 창으로 열리는 HTML 태그로 쓴다, 방문자 수 같은 실측 수치는 공개 글에 넣지 않는다, 한국어 글을 고치면 영어 번역본도 같은 경로에서 고친다, 에이전트 스킬을 수정하면 Claude Code용과 Cursor용 두 곳을 함께 갱신한다. 이런 규칙은 전부 Markdown 문서에 적혀 있고, Claude Code는 세션을 시작할 때 그 문서를 읽습니다.
그런데 이 규칙들은 자주 어겨집니다. 어기는 쪽은 사람이 아니라 에이전트입니다. 긴 세션에서 컨텍스트가 압축되고 나면 “저는"이 다시 나타나고, 한국어 글만 고치고 영어 글은 그대로 두는 일이 생깁니다. 규칙을 더 굵은 글씨로 쓰거나 문서 맨 위로 올려도 근본적으로 달라지지 않습니다. 규칙이 모델의 기억에 들어 있는 한, 지켜지는 정도는 확률의 문제이기 때문입니다.
지난 AI 뉴스 브리프에서 Claude Code Mods를 다루면서 필자는 관심 포인트에 이렇게 적었습니다. “지금 문서 규칙으로 적어 둔 금지 사항 일부를 Mod로 옮겨 강제할 수 있을지 시험해 볼 생각입니다.” 이 글은 그 시험을 하기 전에 먼저 따져 본 기록입니다. Mods가 정확히 무엇인지, Mods가 없던 때에는 같은 요구를 어떻게 처리했을지, 그리고 Mods로 바꾸면 무엇이 실제로 달라지는지를 같은 가드 하나를 놓고 비교했습니다.
결론부터 적으면 이렇습니다. 문서 규칙에서 설정 hook으로 가는 것은 “기억에서 강제로"의 전환이고, 설정 hook에서 Mod로 가는 것은 “스크립트에서 상태를 가진 프로그램으로"의 전환입니다. 단순한 금지 규칙 하나만 놓고 보면 두 번째 전환의 이득은 작습니다. 이득이 커지는 것은 규칙이 여러 도구 호출에 걸쳐 있거나, 사람의 판단을 중간에 끼워야 할 때입니다.
Mods는 무엇인가#
아래 내용은 2026년 10월 기준 Anthropic의 발표 글과 Claude Code 공식 문서를 바탕으로 정리했습니다.
Mod는 Claude Code 프로세스 안에서 실행되는 JavaScript 또는 TypeScript 이벤트 핸들러가 담긴 플러그인입니다. Claude Code는 도구를 호출하거나, 프롬프트를 받거나, 화면의 한 부분을 그리는 것처럼 무언가를 하려는 순간마다 이벤트를 냅니다. Mod는 그 이벤트에 끼어들어 지켜보거나, 고쳐 쓰거나, 아예 대신 처리합니다.
Claude Code에는 이미 hook이라는 장치가 있었습니다. 설정 파일에 셸 명령이나 HTTP 요청을 등록해 두면 특정 이벤트에서 실행되는 방식입니다. 공식 문서는 이 둘을 구분하기 위해 기존 것을 설정 hook(settings hook) 이라 부르고, Mod의 핸들러를 그냥 hook이라고 부릅니다. 차이는 실행 위치입니다. 설정 hook은 Claude Code 바깥에서 별도 프로세스로 돌고, Mod는 안에서 함수로 돕니다. 그래서 Mod만 할 수 있는 일이 생깁니다. 화면에 창(pane)이나 버튼을 그리고, Claude Code가 그리는 스피너나 도구 결과 행을 바꾸고, 도구 호출을 붙잡아 둔 채 사용자에게 질문하고, 슬래시 명령을 Claude의 턴(프롬프트 하나에 대해 Claude가 응답을 마칠 때까지의 작업 단위) 없이 즉시 실행하는 일입니다.
작은 Mod는 파일 세 개로 이루어집니다. 플러그인 매니페스트 plugin.json, 코드 파일의 위치를 가리키는 hooks.json, 그리고 실제 코드인 register.js입니다. 코드의 뼈대는 on(이벤트, 매처, 핸들러) 한 가지입니다. 매처는 그 이벤트 중 어떤 것만 받을지 거르는 조건이고, 핸들러는 늘 세 인자를 받습니다.
$는 Mods API입니다. 파일을 읽고, 프로세스를 띄우고, 네트워크에 접근하고, 화면을 그리고, 모델을 부르는 모든 바깥일은 이 객체를 거칩니다. Mod는 DOM도 Node도 없는 격리된 실행 환경에서 돌기 때문에 다른 길이 없습니다.e는 이벤트의 입력값입니다. 깊이 동결돼 있어서 직접 고치면 예외가 납니다.next(e)는 다음 Mod의 핸들러를 부르고, 체인 끝에서는 Claude Code의 기본 동작을 실행해 그 결과를 돌려줍니다.
next를 어떻게 쓰느냐가 곧 핸들러의 성격입니다. 작업 후 next(e)를 그대로 돌려주면 관찰, next({ ...e, text: 바뀐값 })처럼 복사본을 넘기면 고쳐 쓰기, next를 부르지 않고 { deny: '사유' }를 돌려주면 대신 응답입니다. await next(e)로 결과를 받은 뒤 다시 시도하거나 결과를 숨기는 사후 처리도 가능합니다.
여러 Mod가 같은 이벤트를 받으면 먼저 로드된 Mod가 가장 바깥에 놓입니다. 이벤트를 가장 먼저 보고 결과를 가장 나중에 봅니다. 순서는 조직이 관리하는 Mod, 사용자가 설치한 Mod, 조직이 뒤에 붙인 Mod, 그 밖의 내장 Mod 순입니다. Team / Enterprise 플랜으로 로그인했거나 기기에 관리형 설정이 있으면 sec-default라는 내장 가드 Mod가 맨 앞에 서서, 사용자의 Mod가 조직의 거부 규칙을 뒤집거나 시스템 프롬프트를 고치지 못하게 막습니다.
한 가지는 미리 알아 둬야 합니다. Mod는 샌드박스가 없습니다. 설치한 사용자의 권한으로 파일, 프로세스, 네트워크, 환경 변수에 접근합니다. Bash 샌드박스를 켜도 Mod가 띄운 프로세스는 그 바깥에서 돕니다. 공식 문서는 설치 전에 claude plugin validate 명령으로 그 Mod가 어떤 이벤트를 받고 어떤 API를 부르는지 실행 없이 확인하라고 권합니다.
이미 Claude Code의 일부 기능은 Mod로 다시 만들어졌습니다. /diff 명령, AGENTS.md 읽기, 사용량 분석 전송이 그렇습니다. Anthropic은 샘플 Mod도 공개했는데, 컨텍스트 창이 얼마나 찼는지 프롬프트 위에 일기 예보처럼 보여 주는 token-weather, rm -rf 같은 위험한 명령을 붙잡고 무엇이 바뀔지 보여 준 뒤 진행 여부를 묻는 blast-radius, 지난 턴에서 Claude가 고친 파일을 /replay 명령으로 한 단계씩 되짚는 replay-theater가 있습니다.
같은 가드를 세 가지 방식으로#
이제 처음의 문제로 돌아옵니다. “한국어 글에 ‘저는’이 들어가면 막는다"는 가드 하나를 세 가지 방식으로 만들어 보겠습니다.
문서 규칙: 기억에 맡기기#
지금 이 블로그가 있는 단계입니다. 규칙 문서에 한 줄을 적어 두고 모델이 지키기를 기대합니다. 통제 지점이 없으니 가장 쉽고, 그래서 가장 자주 어겨집니다. 규칙이 지켜지는 정도는 모델의 성능, 컨텍스트의 길이, 그 규칙이 압축 요약에서 살아남았는지에 달려 있습니다.
설정 hook: 바깥에서 막기#
Mods 이전에도 강제할 방법은 있었습니다. PreToolUse 설정 hook입니다. 설정 파일에 Edit와 Write 도구를 대상으로 셸 스크립트를 등록하면, Claude Code는 도구를 실행하기 직전에 그 스크립트를 띄우고 표준 입력으로 도구 인자를 JSON으로 넘깁니다. 스크립트가 종료 코드 2로 끝나거나 거부 결정을 담은 JSON을 출력하면 도구는 실행되지 않고, Claude는 표준 오류에 적힌 사유를 도구 결과로 읽습니다.
{ "hooks": { "PreToolUse": [{ "matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/guard.sh" }] }] } }# .claude/hooks/guard.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path')
TEXT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content')
case "$FILE" in
*/content.ko/*)
if echo "$TEXT" | grep -q '저는'; then
echo '"저는" 대신 "필자"를 쓰세요' >&2
exit 2
fi ;;
esac
exit 0이것만으로도 첫 번째 전환은 끝납니다. 규칙이 모델의 기억에서 실행 경로 위의 검문소로 옮겨 왔습니다. 모델이 규칙을 잊어도 파일은 바뀌지 않습니다.
Mod: 안에서 막기#
같은 가드를 Mod로 쓰면 이렇습니다.
on('tool.call', { tool: ['Edit', 'Write'] }, ($, e, next) => {
const text = e.tool === 'Edit' ? e.new_string : e.content
if (/content\.ko\//.test(e.file_path) && /저는/.test(text)) {
return { deny: '"저는" 대신 "필자"를 쓰세요. 수정해서 다시 시도하세요.' }
}
return next(e)
})솔직히 말하면, 이 수준에서는 설정 hook과 거의 같습니다. 둘 다 Edit 직전에 막고, 둘 다 Claude에게 사유를 돌려주고, 둘 다 다음 시도에서 Claude가 고쳐 씁니다. jq로 JSON을 파싱하는 대신 e.new_string처럼 타입이 있는 필드를 바로 읽는다는 편의 차이가 있을 뿐입니다. 정규식 하나짜리 금지 규칙이라면 설정 hook이 오히려 가볍습니다.
차이가 벌어지는 지점#
차이는 가드가 조금만 복잡해지면 바로 나타납니다. 공식 문서를 기준으로 정리하면 다음과 같습니다.
| 관점 | 설정 hook | Mod |
|---|---|---|
| 실행 | 호출마다 셸 프로세스 생성 | 같은 프로세스 안의 함수 |
| 개입 범위 | 실행 전 거부, 인자 교체 | 실행 전후 양쪽, 재시도, 결과 직접 반환, 결과 숨기기 |
| 호출 사이의 상태 | 없음. 파일에 써야 함 | 모듈 변수, 세션 상태, 세션 간 저장소 |
| 사용자에게 묻기 | 권한 프롬프트로 승격만 가능 | 자체 질문을 띄우고 호출을 붙잡아 둠 |
| 권한 결정 변경 | 불가 | tool.check로 허용 / 질문 / 거부를 상황에 따라 변경 |
| 테스트 | 가짜 JSON을 직접 넣어 실행 | claude plugin test로 세션 없이 이벤트 발사 |
| 배포와 점검 | 설정 파일과 스크립트를 따로 복사 | 플러그인 하나로 묶임. validate로 사전 점검 |
이 표에서 필자가 가장 중요하게 본 줄은 호출 사이의 상태입니다. 이 블로그의 규칙 중 가장 자주 어겨지는 것은 “저는” 같은 한 줄짜리 금지가 아니라, “한국어 글을 고쳤으면 영어 글도 고친다”, “Claude Code용 스킬을 고쳤으면 Cursor용도 고친다” 같은 짝 규칙입니다. 이런 규칙은 도구 호출 한 번을 보고는 판단할 수 없습니다. 한 턴 동안 어떤 파일이 바뀌었는지 기억해 두었다가 턴이 끝날 때 짝이 빠졌는지 확인해야 합니다.
설정 hook으로 이것을 하려면 PreToolUse 스크립트가 임시 파일에 경로를 적고, 턴이 끝날 때 도는 Stop 스크립트가 그 파일을 읽어 비교해야 합니다. 두 스크립트를 파일 하나로 잇는 셈입니다. Mod에서는 tool.call 핸들러가 모듈 변수에 경로를 넣고 turn.complete 핸들러가 그 변수를 읽으면 끝입니다. 두 핸들러가 같은 파일 안에 있으니 설계상 자연스럽습니다.
두 번째로 중요한 줄은 사용자에게 묻기입니다. 실측 수치 노출 금지 규칙을 생각해 보겠습니다. 글에 숫자가 들어간다고 다 실측치는 아닙니다. 정규식으로는 “이 숫자가 방문자 통계에서 나온 값인가"를 가릴 수 없습니다. Mod는 의심스러운 경우에 $.ui.ask로 호출을 붙잡아 두고 사람에게 묻습니다. 설정 hook은 권한 프롬프트로 올리는 것까지만 가능하고, 질문의 선택지를 직접 설계할 수는 없습니다.
세 번째는 테스트입니다. 규칙이 열 개쯤 되면 셸 스크립트는 읽기 어려워지고, 규칙 하나를 고쳤을 때 다른 규칙이 깨졌는지 확인할 방법이 마땅치 않습니다. Mod는 플러그인 안에 테스트 파일을 두고 세션 없이 이벤트를 발사해 검증할 수 있습니다.
반대로 Mod가 특별히 더 안전하지 않은 지점도 있습니다. 실패 시 기본 동작은 양쪽 모두 통과(fail-open)입니다. 설정 hook은 시간 초과 시 도구 호출이 그대로 진행되고, Mod는 핸들러가 예외를 던지거나 시간을 넘기면 건너뜁니다. 설정 hook은 최근 버전부터 onFailure: "block" 옵션으로, Mod는 .catch 핸들러로 실패 시 차단(fail-closed)으로 바꿀 수 있습니다. 어느 쪽이든 이 설정을 명시적으로 걸어야 비로소 “강제"라고 부를 수 있습니다.
화면이 없는 곳에서의 Mod#
조사하다 보니 필자의 작업 환경에 직접 걸리는 사실이 하나 있었습니다. 필자는 Cursor IDE 안의 Claude Code 확장으로 작업하는데, 공식 문서의 실행 환경 표에 따르면 VS Code 계열 확장의 채팅 패널에서는 Mod의 핸들러는 실행되지만 화면에는 아무것도 그려지지 않습니다. Cursor는 같은 확장을 쓰므로 똑같이 해당됩니다. 터미널과 데스크톱 앱의 Code 탭에서만 창과 버튼이 보입니다.
처음에는 “그러면 Mod를 쓸 의미가 없는 것 아닌가” 싶었습니다. 그런데 따져 보니 그렇지 않았습니다. 앞에서 비교한 가드들은 하나도 화면이 필요 없습니다. 거부 사유는 화면이 아니라 Claude가 읽는 도구 결과로 전달되기 때문입니다. 짝 검사의 경고도 턴이 끝날 때 대화에 한 줄 덧붙이는 방식으로 전달할 수 있습니다. 화면이 꼭 필요한 것은 토큰 모니터 같은 창이나 사람에게 묻는 질문뿐이고, 그것이 필요하면 Cursor의 통합 터미널에서 claude를 직접 실행하면 됩니다. 공식 문서는 편집기의 통합 터미널을 화면까지 모두 지원하는 환경으로 분류합니다.
그렇다면 확장에서 화면이 안 보이는 이유는 Mods의 설계 제한일까요, 아니면 확장이 아직 대응하지 않은 것일까요. 이 부분은 공식 문서에 설명이 없어서 필자가 Claude Code가 생성하는 TypeScript 타입 선언 파일을 직접 읽어 추정했습니다. 타입 선언에는 Mod가 그릴 수 있는 화면 종류로 terminal, desktop, mobile, vscode 네 가지가 이미 정의돼 있고, vscode는 “Claude Code for VS Code"라고 명시돼 있습니다. 구조를 보면 터미널만 엔진이 직접 그리고, 나머지 셋은 엔진에 그릴 내용을 요청해서 받은 트리를 각자 그리는 방식입니다. vscode가 쓸 수 있는 요소 목록도 따로 있습니다. 엔진과 프로토콜은 준비돼 있고 확장의 화면 쪽 구현이 아직 따라오지 않은 것으로 읽힙니다. 다만 이것은 코드에서 읽어 낸 추정이지 Anthropic이 밝힌 일정이 아닙니다. Cursor는 확장 마켓플레이스가 따로 있어 VS Code보다 늦게 들어올 가능성도 있습니다.
이 블로그에 적용해 볼 Mod#
조사한 내용을 바탕으로 필자가 실제로 만들어 볼 생각인 것들입니다. 앞의 다섯 개는 화면 없이 지금의 확장 환경에서도 바로 효과를 볼 수 있고, 뒤의 둘은 터미널에서 돌릴 때 의미가 있습니다.
- 콘텐츠 규칙 가드: 콘텐츠 디렉터리의 파일을 고칠 때 “저는”, Jekyll 방식의 링크 문법, 금지된 번역어처럼 패턴으로 잡히는 규칙 위반을 실행 전에 거부합니다. 규칙은 배열 하나에 모으고 규칙마다 테스트를 붙입니다. 첫 번째로 만들 것입니다.
- 짝 검사: 한 턴 동안 바뀐 파일을 기억했다가 턴이 끝날 때 한국어 글만 바뀌고 영어 글이 그대로인지, Claude Code용 스킬만 바뀌고 Cursor용이 그대로인지 확인해 알려 줍니다. 설정 hook보다 Mod가 자연스러운 대표 사례입니다.
- 프론트매터와 이미지 검증: 콘텐츠 파일을 쓸 때 필수 필드가 빠졌거나
images에 적힌 파일이 실제로 없으면 쓰기 전에 돌려보냅니다. 지금은 커밋 직전에 도는 pre-commit hook이 잡는데, 그보다 훨씬 앞에서 잡는 셈입니다. - 배포 명령 보류: 배포 스크립트를 실행하려는 Bash 호출을 붙잡고 로컬 빌드가 성공했는지 먼저 확인합니다.
blast-radius샘플과 같은 발상입니다. - 턴 끝 빌드 점검: 콘텐츠를 고친 턴이 끝나면 Hugo 빌드를 조용히 돌려 보고 실패하면 알립니다. 깨진 문법을 다음 세션이 아니라 그 자리에서 발견하게 됩니다.
- 실측치 확인 질문: 글에 숫자와 분석 도구 이름이 함께 등장하면 호출을 붙잡고 “실측치로 보입니다. 그대로 쓸까요?“라고 사람에게 묻습니다. 화면이 없는 환경에서는 질문이 전달되지 않으므로, 그때는 기본값으로 막도록
catch쪽을 설계해야 합니다. - 토큰과 캐시 모니터: 모델 요청마다 캐시 적중과 토큰 사용량을 모아 창에 띄웁니다.
token-weather샘플이 하는 일에 가깝고, 지난 프롬프트 캐싱 글에서 궁금했던 것을 눈으로 보는 용도입니다.
어느 것을 만들든 공통으로 지킬 것이 있습니다. 차단하는 Mod에는 반드시 .catch를 붙여 실패 시 차단으로 두고, 거부 사유는 Claude가 읽고 바로 고칠 수 있는 지시문으로 씁니다. 그리고 패턴으로 잡을 수 없는 규칙, 예컨대 “용어를 처음 쓸 때 풀어서 설명한다"나 “문체가 자연스러운가” 같은 것은 Mod로 옮기지 않고 문서에 그대로 둡니다. Mod에서 모델을 불러 판정하게 할 수는 있지만, 호출마다 비용과 지연이 붙는 데다 그것은 결국 기억에 맡기는 방식을 한 겹 더 감싼 것에 지나지 않습니다.
맺으며#
Mods를 처음 봤을 때 필자의 관심은 “규칙을 코드로 강제한다"는 한 문장에 있었습니다. 조사를 마치고 나니 그 문장은 반만 맞습니다. 강제 자체는 설정 hook으로도 이미 가능했습니다. Mods가 새로 연 것은 강제가 아니라, 규칙이 상태를 갖고, 사람에게 묻고, 테스트되고, 플러그인 하나로 묶이는 것입니다. 다시 말해 규칙이 스크립트에서 프로그램이 되는 것입니다.
그래서 이 블로그의 다음 단계는 분명합니다. 지금은 모든 규칙이 기억에 있으니, 어느 쪽으로 옮기든 지금보다 낫습니다. 한 줄짜리 금지는 설정 hook이어도 충분하지만, 짝 검사처럼 턴을 가로지르는 규칙이 가장 자주 어겨지는 규칙이라면 처음부터 Mod로 가는 편이 맞습니다. 필자는 “저는” 가드와 짝 검사를 하나의 플러그인으로 묶어 만들어 보고, 그 과정에서 발견한 것을 다음 글로 정리할 생각입니다.
한 가지 더 남는 것은 Mod가 코드라는 사실 자체입니다. 지금까지 에이전트를 다루는 일은 대부분 문서를 쓰는 일이었습니다. Mods는 그 일부를 다시 프로그래밍으로 돌려놓습니다. 문서로 설득하던 것을 코드로 확정하는 쪽이 늘 나은 것은 아니지만, 적어도 “어겨지는 규칙"이 무엇인지를 알고 있다면 그 규칙만큼은 코드 쪽에 두는 것이 맞다고 필자는 생각합니다.