Claude Code 플러그인을 출시하며 발생한 두 가지 문제 — 그리고 그중 하나의 해결책은 카고 컬트(cargo cult)였다
요약
Claude Code 플러그인을 배포하는 과정에서 발생한 기술 호출 경로 문제와 CLI 실행 방식의 오류를 다룹니다. 플러그인 접두사로 인한 기술 호출 실패와 로컬 환경에 의존적인 CLI 명령어를 npx를 사용하는 방식으로 해결하는 과정을 설명합니다.
핵심 포인트
- 플러그인 설치 시 기술 호출 시 접두사(prefix)가 붙어 호출이 깨질 수 있음
- 기술 간 호출 시 정규화된 이름과 일반 이름을 모두 시도하는 방식으로 해결 가능
- SKILL.md 내 CLI 실행 시 로컬 저장소 명령어가 아닌 npx를 사용하여 호환성 확보
제 기술(skills)들은 잘 작동했습니다. 몇 주 동안 로컬에서 사용해 왔으니까요. 그러다 그것들을 플러그인으로 패키징하고, 낯선 사람이 설치하는 방식대로 설치한 뒤, 로컬에는 존재하지 않으면서 원인을 가리키는 에러 메시지도 생성하지 않는 요소들을 건드렸습니다.
그중 두 가지는 실제 문제였습니다. 세 번째는 제가 몇 달 동안 반복해 온 규칙이었는데, 알고 보니 완전히 다른 것에 관한 것이었습니다. 이 포스트를 작성하면서 그것을 확인하게 되었습니다.
1. 기술(skill)의 이름이 변경되며, 기술 간 호출(skill-to-skill calls)이 조용히 깨짐
~/.claude/skills/story/SKILL.md에 있는 기술은 다음과 같이 호출됩니다:
/story
mulmocast라는 이름의 플러그인 내부에 포함되어 배포된 동일한 파일은 다음과 같이 호출됩니다:
/mulmocast:story
플러그인 이름이 접두사(prefix)가 됩니다. 기술이 다른 기술을 호출할 수 있다는 점을 기억하기 전까지는 괜찮습니다.
제 mulmocast 기술은 라우터(router)입니다. 사용자가 요청한 내용을 읽고 story, narrate, illustrate 등으로 전달합니다. 일반적인 방식으로 작성하면, 그 전달 과정은 story라고 명시됩니다. 따라서:
- 로컬 환경 —
mulmocast→story→ 작동함 - 마켓플레이스에서 설치 시 —
mulmocast→story→ 해당 기술 없음
원인을 명시하는 에러를 본 적이 없습니다. 전달이 그냥 일어나지 않았고, 기술은 마치 그 단계가 선택 사항이었던 것처럼 계속 진행되었습니다.
해결책은 두 이름을 모두 작성하되, 정규화된(qualified) 이름을 먼저 시도하는 것입니다:
1. `mulmoccast:story` 시도 → 찾을 수 없으면 `story` 시도
2. `mulmocast:narrate` 시도 → 찾을 수 없으면 `narrate` 시도
표기법에 관한 참고 사항: 사용자는 기술을 호출하기 위해 /story를 입력합니다. SKILL.md 내부에서 당신은 모델이 디스패치(dispatch)할 기술의 이름을 지정하는 것이지 슬래시 명령어를 입력하는 것이 아닙니다. 따라서 위와 같이 슬래시 없이 작성하세요.
2. CLI의 이름도 변경됨
문제의 형태는 같지만 계층이 다릅니다.
제 기술들은 자체 npm 패키지로 배포되는 CLI를 구동합니다. 플러그인 저장소(repo)는 기술, 참조 및 예제 스크립트를 포함하며, CLI는 포함하지 않습니다. CLI를 개발하는 동안에는 해당 저장소 내부에서 실행했습니다:
yarn run cli images script.json
그 명령어는 제가 소유한 터미널에서는 괜찮지만, SKILL.md 파일 안에서는 잘못된 것입니다. 왜냐하면 그것을 읽는 사람은 플러그인을 설치했기 때문입니다. 그들에게는 해당 체크아웃(checkout)된 저장소가 없으므로, yarn run cli는 아무것도 해결(resolve)하지 못합니다.
명확한 해결책은 대신 게시된 bin 이름을 작성하는 것입니다:
{
"name": "mulmocast",
"bin": {
...
만약 사용자가 패키지를 전역(globally)으로 설치했다면 이 방법은 작동합니다. 하지만 사용자가 그렇게 하지 않았을 수도 있으며, 스킬(skill) 내부에서는 이를 확인할 방법이 없습니다.
그래서 제 스킬들이 실제로 말하는 내용은 다음과 같습니다:
npx mulmocast@latest movie script.json
npx는 설치된 것이 사양(spec)을 충족하지 않을 때 패키지를 해결하고 임시 복사본을 다운로드합니다. 문서화해야 할 설치 단계도 없고, 본문과 동기화해야 할 버전도 없습니다. 만약 yarn run cli 라인을 유지하고 싶다면, 그것이 저장소 내부에서 작업하는 사람들을 위한 것임을 명시하세요. 그것은 사용자를 위한 대체 수단(fallback)이 아니라, 자기 자신을 위한 메모일 뿐입니다.
이 두 가지 문제의 근본 원인은 동일합니다: 당신은 독자가 있지 않은 환경 내부에서 지침을 작성하고 있다는 점입니다. SKILL.md에 적는 모든 이름 — 스킬, 명령어, 경로 — 은 다른 어딘가에서 해결(resolve)됩니다.
3. 내가 틀렸던 한 가지
여기에 제가 글로 써서라도 제공해 왔던 조언이 있습니다:
마켓플레이스(marketplace)와 플러그인에 동일한
name을 절대 부여하지 마세요. 그렇게 하면 Linux에서EXDEV에러와 함께 설치에 실패합니다.
저는 그것을 믿었습니다. 제 저장소도 그렇게 설정했습니다. 어디서 처음 그 이야기를 들었는지는 기억나지 않습니다.
저는 Linux를 사용하지 않습니다. 저는 이 문제를 겪어본 적도, 재현해 본 적도, 확인해 본 적도 없습니다. 이 포스트를 작성하면서 마침내 그 출처를 찾아보게 되었습니다 — anthropics/claude-code#14799.
그것은 이름과는 아무런 상관이 없었습니다.
Error: Failed to install: EXDEV: cross-device link not permitted,
rename '/home/user/.claude/plugins/cache/…' -> '/tmp/claude-plugin-temp-…'
대부분의 현재 Linux 배포판에서 /tmp는 tmpfs인 반면, ~/.claude는 실제 디스크에 위치합니다. 서로 다른 두 개의 파일 시스템입니다. fs.rename()은 이 경계를 넘어 파일을 이동할 수 없는데, 설치 프로그램이 이 사이에서 이름을 변경(rename)하려고 시도했던 것입니다.
그래서 플러그인의 이름이 무엇이든 상관없이 모든 플러그인에서 이 문제가 발생했습니다. 해결책은 임시 디렉터리(temp directory)를 동일한 파일 시스템에 두는 것이었습니다:
export TMPDIR="$HOME/.claude/tmp"
그리고 이 문제는 해결되었습니다 — 2월에 이슈가 종료되었습니다. 최신 버전의 Claude Code를 사용 중이라면 사용자가 별도로 조치할 사항은 없습니다.
잘못된 규칙이 그토록 오래 살아남은 이유
이 부분이 제가 계속 간직하고 싶은 대목입니다.
그 규칙은 해롭지 않았습니다. 마켓플레이스(marketplace)와 플러그인에 서로 다른 이름을 부여하는 것은 비용이 들지 않고, 무엇도 망가뜨리지 않으며, 보기에도 깔끔합니다. 그래서 저는 그 규칙을 따랐고, 모든 것이 잘 작동했으며, 그 누구도 제게 반박하지 않았습니다.
비용이 발생하는 규칙은 도전을 받습니다 — 조만간 누군가가 그것이 그만한 가치가 있는지 묻게 됩니다. 하지만 비용이 들지 않는 규칙은 그저 쌓여만 갑니다. 저는 단 한 번도 테스트해 보지 않은 진단을 전달해 오고 있었으며, 제가 이를 알게 된 유일한 이유는 그것을 사실로서 기록하기 위해 자리에 앉았기 때문입니다.
참고로, 정상적으로 작동하는 설정에서의 이름들은 다음과 같습니다 — 이 중 두 개는 서로 일치해야 한다는 점에 유의하세요:
// .claude-plugin/marketplace.json
{
"name": "mulmocast-plugins", // 마켓플레이스
...
말이 나온 김에: 이름은 하나가 아니라 세 개입니다
위의 내용이 혼란스러운 이유는 플러그인에 세 개의 별개 이름이 관여하며, 문서에서 이를 서로 다른 위치에 사용하기 때문입니다:
receptron/mulmocast-claude-plugin ← GitHub 리포지토리 (repo)
↓
marketplace.json "name": "mulmocast-plugins" ← 마켓플레이스 이름
...
| 이름 | 정의된 곳 | 사용처 |
|---|---|---|
| GitHub 리포지토리 (repo) | GitHub | marketplace add |
| ... |
리포지토리 이름은 마켓플레이스에 등록할 때 이 시퀀스에서 단 한 번 사용됩니다. 그 이후에는 JSON 내부의 이름들을 바탕으로 작업하게 됩니다.
claude plugin marketplace add receptron/mulmocast-claude-plugin # 리포지토리 이름 (repo name)
claude plugin install mulmocast@mulmocast-plugins # 플러그인@마켓플레이스 (plugin@marketplace)
최소 레이아웃 (Minimum layout). 이는 두 개의 리포지토리일 수도 있고 하나일 수도 있습니다 — 제 경우는 하나입니다:
my-plugin-repo/
.claude-plugin/
marketplace.json # 카탈로그 (the catalogue)
...
어떤 배포 방식을 선택할 것인가
세 가지 방식이 있으며, 이들은 서로 동일한 역할을 위해 경쟁하는 것이 아닙니다.
| 도달 범위 (Reach) | MCP 서버 제공 (Ships MCP servers) | 훅 제공 (Ships hooks) | 설치 (Install) | |
|---|---|---|---|---|
| 자체 마켓플레이스 (Your own marketplace) | Claude Code | ✅ | ✅ | /plugin install name@market |
| ... |
대략적으로는 다음과 같습니다:
- Claude Code 전용이며, MCP 서버나 훅을 사용하는 경우 → 플러그인 (plugin). 자체 마켓플레이스로 시작하여, 검증된 후에 공식 스토어에 신청하세요.
- Cursor, Codex 및 기타 도구에서도 작동하기를 원하는 경우 → Skills CLI. 이는
SKILL.md를 각 에이전트의 디렉토리에 심볼릭 링크 (symlinks)로 연결하므로, 하나의 리포지토리로 이들 모두를 커버할 수 있습니다. 대신 MCP와 훅은 포기해야 합니다.
표의 열에서 '제공 (ships)'이라는 단어를 의도적으로 사용했습니다. Skills CLI는 SKILL.md 파일을 배포할 뿐, 플러그인 매니페스트 (plugin manifests)를 설치하지 않으므로 MCP 서버와 플러그인 훅이 함께 제공되지 않습니다. 대상 에이전트가 자체적으로 훅을 지원할 수도 있습니다.
그리고 제가 공식 스토어에 대해 실제로 저울질할 부분은 발견 가능성 (discovery)이 아니라, 사용자들이 marketplace add 단계를 완전히 건너뛴다는 점입니다. 누군가가 포기하게 만드는 단계가 하나 줄어드는 것이죠.
세 방식의 공통점
처음 두 방식은 같은 곳에 존재합니다: 저는 이름이 우연히 맞았던 곳에서만 이 작업을 실행해 왔습니다. 로컬에서 스킬 (skill)은 story이고 CLI는 yarn run cli입니다. 배포되면 mulmocast:story와 mulmocast가 됩니다. 두 가지 실패 사례 모두 그 간극에 위치합니다.
세 번째 방식은 반대편에서의 동일한 간극입니다. 저는 macOS를 사용하므로, Linux 전용 실패 사례는 저에게 도달할 리가 없었습니다. 그리고 그에 대한 저의 해결책(workaround)이 무료였기 때문에, 어떠한 증거도 저에게 도달하지 못했습니다. 저는 제가 서 있는 위치에서 반증할 수 없는 믿음을 구축해 왔던 것입니다.
따라서 유용한 습관은 "플러그인을 테스트하는 것"이 아닙니다. 누군가에게 이것이 존재한다고 말하기 전에, 낯선 사람이 사용하는 방식 그대로, 당신의 것이 아닌 머신(machine)에 설치해 보는 것입니다.
그리고 만약 당신이 한 번도 실패하는 것을 본 적 없는 규칙을 따르고 있다면, 그것은 그 규칙이 작동한다는 증거가 아닙니다. 그저 비용이 저렴한 것일 수도 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기