하네스 엔지니어링 완전 가이드 | Claude Code로 배우는 AI 에이전트 만들기
프롬프트만으로는 LLM을 제대로 다룰 수 없다. 도구·컨텍스트·제어 루프를 엮는 '하네스'를 실행 가능한 코드와 Claude Code의 설계로 차근차근 해부한다.
“ChatGPT에 프롬프트만 던지면 끝”이라는 시대는 끝났다. 2025년 이후 AI 엔지니어링의 중심은 빠르게 하네스 엔지니어링(harness engineering) 으로 옮겨가고 있다. Anthropic의 사내 블로그에서도, OpenAI의 에이전트 연구에서도 가장 자주 등장하는 키워드 중 하나다.
그런데 “하네스가 뭐냐”고 물으면 깔끔하게 대답할 수 있는 사람은 아직 드물다. 이 글에서는 실행 가능한 코드 와 Claude Code 자체의 설계 를 재료 삼아 하네스 엔지니어링을 차분히 풀어낸다. 다 읽고 나면 처음부터 내 에이전트를 조립할 수 있게 될 것이다.
하네스란 “AI를 감싸는 비계”다
“harness”는 원래 말 마구(馬具)나 안전벨트를 뜻한다. 소프트웨어 업계에서는 “테스트 하네스(test harness)“처럼 무언가를 돌리기 위한 바깥 구조를 가리킨다.
AI에서 하네스란 LLM을 감싸는 래퍼 계층이다. 조금 더 구체적으로 말하면, LLM이 현실 과제를 해결하기 위해 필요한 다음 요소들을 묶어낸 것이다.
- 도구들: 파일 읽기, 커맨드 실행, API 호출 등
- 컨텍스트 관리: 무엇을 기억하고, 무엇을 잊고, 무엇을 압축할지
- 제어 루프: 언제 부르고, 언제 멈추고, 언제 재시도할지
- 권한과 안전장치: 파괴적 작업이 멋대로 실행되지 않게 하는 장치
- 메모리: 세션을 넘어 유지되는 지식
프롬프트는 이 하네스에 넣는 한 개의 입력에 불과하다. 하네스가 빈약하면 아무리 영리한 프롬프트를 써도 성능이 천장에 부딪힌다. 최근 “프롬프트 엔지니어링만으로는 부족하다”는 말이 나오는 이유다.
하네스가 중요한 이유: OODA 루프로 생각하기
LLM 혼자서는 “다음 토큰을 생성하는 일”밖에 못 한다. 현실 과제를 풀려면 군사 전략의 OODA 루프(Observe → Orient → Decide → Act)를 돌려야 한다.
| 단계 | 내용 | 담당 |
|---|---|---|
| Observe (관찰) | 환경 상태 읽기 (파일 읽기, DB 질의) | 하네스 |
| Orient (상황 정리) | 얻은 정보를 정리해 LLM에 전달 | 하네스 |
| Decide (결정) | 다음에 무엇을 할지 결정 | LLM |
| Act (실행) | 결정 내용 실행 (커맨드, API 호출) | 하네스 |
보시다시피 네 단계 중 세 단계가 하네스 담당이다. LLM이 잘하는 건 Decide뿐이다. 이를 떠받치는 비계의 품질이 에이전트 전체의 품질을 결정한다.
실제 예로 이해하는 “세 단계의 하네스”
같은 “블로그 글을 생성해 줘”라는 과제를 3단계 하네스로 풀어본다.
레벨 1: 맨몸 API 호출 (하네스 거의 없음)
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const res = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 4096,
messages: [{ role: "user", content: "블로그 글을 써 줘" }],
});
console.log(res.content[0].text);
결과: 밋밋하고 알맹이 없는 텍스트가 돌아올 뿐. 주제도 구조도 매번 제각각이다.
레벨 2: 도구 제공 (중간 하네스)
const tools = [
{
name: "read_existing_posts",
description: "기존 블로그 글의 목록과 제목을 가져온다",
input_schema: { type: "object", properties: {} },
},
{
name: "write_post",
description: "MDX 파일을 기록한다",
input_schema: {
type: "object",
properties: {
slug: { type: "string" },
frontmatter: { type: "object" },
body: { type: "string" },
},
required: ["slug", "frontmatter", "body"],
},
},
];
async function runAgent(userGoal: string) {
let messages = [{ role: "user", content: userGoal }];
while (true) {
const res = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 4096,
tools,
messages,
});
if (res.stop_reason === "end_turn") break;
// 도구 호출은 하네스가 실행
const toolUse = res.content.find((c) => c.type === "tool_use");
const result = await executeTool(toolUse.name, toolUse.input);
messages.push({ role: "assistant", content: res.content });
messages.push({
role: "user",
content: [{ type: "tool_result", tool_use_id: toolUse.id, content: result }],
});
}
}
결과: 기존 글과 겹치지 않는 주제로, 올바른 형식의 MDX가 생성된다. 도구를 제공한 것만으로 품질이 크게 바뀐다.
레벨 3: Claude Code 급의 완전한 하네스
- 자동 루프 (사용자 승인, 오류 재시도)
- 컨텍스트 압축 (긴 대화를 요약해 토큰 절약)
- 서브에이전트 위임 (번역은 별도 컨텍스트에서)
- 프롬프트 캐싱 (고정 부분은 재전송하지 않음)
- Hooks (커밋 전에 자동 lint)
이걸 전부 직접 짜는 건 만만치 않다. 그래서 Claude Code를 “구현 참고서”로 삼아 연구할 가치가 있다.
Claude Code의 하네스 구조 분해
Claude Code는 Anthropic 내부에서 가장 잘 다듬어진 에이전트 하네스다. 다음 5개 계층으로 이해할 수 있다.
1계층: 도구 설계
Read, Edit, Write, Bash, Glob, Grep, Agent 같은 도구가 기본 제공된다. 주목할 점은 도구의 세분화다.
Grep은 단순한grep이 아닌 ripgrep 래퍼. 정확하고 빠르다Edit은 전체 파일 재작성이 아니라 특정 문자열 치환. 최소 diffAgent는 서브에이전트를 스폰하여 컨텍스트를 격리
도구 품질은 곧 에이전트 품질로 직결된다. “돌아가기만 하면 된다”로는 부족하다. 멱등성, 명확한 오류 메시지, 최소 책임을 의식하여 설계해야 한다.
2계층: 컨텍스트의 계층화
~/.claude/CLAUDE.md ← 전역 규약
./CLAUDE.md ← 프로젝트 규약 (자동 로드)
~/.claude/memory/ ← 장기 기억 (여러 세션 가로질러)
├── user_profile.md
├── feedback_xxx.md
└── project_xxx.md
대화 기록 ← 최근 주고받음
작업/계획 ← 현재 세션의 진행 상황
수명과 역할이 다르므로, 기록 위치를 잘못 고르면 정보가 금방 사라지거나 반대로 낡은 정보가 계속 남는다. “이 세션만”은 작업에, “여러 번 쓰일 것”은 메모리에 넣어서 구분하자.
3계층: 서브에이전트 위임
Agent 도구로 별도 컨텍스트의 에이전트를 스폰할 수 있다.
# 메인은 지시만, 고된 작업은 subagent에게
Agent(
subagent_type: "general-purpose",
prompt: "blog/harness.mdx를 영어 + 8개 언어로 번역하여
각 blog-{lang}/에 저장하고 보고해 줘"
)
이렇게 하면 메인 컨텍스트가 상세 로그로 더럽혀지지 않는다. 오래 걸리는 빌드 로그, 번역 중간 결과, 검색 결과처럼 “완성물만 필요한 작업”은 통째로 맡길 수 있다.
4계층: Hooks (결정론적 처리)
.claude/settings.json에서 도구 호출 전후에 셸 커맨드를 끼워 넣을 수 있다.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "npx tsc --noEmit" }
]
}
]
}
}
파일 편집 후 자동으로 타입 체크가 실행된다. “매번 LLM에 부탁할 게 아니라 결정론적으로 처리해야 할 일” 은 훅으로 흡수하는 게 정석이다.
5계층: 권한 모드
{
"permissions": {
"allow": ["Read", "Grep", "Glob"],
"deny": ["Bash(rm -rf*)", "Bash(git push --force*)"],
"ask": ["Write", "Edit", "Bash"]
}
}
파괴적 커맨드는 명시적으로 거부하고, 쓰기 작업은 승인받게 한다. 사고는 “멋대로 실행된 순간”에 생긴다. 그러니 이 계층 설계가 운영 안전성을 결정한다.
함정 5가지
1. 도구를 너무 많이 준다 30개씩 도구를 주면 모델이 선택을 헤매고 정확도가 떨어진다. 5~15개로 좁히는 게 경험칙이다. 모자란 기능은 서브에이전트 쪽에 맡기자.
2. 프롬프트 캐시를 살리지 못한다
Claude API의 cache_control을 쓰지 않으면 긴 system prompt를 매번 통째로 보내 비용이 치솟는다. 5분 TTL을 의식하고 변하지 않는 부분은 캐시에.
messages: [{
role: "system",
content: [
{ type: "text", text: longStaticInstructions,
cache_control: { type: "ephemeral" } }, // ← 이 줄
{ type: "text", text: dynamicContext },
],
}]
3. 오류 메시지가 LLM이 읽을 수 없는 형태
도구가 Error: undefined만 반환하면 모델이 자가 복구하지 못한다. “무엇이 잘못됐고 어떻게 고치면 되는지” 까지 써야 한다.
throw new Error(
`파일 '${path}'이(가) 존재하지 않습니다. ` +
`현재 scripts/ 디렉터리의 파일 목록: ${list.join(", ")}`
);
4. 사람 승인을 건너뛴다 파괴적 작업(삭제, force push, DB 업데이트)을 자동 승인하면 언젠가 반드시 사고가 난다. “쓰기는 ask, 삭제는 deny” 를 기본값으로.
5. 메모리를 정리하지 않는다
시간이 지나 낡아버린 정보가 남으면 에이전트가 잘못된 전제로 움직인다. 메모리도 정기적인 단사리가 필요하다(Claude Code라면 /compact나 수동 편집).
직접 만드는 미니 하네스 돌려 보기
마지막으로 로컬에서 돌릴 수 있는 최소 하네스를 Node.js + TypeScript로.
// mini-harness.ts
import Anthropic from "@anthropic-ai/sdk";
import { readFileSync, writeFileSync } from "fs";
const client = new Anthropic();
const tools = [
{ name: "read_file",
description: "텍스트 파일 읽기",
input_schema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
{ name: "write_file",
description: "텍스트 파일 쓰기",
input_schema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } },
];
const executors = {
read_file: ({ path }) => readFileSync(path, "utf-8"),
write_file: ({ path, content }) => { writeFileSync(path, content); return `written ${path}`; },
};
async function loop(goal: string, maxSteps = 10) {
const messages: any[] = [{ role: "user", content: goal }];
for (let i = 0; i < maxSteps; i++) {
const res = await client.messages.create({
model: "claude-opus-4-6", max_tokens: 4096, tools, messages,
});
messages.push({ role: "assistant", content: res.content });
if (res.stop_reason === "end_turn") return res.content;
const toolUse = res.content.find((c: any) => c.type === "tool_use") as any;
if (!toolUse) return res.content;
const result = executors[toolUse.name](toolUse.input);
messages.push({
role: "user",
content: [{ type: "tool_result", tool_use_id: toolUse.id, content: String(result) }],
});
}
}
await loop("README.md를 읽고 TL;DR.md에 3줄 요약으로 저장해 줘");
이것만으로 “기존 파일을 읽고 새 파일을 기록” 하는 미니 에이전트가 완성된다. 여기에 Grep 도구, Bash 도구, Agent 도구를 더해 나가면 Claude Code 축소판이 된다.
정리: 프롬프트 작가에서 하네스 설계자로
| 종래 관점 | 앞으로의 관점 |
|---|---|
| 좋은 프롬프트 = 좋은 출력 | 좋은 하네스 = 좋은 출력 |
| 모델 고르기 | 모델 + 도구 + 컨텍스트 + 권한 설계 |
| 단발성 질문 | 지속적인 루프 운영 |
Claude Code는 이런 관점 전환을 체감하기에 최적의 교재다. 쓰는 데서 그치지 말고, 구조를 분해해 자신의 에이전트에 녹여라. 2026년 이후 AI 엔지니어에게 요구되는 자세다.
먼저 위의 미니 하네스를 복사해 실행해 보자. 10분 뒤에는 당신만의 에이전트의 첫걸음을 뗄 수 있다.
관련 글
참고 자료
Claude Code 워크플로우를 한 단계 업그레이드하세요
지금 바로 Claude Code에 복사해 쓸 수 있는 검증된 프롬프트 템플릿 50선.
무료 PDF: 5분 완성 Claude Code 치트시트
이메일 주소만 등록하시면 A4 한 장짜리 치트시트 PDF를 즉시 보내드립니다.
개인정보는 엄격하게 관리하며 스팸은 보내지 않습니다.
이 글을 작성한 사람
Masa
Claude Code를 적극 활용하는 엔지니어. 10개 언어, 2,000페이지 이상의 테크 미디어 claudecode-lab.com을 운영 중.
관련 글
Claude Code 보안 완전 가이드: API 키 관리, 권한 설정, 프로덕션 보호
Claude Code를 안전하게 사용하기 위한 실전 보안 가이드. API 키 관리부터 권한 설정, Hooks 기반 자동화, 프로덕션 환경 보호까지 — 바로 동작하는 코드 예제와 함께 설명합니다.
Claude Code 보안 실패 사례 7선 | 실제 발생한 사고와 방지책
Claude Code에서 실제로 발생한 보안 사고 7가지: .env 유출, 운영 DB 실수 삭제, 과금 폭발 등 — 각 사례별 원인과 재발 방지 코드를 상세히 해설합니다.
Claude Code 권한 설정 완벽 가이드 | settings.json·Hooks·allowlist 철저 해설
Claude Code 권한 설정 완벽 해설. allow/deny/ask 구분 사용법, Hooks를 활용한 자동화, 환경별 settings.json, 실전 패턴 모음까지 동작하는 코드로 소개합니다.