Hermes 플러그인 만들기
기준일: 2026-08-02
난이도: 고급
공식 기준: Build a Hermes Plugin
개요
이 가이드는 계산기 플러그인 예제로 도구 여러 개, 라이프사이클 훅, 데이터 파일 번들링, 스킬 번들링까지 플러그인 시스템이 지원하는 것을 처음부터 끝까지 따라 만들어봅니다. (공식 canonical 경로: developer-guide/plugins)
핵심 개념
| 구성 | 역할 |
|---|---|
plugin.yaml |
매니페스트 — 이름·버전·제공하는 tools/hooks 선언 |
register(ctx) |
__init__.py의 등록 함수 — 시작 시 한 번 호출 |
requires_env |
API 키 등 자격증명 게이트, 설치 시 대화형 입력 |
kind |
매니페스트의 플러그인 종류(일반/platform/model-provider 등) |
선택 기준
Hermes는 확장 표면이 여러 개이고, 일부는 Python register_* API를, 일부는 설정 기반이나 드롭인 디렉터리를 씁니다. 무엇을 만들고 싶은지에 따라 참고할 가이드가 다릅니다.
| 원하는 것 | 참고 |
|---|---|
| 커스텀 도구·훅·슬래시 명령·스킬·CLI 서브커맨드 | 이 가이드(일반 플러그인 표면) |
| 네이티브 데스크톱 앱 확장 | Desktop Plugin SDK |
| 웹 대시보드 확장 | Extending the Dashboard |
| LLM/추론 백엔드 | Model Provider Plugins |
| 게이트웨이 채널(Discord/Telegram 등) | Adding Platform Adapters |
| 메모리 백엔드 | Memory Provider Plugins |
| 컨텍스트 압축 엔진 | Context Engine Plugins |
| 이미지/영상 생성 백엔드 | Image/Video Generation Provider Plugins |
| 웹 검색/추출 백엔드 | Web Search Provider Plugins |
| 시크릿 매니저 백엔드 | Secret Source Plugins |
| MCP로 외부 도구 연결 | config.yaml의 mcp_servers.<name> |
| 게이트웨이 이벤트 훅 | ~/.hermes/hooks/<name>/에 HOOK.yaml + handler.py |
| 셸 훅 | config.yaml의 hooks: |
| 추가 스킬 소스 | hermes skills tap add <repo> |
| 코어 추론 provider(플러그인 아님) | Adding Providers |
다른 제품(관측성 백엔드, 벤더 SaaS 커넥터, 유료 서비스 연동 등)을 감싸는 플러그인은 코어 트리에 병합하지 않고 독립 저장소로 배포합니다. ~/.hermes/plugins/나 pip entry point로 설치하는 방식은 동일하게 동작합니다. 이는 품질 기준이 아니라 결합도·유지보수 책임 문제이며, Nous Research Discord #plugins-skills-and-skins 채널에서 홍보할 수 있습니다.
실습
무엇을 만드는가
두 도구를 가진 계산기 플러그인입니다.
calculate— 수식 계산(2**16,sqrt(144),pi * 5**2)unit_convert— 단위 변환(100 F → 37.78 C, 5 km → 3.11 mi)
여기에 모든 도구 호출을 로깅하는 훅과 번들 스킬 파일 하나를 더합니다.
1단계: 플러그인 디렉터리 생성
mkdir -p ~/.hermes/plugins/calculator
cd ~/.hermes/plugins/calculator
2단계: 매니페스트 작성
plugin.yaml을 만듭니다.
name: calculator
version: 1.0.0
description: Math calculator — evaluate expressions and convert units
provides_tools:
- calculate
- unit_convert
provides_hooks:
- post_tool_call
이 매니페스트는 Hermes에게 "나는 calculator라는 플러그인이고, tools와 hooks를 제공한다"고 알립니다. provides_tools, provides_hooks는 플러그인이 등록하는 항목의 목록입니다.
추가할 수 있는 선택 필드입니다.
author: Your Name
requires_env: # 환경 변수로 로드를 게이트, 설치 시 입력 요청
- SOME_API_KEY # 단순 형식 — 없으면 플러그인 비활성화
- name: OTHER_KEY # 상세 형식 — 설치 중 설명/URL 표시
description: "Key for the Other service"
url: "https://other.com/keys"
secret: true
3단계: 도구 스키마 작성
schemas.py를 만듭니다. LLM이 도구를 언제 호출할지 판단하는 근거가 되는 파일입니다.
"""Tool schemas — what the LLM sees."""
CALCULATE = {
"name": "calculate",
"description": (
"Evaluate a mathematical expression and return the result. "
"Supports arithmetic (+, -, *, /, **), functions (sqrt, sin, cos, "
"log, abs, round, floor, ceil), and constants (pi, e). "
"Use this for any math the user asks about."
),
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression to evaluate (e.g., '2**10', 'sqrt(144)')",
},
},
"required": ["expression"],
},
}
UNIT_CONVERT = {
"name": "unit_convert",
"description": (
"Convert a value between units. Supports length (m, km, mi, ft, in), "
"weight (kg, lb, oz, g), temperature (C, F, K), data (B, KB, MB, GB, TB), "
"and time (s, min, hr, day)."
),
"parameters": {
"type": "object",
"properties": {
"value": {
"type": "number",
"description": "The numeric value to convert",
},
"from_unit": {
"type": "string",
"description": "Source unit (e.g., 'km', 'lb', 'F', 'GB')",
},
"to_unit": {
"type": "string",
"description": "Target unit (e.g., 'mi', 'kg', 'C', 'MB')",
},
},
"required": ["value", "from_unit", "to_unit"],
},
}
스키마가 중요한 이유: description 필드가 LLM이 이 도구를 언제 쓸지 결정하는 근거입니다. 무엇을 하는지, 언제 써야 하는지 구체적으로 적습니다. parameters는 LLM이 넘길 인자를 정의합니다.
4단계: 도구 핸들러 작성
tools.py를 만듭니다. LLM이 도구를 호출했을 때 실제로 실행되는 코드입니다.
"""Tool handlers — the code that runs when the LLM calls each tool."""
import json
import math
# Safe globals for expression evaluation — no file/network access
_SAFE_MATH = {
"abs": abs, "round": round, "min": min, "max": max,
"pow": pow, "sqrt": math.sqrt, "sin": math.sin, "cos": math.cos,
"tan": math.tan, "log": math.log, "log2": math.log2, "log10": math.log10,
"floor": math.floor, "ceil": math.ceil,
"pi": math.pi, "e": math.e,
"factorial": math.factorial,
}
def calculate(args: dict, **kwargs) -> str:
"""Evaluate a math expression safely.
Rules for handlers:
1. Receive args (dict) — the parameters the LLM passed
2. Do the work
3. Return a JSON string — ALWAYS, even on error
4. Accept **kwargs for forward compatibility
"""
expression = args.get("expression", "").strip()
if not expression:
return json.dumps({"error": "No expression provided"})
try:
result = eval(expression, {"__builtins__": {}}, _SAFE_MATH)
return json.dumps({"expression": expression, "result": result})
except ZeroDivisionError:
return json.dumps({"expression": expression, "error": "Division by zero"})
except Exception as e:
return json.dumps({"expression": expression, "error": f"Invalid: {e}"})
# Conversion tables — values are in base units
_LENGTH = {"m": 1, "km": 1000, "mi": 1609.34, "ft": 0.3048, "in": 0.0254, "cm": 0.01}
_WEIGHT = {"kg": 1, "g": 0.001, "lb": 0.453592, "oz": 0.0283495}
_DATA = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3, "TB": 1024**4}
_TIME = {"s": 1, "ms": 0.001, "min": 60, "hr": 3600, "day": 86400}
def _convert_temp(value, from_u, to_u):
# Normalize to Celsius
c = {"F": (value - 32) * 5/9, "K": value - 273.15}.get(from_u, value)
# Convert to target
return {"F": c * 9/5 + 32, "K": c + 273.15}.get(to_u, c)
def unit_convert(args: dict, **kwargs) -> str:
"""Convert between units."""
value = args.get("value")
from_unit = args.get("from_unit", "").strip()
to_unit = args.get("to_unit", "").strip()
if value is None or not from_unit or not to_unit:
return json.dumps({"error": "Need value, from_unit, and to_unit"})
try:
# Temperature
if from_unit.upper() in {"C","F","K"} and to_unit.upper() in {"C","F","K"}:
result = _convert_temp(float(value), from_unit.upper(), to_unit.upper())
return json.dumps({"input": f"{value} {from_unit}", "result": round(result, 4),
"output": f"{round(result, 4)} {to_unit}"})
# Ratio-based conversions
for table in (_LENGTH, _WEIGHT, _DATA, _TIME):
lc = {k.lower(): v for k, v in table.items()}
if from_unit.lower() in lc and to_unit.lower() in lc:
result = float(value) * lc[from_unit.lower()] / lc[to_unit.lower()]
return json.dumps({"input": f"{value} {from_unit}",
"result": round(result, 6),
"output": f"{round(result, 6)} {to_unit}"})
return json.dumps({"error": f"Cannot convert {from_unit} → {to_unit}"})
except Exception as e:
return json.dumps({"error": f"Conversion failed: {e}"})
핸들러의 핵심 규칙입니다.
- 시그니처:
def my_handler(args: dict, **kwargs) -> str - 반환값: 성공이든 에러든 항상 JSON 문자열
- 절대 raise하지 않는다: 모든 예외를 잡아 에러 JSON으로 반환
**kwargs를 받는다: Hermes가 앞으로 추가 컨텍스트를 넘길 수도 있음
5단계: 등록 작성
__init__.py를 만듭니다. 스키마와 핸들러를 연결합니다.
"""Calculator plugin — registration."""
import logging
from . import schemas, tools
logger = logging.getLogger(__name__)
# Track tool usage via hooks
_call_log = []
def _on_post_tool_call(tool_name, args, result, task_id, **kwargs):
"""Hook: runs after every tool call (not just ours)."""
_call_log.append({"tool": tool_name, "session": task_id})
if len(_call_log) > 100:
_call_log.pop(0)
logger.debug("Tool called: %s (session %s)", tool_name, task_id)
def register(ctx):
"""Wire schemas to handlers and register hooks."""
ctx.register_tool(name="calculate", toolset="calculator",
schema=schemas.CALCULATE, handler=tools.calculate)
ctx.register_tool(name="unit_convert", toolset="calculator",
schema=schemas.UNIT_CONVERT, handler=tools.unit_convert)
# This hook fires for ALL tool calls, not just ours
ctx.register_hook("post_tool_call", _on_post_tool_call)
register(ctx)가 하는 일입니다.
- 시작 시 정확히 한 번 호출된다
ctx.register_tool()은 도구를 레지스트리에 등록한다 — 모델이 즉시 볼 수 있다ctx.register_hook()은 라이프사이클 이벤트를 구독한다ctx.register_cli_command()는 CLI 서브커맨드(hermes my-plugin <subcommand>)를 등록한다ctx.register_command()는 세션 내 슬래시 명령(/myplugin <args>)을 등록한다ctx.dispatch_tool(name, arguments)는 부모 에이전트의 컨텍스트(승인, 자격증명, task_id)를 자동으로 붙여 다른 도구(내장 또는 다른 플러그인)를 호출한다. 슬래시 명령 핸들러가terminal,read_file같은 도구를 모델이 직접 부른 것처럼 호출해야 할 때 유용하다- 이 함수가 크래시해도 플러그인만 비활성화되고 Hermes는 계속 정상 동작한다
dispatch_tool 예시 — 도구를 실행하는 슬래시 명령입니다.
def handle_scan(ctx, raw_args: str):
"""Implement /scan by invoking the terminal tool through the registry."""
result = ctx.dispatch_tool("terminal", {"command": f"find . -name '{raw_args}'"})
return result # returned to the caller's chat UI
def register(ctx):
# Handlers receive a single raw_args string; close over ctx via a lambda.
ctx.register_command(
"scan",
lambda raw: handle_scan(ctx, raw),
description="Find files matching a glob",
)
dispatch된 도구는 일반 승인·redaction·budget 파이프라인을 그대로 통과합니다 — 이를 우회하는 지름길이 아니라 실제 도구 호출입니다.
6단계: 테스트
Hermes를 시작합니다.
hermes
배너의 도구 목록에 calculator: calculate, unit_convert가 보여야 합니다.
다음 프롬프트로 시험해봅니다.
2의 16제곱은 얼마야?
화씨 100도를 섭씨로 변환해줘
2에 파이를 곱한 값의 제곱근은?
1.5테라바이트는 몇 기가바이트야?
플러그인 상태를 확인합니다.
/plugins
출력 예시입니다.
Plugins (1):
✓ calculator v1.0.0 (2 tools, 1 hooks)
플러그인 탐색 디버깅
플러그인이 안 보이거나, 보이는데 로드되지 않으면 HERMES_PLUGINS_DEBUG=1로 자세한 탐색 로그를 stderr에 출력합니다.
HERMES_PLUGINS_DEBUG=1 hermes plugins list
플러그인 소스(bundled, user, project, entry-points)마다 다음을 볼 수 있습니다.
- 어떤 디렉터리를 스캔했고 각각 몇 개의 매니페스트를 찾았는지
- 매니페스트별 resolved key, name, kind, source, 디스크 경로
- 스킵 사유: config에서 비활성화됨, config에서 미활성화, exclusive 플러그인,
plugin.yaml없음, 중첩 깊이 초과 - 로드 시: import되는 플러그인과
register(ctx)가 등록한 내용(tools, hooks, 슬래시 명령, CLI 명령) 한 줄 요약 - 파싱 실패 시: 예외의 전체 트레이스백(YAML 스캐너 오류 등)
register()실패 시:__init__.py에서 raise된 줄을 가리키는 전체 트레이스백
같은 로그는 환경 변수를 켜지 않아도 ~/.hermes/logs/agent.log에 WARNING 레벨(실패만)로, 켜면 DEBUG 레벨(전체)로 항상 기록됩니다. 게이트웨이 안이라 환경 변수를 못 쓴다면 로그 파일을 tail합니다.
hermes logs --level WARNING | grep -i plugin
플러그인이 안 보이는 흔한 이유입니다.
- config에서 활성화되지 않음 — 플러그인은 opt-in입니다.
hermes plugins enable <name>을 실행합니다(이름은 plugins list 출력에서 가져오며, 중첩 레이아웃이면<category>/<plugin>형식일 수 있습니다). - 디렉터리 레이아웃이 잘못됨 —
~/.hermes/plugins/<plugin-name>/plugin.yaml(평평한 구조) 또는~/.hermes/plugins/<category>/<plugin-name>/plugin.yaml(카테고리 한 단계 중첩까지만) 형식이어야 합니다. 더 깊은 구조는 무시됩니다. __init__.py가 없음 — 플러그인 디렉터리에는plugin.yaml과register(ctx)함수가 있는__init__.py가 모두 필요합니다.kind가 잘못됨 — 게이트웨이 어댑터는 매니페스트에kind: platform이 필요합니다. 메모리 provider는kind: exclusive로 자동 감지되어plugins.enabled가 아니라memory.provider설정을 통해 라우팅됩니다.
최종 구조
~/.hermes/plugins/calculator/
├── plugin.yaml # "I'm calculator, I provide tools and hooks"
├── __init__.py # Wiring: schemas → handlers, register hooks
├── schemas.py # What the LLM reads (descriptions + parameter specs)
└── tools.py # What runs (calculate, unit_convert functions)
파일 네 개로 역할이 명확히 나뉩니다. 매니페스트는 플러그인이 무엇인지 선언하고, 스키마는 LLM에게 도구를 설명하고, 핸들러는 실제 로직을 구현하고, 등록은 모든 것을 연결합니다.
플러그인으로 더 할 수 있는 것
데이터 파일 번들 — 플러그인 디렉터리에 파일을 두고 import 시점에 읽습니다.
# In tools.py or __init__.py
from pathlib import Path
_PLUGIN_DIR = Path(__file__).parent
_DATA_FILE = _PLUGIN_DIR / "data" / "languages.yaml"
with open(_DATA_FILE) as f:
_DATA = yaml.safe_load(f)
스킬 번들 — 플러그인은 에이전트가 skill_view("plugin:skill")로 불러오는 스킬 파일을 함께 배포할 수 있습니다.
~/.hermes/plugins/my-plugin/
├── __init__.py
├── plugin.yaml
└── skills/
├── my-workflow/
│ └── SKILL.md
└── my-checklist/
└── SKILL.md
from pathlib import Path
def register(ctx):
skills_dir = Path(__file__).parent / "skills"
for child in sorted(skills_dir.iterdir()):
skill_md = child / "SKILL.md"
if child.is_dir() and skill_md.exists():
ctx.register_skill(child.name, skill_md)
플러그인 스킬은 읽기 전용이며 ~/.hermes/skills/에 들어가지 않고 skill_manage로 편집할 수 없습니다. system prompt의 <available_skills> 목록에도 나타나지 않는 명시적 opt-in 로드입니다. 네임스페이스 덕분에 내장 스킬과 이름이 겹치지 않습니다.
환경 변수로 게이트 — API 키가 필요하면 단순 형식 또는 상세 형식(설명·URL·secret 플래그)으로 requires_env를 씁니다. 값이 없으면 크래시 없이 "Plugin weather disabled (missing: WEATHER_API_KEY)"처럼 명확히 비활성화됩니다. hermes plugins install 실행 시 누락된 값을 대화형으로 물어보고 .env에 자동 저장합니다.
| 필드 | 필수 | 설명 |
|---|---|---|
name |
예 | 환경 변수 이름 |
description |
아니오 | 설치 프롬프트에서 사용자에게 표시 |
url |
아니오 | 자격증명을 받을 수 있는 곳 |
secret |
아니오 | true면 비밀번호 필드처럼 입력을 가림 |
무거운 선택적 의존성은 지연 설치 — 모든 사용자가 갖고 있지 않을 SDK를 감쌀 때는 모듈 상단에서 import하지 말고 도구 핸들러 안에서 tools.lazy_deps.ensure(...)를 씁니다. security.allow_lazy_installs 설정에 따라 최초 사용 시 설치됩니다. feature key는 반드시 내장 allowlist에 있어야 하고, PyPI 이름만 지정할 수 있습니다(--index-url, git+https://, file: 경로는 불가).
스레드 세이프 lazy singleton — 비싼 객체(SDK 클라이언트, HTTP 세션 등)를 모듈 전역 변수로 캐싱하는 손수 짠 패턴은 여러 스레드가 동시에 초기화를 통과해 자원이 새는 TOCTOU 경쟁 상태를 만듭니다. plugins/plugin_utils.py의 lazy_singleton/SingletonSlot 헬퍼를 씁니다.
from plugins.plugin_utils import lazy_singleton, SingletonSlot
@lazy_singleton
def get_client():
return ExpensiveClient(load_config()) # runs exactly once
client = get_client() # safe across threads
get_client.reset() # drop the instance (tests / teardown)
도구 조건부 노출 — 선택적 라이브러리에 의존하는 도구는 check_fn으로 숨길 수 있습니다.
ctx.register_tool(
name="my_tool",
schema={...},
handler=my_handler,
check_fn=lambda: _has_optional_lib(), # False = tool hidden from model
)
내장 도구 오버라이드 — override=True로 내장 도구를 자신의 구현으로 교체할 수 있습니다. 다른 toolset에서 기존 도구 이름을 가리는 등록은 기본적으로 거부되며, 오버라이드하려면 config.yaml의 plugins.entries.<plugin_id>.allow_tool_override: true로 운영자가 명시적으로 동의해야 합니다.
여러 훅 등록 — pre_tool_call, post_tool_call, pre_llm_call, post_llm_call, on_session_start, on_session_end, on_session_finalize, on_session_reset, kanban_task_claimed, kanban_task_completed, kanban_task_blocked 등을 등록할 수 있습니다. 대부분은 반환값이 무시되는 관찰용 훅이고, pre_llm_call만 컨텍스트를 주입할 수 있으며 pre_tool_call만 차단·승인 지시를 반환할 수 있습니다.
pre_llm_call이 {"context": "..."} 또는 문자열을 반환하면 그 내용이 시스템 프롬프트가 아니라 현재 턴의 사용자 메시지에 추가됩니다. 시스템 프롬프트를 그대로 유지해야 프롬프트 캐시가 깨지지 않기 때문입니다. 훅별 컨텍스트는 기본 10,000자로 제한되며, 초과분은 $HERMES_HOME/hook_outputs/<session_id>/<uuid>.txt에 저장되고 미리보기로 대체됩니다.
CLI·슬래시 명령 등록 — ctx.register_cli_command()로 hermes <plugin> <subcommand> 트리를, ctx.register_command()로 세션 내 /이름 슬래시 명령을 등록합니다. 슬래시 명령은 CLI와 게이트웨이(Telegram, Discord 등) 양쪽에서 동작하며 동기·비동기 핸들러를 모두 지원합니다. 내장 명령과 이름이 겹치면 등록은 조용히 거부되고 로그에 경고만 남습니다.
특화 플러그인 종류
일반 표면 외에 다섯 가지 특화 플러그인 종류가 있습니다. 각각 plugins/<category>/<name>/(번들) 또는 ~/.hermes/plugins/<category>/<name>/(사용자) 아래에 배치하며 카테고리마다 계약이 다릅니다.
모델 provider 플러그인 — LLM 백엔드 추가
# plugins/model-providers/acme/__init__.py
from providers import register_provider
from providers.base import ProviderProfile
register_provider(ProviderProfile(
name="acme",
aliases=("acme-inference",),
display_name="Acme Inference",
env_vars=("ACME_API_KEY", "ACME_BASE_URL"),
base_url="https://api.acme.example.com/v1",
auth_type="api_key",
default_aux_model="acme-small-fast",
fallback_models=("acme-large-v3", "acme-medium-v3"),
))
get_provider_profile()이나 list_providers()를 처음 호출할 때 지연 탐색됩니다. 사용자 플러그인이 같은 이름의 번들 provider를 덮어씁니다.
플랫폼 플러그인 — 게이트웨이 채널 추가는 BasePlatformAdapter를 구현하고 ctx.register_platform()으로 등록합니다.
메모리 provider 플러그인은 MemoryProvider를 구현하며 memory.provider 설정으로 단일 선택됩니다.
컨텍스트 엔진 플러그인은 ContextEngine을 구현하며 context.engine 설정으로 단일 선택됩니다.
이미지 생성 백엔드는 ImageGenProvider를 구현합니다.
MCP·이벤트 훅·셸 훅 (비-Python 확장)
Python 없이도 확장할 수 있는 표면이 있습니다.
MCP 서버는 config.yaml의 mcp_servers에 선언하면 시작 시 Hermes가 연결해 도구 목록을 가져오고 내장 도구와 함께 등록합니다.
mcp_servers:
filesystem:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
timeout: 120
linear:
url: "https://mcp.linear.app/sse"
auth:
type: "oauth"
게이트웨이 이벤트 훅은 ~/.hermes/hooks/<name>/에 HOOK.yaml과 handler.py를 두면 됩니다. 이벤트에는 gateway:startup, session:start/end/reset, agent:start/step/end, 와일드카드 command:*가 있으며, 훅에서 발생한 오류는 잡혀서 로그로만 남고 메인 파이프라인을 막지 않습니다.
셸 훅은 config.yaml의 hooks:에 이벤트·명령·조건만 선언하면 됩니다(Python 불필요).
hooks:
- event: post_tool_call
command: "notify-send 'Tool ran: {tool_name}'"
when:
tools: [terminal, patch, write_file]
**스킬 소스(tap)**는 GitHub 저장소를 추가해 확장합니다.
hermes skills tap add myorg/skills-repo
hermes skills search my-workflow --source myorg/skills-repo
hermes skills install myorg/skills-repo/my-workflow
TTS/STT도 커맨드 템플릿으로 코드 없이 연결할 수 있습니다.
tts:
provider: voxcpm
providers:
voxcpm:
type: command
command: "voxcpm --ref ~/voice.wav --text-file {input_path} --out {output_path}"
output_format: mp3
voice_compatible: true
배포
pip으로 배포
# pyproject.toml
[project.entry-points."hermes_agent.plugins"]
my-plugin = "my_plugin_package"
pip install hermes-plugin-calculator
# Plugin auto-discovered on next hermes startup
NixOS로 배포 — Nix/NixOS는 더 이상 공식 지원 설치 경로가 아니고 best-effort로만 유지됩니다. pyproject.toml에 entry point가 있으면 선언적으로 설치할 수 있습니다.
# User's configuration.nix
services.hermes-agent.extraPythonPackages = [
(pkgs.python312Packages.buildPythonPackage {
pname = "my-plugin";
version = "1.0.0";
src = pkgs.fetchFromGitHub {
owner = "you";
repo = "hermes-my-plugin";
rev = "v1.0.0";
hash = "sha256-..."; # nix-prefetch-url --unpack
};
format = "pyproject";
build-system = [ pkgs.python312Packages.setuptools ];
})
];
pyproject.toml이 없는 디렉터리 플러그인이라면 extraPlugins에 fetchFromGitHub로 바로 넣을 수도 있습니다.
흔한 실수
핸들러가 JSON 문자열을 반환하지 않음
# Wrong — returns a dict
def handler(args, **kwargs):
return {"result": 42}
# Right — returns a JSON string
def handler(args, **kwargs):
return json.dumps({"result": 42})
핸들러 시그니처에 **kwargs 누락
# Wrong — will break if Hermes passes extra context
def handler(args):
...
# Right
def handler(args, **kwargs):
...
핸들러가 예외를 raise함
# Wrong — exception propagates, tool call fails
def handler(args, **kwargs):
result = 1 / int(args["value"]) # ZeroDivisionError!
return json.dumps({"result": result})
# Right — catch and return error JSON
def handler(args, **kwargs):
try:
result = 1 / int(args.get("value", 0))
return json.dumps({"result": result})
except Exception as e:
return json.dumps({"error": str(e)})
스키마 설명이 너무 모호함
# Bad — model doesn't know when to use it
"description": "Does stuff"
# Good — model knows exactly when and how
"description": "Evaluate a mathematical expression. Use for arithmetic, trig, logarithms. Supports: +, -, *, /, **, sqrt, sin, cos, log, pi, e."
Hermes에 입력할 프롬프트
~/.hermes/plugins/calculator 플러그인 초안을 검토해줘.
schemas.py의 description이 충분히 구체적인지, tools.py의 핸들러가 항상 JSON 문자열을 반환하고 예외를 삼키는지 확인해줘.
체크리스트
-
plugin.yaml에provides_tools/provides_hooks를 선언했다. - 모든 핸들러가
**kwargs를 받고 예외 없이 JSON 문자열만 반환한다. - 스키마
description이 언제·어떻게 쓸지 구체적으로 적혀 있다. -
hermes plugins enable <name>으로 활성화했다(플러그인은 기본이 opt-in). -
/plugins와HERMES_PLUGINS_DEBUG=1로 로드 여부를 확인했다. - 자격증명이 필요하면
requires_env로 게이트했다.