Harness Engineering이란? Codex 사례로 배우는 안전한 AI 에이전트 설계
OpenAI의 Codex 사례부터 파일 권한, 사람의 승인, 실행 가능한 테스트까지 Harness Engineering을 초보자 눈높이에서 설명합니다.
AI 에이전트에게 “저장소를 정리해 줘”라고 했더니 관계없는 설정까지 바뀌고, “테스트한 뒤 알려 줘”라고 했더니 실행한 명령도 없이 성공했다는 답만 돌아올 때가 있습니다. 개발 업무를 처음 AI에 맡기는 팀에서 흔히 겪는 문제입니다.
원인은 프롬프트 하나에만 있지 않습니다. 에이전트가 무엇을 볼 수 있는지, 어떤 도구를 쓸 수 있는지, 어디에서 멈춰야 하는지, 무엇을 완료로 인정할지가 정해지지 않았기 때문입니다. **Harness Engineering(에이전트가 일할 수 있도록 발판과 안전장치를 설계하는 일)**은 바로 이 바깥 환경을 만드는 작업입니다.
이 글은 OpenAI가 공개한 Codex 사례로 개념을 확인한 뒤, 파일 접근 범위를 제한하고 덮어쓰기를 막는 작은 Harness를 구현합니다. 마지막에는 API 비용 없이 실행할 수 있는 Node.js 테스트와 실제 검증 범위를 분명히 구분합니다.
핵심 요약
- Harness는 단순한 래퍼 스크립트가 아니라 저장소 지식, 도구, 권한, 테스트, 로그, 복구 경로, 사람의 승인을 묶은 시스템입니다.
- OpenAI의 Codex 사례는 저장소 구조와 애플리케이션 상태, 품질 규칙을 에이전트가 읽고 실행할 수 있게 만드는 일이 왜 필요한지 보여 줍니다.
- 검색, 초안 작성, 반복 실행은 AI에 맡길 수 있지만 삭제, 운영 환경 변경, 외부 전송, 비용 지출은 사람이 판단해야 합니다.
- 문자열 경로 검사만으로는 완전한 샌드박스가 되지 않습니다. 심볼릭 링크, 덮어쓰기, 프로세스 권한, 운영체제 격리까지 고려해야 합니다.
- “검증 완료”라고 쓰려면 명령, 결과, 검증 범위를 함께 남겨야 합니다. 모델의 성공 메시지는 증거가 아닙니다.
Harness Engineering을 쉽게 이해하기
프롬프트가 “이번 실행에서 원하는 일”을 말한다면, Harness는 “그 일을 어떤 환경과 규칙 안에서 시도할지”를 정합니다.
| 구성 요소 | 답해야 할 질문 | 최소 예시 |
|---|---|---|
| 컨텍스트 | 에이전트가 무엇을 알 수 있는가? | AGENTS.md, 제한된 디렉터리, 버전 관리되는 명세 |
| 도구 | 무엇을 실행할 수 있는가? | 읽기, 테스트, 초안 생성 |
| 권한 | 어디에서 반드시 멈춰야 하는가? | 삭제나 전송 전 사람의 승인 |
| 검증 | 무엇을 완료로 인정하는가? | npm test가 종료 코드 0으로 끝남 |
| 관찰 가능성 | 실패 원인을 어떻게 찾는가? | 명령, diff, 핵심 오류 로그 |
| 복구 | 잘못된 실행을 어떻게 되돌리는가? | 작은 커밋, dry run, 롤백 절차 |
모델은 이 순환 구조의 한 부품일 뿐입니다.
사람의 목표와 승인 정책
↓
저장소 규칙 → AI 에이전트 → 허용된 도구
↑ ↓
명세와 문서 ← 테스트, 로그, 변경 내역
└── 실패하면 보완하고, 판단이 필요하면 사람에게 넘김 ──┘
설계 결정이 누군가의 머릿속에만 있다면 더 강한 모델로 바꿔도 알 수 없습니다. 반대로 필요한 지식을 찾기 쉬운 위치에 두고 합격 조건을 실행 가능한 검사로 만들면, 같은 모델도 훨씬 안정적으로 일합니다.
2026년에 주목받은 이유: OpenAI의 Codex 사례
현재의 관심을 키운 중요한 자료가 OpenAI가 2026년 2월 11일 공개한 Harness engineering: leveraging Codex in an agent-first world입니다.
OpenAI의 설명에 따르면 이 실험은 빈 Git 저장소에서 시작했습니다. 약 5개월 뒤에는 엔지니어 3명이 Codex를 운용해 약 1,500개의 Pull Request를 열고 병합했으며, 저장소는 약 100만 줄 규모가 되었습니다. 애플리케이션 코드뿐 아니라 테스트, CI 설정, 문서, 관찰 도구와 내부 개발 도구도 Codex가 작성했고, 실제 내부 사용자와 외부 알파 테스터가 제품을 사용했습니다.
중요한 교훈은 1,500이라는 숫자 자체가 아닙니다. 이 사례가 어떤 프로젝트에서나 같은 결과를 보장하는 것도 아닙니다. OpenAI도 기능 전체를 맡기는 자율성이 해당 저장소의 구체적인 구조와 도구에 크게 의존하며, 같은 투자 없이 일반화해서는 안 된다고 밝혔습니다.
공개된 운영 방식에는 다음이 포함됩니다.
- 계획과 설계 결정을 버전 관리되는 저장소 문서로 남김
- UI, 로그, 메트릭, 트레이스를 에이전트가 직접 살펴볼 수 있게 함
- 의존성 방향과 같은 불변 규칙을 구조 테스트와 자체 lint로 강제함
- 실패했을 때 “더 열심히 해 봐”라고 하기보다 부족한 도구, 규칙, 추상화를 찾음
- 오래된 문서와 구조적 드리프트를 정기적으로 찾아 정리함
또 하나의 핵심은 에이전트에게 1,000쪽짜리 설명서를 넣는 대신 지도를 주는 것입니다. 짧은 AGENTS.md가 목차 역할을 하고, 세부 지식은 주제별 문서와 실행 가능한 검사로 나눕니다. 이렇게 해야 컨텍스트를 낭비하지 않으면서도 문서의 오래된 부분을 기계적으로 찾을 수 있습니다.
Claude Code에서도 같은 원리를 적용할 수 있습니다. 공식 Claude Agent SDK hooks 문서에 따르면 hook은 도구 요청을 검사하고 거부하거나, 입력을 바꾸거나, 감사 로그를 남길 수 있습니다. 제품마다 제어 방식은 달라도 에이전트 바깥에서 경계와 피드백 순환을 만든다는 점은 같습니다.
AI에 맡길 일과 사람이 판단할 일
처음부터 모든 권한을 주지 마세요. 되돌리기 쉬운 작업부터 자동화하고 고객, 비용, 운영 환경에 영향을 주는 행동은 승인 뒤에 두는 편이 안전합니다.
| 먼저 자동화하기 좋은 일 | 조건을 갖춘 뒤 위임할 일 | 사람이 결정할 일 |
|---|---|---|
| 파일 검색 | 기존 파일 수정 | 운영 데이터 삭제 |
| 테스트 실행 | 의존성 추가 | 고객 이메일 전송 |
| diff 요약 | 스테이징 배포 | 결제나 계약 변경 |
| 초안 생성 | 브랜치 push | 민감한 개인정보 처리 |
판단 기준은 두 가지입니다. 문제가 생겼을 때 값싸게 되돌릴 수 있는가, 그리고 팀 밖의 사람에게 영향을 주는가? 처음에는 읽기와 임시 출력만 허용합니다. 성공과 실패를 모두 관찰하고 재현할 수 있게 된 다음, 검증된 작업만 자동 실행으로 올립니다.
최소 Harness 구현하기
아래 예제에서 모델에게 주는 기능은 두 가지뿐입니다.
sandbox안의 텍스트 파일 읽기sandbox안에 새로운 텍스트 파일 만들기
삭제, 덮어쓰기, Shell, 네트워크 도구는 제공하지 않습니다. 예제는 Node.js 22에서 확인했으며, SDK 버전도 검증에 사용한 버전으로 고정합니다.
mkdir harness-demo
cd harness-demo
npm init -y
npm install @anthropic-ai/[email protected]
mkdir sandbox
echo "# meeting notes" > sandbox/note.md
policy.json을 만듭니다.
{
"workspace": "./sandbox",
"maxSteps": 6,
"maxToolResultChars": 4000
}
1. 코드로 파일 경계를 강제하기
safe-files.mjs를 만듭니다. candidate.startsWith(root) 같은 검사 하나만으로는 부족합니다. 비슷한 이름의 디렉터리가 잘못 일치할 수 있고, 작업 폴더 안의 심볼릭 링크가 외부를 가리킬 수도 있습니다. 아래 코드는 읽기 전에 실제 경로를 다시 확인하고, 쓰기는 새 파일 생성만 허용합니다.
import { open, readFile, realpath } from "node:fs/promises";
import path from "node:path";
function assertInside(root, candidate) {
if (candidate !== root && !candidate.startsWith(root + path.sep)) {
throw new Error(`outside workspace: ${candidate}`);
}
}
export async function createFileGate(workspace) {
const root = await realpath(path.resolve(workspace));
async function readText(relativePath) {
const requested = path.resolve(root, relativePath);
assertInside(root, requested);
const actual = await realpath(requested);
assertInside(root, actual);
return readFile(actual, "utf8");
}
async function createText(relativePath, content) {
const requested = path.resolve(root, relativePath);
assertInside(root, requested);
const actualParent = await realpath(path.dirname(requested));
assertInside(root, actualParent);
let handle;
try {
handle = await open(requested, "wx", 0o600);
await handle.writeFile(content, "utf8");
} catch (error) {
if (error.code === "EEXIST") {
throw new Error(`refusing to overwrite: ${relativePath}`);
}
throw error;
} finally {
await handle?.close();
}
return "created";
}
return { readText, createText };
}
이 코드는 애플리케이션 수준의 방어선이지 완전한 보안 경계는 아닙니다. 더 강한 격리가 필요하면 컨테이너, 가상 머신, 운영체제 권한 또는 제품의 샌드박스를 함께 사용해야 합니다. 애플리케이션 검사는 관리자 권한으로 실행되는 프로세스까지 막지 못합니다.
2. 모델에는 두 도구만 공개하기
agent.mjs를 만듭니다. 계정별 접근 권한과 모델 제공 상태가 달라질 수 있으므로 모델 이름을 글에 고정하지 않고 ANTHROPIC_MODEL로 받습니다.
import Anthropic from "@anthropic-ai/sdk";
import { readFile } from "node:fs/promises";
import { createFileGate } from "./safe-files.mjs";
const model = process.env.ANTHROPIC_MODEL;
if (!model) throw new Error("Set ANTHROPIC_MODEL to a model available to your account.");
const policy = JSON.parse(await readFile("./policy.json", "utf8"));
const gate = await createFileGate(policy.workspace);
const client = new Anthropic();
const tools = [
{
name: "read_file",
description: "Read a UTF-8 text file inside the workspace",
input_schema: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
additionalProperties: false
}
},
{
name: "create_file",
description: "Create a new UTF-8 file; existing files cannot be overwritten",
input_schema: {
type: "object",
properties: {
path: { type: "string" },
content: { type: "string" }
},
required: ["path", "content"],
additionalProperties: false
}
}
];
async function runTool(name, input) {
if (name === "read_file") return gate.readText(input.path);
if (name === "create_file") return gate.createText(input.path, input.content);
throw new Error(`unknown tool: ${name}`);
}
const prompt = process.argv.slice(2).join(" ") ||
"Read note.md and create summary.md with a three-line summary.";
const messages = [{ role: "user", content: prompt }];
for (let step = 0; step < policy.maxSteps; step += 1) {
const response = await client.messages.create({
model,
max_tokens: 1200,
system: "Use only the supplied tools. Never claim a file was created unless the tool succeeded.",
tools,
messages
});
messages.push({ role: "assistant", content: response.content });
const calls = response.content.filter((block) => block.type === "tool_use");
if (calls.length === 0) {
console.log(response.content.find((block) => block.type === "text")?.text ?? "done");
process.exit(0);
}
const results = [];
for (const call of calls) {
try {
const value = await runTool(call.name, call.input);
results.push({
type: "tool_result",
tool_use_id: call.id,
content: String(value).slice(0, policy.maxToolResultChars)
});
} catch (error) {
results.push({
type: "tool_result",
tool_use_id: call.id,
is_error: true,
content: error.message
});
}
}
messages.push({ role: "user", content: results });
}
throw new Error(`step limit exceeded: ${policy.maxSteps}`);
중요한 점은 프롬프트의 길이가 아닙니다. 모델이 삭제 도구를 아예 받지 않았고, 기존 파일 덮어쓰기도 파일 게이트가 거부한다는 점입니다. 모델이 note.md를 바꾸려고 해도 도구 오류가 반환되며, 정해진 단계 안에서 다른 방법을 찾거나 멈춰야 합니다.
3. 모델을 호출하기 전에 게이트 테스트하기
핵심 경계는 API 비용 없이 로컬에서 검사할 수 있습니다. safe-files.test.mjs를 만듭니다.
import assert from "node:assert/strict";
import test from "node:test";
import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { createFileGate } from "./safe-files.mjs";
test("file gate blocks traversal, overwrite, and outside symlinks", async () => {
const base = await mkdtemp(path.join(tmpdir(), "harness-test-"));
const root = path.join(base, "sandbox");
const outside = path.join(base, "outside.txt");
try {
await mkdir(root);
await writeFile(path.join(root, "note.md"), "hello", "utf8");
await writeFile(outside, "secret", "utf8");
const gate = await createFileGate(root);
assert.equal(await gate.readText("note.md"), "hello");
await assert.rejects(() => gate.readText("../outside.txt"), /outside workspace/);
await assert.rejects(() => gate.createText("note.md", "replace"), /refusing to overwrite/);
try {
await symlink(outside, path.join(root, "outside-link.txt"), "file");
await assert.rejects(() => gate.readText("outside-link.txt"), /outside workspace/);
} catch (error) {
if (error.code !== "EPERM") throw error;
}
assert.equal(await gate.createText("summary.md", "safe"), "created");
} finally {
await rm(base, { recursive: true, force: true });
}
});
먼저 오프라인 검사를 실행합니다.
node --test safe-files.test.mjs
node --check agent.mjs
두 명령이 통과한 다음 ANTHROPIC_API_KEY와 ANTHROPIC_MODEL을 설정하고 node agent.mjs를 실행합니다. 인증 정보는 소스 코드나 policy.json에 넣지 마세요.
실무에 적용하는 세 가지 Use case
1. 소프트웨어 팀: Pull Request 구현과 검증
범위가 분명한 이슈, 관련 디렉터리, 테스트 명령을 에이전트에 줍니다. “코드를 작성했다”는 완료 조건이 아닙니다. 실패를 재현한 결과, 수정 뒤 통과한 테스트, 사람이 읽을 수 있는 diff를 요구해야 합니다. 운영 배포와 데이터베이스 마이그레이션은 사람의 승인 뒤에 둡니다.
에이전트는 오류 위치 검색, 재현 테스트 추가, 코드 수정, 브랜치 생성까지 반복할 수 있습니다. 사람은 제품 동작이 요구사항에 맞는지, 마이그레이션이 실제 데이터에 어떤 영향을 주는지 판단합니다.
2. 콘텐츠 운영: 글의 발행 전 품질 게이트
글 생성과 발행 승인을 분리합니다. 에이전트가 초안을 만들더라도 중복 주제, 내용 깊이, 코드 문법, 링크, 모바일 화면 검사가 통과해야 발행할 수 있게 합니다. 실패한 검사는 구체적인 수정 이유를 반환해야 합니다.
예를 들어 코드 블록은 문법 검사를 통과하고, 페이지는 모바일 스크린샷으로 확인하며, 본문의 “검증했다”는 표현은 실제 명령 기록과 연결합니다. 글 자체의 자신감이 아니라 외부 증거가 발행 여부를 결정합니다.
3. 고객 운영: 문의 분류와 답장 초안
에이전트가 문의를 분류하고 근거와 함께 답장 초안을 작성하게 할 수 있습니다. 고객 기록 변경과 실제 전송은 사람이 승인합니다. 분류에 꼭 필요한 개인정보만 전달하고, 전체 메시지를 장기 로그에 남기지 않습니다.
이 구조에서는 초안이 틀려도 고객에게 바로 전송되지 않습니다. 목표는 사람을 없애는 것이 아니라 반복적인 정리 시간을 줄이고, 최종 판단에 집중하게 만드는 것입니다.
ROI는 생성량이 아니라 검토 시간으로 계산하기
생성한 토큰 수보다 사람이 절약한 검토 시간과 줄어든 재작업을 측정하세요.
한 팀이 매주 15개 작업을 검토하고 작업당 20분이 든다면 주 5시간입니다. 첫 Harness 구축에 6시간이 들고 이후 유지보수가 주 1시간이라면, 매주 약 4시간을 절약하므로 초기 비용은 약 1주 반 만에 회수됩니다.
이는 계산 예시일 뿐, 보장된 성과가 아닙니다. 도입 전후 2주 동안 다음을 기록하세요.
- 작업당 사람의 투입 시간
- 재작업 비율
- 운영 반영 전에 발견한 결함 수
- 사람의 승인으로 넘긴 횟수
승인 요청이 지나치게 많다면 검증된 저위험 작업의 범위를 더 좁혀 자동화할 수 있습니다. 결함이나 재작업이 늘었다면 권한을 넓히지 말고 검사나 컨텍스트를 보완해야 합니다.
Pitfall: 자주 하는 실수와 고치는 법
폴더 이름 검사만으로 샌드박스라고 생각하기
문자열상 작업 폴더 안에 있어 보여도 심볼릭 링크의 실제 대상은 밖일 수 있습니다. 실제 경로를 확인하고, 덮어쓰기를 거부하며, 운영체제 권한을 두 번째 경계로 사용하세요. 민감한 작업은 컨테이너나 격리 환경에서 실행해야 합니다.
프롬프트에 “위험한 작업 금지”만 쓰기
문장은 지침이지 강제 장치가 아닙니다. 위험한 도구를 제공하지 않거나 실행 전 hook에서 거부해야 합니다. 실제 설정 방법은 한국어 Claude Code 권한 설정 가이드에서 확인할 수 있습니다.
에이전트의 “테스트 통과”를 그대로 믿기
명령, 종료 상태, 검증 범위를 기록하세요. UI 작업이라면 실제 조작이나 스크린샷 증거도 필요합니다. 검증 영수증 워크플로를 함께 사용하면 다음 작업자가 무엇을 확인했는지 추적할 수 있습니다.
매번 모든 문서를 컨텍스트에 넣기
긴 컨텍스트는 정작 중요한 제약을 묻어 버립니다. 짧고 안정적인 입구 문서에서 주제별·버전별 자료로 연결하고, 문서의 최신성과 검증 상태를 추적하세요. 중요한 규칙은 문장에만 두지 말고 lint나 테스트로 강제합니다.
정리와 다음 단계
Harness Engineering은 프롬프트를 길게 쓰는 기술이 아닙니다. 필요한 지식을 찾을 수 있게 만들고, 도구를 제한하고, 민감한 행동을 승인 뒤에 두고, 결과를 명령으로 검사하며, 실패를 더 나은 규칙과 테스트로 되돌리는 설계입니다.
첫 단계로 업무 하나를 골라 입력, 허용할 행동, 합격 명령, 사람의 승인이 필요한 행동을 네 줄로 적어 보세요. 실제 팀 저장소에 권한·검증·리뷰 장치를 설계해야 한다면 주된 상담 창구는 Claude Code 교육 및 도입 상담입니다.
실제 검증 결과
2026년 7월 21일, 이 글의 safe-files.mjs와 safe-files.test.mjs 코드 블록을 임시 디렉터리에 추출해 Node.js로 실행했습니다. 정상 읽기, 새 파일 생성, ../ 경로 이탈 차단, 기존 파일 덮어쓰기 거부를 확인했습니다. 테스트 프로세스가 심볼릭 링크를 만들 수 있는 시스템에서는 작업 공간 밖을 가리키는 링크도 거부하는지 검사합니다. agent.mjs는 문법 검사를 통과했습니다.
이번 검증 범위에는 Anthropic API 실제 호출이 포함되지 않습니다. 모델 접근 권한과 비용이 계정마다 다르기 때문입니다. “게시됨”, “문법 확인”, “오프라인 테스트 통과”, “유료 외부 API 호출”은 서로 다른 주장입니다. 신뢰할 수 있는 Harness라면 현재 증거가 어디까지인지 구분해 기록해야 합니다.
관련 글
Claude Code와 Codex, 결국 뭘 써? 사고 안 나는 '병행'의 현실적 답
OpenAI의 Codex와 Claude Code, 뭐가 잘하고 뭘 맡길까? 둘을 안전하게 병행하는 역할 분담과 권한·검증 워크플로를, 제 실패담과 함께 풀어드립니다.
Claude Code/Codex 초보자를 위한 프롬프트 5가지 실전 패턴
Claude Code/Codex 초보자를 위해 범위, 저장소 맥락, 완료 기준, 검증 증거, 인수인계 패턴을 실전 예시로 설명합니다.
Claude Code Harness Smoke Test: 에이전트를 믿기 전 15분 검증 루프
Claude Code 작업 전에 범위, 금지 영역, 증거 명령, 공개 URL, 수익 CTA를 확인하는 실무 체크입니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.