하네스는 자동화가 아니다
하네스(harness)는 원래 배선 다발을 뜻하는 말이다. 엔진이나 계기를 새로 만드는 물건이 아니라, 이미 있는 부품들을 하나의 계통으로 묶어 전원과 신호가 흐르게 하는 배선이다. AI 하네스도 똑같다. 모델을 새로 만들지 않는다. 이미 갖고 있는 스케줄러와 스크립트와 규칙 문서를 모델 주위에 배선해서, 사람이 앉아 있지 않아도 판단이 흐르게 만드는 것이다.
자동화와 갈라지는 지점은 하나다. 자동화는 정해진 순서를 그대로 실행한다. 하네스는 그 순서 안에 판단이 필요한 칸을 만들고, 그 칸에서 모델을 부른다.
내 경우 순서는 이미 돌고 있었다. 매일 정해진 시각에 스크립트를 깨우는 통로는 진작 뚫려 있었고, 그 통로로 지나가는 것은 Node 스크립트뿐이었다. 그래서 판단이 필요한 일 - 원고 검수, 중복 대조, 수치 해석 - 은 전부 손에 남아 있었다. 없던 것은 자동화가 아니라 그 안에서 모델을 부르는 한 줄이었다.
같은 프롬프트를 매일 손으로 붙여넣는 것은 하네스가 아니다. 조건은 셋이다. 사람 없이 기동하고, 실패를 스스로 알리고, 얼마를 썼는지 남긴다. 셋 중 하나라도 빠지면 그냥 반복 작업이다.
부품부터 센다
구축을 시작할 때 제일 먼저 한 일은 코딩이 아니라 재고 조사였다. 세어 보니 새로 만들 것보다 이어 붙일 것이 훨씬 많았다.
| 부품 | 역할 | 이미 갖고 있을 법한 것 |
|---|---|---|
| 가이드 | 행동 전에 모델이 읽는 규칙 | 프로젝트 규칙 문서, 메모리, 재사용 프롬프트 |
| 실행 통로 | 사람 없이 정해진 시각에 깨우는 장치 | 윈도우 작업 스케줄러, cron, launchd |
| 센서 | 행동 후에 무슨 일이 있었는지 남기는 장치 | 훅, 실행 로그, 결과 JSON |
| 게이트 | 통과하지 못하면 멈추는 장치 | 검사 스크립트, 종료 코드 |
| 계기판 | 쌓인 기록을 한 화면에 모으는 것 | 정적 HTML 한 장이면 충분하다 |
다섯 칸 중 나에게 진짜로 없던 것은 계측 하나뿐이었다. 통로도 규칙도 검사 절차도 다 있었고, 서로 연결돼 있지 않았을 뿐이다. 하네스 구축이 대체로 신축이 아니라 배선 공사가 되는 이유가 여기 있다.
그러니 순서는 이렇다. 지금 자기 PC에서 사람 손 없이 도는 일이 무엇인지 먼저 센다. 하나도 없다면 거기부터가 1단계고, 하나라도 있다면 그 통로에 모델을 태우는 것이 1단계다.
핵심은 한 줄이다
구조를 아무리 그려도 실제로 하네스를 작동시키는 것은 스케줄러가 부르는 명령 한 줄이다. 나머지는 전부 이 줄의 앞뒤에 붙는다. 배치 파일이 아니라 PowerShell로 적은 이유는 다음 절 04번에 있다.
$json = & claude -p "/마감 채널 수치를 확인해 데이터 파일을 갱신하고 build.mjs를 실행한다" `
--model haiku `
--add-dir "<작업 폴더 밖에서 읽어야 할 경로>" `
--allowedTools "Read,Edit,Write,Glob,Grep,Bash(node *)" `
--output-format json
| 요소 | 하는 일 |
|---|---|
-p "…" | 비대화 실행. 프롬프트를 주면 일을 끝내고 스스로 종료한다. 사람이 앉아 있을 필요가 없다 |
프롬프트 안의 /명령 | 문자열 안에 넣은 슬래시 명령이 그대로 확장된다. 대화창에서 다듬어 둔 절차를 무인 실행에 그대로 재사용할 수 있다 |
--model | 등급 고정. 이 한 칸이 비용의 대부분을 정한다 |
--add-dir | 세션은 작업 디렉터리에 묶인다. 그 밖의 폴더는 여기에 적어 따로 열어 준다 |
--allowedTools | 미리 열어 둘 도구. 이게 없으면 권한 확인 앞에서 멈춰 선다 |
--output-format json | 결과에 비용·토큰·모델 내역이 붙어 나온다. 계측이 공짜로 따라온다 |
| 결과를 받는 변수 | 이 JSON을 파일로 남기면 그게 다음 단계에서 계기판의 입력이 된다 |
프롬프트 문자열 안에서 슬래시 명령이 확장된다는 점이다. 대화창에서 쓰던 절차를 무인용으로 새로 옮겨 적지 않아도 된다. 이미 만들어 둔 절차가 많을수록 하네스 구축은 짧아진다.
무인 실행에서 걸리는 함정 일곱
여기가 이 글에서 제일 쓸모 있는 대목이다. 아래 일곱 개는 전부 첫 주에 실제로 밟은 것들이고, 하나같이 화면을 보고 있었으면 30초 만에 알아챘을 종류다. 무인 실행에서는 그 30초가 없다.
시작을 앞당기는 옵션이 하네스를 통째로 걷어낸다
기동을 빠르게 해 주는 최소 실행 모드가 있다. 그런데 이 모드는 구독 로그인을 아예 읽지 않고 별도 API 키만 받는다. 즉 쓰던 한도가 아니라 별도 과금이 된다. 더 큰 문제는 훅, 자동 메모리, 프로젝트 규칙 문서 자동 탐색을 통째로 건너뛴다는 점이다. 슬래시 명령 자체는 살아 있어서 겉보기에는 도는 것처럼 보이는데, 그 명령이 기대고 있던 규칙과 훅이 빠진 채로 돈다. 하네스를 만들려고 켠 실행에서 하네스만 빠지는 셈이다. 필요한 것을 인자로 하나씩 다시 넣어 줄 각오가 없으면 쓰지 않는 편이 낫다.
권한 프롬프트 하나면 무인 실행은 그 자리에 선다
열어 둘 도구를 미리 정해 두지 않으면 확인 창 앞에서 영원히 대기한다. 실행 기록을 열어 보니 권한이 거부된 호출이 거의 매번 남아 있었다. 가벼운 루프는 회당 0~3건, 글을 쓰는 무거운 루프는 9~11건이었다. 나쁜 신호만은 아니다. 어디서 막혔는지를 알려 주는 지도라서, 이 목록을 보고 다음 실행의 허용 범위를 좁혀 잡았다. 다만 삭제 계열은 끝까지 열지 않았다. 무인 상태에서는 되돌릴 사람이 없다.
작업 디렉터리를 벗어나면 파일이 안 보이는데 종료 코드는 0이다
세션은 작업 디렉터리에 묶인다. 엉뚱한 위치에서 실행하면 대상 파일에 손이 닿지 않는다. 그런데 그러고도 정상 종료로 끝났다. 조용한 실패의 전형이다. 실행 스크립트 안에서 작업할 폴더로 먼저 이동시키고, 그 밖에 필요한 폴더는 --add-dir로 따로 열어 준다.
한글이 섞인 경로에서 배치 파일이 죽는다
윈도우 배치 파일은 옛 인코딩으로 파싱된다. 한글이 들어간 경로를 인자로 넘기는 순간 그 자리에서 깨진다. 실행 껍데기를 PowerShell 스크립트로 옮기면 풀린다. 배치 파일은 그 PowerShell을 부르는 한 줄만 남긴다.
BOM은 붙일 데와 뗄 데가 정반대다
이게 제일 얄궂었다. PowerShell 5.1은 BOM 없는 .ps1을 옛 인코딩으로 읽는다. 그래서 스크립트 파일에는 BOM이 있어야 한글 주석과 문자열이 살아남는다. 그런데 그 스크립트가 저장하는 결과 JSON에 BOM이 붙으면 JSON.parse가 첫 글자에서 죽는다. 같은 날 두 방향으로 한 번씩 밟았고, 결과 파일 두 건을 파싱하지 못한 채로 남겼다.
# 이렇게 쓰면 BOM이 붙어 나중에 파싱이 깨진다 $json | Set-Content -Path $out -Encoding UTF8 # BOM 없이 쓴다 [IO.File]::WriteAllText($out, ($json -join "`n"), (New-Object Text.UTF8Encoding $false))
종료 코드 0은 성공이 아니다
03과 05가 겹치면 종료 코드만 보는 감시는 아무것도 잡지 못한다. 실행은 끝났고 코드는 0인데 한 일이 없다. 그래서 결과 문자열 안에 판정 토큰을 심고, 그 토큰이 찍혔을 때만 성공으로 세도록 바꿨다. 종료 코드는 "프로세스가 끝났다"만 알려 주지 "일이 됐다"는 알려 주지 않는다.
상속된 환경변수가 동작을 바꾼다
대화 세션 안에서 실행을 걸어 시험하면, 그 세션이 쓰던 환경변수가 자식 프로세스로 넘어가 중첩 실행 동작이 달라진다. 손으로 시험할 때는 되는데 스케줄러가 부르면 다르게 도는 유형이다. 스크립트 첫머리에서 해당 변수를 비우고 시작한다.
일곱 개 중 다섯 개는 모델과 아무 상관이 없다. 인코딩, 작업 디렉터리, 환경변수, 종료 코드다. 하네스 구축에서 실제로 시간을 잡아먹는 것은 프롬프트가 아니라 실행 껍데기였다.
게이트 - 위험한 일은 코드가 한다
무인 발행은 되돌리기 어렵다. 그래서 원칙을 한 줄로 못 박고 시작했다. 위험한 동작은 코드가 하고, 판단은 모델이 한다.
내 발행 루프에서 모델이 하는 일은 원고를 쓰고, 검사 절차를 불러 지적을 받고, 그 지적을 고치는 데까지다. 목록 카드 추가, 색인 재생성, 업로드처럼 한 번 나가면 주워 담기 힘든 동작은 전부 스크립트가 한다. 모델은 그 스크립트를 부를 뿐이고, 스크립트는 조건이 맞지 않으면 실행을 거절한다.
| 종료 코드 | 뜻 | 사람이 할 일 |
|---|---|---|
| 0 | 게이트 통과, 작업 완료 | 없음 |
| 20 | 게이트 불합격, 스스로 멈춤 | 지적 사항 확인. 고장이 아니라 정상 동작이다 |
| 그 외 | 실행 자체가 실패 | 원인 확인 |
불합격에 별도 코드를 준 것이 생각보다 중요했다. 이걸 실패와 같은 칸에 넣으면 게이트가 제 일을 한 날에도 알림이 사고처럼 울린다. 며칠이면 알림을 무시하게 되고, 그때부터 계기판은 장식이 된다.
- 판정은 실행하는 쪽이 아니라 별도 절차가 한다. 검사를 자기 자신에게 맡기면 자기 결과물에 합격을 준다
- 분량 하한 같은 수치는 프롬프트가 아니라 코드에 박는다. 프롬프트에 적은 규칙은 지켜질 때도 있고 아닐 때도 있지만, 코드에 박은 규칙은 항상 지켜진다
- 불합격은 조용히 넘어가지 않게 한다. 멈추고, 왜 멈췄는지 남긴다
계측 - 재지 않으면 하네스가 아니다
계측은 별도 공사가 아니었다. 결과를 JSON으로 받게 해 두면 비용과 토큰과 모델 내역이 딸려 나온다. 실행 스크립트가 그걸 한곳에 쌓고, 계기판이 그 파일을 읽는다.
{
"date": "2026-08-11",
"task": "dashboard",
"exit": 0,
"durationMs": 42597,
"model": "claude-haiku-4-5",
"tokens": { "in": 33, "out": 975, "cacheRead": 128128 },
"costUsd": 0.0466
}
쌓아 놓고 보니 예상과 가장 어긋난 값은 캐시였다. 글을 쓰는 루프 한 번의 내역은 이랬다.
| 항목 | 값 |
|---|---|
| 새로 보낸 입력 토큰 | 761 |
| 캐시에서 읽은 토큰 | 2,034,004 |
| 캐시에 새로 쓴 토큰 | 195,517 |
| 출력 토큰 | 57,438 |
| 비용 | 2.44달러 |
새로 보낸 입력은 761토큰인데 캐시에서 읽은 것은 203만 토큰이다. 세 자릿수와 일곱 자릿수의 차이다. 무인 루프의 비용은 내가 프롬프트에 적는 글자 수가 아니라, 그 루프가 매 턴 다시 읽는 문맥의 크기가 정한다는 뜻이다. 프롬프트를 짧게 다듬는 것보다 루프가 읽어야 하는 파일 수를 줄이는 편이 훨씬 크게 듣는다.
세 가지면 충분했다. 마지막으로 성공한 시각, 최근 며칠의 소모 추이, 연속 실패 횟수. 특히 마지막 항목이 없으면 사흘째 안 돌고 있어도 모른다. 무인 실행의 기본값은 성공도 실패도 아닌 침묵이다.
실측 비용 - 등급 하나로 4.4배
닷새간 남은 실행 기록 열한 건이다. 개인 작업 규모라 표본이 작지만, 자릿수를 잡는 데는 충분했다.
| 루프 | 하는 일 | 모델 등급 | 실행 시간 | 1회 비용 |
|---|---|---|---|---|
| 대시보드 갱신 (첫 시도) | 수치 확인 후 재생성 | Opus 5 | 23초 | 0.2166달러 |
| 대시보드 갱신 (교체 후 7회) | 똑같은 일 | Haiku 4.5 | 25~43초 | 0.0452~0.0592달러 |
| 글 발행 | 원고 작성·검사·수정·업로드 | Sonnet 5 | 11분 | 2.4363달러 |
| 글 발행 (턴이 두 배) | 같은 일 | Sonnet 5 | 14분 | 3.4089달러 |
| 표기 정정 | 계산과 치환만 | 모델 없음 | 초 단위 | 0달러 |
읽을 것 세 가지
첫째, 등급만 바꿨더니 평균 0.0489달러로 내려갔다. 첫 실행의 4.4분의 1이다. 수치를 세어 파일에 적고 빌드 명령을 부르는 일에는 상위 등급이 필요하지 않았다. 실행마다 읽는 양이 조금씩 달라 엄밀한 통제 비교는 아니지만, 자릿수가 바뀌는 차이라 방향은 분명하다.
둘째, 판단이 많은 루프는 자릿수 자체가 다르다. 글을 쓰는 루프는 대시보드 루프의 50배가 넘는다. 이건 낭비가 아니라 일의 크기다. 여기서 줄일 것은 비용이 아니라 이 루프를 부르는 횟수이고, 그래서 게이트가 앞에 선다.
셋째, 모델이 없는 루프도 하네스다. 표기를 세어 고치는 루프에는 모델을 넣지 않았다. 비용이 0이고, 대신 규칙을 코드로 남김없이 적어야 한다. 이름이 AI 하네스라고 해서 모든 칸에 모델을 넣을 이유는 없다.
결과가 틀렸을 때 사람이 나중에라도 알아챌 수 있으면 낮은 등급, 틀린 채로 지나갈 수 있으면 높은 등급으로 잡았다. 대시보드 숫자는 다음 날 보면 이상한 게 눈에 띈다. 이미 발행돼 나간 글은 눈에 띄지 않는다.
3단계로 시작하기
넉 달치 설계도를 그리는 것보다 스케줄러에 파일 하나를 거는 편이 빠르다. 위의 함정 일곱 중 다섯은 어차피 첫 주에 다 만난다.
가장 작은 무인 루프 - 1주차
되돌릴 수 있는 일 하나를 고른다. 파일 한 개를 갱신하는 정도면 된다. 스케줄러에 걸고, 다음 날 아침 그 파일의 날짜가 바뀌어 있으면 통과다. 이 단계에서 배우는 것은 자동화가 아니라 실행 껍데기의 함정들이다.
계측 붙이기 - 2주차
결과 JSON을 한곳에 쌓고, 실패와 소모 급증을 한 화면에 띄운다. 계기판은 정적 HTML 한 장으로 충분하다. 성공 판정은 명확하다. 계기판만 열어도 어제 몇 번 돌았고 얼마를 썼는지가 보이면 된다.
게이트 붙이기 - 3~4주차
되돌리기 어려운 일을 루프 안에 넣되, 그 앞에 판정 절차를 세운다. 불합격이면 멈추고 알린다. 이 단계까지 와도 마지막 버튼은 사람이 누르게 남겨 두는 편이 낫다는 것이 지금까지의 판단이다.
마무리
닷새를 굴려 보고 남은 결론은 좀 심심하다. 하네스에서 제일 중요한 부품은 모델이 아니라 게이트와 계측이었다. 게이트가 없으면 사고가 조용히 발행되고, 계측이 없으면 멈춘 줄도 모른다. 모델은 그 사이에서 판단을 맡는 칸 하나일 뿐이고, 그 칸의 등급을 한 번 바꾸는 것으로 비용은 4.4배가 갈렸다.
그리고 시작점은 생각보다 앞에 있다. 새 도구를 알아보기 전에, 지금 자기 PC에서 사람 손 없이 도는 일이 무엇인지부터 세어 보는 편이 빠르다. 통로가 이미 뚫려 있다면 남은 일은 그 통로에 한 줄을 태우는 것뿐이다.