Claude Code의 훅을 플러그인에 넣으면 무엇이 달라지는가【mods 입문 ②】
요약
본 글은 Claude Code의 'mod'를 플러그인 형태로 구현하는 방법을 다루며, 훅(Hook)을 `settings.json`과 플러그인 내부에 각각 작성했을 때의 차이점을 비교합니다. 플러그인은 이름, 버전 등의 메타데이터가 담긴 `.claude-plugin/plugin.json` 파일과 실제 내용물이 들어가는 `hooks/hooks.json`으로 구성됩니다.
핵심 포인트
- 플러그인(Plugin)은 훅, 스킬 등을 모아 Claude Code에 추가하는 컨테이너 역할을 합니다.
- 플러그인의 메타데이터는 `.claude-plugin/plugin.json` 파일에 작성합니다.
- 훅 설정(`hooks.json`)의 구조와 내용은 `settings.json`과 유사하게 작성할 수 있습니다.
서론
Claude Code mod로 무엇을 만들 수 있는지 전체적인 그림을 파악하기 위해, 기초부터 순서대로 직접 만져보며 확인하는 시리즈의 두 번째 글입니다.
mod는 '플러그인 형태로 작성한다'고 설명되어 있으므로, mod 본체에 들어가기 전에 플러그인이란 그릇(컨테이너)을 먼저 확인해 보겠습니다.
지난번에는 settings.json에 훅을 작성했습니다.
이번에는 같은 훅을 플러그인 안에 넣어서 로드하고, settings.json의 훅과 무엇이 같고 무엇이 다른지, 어떻게 넣고 어떻게 제거하는지 살펴보겠습니다.
| 용어 | 의미 |
|---|---|
| 플러그인 (Plugin) | 훅(Hook), 스킬(Skill), MCP 서버 등을 하나의 폴더에 모아 Claude Code에 추가하는 메커니즘 |
| mod | 플러그인 형태로 작성하는 함수 훅. 화면에 페인이나 알림을 추가하거나, 처리를 삽입할 수 있음 |
급한 사람을 위한 요약
확인한 질문과 답변.
| 질문 | 답변 |
|---|---|
| 플러그인의 훅은 어떻게 작성하는가 | hooks/hooks.json에, settings.json과 같은 형태(가장 바깥쪽이 ` |
settings.json
측면에서는 Bash를 막지 않는다 -
hook-input.jsonl
줄이 늘어날수록, settings.json의
훅이 실행되었다는 증거가 된다 -
hook-input.jsonl
에는 이전 세션의 줄이 1줄 들어있다.
직접 해보기
settings.json과 플러그인 모두에 훅을 작성하여, 둘 다 작동하는지 확인하기
플러그인 폴더 만들기
# 플러그인 본체 폴더와 명찰 놓는 곳 .claude-plugin, 훅 놓는 곳 hooks를 한 번에 생성
mkdir -p hook-plugin/.claude-plugin hook-plugin/hooks && cat > hook-plugin/.claude-plugin/plugin.json <<'EOF'
{ "name": "hook-plugin", "version": "0.1.0", "description": "PreToolUse(Bash)를 막는 연습용 플러그인" } ...
{ "name": "hook-plugin", "version": "0.1.0", "description": "PreToolUse(Bash)를 막는 연습용 플러그인" }
hook-plugin
hook-plugin/hooks ...
플러그인은 하나의 폴더.
명찰인 .claude-plugin/plugin.json (이름・버전・설명)과, 내용물을 담는 폴더(이번에는 hooks/)로 구성된다.
name이 플러그인의 식별명이 된다.
| 용어 | 의미 |
|---|---|
| plugin.json | 플러그인의 이름・버전・설명 등을 작성하는 명찰 파일. .claude-plugin/ 안에 둔다 |
플러그인에 훅을 작성하기
이전 JSON 버전의 deny와 같은 훅이다.
구별할 수 있도록, 이유 문구만 「플러그인에서 deny 중」으로 변경한다.
# 여기부터 EOF까지의 내용을 hook-plugin/hooks/hooks.json에 작성
cat > hook-plugin/hooks/hooks.json <<'EOF'
{
...
{
"hooks": {
"PreToolUse": [
...
hooks.json은 settings.json과 같은 형태이다.
가장 바깥쪽이 `
대화 로그(jsonl)에서 Claude 측 메시지 내의 tool_use만 추출하여, 툴 이름과 인수를 한 줄씩 표시
jq -c 'select(.type=="assistant") | .message.content[]? | select(.type=="tool_use") | {name, input}' ~/.claude/projects/-Users-ando-projects-my-agent-mods-lab/<세션ID>.jsonl
같은 대화 로그에서 사용자 측 메시지 내의 tool_result만 추출하여, 에러 여부와 내용을 한 줄씩 표시
...
{"name":"Bash","input":{"command":"ls","description":"현재 디렉토리 파일 목록 보기"}}
{"is_error":true,"content":"PreToolUse:Bash hook error: 플러그인에서 거부함"}
settings.json에 deny를 작성하지 않았음에도 불구하고, 플러그인의 hooks.json의 훅(hook)으로 인해 Bash가 중단되었다.
Claude에게 전달되는 이유는 플러그인에 작성한 permissionDecisionReason 때문이다.
| 용어 | 의미 |
|---|---|
--plugin-dir | 플러그인의 폴더를 해당 세션에만 읽는 실행 옵션. 설치하지 않음 |
계속해서 /hooks를 열었다.
Hooks
3 hooks on 3 events
This menu is read-only. To add or change a hook, edit settings.json or ask Claude. Learn more
...
참고: https://code.claude.com/docs/en/hooks 「The /hooks menu」
settings.json 측의 훅도 작동했는지 확인하기
# hook-input.jsonl의 각 행에서 세션 ID의 처음 8글자와 실행된 명령어만 한 줄씩 표시
jq -c '{s: .session_id[0:8], cmd: .tool_input.command}' hook-input.jsonl
{"s":"65381673","cmd":"ls"}
{"s":"d7d4a1c9","cmd":"ls"}
- 1번째 줄은 이전 세션
- 2번째 줄의
d7d4a1c9가--plugin-dir로 시작된 이번 세션 - 플러그인이 거부한 것과 같은ls였고,
settings.json의 기록 훅도 작동했다 - 두 개의 훅은 서로 덮어쓰지 않고, 모두 실행된다.
다만, PreToolUse가 두 번 발생한 것은 아니다.
툴 호출 1회당, PreToolUse 이벤트는 단 한 번만 발생한다.
그 1회 안에서, 조건(matcher)에 맞는 훅이 settings.json의 것과 플러그인의 것을 포함하여 모두 병렬로 실행된다.
따라서 기록에 남는 것은 각각 하나씩이다.
| 보는 곳 | 추가된 것 | 이유 |
|---|---|
hook-input.jsonl | 1줄 | 파일에 쓰는 것은 settings.json의 기록 훅만이다. 플러그인 훅은 JSON을 반환하기만 하고 아무것도 쓰지 않는다 |
대화 로그의 툴 결과 | 1개(플러그인에서 거부함) | Claude에게 돌아가는 결과는 툴 호출 1회당 1개다. settings.json의 훅은 아무것도 반환하지 않으므로 흔적이 남지 않는다 |
대화 로그에는 PreToolUse 훅별 실행 기록이 남아있지 않았다.
여러 훅이 각기 다른 판단을 내렸을 때는, deny > defer > ask > allow 순서로 강한 것이 채택된다.
또한, settings.json에서(사용자용과 프로젝트용 등) 완전히 동일한 훅을 작성했을 경우에는 한 번만 작동하지만, 플러그인에 넣은 같은 훅은 별개의 것으로 취급된다.
참고: https://code.claude.com/docs/en/hooks 「Hook handler fields」
참고: https://code.claude.com/docs/en/hooks 「PreToolUse decision control」
| 용어 | 의미 |
|---|---|
| 이벤트 | 훅이 작동하는 계기가 되는 사건. PreToolUse는 도구 호출 1회당 한 번 발생 |
| 병렬 실행 | 여러 훅을 순차적으로 기다리게 하지 않고, 동시에 실행하는 것 |
--plugin-dir 없이 시작하기
# 플래그를 붙이지 않고 Claude Code를 시작 (플러그인은 로드되지 않음)
claude
'ls를 실행해 줘'라고 요청하고, /exit으로 종료한다.
{"name":"Bash","input":{"command":"ls","description":"현재 디렉토리의 파일 목록을 나열합니다"}}
{"is_error":false,"content":"hook-input.jsonl\nhook-plugin"}
ls는 정상적으로 실행되었다 (is_error: false).
# hook-input.jsonl의 각 행에서 세션 ID의 처음 8글자와 실행된 명령어만 한 줄씩 표시
jq -c '{s: .session_id[0:8], cmd: .tool_input.command}' hook-input.jsonl
{"s":"65381673","cmd":"ls"}
{"s":"d7d4a1c9","cmd":"ls"}
{"s":"5f4c34c7","cmd":"ls"}
- 3번째 줄의
5f4c34c7
이 이번 세션 -
settings.json
의 훅은, 플러그인이 없어도 계속 작동하고 있다 -
--plugin-dir
을 붙이든 안 붙이든 변하는 것은 플러그인 쪽만 참고:
참고: https://code.claude.com/docs/en/plugins/install 「Choose an install scope」「Uninstall a plugin the project enables」
참고: https://code.claude.com/docs/en/mcp 「Scope hierarchy and precedence」「Plugin-provided MCP servers」
| 용어 | 의미 |
|---|---|
| MCP 서버 | Claude가 사용할 수 있는 도구를 외부에서 추가하는 서버. context7은 라이브러리 문서를 가져오는 MCP 서버 |
| 스코프 | 설정이 어디까지 유효한지. user=자신의 모든 프로젝트, project=레포지토리의 모두, local=나만/이 레포지토리만 |
마켓플레이스 없이 플러그인을 항상 활성화하기
--plugin-dir은 매번 붙이지 않으면 작동하지 않는다.
정식적인 '설치'(claude plugin install)는 마켓플레이스를 거치는 것을 전제로 하고 있다.
활성화한 기록도 <이름>@<마켓플레이스 이름> 형태로 남는다.
반면, 항상 활성화만 하려면 마켓플레이스는 필요하지 않다.
프로젝트의 .claude/skills/<이름>/ (또는 ~/.claude/skills/<이름>/)에 둔 플러그인은 다음 세션부터 자동으로 로드된다.
참고: https://code.claude.com/docs/en/plugins/install 「Install a plugin」「Choose an install scope」
| 용어 | 의미 |
|---|---|
| 마켓플레이스 | 플러그인 목록(카탈로그). 등록해 두면 거기서 이름으로 지정하여 설치할 수 있다 |
skills 폴더에 플러그인을 두기
# 프로젝트의 skills 폴더를 만들고, hook-plugin을 통째로 복사한 후, 내용을 나열
mkdir -p .claude/skills && cp -R hook-plugin .claude/skills/hook-plugin && find .claude/skills
.claude/skills
.claude/skills/hook-plugin
.claude/skills/hook-plugin/hooks
...
플래그 없이 실행하고, ls를 요청하기
# 플래그를 붙이지 않고 Claude Code를 시작
claude
'ls를 실행해줘'라고 요청하고, /exit로 종료한다.
{"name":"Bash","input":{"command":"ls","description":"현재 디렉토리의 파일 목록을 나열합니다"}}
{"is_error":true,"content":"PreToolUse:Bash hook error: 플러그인에서 거부함"}
- 직전과 같은 플래그 없이 시작했음에도, 플러그인의 훅이 작동하여 ls가 멈췄다.
- 차이점은
.claude/skills/hook-plugin/을 배치한 것뿐이다. - 마켓플레이스도--plugin-dir도 없어도, 이 프로젝트에서는 항상 유효하다. -
hook-input.jsonl에도 이번 세션의 줄이 추가되었으며,settings.json의 훅과의 병합 또한 동일하게 발생한다.
제거하기
# skills 폴더 전체를 삭제하여, 항상 유효했던 플러그인을 제거합니다
rm -rf .claude/skills && find .claude
.claude
.claude/settings.json
제거할 때는 폴더만 지우면 된다.
settings.json의 훅은 그대로 남아있다.
요약
settings.json의 훅과 플러그인의 훅은 내용 작성 방식이 같았다.
다른 점은 '넣는 곳'이다. 플러그인은 폴더 단위로 넣거나 빼낼 수 있고, 형식을 사전에 확인할 수 있으며, 스킬이나 MCP 서버, 그리고 mod를 같은 폴더에 모을 수 있다.
넣는 방법은 목적에 따라 선택한다.
| 하고 싶은 것 | 넣는 방법 | 뺄 방법 |
|---|---|---|
| 해당 세션에서만 시도하기 | claude --plugin-dir <폴더> | 플래그를 붙이지 않고 시작 |
| 이 프로젝트에서 항상 유효하게 하기 | .claude/skills/<이름>/에 배치 | 폴더를 삭제한다 |
| 공식적으로 설치하여 배포하기 | 마켓플레이스에서 claude plugin install (이번에는 미실시) | claude plugin uninstall |
다음 시간에는 settings 훅과 mod의 비교를 통해 드디어 Mods 기능에 대해 다루겠습니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기