Superpowers의 스킬 강제 실행 메커니즘

17 min read
TL;DR
  • Superpowers는 세션 시작 시 using-superpowers 스킬 내용을 컨텍스트에 강제 주입해 모델이 첫 응답 전부터 스킬 규칙을 인지하게 만듭니다.
  • 기본 Claude Code 스킬 시스템은 name과 description만 보고 모델이 직접 Skill 도구 호출을 판단해야 해서 실행률이 낮을 수 있습니다.
  • SessionStart 훅, additionalContext 주입, 플랫폼별 포맷 분기, async: false 설정이 Superpowers 실행률 개선의 핵심입니다.
작은 로봇에게 SessionStart 훅과 컨텍스트 주입 흐름이 전달되는 클레이 페이퍼 스타일 이미지

Superpowers의 스킬 강제 실행 메커니즘

이 글은 약 한 달 전에 작성한 내용을 바탕으로 정리한 글입니다. Superpowers와 각 CLI의 훅/스킬 동작은 빠르게 바뀌고 있어, 일부 구현 세부사항은 현재 버전과 다를 수 있습니다.

TL;DR

AI를 오래 쓰다 보면 스킬이 생각보다 자동으로 잘 안 탄다는 걸 느끼게 됩니다. 그런데 Superpowers는 유독 잘 됩니다. 비결은 SessionStart 훅입니다. 세션 시작 시 using-superpowers 스킬 전체 내용을 컨텍스트에 강제 주입해, 모델이 "스킬을 써야 한다"는 규칙을 첫 응답 전에 이미 인지하게 만드는 방식이죠. 단순한 플러그인 구조처럼 보이지만, 스킬 실행률을 10%에서 66%까지 끌어올린 꽤 정교한 접근입니다.

스킬, 왜 자동으로 안 탈까

요즘 AI 쓰시는 분들 중에 스킬 모르시는 분은 없을 겁니다. 그런데 오래 쓰다 보면 한 가지를 느끼게 됩니다. 커맨드를 직접 호출하거나 명시적으로 지정하지 않으면 스킬이 생각대로 동작하지 않는다는 것입니다.

원하는 건 모델이 title과 description frontmatter를 읽고 추론해서 99% 확률로 알아서 적절한 스킬을 골라 실행해주는 것이지만, 현실은 그렇지 않습니다. GSD나 gstack 같은 스킬들은 직접 커맨드로 호출하지 않으면 제대로 작동하지 않는 경우가 많습니다.

회사 동료에게 Superpowers를 사용해보라고 추천했는데 추천하는 커맨드나 스킬이 있냐고 하길래 "그냥 자연스럽게 쓰면 돼"라고 했는데, 문득 의문이 생겼습니다. 여러 스킬이 섞여 있으면 필요한 스킬을 못 탈 수도 있는데, Superpowers는 왜 유독 잘 되는 걸까? 그래서 직접 코드를 뜯어봤습니다.

살펴보니 자체 훅을 설정해두고, 스크립트를 통해 높은 확률로 스킬셋에서 적절한 스킬을 사용하도록 강제하는 로직이 존재했습니다. 다른 서비스에도 충분히 응용 가능한 패턴이라 판단해서, Superpowers가 사용자 입력부터 훅 실행, 스킬 탐색, 최종 실행까지 어떻게 동작하는지 순서대로 정리해봤습니다.

스킬 시스템이 기본적으로 작동하는 방식

작은 로봇이 흩어진 스킬 카드 사이에서 어떤 스킬을 불러야 할지 혼란스러워하는 클레이 페이퍼 스타일 이미지

Claude Code는 세션 시작 시 세 경로에서 스킬을 스캔합니다. 조직 전체(/etc/claude-code/.claude/skills/), 사용자 개인(~/.claude/skills/), 프로젝트 단위(.claude/skills/)입니다.

스캔이 끝나면 모델이 받는 건 스킬의 name과 한 줄짜리 description 뿐입니다. 실제 스킬 전체 내용은 모델이 직접 Skill 도구를 호출해야 컨텍스트에 들어옵니다. 즉, 모델 스스로 "이 작업엔 이 스킬이 필요하다"고 판단해야 실행이 일어나는 구조입니다.

이게 문제의 근본 원인입니다. 이름 하나, 설명 한 줄만 보고 모델이 적절한 판단을 내리는 건 생각보다 훨씬 불안정합니다. 실험 데이터에 따르면 스킬 실행률이 멀티턴 기준 10%, 싱글턴 기준 6%에 불과합니다.

Superpowers가 이 문제를 우회하는 방법

동기식 훅 장치가 로봇에게 빛나는 컨텍스트 패키지를 먼저 주입하는 클레이 페이퍼 스타일 이미지

Superpowers는 스킬 시스템 자체에 의존하지 않습니다. 훅으로 그 단계를 통째로 건너뜁니다. 동작 순서는 다음과 같습니다.

1단계: 훅 등록 (hooks/hooks.json)

hooks.jsonSessionStart 이벤트 훅을 등록합니다. 실제 코드를 보면 matcherstartup|clear|compact 세 가지 트리거 조건을 걸어두고, run-hook.cmd를 통해 session-start 스크립트를 실행합니다. async: false로 설정되어 있어 스크립트가 완전히 끝난 뒤에야 모델의 첫 응답이 시작됩니다.

{
  "SessionStart": [
    {
      "matcher": "startup|clear|compact",
      "hooks": [
        {
          "type": "command",
          "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
          "async": false
        }
      ]
    }
  ]
}

2단계: 스크립트 실행 (hooks/session-start)

session-start 스크립트가 실행되면 ${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md 파일을 통째로 읽어 변수에 담습니다. 그리고 이를 <EXTREMELY_IMPORTANT> 태그로 감싼 뒤 JSON 형태로 출력합니다. 실제 출력 구조는 다음과 같습니다.

{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n..."
  }
}

현재 코드(v5.x)는 플랫폼별로 출력 포맷을 분기합니다. Claude Code는 hookSpecificOutput.additionalContext, Cursor는 additional_context, Copilot CLI는 최상위 additionalContext를 기대하기 때문에 환경 변수를 보고 적절한 형태로 내보내는 방식입니다.

3단계: 컨텍스트 주입

Claude Code가 hookSpecificOutput.additionalContext 값을 <system-reminder> 메시지로 변환해 컨텍스트에 주입합니다. 모델의 첫 응답이 나오기 전에 이미 using-superpowers 스킬 전체가 컨텍스트 안에 들어가 있는 상태가 됩니다.

4단계: 모델이 스킬 인지 상태로 대화 시작

모델은 첫 응답 전에 "어떤 스킬이 있고, 언제 써야 하고, 왜 반드시 써야 하는지"를 이미 읽은 상태입니다. 스킬을 스스로 찾아 호출하는 판단 단계 자체가 불필요해집니다.

using-superpowers가 강제하는 방식

규칙 비콘과 갈림길이 로봇의 스킬 호출 흐름과 남은 한계를 상징하는 클레이 페이퍼 스타일 이미지

주입되는 내용은 단순한 안내가 아닙니다. <EXTREMELY_IMPORTANT> 태그로 감싸진 명령문이며, using-superpowers/SKILL.md 안에는 스킬 실행 플로우를 표현한 결정 그래프까지 포함되어 있습니다. 흐름은 대략 이렇습니다.

  1. 사용자 메시지 수신
  2. EnterPlanMode 진입 직전이면 → brainstorming 스킬 체크
  3. 스킬이 적용될 가능성이 1%라도 있으면 → Skill 도구로 전체 내용 로드
  4. 스킬 내용 따라 실행, 체크리스트가 있으면 TodoWrite로 항목화

그리고 "The Rule"을 명시적으로 선언합니다. "응답이나 액션 전에 관련 스킬을 반드시 호출하라." 심지어 모델이 스킬을 건너뛸 때 쓸 법한 내면의 합리화 패턴을 미리 열거하고, 각각을 rationalization으로 차단합니다.

  • "이건 간단한 질문이야" → Rationalization
  • "스킬이 과한 것 같아" → Rationalization
  • "맥락이 더 필요해" → Rationalization

플랫폼별 호출 방식

Superpowers는 Claude Code 외에 Cursor, Codex, OpenCode, Copilot CLI, Gemini CLI까지 지원합니다. 그런데 플랫폼마다 훅 시스템이 다르다 보니 session-start가 호출되는 방식 자체가 다릅니다.

Claude Code / Cursor / Copilot CLI: 훅 기반 주입 방식입니다. 각 플랫폼별 hooks.json(또는 hooks-cursor.json)에서 SessionStart 이벤트를 등록하고, session-start 스크립트가 환경 변수(CURSOR_PLUGIN_ROOT, CLAUDE_PLUGIN_ROOT, COPILOT_CLI 등)를 감지해서 플랫폼별 JSON 포맷으로 컨텍스트를 출력합니다. Claude Code는 hookSpecificOutput.additionalContext, Cursor는 additional_context, Copilot CLI는 SDK 표준 additionalContext를 사용합니다.

Codex: 훅 시스템 자체가 없습니다. 대신 네이티브 스킬 디스커버리를 씁니다. 설치는 단순한 심링크 하나로 끝납니다.

gh repo clone obra/superpowers ~/.codex/superpowers
mkdir -p ~/.agents/skills
ln -s ~/.codex/superpowers/skills ~/.agents/skills/superpowers

Codex는 시작 시 ~/.agents/skills/ 디렉토리를 자동 스캔해서 SKILL.md 파일들을 frontmatter 메타데이터 기반으로 로드합니다. plugin.jsonhooks.json도 필요 없습니다. 대신 using-superpowers 메타 스킬의 description 필드가 Codex의 자동 활성화 트리거 조건 역할을 합니다.

Gemini CLI: activate_skill 도구를 씁니다. 세션 시작 시 스킬 메타데이터만 로드하고, 전체 내용은 모델이 activate_skill을 호출할 때 온디맨드로 활성화하는 방식입니다.

플랫폼별 동작 차이를 정리하면 이렇습니다.

플랫폼방식트리거
Claude Code훅 + additionalContext 주입SessionStart 이벤트
Cursor훅 + additional_context 주입SessionStart 이벤트
Copilot CLI훅 + SDK 표준 주입SessionStart 이벤트
Codex심링크 + 네이티브 디스커버리디렉토리 스캔
Gemini CLIactivate_skill 도구메타데이터 기반 활성화

같은 스킬 라이브러리를 쓰지만 진입 방식은 플랫폼마다 다르게 구현되어 있는 거죠. 그래서 Codex나 Gemini에서 스킬이 잘 타는 체감을 받는다면, 그건 Superpowers 훅 덕분이 아니라 플랫폼 자체의 스킬 디스커버리가 더 적극적이기 때문일 수 있습니다.

강제 실행까지 오기까지의 히스토리

릴리즈 노트와 커밋 히스토리를 보면 지금의 구조가 처음부터 이랬던 건 아닙니다.

초기: 훅이 getting-started/SKILL.md 경로만 던지고 모델이 직접 읽도록 유도하는 방식이었습니다. 실제로 초기 세션 시작 시 주입되는 내용은 이런 식이었습니다.

<EXTREMELY_IMPORTANT>
You have Superpowers. RIGHT NOW, go read:
@/path/to/skills/getting-started/SKILL.md
</EXTREMELY_IMPORTANT>

모델에게 "읽어라"고 시키는 방식입니다. 그런데 모델이 안 읽는 경우가 생겼습니다.

중간: getting-startedusing-superpowers로 이름이 바뀌면서 방식 자체가 바뀌었습니다. 파일 경로를 넘기는 대신 스크립트가 SKILL.md 전체 내용을 직접 읽어서 additionalContext로 통째로 주입하는 구조로 전환됐습니다. 모델이 읽을지 말지 판단하는 단계 자체를 없애버린 거죠.

이후 지속적인 강화: 그 이후로도 모델이 스킬을 건너뛰는 케이스가 계속 발견됐고, 버전마다 조여갔습니다.

  • <EXTREMELY_IMPORTANT> 블록과 절대적 언어 추가, 합리화 패턴을 미리 열거하는 Red Flags 테이블 추가
  • "Check for skills" → "Invoke relevant or requested skills"로 표현 강화. 사용자가 스킬 이름을 직접 말해도 "내가 이미 알아" 하고 그냥 시작하는 케이스가 발견됐기 때문
  • "응답 전에" → "응답이나 액션 전에(BEFORE any response or action)"로 수정. 응답 없이 액션을 먼저 취하는 케이스가 발견됐기 때문
  • async: trueasync: false로 전환. 비동기 실행 시 첫 응답 전에 훅이 완료되지 않는 레이스 컨디션 발견

수치로 증명된 개선은 아닙니다. 하지만 패치 히스토리 자체가 솔직한 증거입니다. "읽어라"에서 시작해서 "무조건 호출하라"까지, 모델이 스킬을 스스로 고르게 만드는 게 쉽지 않다는 걸 버전마다 증명해왔습니다.

남아 있는 한계

이 방식도 완벽하지는 않습니다. 서브에이전트 세션에는 SessionStart 훅이 트리거되지 않아, 서브에이전트는 주입된 컨텍스트 없이 일반 모델처럼 동작합니다. GitHub 이슈 #237에서 SubagentStart 훅 추가가 논의 중입니다. 또 컨텍스트 압축(compaction) 이후에는 주입된 내용이 드롭될 수 있어, 긴 세션에서는 재호출이 필요한 경우도 있습니다.

Windows 환경에서는 훅 실행 자체가 불안정한 경우가 있습니다. .sh 파일 직접 실행, run-hook.cmd 래퍼 사용 등 버전마다 방식이 바뀌었고, 현재도 관련 이슈가 열려 있습니다.

마치며

Superpowers의 핵심 아이디어는 생각보다 단순합니다. 모델이 스킬을 스스로 찾게 하는 대신, 세션이 열리는 순간 규칙을 통째로 밀어 넣는 거죠. 강제성이 꽤 공격적으로 느껴지기도 하지만, 실제 실행률 데이터를 보면 효과는 분명합니다.

다른 스킬 기반 워크플로우에도 충분히 적용할 수 있는 패턴입니다. 반복 적용이 필요한 베스트 프랙티스는 CLAUDE.md에, 특정 상황에서만 필요한 절차는 스킬에 두고, 스킬 실행률이 낮다면 SessionStart 훅으로 핵심 지침을 주입하는 방식으로 동일한 효과를 가볍게 달성할 수 있습니다.

Refs