[opencode 시즌 2 심화 — 나만의 도메인 특화 에이전트 만들기] 2/12화: opencode 커스텀 에이전트 만들기 2가지 방법 — JSON vs Markdown 완전 비교 (2026)
이 글은 「opencode 시즌 2 심화 — 나만의 도메인 특화 에이전트 만들기」 2일차입니다.
시즌 1 3일차에서 opencode.json 설정 파일을 처음 열어봤던 기억이 나시나요? 오늘은 그 파일 안에서 opencode 커스텀 에이전트를 직접 정의하고, 두 번째 방법인 Markdown 파일 기반 정의까지 나란히 비교해봅니다.
어제 1일차에서는 Primary(Build·Plan)와 Subagent(General·Explore·Scout) 5계층 구조를 해부했습니다. 오늘은 그 구조 위에 내가 만든 에이전트를 올려보는 첫 실습입니다.
오늘의 핵심 3가지
- 두 갈래 정의 방식 —
opencode.json의agent키 vs.opencode/agents/*.mdYAML frontmatter - 스코프와 폴백 순서 — 프로젝트 로컬 → 글로벌, 어디에 두느냐가 에이전트의 생존 범위를 결정
- default_agent 설정 — 대화 시작 시 어떤 에이전트가 먼저 응답할지 지정하는 법

왜 두 가지 방식이 존재하는가
opencode는 에이전트를 정의하는 경로를 의도적으로 두 개 열어뒀습니다. 하나는 구조화된 JSON, 다른 하나는 자유로운 Markdown입니다. 이건 취향의 문제가 아니라, 각 방식이 해결하는 문제가 다릅니다.
opencode.json의 agent 키는 에이전트의 메타데이터(모델·권한·도구·설명)를 선언적으로 관리하기에 최적입니다. 반면 .opencode/agents/*.md는 시스템 프롬프트 본문을 길고 풍부하게 작성할 때 빛납니다. 실전에서는 두 방식을 섞어 씁니다 — JSON으로 뼈대를 잡고, Markdown으로 프롬프트를 외부화하는 식이죠.
방식 1 — opencode.json의 agent 키
기본 구조
opencode.json(또는 .opencode.json)은 프로젝트 루트에 놓이는 설정 파일입니다. 여기에 "agent" 키를 추가하면 opencode 커스텀 에이전트가 등록됩니다.
{
"$schema": "https://opencode.ai/schema.json",
"agent": {
"CodeReviewer": {
"model": "anthropic/claude-sonnet-4-1",
"description": "코드 리뷰 전문가. PR diff를 분석하고 개선점을 제안한다.",
"instructions": "당신은 시니어 코드 리뷰어입니다. 다음 원칙을 따르세요:\n1. 버그 가능성이 있는 패턴을 우선 지적\n2. 성능 영향이 큰 변경을 두 번째로 검토\n3. 스타일/컨벤션은 마지막에 언급",
"tools": ["Read", "Glob", "Grep", "Agent"],
"permission": {
"allow": ["Read", "Glob", "Grep"],
"deny": ["Write", "Edit", "Bash"]
}
}
}
}
이 한 블록이면 CodeReviewer라는 Subagent가 생깁니다. Primary 에이전트가 코드 리뷰 관련 요청을 받으면 description을 보고 자동으로 이 에이전트에게 위임하거나, 사용자가 @CodeReviewer로 직접 호출할 수 있습니다.
주요 필드 해설
model—provider/model-id형식. 생략하면 프로젝트 기본 모델을 상속합니다.description— Primary가 위임 판단에 사용하는 핵심 텍스트. 짧고 명확하게 써야 자동 위임 정확도가 올라갑니다.instructions— 시스템 프롬프트. JSON 안에 넣으므로 줄바꿈은\n으로 이스케이프해야 합니다.tools— 이 에이전트가 사용할 수 있는 도구 목록.permission—allow/ask/deny세 등급으로 도구 접근을 제어합니다.
JSON 방식의 장점과 한계
장점: 한 파일에서 여러 에이전트를 한눈에 관리. 버전 관리(git diff)가 깔끔. IDE 자동완성과 $schema 검증 지원.
한계: 시스템 프롬프트가 길어지면 JSON 이스케이프 지옥. 50줄짜리 프롬프트를 \n으로 연결한 한 줄 문자열로 관리하는 건 고통입니다.
방식 2 — .opencode/agents/*.md (Markdown 정의)
기본 구조
프로젝트 루트에 .opencode/agents/ 디렉토리를 만들고, 에이전트 하나당 Markdown 파일 하나를 둡니다. 파일 이름(확장자 제외)이 에이전트 이름이 됩니다.
# .opencode/agents/CodeReviewer.md
---
model: anthropic/claude-sonnet-4-1
description: "코드 리뷰 전문가. PR diff를 분석하고 개선점을 제안한다."
tools:
- Read
- Glob
- Grep
- Agent
permission:
allow:
- Read
- Glob
- Grep
deny:
- Write
- Edit
- Bash
---
당신은 시니어 코드 리뷰어입니다. 다음 원칙을 따르세요:
## 리뷰 우선순위
1. **버그 가능성** — null 참조, 경계 조건, 동시성 이슈를 가장 먼저 지적합니다.
2. **성능 영향** — O(n²) 루프, 불필요한 I/O, 메모리 누수 가능성을 검토합니다.
3. **스타일/컨벤션** — 네이밍, 포매팅, 프로젝트 컨벤션 위반은 마지막에 언급합니다.
## 피드백 형식
- 각 지적은 **파일:라인** 형태로 위치를 명시합니다.
- 심각도를 🔴 Critical / 🟡 Warning / 🔵 Info 로 구분합니다.
- 수정 제안은 코드 블록으로 제시합니다.
YAML frontmatter(두 개의 --- 사이)가 메타데이터, 그 아래 본문 전체가 시스템 프롬프트입니다. JSON에서는 \n 지옥이던 프롬프트가 여기서는 자연스러운 Markdown으로 펼쳐집니다.

Markdown 방식의 장점과 한계
장점: 프롬프트를 마크다운 문법 그대로 작성. 헤딩·목록·코드블록을 자유롭게 사용. 에이전트별 파일 분리로 협업 시 충돌 최소화.
한계: 에이전트가 10개 넘어가면 디렉토리 관리 부담. 전체 에이전트 목록을 한 눈에 보려면 ls .opencode/agents/를 쳐야 합니다.
opencode 커스텀 에이전트 — 두 방식 완전 비교표
같은 에이전트를 두 방식으로 정의할 때의 차이를 정리합니다.
| 항목 | opencode.json agent 키 | .opencode/agents/*.md |
|---|---|---|
| 에이전트 이름 | JSON 키 이름 | 파일명 (확장자 제외) |
| 메타데이터 | JSON 필드 | YAML frontmatter |
| 시스템 프롬프트 | instructions 문자열 (이스케이프 필요) |
frontmatter 아래 Markdown 본문 |
| 프롬프트 외부화 | {file:./prompts/review.md} 패턴 |
본문 자체가 프롬프트 (추가 외부화 가능) |
| 여러 에이전트 관리 | 한 파일에 N개 객체 | N개 파일 |
| IDE 지원 | JSON Schema 자동완성 | Markdown 프리뷰 |
| git 충돌 가능성 | 높음 (한 파일 수정) | 낮음 (파일 분리) |
| 적합한 상황 | 에이전트 3개 이하, 프롬프트 짧을 때 | 에이전트 많거나 프롬프트 길 때 |
| 우선순위 | 로딩 시 먼저 읽힘 | 같은 이름일 경우 md가 오버라이드 |
핵심 포인트: 같은 이름의 에이전트가 양쪽에 정의되어 있으면 .opencode/agents/*.md가 우선합니다. 이건 의도된 설계로, JSON에서 기본 뼈대를 잡고 Markdown에서 프롬프트를 오버라이드하는 계층 패턴을 가능하게 합니다.
스코프 — 글로벌 vs 프로젝트 로컬
두 개의 설정 위치
opencode 커스텀 에이전트는 두 곳에 놓을 수 있습니다.
- 글로벌:
~/.config/opencode/(Linux/macOS) 또는%APPDATA%/opencode/(Windows)~/.config/opencode/opencode.json의agent키~/.config/opencode/agents/*.md
- 프로젝트 로컬: 프로젝트 루트
./opencode.json(또는./.opencode.json)의agent키./.opencode/agents/*.md
폴백 순서 (해상도 체인)
opencode가 에이전트를 찾는 순서는 다음과 같습니다. 가장 구체적인(로컬) 정의가 이깁니다.
1. .opencode/agents/에이전트명.md (프로젝트 로컬 Markdown) ← 최우선
2. opencode.json → agent.에이전트명 (프로젝트 로컬 JSON)
3. ~/.config/opencode/agents/에이전트명.md (글로벌 Markdown)
4. ~/.config/opencode/opencode.json → agent.에이전트명 (글로벌 JSON)
5. opencode 내장 에이전트 (General, Explore, Scout 등) ← 최후 폴백
이 순서가 왜 중요할까요? 예를 들어 글로벌에 범용 CodeReviewer를 두고, 특정 프로젝트에서는 해당 프로젝트의 코딩 컨벤션을 반영한 CodeReviewer.md를 오버라이드할 수 있습니다. 글로벌은 기본값, 로컬은 특수화 — 이 패턴이 12일차 도메인 특화 시스템의 기초가 됩니다.

실전 권장 — 어디에 무엇을
- 글로벌에 두기 좋은 것: 프로젝트에 무관한 범용 유틸리티 에이전트 (예:
Translator,DocWriter,GitHelper) - 프로젝트 로컬에 두기 좋은 것: 해당 코드베이스의 아키텍처·컨벤션·기술 스택에 특화된 에이전트 (예:
SpringReviewer,ReactOptimizer) - 양쪽에 두기 좋은 것: 글로벌에서 골격을 정의하고, 로컬에서 프롬프트만 오버라이드 (예: 글로벌
SecurityAuditor+ 로컬에서OWASP Top 10 중 이 프로젝트에서 특히 주의할 3가지추가)
default_agent — 대화의 첫 타석 결정
opencode를 실행하면 기본적으로 Build Primary 에이전트가 대화를 받습니다. 이걸 바꾸고 싶다면 default_agent를 설정합니다.
{
"$schema": "https://opencode.ai/schema.json",
"default_agent": "CodeReviewer",
"agent": {
"CodeReviewer": {
"model": "anthropic/claude-sonnet-4-1",
"description": "코드 리뷰 전문가",
"instructions": "..."
}
}
}
default_agent에 커스텀 에이전트 이름을 넣으면, opencode 시작 시 해당 에이전트가 Primary 역할로 대화를 시작합니다. 주의할 점은 이 설정이 Primary를 교체하는 것이 아니라, 기본 활성 에이전트를 지정하는 것이라는 점입니다. 내장 Primary(Build·Plan)는 여전히 존재하고, 사용자가 @Build로 전환할 수 있습니다.
이 기능은 특정 프로젝트에서 항상 특정 역할로 시작하고 싶을 때 유용합니다. 예를 들어 코드 리뷰만 전담하는 저장소라면 매번 @CodeReviewer를 부르는 대신 default_agent로 고정하면 됩니다.
실습 — 첫 커스텀 에이전트를 양쪽 방식으로 만들기
실제로 손에 잡히는 에이전트를 만들어봅시다. 시나리오: API 스펙 검증 에이전트 — REST API 코드를 읽고 OpenAPI 3.1 스펙과의 불일치를 찾아주는 에이전트입니다.
Step 1 — JSON으로 빠르게 프로토타이핑
프로젝트 루트의 opencode.json에 추가합니다.
{
"$schema": "https://opencode.ai/schema.json",
"agent": {
"APISpecChecker": {
"model": "anthropic/claude-sonnet-4-1",
"description": "REST API 구현 코드와 OpenAPI 스펙 파일을 비교하여 불일치를 찾는 검증 에이전트",
"instructions": "당신은 API 스펙 검증 전문가입니다. 사용자가 요청하면 코드의 라우트 핸들러와 OpenAPI(Swagger) YAML/JSON 스펙을 비교하여 불일치를 보고합니다. 경로·메서드·파라미터·응답 스키마·상태 코드를 체크합니다.",
"tools": ["Read", "Glob", "Grep", "Agent"],
"permission": {
"allow": ["Read", "Glob", "Grep"],
"deny": ["Write", "Edit", "Bash"]
}
}
}
}
이 상태로 opencode를 실행하면 이미 동작합니다. @APISpecChecker를 호출하거나, “API 스펙이랑 코드가 맞는지 확인해줘”라고 말하면 Primary가 description을 보고 자동 위임합니다.
Step 2 — Markdown으로 프롬프트 확장
프롬프트를 더 정교하게 만들고 싶어졌습니다. JSON의 instructions를 지우고, .opencode/agents/APISpecChecker.md를 만듭니다.
# .opencode/agents/APISpecChecker.md
---
model: anthropic/claude-sonnet-4-1
description: "REST API 구현 코드와 OpenAPI 스펙 파일을 비교하여 불일치를 찾는 검증 에이전트"
tools:
- Read
- Glob
- Grep
- Agent
permission:
allow:
- Read
- Glob
- Grep
deny:
- Write
- Edit
- Bash
---
당신은 API 스펙 검증 전문가입니다.
## 검증 대상
사용자가 요청하면 다음 두 소스를 비교합니다:
1. **구현 코드** — 라우트 핸들러, 컨트롤러, 미들웨어
2. **OpenAPI 스펙** — `openapi.yaml`, `openapi.json`, 또는 `swagger.*` 파일
## 검증 체크리스트
| 항목 | 검증 내용 |
|------|----------|
| 경로 | 스펙에 정의된 모든 경로가 코드에 구현되어 있는가? 반대는? |
| HTTP 메서드 | GET/POST/PUT/DELETE 등 메서드 일치 여부 |
| 경로 파라미터 | `{id}` 같은 path parameter 이름·타입 일치 |
| 쿼리 파라미터 | required/optional 구분, 기본값 일치 |
| 요청 바디 | Content-Type, 스키마 필드, 필수/선택 구분 |
| 응답 스키마 | 상태 코드별 응답 바디 스키마 일치 |
| 인증 | securitySchemes 와 실제 미들웨어 일치 |
## 보고 형식
```
## API 스펙 검증 결과
### 🔴 불일치 (Critical)
- **POST /users** — 스펙: 201 Created / 코드: 200 OK 반환
- **GET /orders/{id}** — 스펙에 존재하나 코드에 라우트 없음
### 🟡 주의 (Warning)
- **GET /products** — 쿼리 파라미터 `category` 스펙은 required, 코드는 optional
### ✅ 일치 확인
- GET /healthz, POST /auth/login, ...
```
## 제약
- 파일을 읽기만 하고 수정하지 않습니다.
- 스펙 파일이 없으면 사용자에게 위치를 물어봅니다.
- 스펙 버전(OpenAPI 2.0/3.0/3.1)을 먼저 확인하고 버전별 차이를 고려합니다.
JSON 한 줄이었던 프롬프트가 구조화된 체크리스트와 보고 형식 템플릿으로 확장됐습니다. 이 상태에서 opencode를 재시작하면, md 파일이 JSON 정의를 오버라이드하여 동일한 APISpecChecker가 더 정교한 프롬프트로 동작합니다.
Step 3 — 하이브리드 패턴 (권장)
실전에서 가장 깔끔한 패턴은 JSON에서는 agent 키를 제거하고, Markdown 파일만 사용하는 것입니다. 메타데이터와 프롬프트가 한 파일에 있으니 관리 포인트가 줄어듭니다.
반대로, 프롬프트가 짧은 유틸리티 에이전트(예: “이 코드의 라인 수를 세줘” 수준)는 JSON에만 한 줄로 두는 것이 오히려 효율적입니다.
정리하면:
- 프롬프트 5줄 이하 → JSON만 사용
- 프롬프트 5줄 초과 → Markdown 파일 사용
- 둘 다 존재 → Markdown이 오버라이드 (의도적 계층화가 아니라면 혼란 유발)
프롬프트 외부화 — {file:} 패턴 미리보기
JSON 방식에서도 프롬프트를 길게 쓸 수 있는 우회 방법이 있습니다. {file:} 패턴입니다.
{
"agent": {
"APISpecChecker": {
"model": "anthropic/claude-sonnet-4-1",
"description": "REST API 스펙 검증 에이전트",
"instructions": "{file:./prompts/api-spec-checker.md}",
"tools": ["Read", "Glob", "Grep"]
}
}
}
./prompts/api-spec-checker.md에 프롬프트 본문을 두면, opencode가 로딩 시 파일 내용으로 치환합니다. 이 패턴은 3일차(시스템 프롬프트 심화)에서 본격적으로 다루겠지만, 오늘 알아둘 것은 JSON과 Markdown 방식 모두에서 프롬프트 외부화가 가능하다는 점입니다.
실전 디렉토리 구조 예시
오늘 만든 것을 포함해, 프로젝트에 에이전트를 3개 세팅한 디렉토리 구조입니다.
my-project/
├── opencode.json # default_agent + 간단한 에이전트(JSON)
├── .opencode/
│ └── agents/
│ ├── APISpecChecker.md # 스펙 검증 (프롬프트 긴 버전)
│ ├── TestWriter.md # 테스트 작성 에이전트
│ └── DocGenerator.md # 문서 생성 에이전트
├── prompts/ # (선택) {file:} 외부화용
│ └── shared-conventions.md
└── src/
└── ...
opencode.json에는 default_agent와 간단한 유틸리티 에이전트만 두고, 프롬프트가 풍부한 에이전트는 .opencode/agents/에 Markdown으로 분리했습니다. 이 구조가 팀 협업에서도 git 충돌을 최소화하는 실전 패턴입니다.
동작 확인 — 에이전트가 잘 등록됐는지 체크
opencode를 실행한 뒤 다음과 같이 확인할 수 있습니다.
# opencode 실행 후 에이전트 목록 확인
# 대화창에서 @를 입력하면 등록된 에이전트 자동완성 목록이 표시됩니다.
# 또는 직접 호출 테스트:
@APISpecChecker 이 프로젝트의 API 스펙과 구현 코드를 비교해줘
등록이 정상이면 @ 입력 시 APISpecChecker가 자동완성 후보에 나타나고, 호출 시 Markdown에 정의한 시스템 프롬프트가 적용된 응답이 돌아옵니다. 만약 나타나지 않는다면 아래 Gotcha 코너를 확인하세요.
Gotcha 미니 코너 — 파일명 대소문자 함정
⚠️ .opencode/agents/의 파일명이 곧 에이전트 이름입니다. apispecchecker.md와 APISpecChecker.md는 다른 에이전트로 등록됩니다.
Windows에서는 파일 시스템이 대소문자를 구분하지 않아서 두 파일이 공존할 수 없지만, macOS/Linux에서는 공존 가능합니다. 팀원 중 OS가 다르면 “내 환경에선 되는데 네 환경에선 안 돼” 문제가 발생합니다.
해결책: 에이전트 파일명은 PascalCase로 통일하고, 팀 위키에 네이밍 규칙을 명시하세요. opencode.json의 agent 키 이름과 md 파일명을 정확히 일치시키는 것도 잊지 마세요.
# ✅ 좋은 예
.opencode/agents/APISpecChecker.md → 에이전트 이름: APISpecChecker
.opencode/agents/TestWriter.md → 에이전트 이름: TestWriter
# ❌ 나쁜 예
.opencode/agents/api-spec-checker.md → 에이전트 이름: api-spec-checker (호출 시 어색)
.opencode/agents/test_writer.md → 에이전트 이름: test_writer (컨벤션 불일치)
오늘 만든 것 정리
opencode.json의agent키로 에이전트를 JSON 선언 — 빠른 프로토타이핑.opencode/agents/APISpecChecker.md로 동일 에이전트를 Markdown 재정의 — 풍부한 프롬프트default_agent로 대화 시작 에이전트 지정- 글로벌 vs 로컬 폴백 순서 이해
아직 프롬프트 내용 자체는 “일단 동작하는” 수준입니다. 내일 3일차에서는 좋은 프롬프트와 나쁜 프롬프트의 실제 차이를 코드 리뷰 에이전트로 대조 실험하며, {file:} 외부화와 모델별 프롬프트 전략(Claude vs GPT)까지 다룹니다. 오늘 만든 에이전트의 프롬프트를 한 단계 끌어올리는 시간이 될 겁니다.
opencode 공식 문서의 에이전트 설정 가이드도 함께 참고하시면 오늘 내용이 더 선명해집니다.
◀ 이전 1화 (다음 차수는 아직 게시되지 않았습니다)