하네스 엔지니어링 — AI 에이전트를 제어하는 기술
하네스 엔지니어링이란
LLM 에이전트에게 코드를 작성하게 할 수 있습니다. 그런데 작성하면 안 되는 코드도 작성할 수 있다는 게 문제입니다.
하네스 엔지니어링은 AI 에이전트의 행동 범위를 설계하고 제한하는 기법입니다. 프롬프트 엔지니어링이 “뭘 하라”를 잘 전달하는 기술이라면, 하네스 엔지니어링은 “뭘 하지 마라”를 시스템 수준에서 강제하는 기술입니다.
말 잘 듣는 직원을 채용하는 게 아니라, 누가 와도 사고가 나지 않는 작업 환경을 설계하는 것에 가깝습니다.
왜 필요한가
ttutak 플러그인을 만들면서 실제로 겪은 상황들입니다.
LLM은 지시를 “대체로” 따릅니다. “main 브랜치에서 직접 커밋하지 마세요”라고 프롬프트에 적어도, 컨텍스트가 길어지거나 복잡한 작업 중에는 무시할 때가 있었습니다. 프롬프트는 부탁이지 강제가 아닙니다.
자동화는 실수도 자동화합니다. 사람이 직접 git push --force를 치면 “이거 괜찮나?” 한 번쯤 생각합니다. 에이전트는 그런 망설임이 없습니다. 빠르게 실행하기 때문에 사고도 빠르게 납니다.
에이전트는 맥락을 잃습니다. 긴 대화에서 초반에 설정한 규칙을 후반에 잊어버리는 경우가 있었습니다. “PR 머지는 하지 마세요”라고 했는데, 대화가 길어지면 “PR을 머지할까요?”라고 묻는 순간이 왔습니다.
이런 문제들을 프롬프트 한 줄로 해결하려고 하면 한계가 있습니다. 시스템 레벨의 장치가 필요했습니다.
3가지 제어 계층
ttutak에서 사용하는 제어 장치는 크게 세 가지 계층으로 나뉩니다.
1계층: allowed-tools — 사용 가능한 도구 제한
각 스킬의 frontmatter에 allowed-tools를 정의하면, 해당 스킬이 실행되는 동안 사용할 수 있는 도구가 제한됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
---
name: commit
allowed-tools:
- Bash(git commit:*)
- Bash(git add:*)
- Bash(git status:*)
- Bash(git diff:*)
- Read
- Glob
- Grep
- AskUserQuestion
---
commit 스킬은 git commit, git add 같은 명령만 실행할 수 있습니다. git push --force나 gh pr merge는 목록에 없으므로 실행이 차단됩니다.
이게 프롬프트 수준의 “하지 마세요”와 다른 점은, 시스템이 강제한다는 것입니다. LLM이 아무리 하고 싶어도 allowed-tools에 없는 도구는 호출 자체가 안 됩니다.
2계층: hooks — 위험한 명령 사전 차단
hooks는 도구가 실행되기 전/후에 셸 스크립트를 실행하는 장치입니다. plugin.json에 정의합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/.claude/hooks/pre-tool-guard.sh"
}
]
}
]
}
}
pre-tool-guard.sh는 Bash 명령이 실행되기 전에 내용을 검사합니다. git push --force, gh pr merge, main 브랜치에서의 직접 커밋 같은 위험한 패턴을 감지하면 실행을 차단합니다.
allowed-tools가 “허용 목록”이라면, hooks는 “차단 목록”입니다. 둘 다 있어야 빈틈이 줄어듭니다.
3계층: rules — 행동 지침 문서
.claude/rules/ 디렉토리에 마크다운 파일로 규칙을 정의합니다. 이건 LLM에게 전달되는 프롬프트 수준의 제어입니다.
1
2
3
4
5
6
7
8
9
# Git 워크플로우
## 브랜치 규칙
코드를 수정할 때는 main에서 직접 수정하지 마세요.
파일을 수정하기 전에 작업 브랜치를 먼저 생성하세요.
## 커밋 규칙
커밋은 /commit, PR은 /pull-request 스킬을 사용하세요.
git 명령어를 직접 실행하지 마세요.
rules는 1, 2계층과 달리 강제력이 없습니다. LLM이 “참고”하는 수준입니다. 그래서 핵심적인 안전장치는 rules에만 의존하지 않고, 반드시 allowed-tools나 hooks로 보강했습니다.
세 계층의 관계를 정리하면 이렇습니다.
| 계층 | 장치 | 강제력 | 역할 |
|---|---|---|---|
| 1계층 | allowed-tools | 시스템 강제 | 이 도구만 쓸 수 있습니다 |
| 2계층 | hooks | 시스템 강제 | 이 패턴은 차단합니다 |
| 3계층 | rules | LLM 참고 | 이렇게 행동하세요 |
에이전트 간 권한 분리
ttutak의 dev 파이프라인에는 여러 에이전트가 참여합니다. 각 에이전트가 할 수 있는 일이 다릅니다.
제품책임자(PO) 에이전트는 PRD를 작성합니다. 코드를 수정하는 도구(Edit, Write)는 갖고 있지 않습니다. PRD를 쓰다가 실수로 소스 코드를 건드릴 수 없는 구조입니다.
QA 에이전트는 코드를 읽고 리뷰합니다. 역시 Edit이 없습니다. 리뷰를 하다가 “이 부분 고치겠습니다”라고 코드를 수정하는 일이 없습니다. 수정이 필요하면 리뷰 결과를 반환하고, 오케스트레이터가 개발자 에이전트에게 수정을 지시합니다.
개발자(coder) 에이전트만 Edit, Write, Bash(./gradlew:*) 같은 도구를 갖고 있습니다. 코드를 수정할 수 있는 에이전트는 오직 coder뿐입니다.
이건 사람 조직의 권한 분리와 같은 원리입니다. DB 접근 권한을 모든 직원에게 주지 않는 것처럼, 에이전트에게도 역할에 맞는 도구만 부여하면 사고 범위가 줄어듭니다.
스킬 간 컨텍스트 전달
에이전트 간에 정보를 전달하는 것도 설계가 필요했습니다.
dev 파이프라인의 마지막 단계(phase-complete)에서 pull-request 스킬을 호출할 때, PRD의 비즈니스 맥락을 PR 본문에 반영하고 싶었습니다. 그런데 pull-request 스킬은 독립 스킬이라 dev의 컨텍스트를 모릅니다.
처음에는 Read()로 스킬 파일을 읽어서 인라인으로 실행하면서 컨텍스트를 직접 주입하는 방식을 썼습니다. 그런데 이러면 앞서 말한 allowed-tools 우회 문제가 생겼습니다.
해결책은 파일 기반 컨텍스트 전달이었습니다.
- dev 오케스트레이터가
.dev/pr-context.md에 비즈니스 맥락을 조립해서 저장합니다 Skill("ttutak:pull-request")를 호출합니다- pull-request 스킬이
.dev/pr-context.md를 자동 감지해서 PR 본문에 반영합니다 - 파일이 없으면 (독립 호출 시) 이 단계를 건너뜁니다
에이전트 간 직접 통신이 아니라, 파일을 매개로 느슨하게 연결하는 구조입니다. 메시지 큐와 비슷한 패턴이었습니다. 덕분에 각 스킬의 독립성을 유지하면서도 파이프라인 내에서 컨텍스트를 공유할 수 있었습니다.
자가점검 체계
코드에 버그가 있으면 테스트가 잡아주듯이, 하네스에도 정합성 검증이 필요했습니다.
ttutak에서는 harsh-critic 에이전트를 돌려서 자가점검을 수행합니다. 파일 간 불일치, Step 번호 오류, 스킬 참조 누락 같은 문제를 CRITICAL/HIGH/MEDIUM으로 분류해서 리포트합니다.
실제로 자가점검에서 발견된 것들입니다.
- index.html에서 “6단계 파이프라인”이라고 했는데 실제로 7개 박스가 표시되고 있었습니다
- dev SKILL.md의
--phase complete설명에 test 단계가 누락되어 있었습니다 - 스킬 참조 목록에
Skill("ttutak:test")가 빠져 있었습니다
사람이 11개 파일을 하나하나 교차 검증하기는 어렵습니다. 에이전트에게 “이 파일들 간에 불일치가 없는지 찾아줘”라고 시키면 10초 만에 리포트가 나옵니다.
프롬프트 엔지니어링과의 차이
프롬프트 엔지니어링은 LLM에게 좋은 결과를 내도록 유도하는 기술입니다. 하네스 엔지니어링은 LLM이 나쁜 결과를 내지 못하도록 제한하는 기술입니다.
둘은 대립하는 게 아니라 보완 관계입니다.
| 프롬프트 엔지니어링 | 하네스 엔지니어링 | |
|---|---|---|
| 관심사 | 출력 품질 | 행동 범위 |
| 질문 | “어떻게 하면 더 잘할까?” | “뭘 못하게 막아야 할까?” |
| 도구 | 시스템 프롬프트, few-shot, CoT | allowed-tools, hooks, rules |
| 실패 모드 | 품질이 낮은 결과 | 의도하지 않은 부수 효과 |
프롬프트만 잘 쓰면 안전할 거라고 생각하기 쉽습니다. 하지만 프롬프트는 LLM에게 보내는 부탁이지, 계약이 아닙니다. 컨텍스트가 길어지면 초반 지시를 잊어버리고, 복잡한 상황에서는 “이게 맞겠지”라고 자의적으로 판단합니다.
그래서 핵심적인 제약은 프롬프트가 아니라 시스템 레벨(allowed-tools, hooks)에서 걸어야 합니다.
적용할 때 고려할 것
하네스 엔지니어링을 처음 적용할 때 제가 실수했던 것들입니다.
너무 느슨하게 시작하면 안 됩니다. 처음에 “일단 다 열어두고 나중에 조이자”고 했다가, “나중에”는 사고가 난 다음이었습니다. 기본은 전부 차단하고, 필요한 것만 열어주는 게 맞았습니다.
rules만으로 안전을 보장하면 안 됩니다. “main에서 커밋하지 마세요”를 rules에만 적어뒀는데, 긴 대화 후에 무시된 적이 있었습니다. 이후 hooks에 main 브랜치 커밋 차단 로직을 추가했습니다.
에이전트별 도구를 분리해야 합니다. 모든 에이전트에게 같은 도구 세트를 주면 편하지만, 리뷰 에이전트가 코드를 수정하거나, PO 에이전트가 빌드를 실행하는 일이 생깁니다. 역할에 맞는 최소한의 도구만 부여하는 게 원칙입니다.
마치며
ttutak을 만들면서 느낀 건, LLM 에이전트는 역량은 높지만 판단력은 불안정하다는 것이었습니다. 코드를 잘 짜는데, 언제 짜면 안 되는지는 잘 모릅니다. PR을 잘 만드는데, 머지까지 해버리면 안 된다는 건 가끔 잊어버립니다.
그래서 에이전트를 잘 쓰려면 “잘 시키는 기술”만큼이나 “못하게 막는 기술”이 중요했습니다. 그게 하네스 엔지니어링이었고, 지금도 계속 다듬고 있습니다.
ttutak 소스코드: github.com/rnqhstmd/ttutak