Tips & Tricks (업데이트: 2026. 6. 3.)

하네스 엔지니어링 완전 가이드 | Claude Code로 배우는 AI 에이전트 만들기

Claude Code 패턴으로 harness engineering을 배우고 Node.js 하네스, policy JSON, 실제 workflow를 구현합니다.

하네스 엔지니어링 완전 가이드 | Claude Code로 배우는 AI 에이전트 만들기

좋은 프롬프트 하나로 AI 에이전트를 운영할 수 있다는 생각은 이제 부족합니다. 2026년에 더 중요해진 것은 harness engineering입니다. 여기서 harness는 초보자에게 “에이전트의 발판(agent scaffold)”이라고 설명할 수 있고, 소프트웨어 테스트의 test harness처럼 모델이 실제 작업을 수행하도록 둘러싼 실행 구조를 뜻합니다.

Claude Code는 이 개념을 배우기 좋은 사례입니다. 단순한 채팅창이 아니라 파일 읽기, 편집, shell 실행, CLAUDE.md, hooks, 권한 모드, subagent, 장기 메모리를 묶은 작업 환경이기 때문입니다. 이 글에서는 왜 harness engineering이 주목받는지, Node.js로 실행 가능한 mini harness와 policy JSON을 어떻게 만드는지, 그리고 content automation, code review, SaaS integration, cloud operations, security boundaries라는 다섯 use case에 어떻게 적용하는지 설명합니다.

왜 지금 harness engineering인가

LLM은 다음 행동을 “결정”하는 데 강합니다. 하지만 실제 업무에는 환경을 읽고, 필요한 정보만 추리고, 도구를 호출하고, 실패를 해석하고, 위험한 행동을 막고, 결과를 검증하는 과정이 필요합니다. 모델만으로는 이 전체 workflow를 안전하게 운영하기 어렵습니다.

Claude Code가 강한 이유도 모델만이 아닙니다. Claude Agent SDK 문서는 Claude Code 스타일의 프로젝트 지시, skills, hooks, permissions를 읽을 수 있다고 설명합니다. 권한 문서는 allow, deny, permission mode, runtime callback을 다룹니다. prompt caching 문서는 긴 고정 컨텍스트를 재사용하는 방법을 설명합니다. 이 모든 것이 harness의 영역입니다.

OODA loop로 보면 역할이 분명해집니다.

단계내용담당
Observe파일, 로그, URL, ticket, API 상태 읽기Harness
Orient정보를 압축하고 컨텍스트로 정리Harness
Decide다음 행동 선택LLM
Act도구 실행, 파일 작성, API 호출, 중지Harness

네 단계 중 세 단계가 대부분 harness의 책임입니다. 그래서 prompt engineering만으로는 한계가 생깁니다.

harness에 들어가야 할 것

실무용 harness는 시작 전에 네 가지를 정해야 합니다. 무엇을 읽을 수 있는가. 어떤 산출물을 만들어야 하는가. 무엇으로 성공을 검증할 것인가. 어떤 행동은 자동, ask-first, 금지로 나눌 것인가.

블로그 workflow라면 “글 써줘”가 아니라 기존 slug 확인, 중복되지 않는 주제 선택, MDX 작성, frontmatter 확인, code fence 검사, 공식 링크와 내부 링크 추가, /products//training/ CTA 삽입, build, 공개 URL 확인까지가 harness입니다. 프롬프트는 그중 하나의 입력일 뿐입니다.

계층pitfall
컨텍스트프로젝트 규칙, 스타일, 과거 실패오래된 전제가 계속 남음
도구read, grep, write, test, API 호출도구가 너무 많거나 넓음
정책allow, ask, deny, sandbox파괴적 행동이 무감독 실행
검증test, diff, screenshot, 공개 URL그럴듯하지만 깨진 결과
메모리반복되는 선호와 결정임시 메모가 영구 규칙화

개념도

harness는 모델 앞뒤에 있는 제어 계층입니다. prompt도 중요하지만 policy, context, tools, permission gate, verification loop가 workflow의 안정성을 결정합니다.

flowchart LR
  A["Goal"] --> B["Harness policy"]
  B --> C["Context"]
  B --> D["Tools"]
  B --> E["Permissions"]
  C --> F["LLM decision"]
  D --> F
  E --> G["Safe action"]
  F --> G
  G --> H["Verification"]
  H --> I["Artifact"]
  H --> B

실행 가능한 Node.js mini harness

아래 예제는 모델, 두 개의 도구, policy, loop, 경로 제한, 읽기 쉬운 오류를 포함합니다. 먼저 ANTHROPIC_API_KEY를 설정한 뒤 임시 폴더에서 실행합니다.

mkdir harness-demo
cd harness-demo
npm init -y
npm install @anthropic-ai/sdk
node -e "const fs=require('node:fs');fs.mkdirSync('sandbox',{recursive:true});fs.writeFileSync('sandbox/README.md','# Demo\nShip a safer agent workflow.\nKeep writes inside sandbox.\n');"

policy.json을 저장합니다.

{
  "workspace": "./sandbox",
  "maxSteps": 6,
  "tools": {
    "read_file": {
      "allow": true,
      "risk": "Read UTF-8 text only inside workspace"
    },
    "write_file": {
      "allow": true,
      "risk": "Write UTF-8 text only inside workspace"
    }
  }
}

mini-harness.mjs를 저장합니다.

import Anthropic from "@anthropic-ai/sdk";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";

const client = new Anthropic();
const policy = JSON.parse(await readFile(new URL("./policy.json", import.meta.url), "utf8"));
const model = process.env.ANTHROPIC_MODEL || "claude-sonnet-4-6";
const workspace = path.resolve(policy.workspace);

function safePath(requestedPath) {
  const resolved = path.resolve(workspace, requestedPath);
  const inside = resolved === workspace || resolved.startsWith(workspace + path.sep);
  if (!inside) {
    throw new Error(`Path escapes workspace: ${requestedPath}. Use a path under ${policy.workspace}.`);
  }
  return resolved;
}

function ensureAllowed(toolName) {
  const rule = policy.tools?.[toolName];
  if (!rule?.allow) {
    throw new Error(`Tool '${toolName}' is not allowed by policy.json.`);
  }
}

const tools = [
  {
    name: "read_file",
    description: "Read a UTF-8 text file from the allowed workspace.",
    input_schema: {
      type: "object",
      properties: { path: { type: "string" } },
      required: ["path"],
      additionalProperties: false
    }
  },
  {
    name: "write_file",
    description: "Write a UTF-8 text file inside the allowed workspace.",
    input_schema: {
      type: "object",
      properties: {
        path: { type: "string" },
        content: { type: "string" }
      },
      required: ["path", "content"],
      additionalProperties: false
    }
  }
];

async function executeTool(name, input) {
  ensureAllowed(name);
  if (name === "read_file") {
    return await readFile(safePath(input.path), "utf8");
  }
  if (name === "write_file") {
    const target = safePath(input.path);
    await mkdir(path.dirname(target), { recursive: true });
    await writeFile(target, input.content, "utf8");
    return `written ${input.path}`;
  }
  throw new Error(`Unknown tool: ${name}`);
}

async function run(goal) {
  const messages = [{ role: "user", content: goal }];

  for (let step = 0; step < policy.maxSteps; step++) {
    const response = await client.messages.create({
      model,
      max_tokens: 1200,
      tools,
      system: "You are a careful file assistant. Use tools when needed. Keep writes under policy workspace.",
      messages
    });

    messages.push({ role: "assistant", content: response.content });
    const toolUses = response.content.filter((block) => block.type === "tool_use");

    if (toolUses.length === 0) {
      const text = response.content
        .filter((block) => block.type === "text")
        .map((block) => block.text)
        .join("\n");
      console.log(text);
      return;
    }

    const results = [];
    for (const toolUse of toolUses) {
      try {
        const output = await executeTool(toolUse.name, toolUse.input);
        results.push({ type: "tool_result", tool_use_id: toolUse.id, content: String(output).slice(0, 8000) });
      } catch (error) {
        results.push({
          type: "tool_result",
          tool_use_id: toolUse.id,
          is_error: true,
          content: error instanceof Error ? error.message : String(error)
        });
      }
    }
    messages.push({ role: "user", content: results });
  }

  throw new Error(`Max steps reached: ${policy.maxSteps}`);
}

const goal = process.argv.slice(2).join(" ") || "Read README.md and write summary.md with three bullet points.";
await run(goal);

실행합니다.

node mini-harness.mjs

작은 예제지만 핵심은 모두 들어 있습니다. tool schema, policy, sandbox 경로, 최대 step, 읽을 수 있는 오류, 산출물입니다. 여기에 grep, test, approval UI, SaaS API, hooks를 붙이면 Claude Code에 가까운 구조가 됩니다.

다섯 가지 use case

1. content automation 약한 프롬프트는 “블로그 글을 써줘”입니다. 강한 harness는 기존 글을 읽고, 중복되지 않는 주제를 고르고, MDX를 작성하고, frontmatter와 code fence를 검사하고, 공식 링크와 내부 링크를 넣고, /products//training/ CTA를 추가하고, build와 공개 URL 확인까지 수행합니다. pitfall은 얕은 번역과 중복 글을 빠르게 양산하는 것입니다.

2. code review review harness는 git diff, 테스트 결과, 변경 파일, 프로젝트 리뷰 규칙을 읽고 findings-first 형식을 강제합니다. risk는 모델이 문제를 찾지 않고 변경 요약만 하는 것입니다. 그래서 severity, 파일 참조, 재현 가능성, 테스트 공백을 요구해야 합니다.

3. SaaS integration Notion, HubSpot, Stripe, CRM 같은 SaaS는 read-only, dry-run, approved write를 분리해야 합니다. 예를 들어 상담 리드를 분류하고 CRM 업데이트 초안을 만들되, 실제 쓰기는 사람 승인 후에만 수행합니다. pitfall은 오분류된 고객 정보나 청구 변경을 생산 시스템에 즉시 쓰는 것입니다.

4. cloud operations 클라우드 운영은 deploy 명령 하나로 끝나지 않습니다. 환경 변수, build, diff, 대상 환경, rollback 계획, health endpoint, 공개 URL을 확인해야 합니다. risk는 로그의 마지막 줄만 보고 잘못된 원인을 고치는 것입니다. 재시도 상한과 로그 요약이 필요합니다.

5. security boundaries 보안 경계는 나중에 붙이는 장식이 아닙니다. Read는 넓어도 Write는 workspace 안으로 제한하고, shell은 allow-list로 묶고, rm, force push, production DB, billing, secret 접근은 deny 또는 ask-first로 둡니다. harness는 모델을 불신하기 위한 장치가 아니라, 과신하지 않기 위한 장치입니다.

Claude Code에서 빌릴 설계

첫째, 컨텍스트를 계층화합니다. 안정적인 프로젝트 규칙은 CLAUDE.md나 동등한 설정에, 이번 세션의 진행은 plan에, 장기 선호는 memory에 둡니다.

둘째, deterministic check는 hooks로 빼냅니다. format, lint, test, 링크 검사, screenshot 확인은 명령이 해야 합니다. Claude는 실패를 해석하고 수정을 제안하면 됩니다.

셋째, 큰 작업은 격리합니다. 긴 로그, 넓은 검색, 다국어 번역, 큰 리팩터링은 subagent나 별도 workflow 단계로 분리합니다. 메인 컨텍스트에는 결정만 남기는 편이 안정적입니다.

흔한 pitfall

도구가 너무 많으면 선택 정확도가 떨어집니다. 처음에는 5~10개의 작고 분명한 도구로 시작하세요.

오류가 읽기 어려우면 자기수정이 불가능합니다. Error: failed 대신 무엇이 없고 어디를 봐야 하는지 알려야 합니다.

prompt caching을 쓰지 않으면 긴 고정 지시를 매번 보내게 됩니다. 정적 컨텍스트와 동적 컨텍스트를 분리하세요.

검증이 없으면 그럴듯한 산출물이 그대로 배포됩니다. 글에는 frontmatter와 code fence 검사, 코드에는 테스트, 클라우드에는 health check, SaaS에는 audit log가 필요합니다.

권한은 쉽게 넓어집니다. 임시 편의를 영구 위험으로 만들지 않도록 allow, ask, deny를 정기적으로 점검해야 합니다.

다음 단계

먼저 안전 경계를 세우려면 Claude Code 권한 가이드를 읽으세요. 프로젝트 지식을 정리하려면 CLAUDE.md 베스트 프랙티스가 좋습니다. 큰 작업을 나누려면 Claude Code subagent 패턴, 비용을 줄이려면 Claude Code 토큰 최적화를 함께 보세요.

빠른 참고 자료로는 무료 Claude Code Quick Reference Cheatsheet가 유용합니다. 템플릿과 플레이북은 /products/에서 확인할 수 있습니다. 팀의 권한, 리뷰 게이트, 검증, 수익 workflow까지 같이 설계해야 한다면 /training/에서 상담을 시작하세요.

실제로 확인한 점

ClaudeCodeLab의 글 운영에 이 패턴을 적용해 보면, harness의 가치는 “AI가 더 멋진 문장을 쓰는 것”보다 “실패가 보이는 것”에 있습니다. 본문 길이, code fence, frontmatter, 링크, CTA, 공개 URL을 각각 검증하면 출력물을 믿는 문제가 아니라 어느 gate에서 멈췄는지 보는 문제가 됩니다.

요약

Harness engineering은 모델이 무엇을 보고, 무엇을 실행하고, 어디서 멈추고, 어떻게 검증되는지 설계하는 기술입니다. Claude Code는 이 구조를 실제로 체험할 수 있는 좋은 교재입니다. 위 mini harness를 실행한 뒤, 자신의 use case에 경계 하나와 검증 하나를 추가하는 것부터 시작하세요.

참고 링크

#claude-code #agent-sdk #harness #prompt-engineering #llm #anthropic
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.