Claude Code에게 여러 일을 부탁했는데 첫 번째만 끝내고 “다음 작업을 원하시면 말씀해 주세요”라며 멈춘 경험이 있을 것이다. 반대로 반복 작업을 걸어 두면 이미 처리한 대상을 또 처리하기도 한다. 강의의 마지막 장은 이슈 작성, 수정 PR, 단순화, 리뷰를 이어 붙인 “소프트웨어 공장”을 만든다. 여기에 쓰는 /goal, /loop, 동적 워크플로는 멈추는 조건이 서로 다르다. /goal은 완료 조건을 별도 모델이 확인할 때까지, /loop는 세션이 열려 있는 동안 정해진 간격마다, 워크플로는 스크립트가 끝날 때까지 돈다. 각 단계에 완료 조건과 “이미 처리함” 표시를 설계해야 공장이 중간에 서거나 같은 일을 되풀이하지 않는다
4편까지의 흐름을 알고, GitHub 이슈와 PR을 써 본 독자를 기준으로 한다. 실습에는 로그인된 GitHub CLI(gh)가 필요하다. 다 읽으면 반복 작업을 스킬, goal, loop, 워크플로 중 어디에 맡길지 판단하고, 훅이 막을 수 있는 범위를 설명할 수 있다
반복하는 절차는 스킬로 만든다
커밋할 때마다 타입 검사를 돌리고, 변경을 스테이징하고, 메시지를 쓰는 일은 매번 같다. 강의는 이 절차를 ship이라는 스킬로 만든다. 2편에서 설치해 쓴 스킬을 이번에는 직접 쓰는 것이다
.claude/skills/ship/SKILL.md를 만든다
--- name: ship description: 보류 중인 변경을 타입 검사한 뒤 커밋한다. 사용자가 현재 작업을 마무리하거나 커밋하려 할 때 사용한다. disable-model-invocation: true --- 다음 작업의 보류 중인 변경을 검증하고 커밋하라: $ARGUMENTS 1. `git status`와 `git diff`로 대상 변경을 확인한다. 2. `npm run typecheck`를 실행하고, 실패하면 커밋하지 않고 오류를 보고한다. 3. 대상 파일만 스테이징한다. 4. 변경 이유가 드러나는 커밋 메시지를 작성해 커밋한다. 5. 커밋 해시를 알려 준다. 원격 저장소로 푸시하지 않는다.
/ship Workers 리팩터링이라고 입력하면 본문의 $ARGUMENTS가 “Workers 리팩터링”으로 바뀐 채 Claude에게 전달된다. 위치별로 받으려면 $ARGUMENTS[0] 또는 줄여서 $0을 쓴다. 현재 문서 기준으로 번호는 0부터 시작한다(skills). 예전 커스텀 명령어 글에는 $1이 첫 번째 인자라고 적었는데, 현재 문서와 다르므로 쓰기 전에 확인한다. 자리표시자가 없는 스킬에 인수를 넘기면 본문 끝에 ARGUMENTS: <입력>이 붙는다
프런트 매터의 disable-model-invocation: true는 강의에 없던 줄이다. 이 설정은 Claude가 스킬을 스스로 불러오지 못하게 하고, 사람이 /ship으로 호출할 때만 실행되게 한다. 문서는 “수동으로 실행하고 싶은 워크플로에 쓰라”고 적는다. 커밋처럼 부작용이 있는 절차에 맞는 설정이다. 이 스킬은 푸시하지 않으므로, 강의에서는 푸시를 4편의 ! 셸 모드로 직접 했다
강사는 스킬에 “작성자 표기나 공동 작성자 문구를 붙이지 말라”는 조건도 넣었다. 이 요구는 설정으로 처리하는 편이 확실하다. 스킬의 지시는 모델이 읽고 따르는 문장이지만, 설정은 Claude Code가 적용한다. 예전 includeCoAuthoredBy 키는 폐기 예정이다. 현재는 attribution.commit과 attribution.pr로 커밋 트레일러와 PR 문구를 바꾸거나 숨긴다(settings-reference). v2.1.282부터는 "attribution": false로 전부 숨길 수 있다. 다만 이전 버전 CLI는 이 값을 담은 설정 파일을 건너뛰므로 여러 버전이 공유하는 파일에는 객체 형식을 유지하라고 변경 기록은 적는다(changelog)
스킬은 비대화형으로도 실행할 수 있다. claude -p '<요청>'은 대화 화면 없이 요청을 처리하고 결과를 출력한 뒤 끝난다. 이렇게 실행한 세션은 --resume 목록에 나타나지 않는다
스킬이 문서를 찾을 수 있게 MCP를 연결한다
예약 취소 기능은 React 화면만이 아니라 데이터베이스와 Worker 백엔드도 건드렸다. 강의는 Cloudflare의 Workers Best Practices 스킬을 설치하고(cloudflare/skills), Cloudflare 문서를 조회하는 MCP 서버를 함께 연결한다. MCP(Model Context Protocol)는 Claude가 외부 도구와 자료에 접근하는 연결 규약이다
claude mcp add --transport http cloudflare-docs https://docs.mcp.cloudflare.com/mcp
옵션은 서버 이름 앞에 둔다. 주소는 Cloudflare 문서에 공개된 Documentation MCP 서버다(Cloudflare). 세션 안에서 /mcp를 실행하면 연결된 서버와 상태, 도구 개수가 보인다
MCP 서버는 범위를 골라 등록한다(mcp)
범위 (--scope) | 적용 대상 | 저장 위치 |
|---|---|---|
local (기본) | 이 프로젝트, 나만 | ~/.claude.json |
project | 이 프로젝트, 팀 공유 | 저장소 루트 .mcp.json |
user | 내 모든 프로젝트 | ~/.claude.json |
강의는 처음에 기본값으로 추가해 Clipper에서만 보이는 것을 확인하고, claude mcp remove cloudflare-docs로 지운 뒤 --scope user로 다시 추가해 다른 프로젝트에서도 쓰게 했다. local은 1편의 settings.local.json처럼 “이 프로젝트에서 나만” 쓰는 범위다. 팀이 함께 쓸 서버라면 project로 .mcp.json에 커밋한다
스킬과 MCP를 한 요청에서 함께 쓴다
Workers Best Practices 스킬로 Worker 코드에 리팩터링할 부분이 있는지 살펴봐 줘. 문서가 필요하면 cloudflare-docs MCP 서버를 사용해 줘.
스킬은 판단 기준을 주고, MCP는 기준을 적용하다 모르는 API가 나올 때 공식 문서를 찾게 한다. 강의에서는 중요한 문제 두 건이 나왔고 둘 다 고치도록 선택했다. 수정이 끝난 뒤 /ship으로 커밋했다
/goal은 완료 조건이 확인될 때까지 멈추지 않는다
공장의 입구는 이슈다. 강의는 먼저 문제를 찾아 이슈로 등록하게 한다. 수정은 시키지 않는다
예약 흐름에 UX 문제나 일관성 문제가 있는지 진단해 줘. 발견한 문제는 각각 명확한 제목과 수정 완료 기준을 갖춘 GitHub 이슈로 등록해 줘.
버그, 접근성 문제, 개선 사항을 포함해 이슈 여덟 개가 생겼다. 이제 /goal로 이 이슈들의 수정안을 만들게 한다. /goal은 목표와 완료 조건을 주고, 조건을 만족할 때까지 Claude가 작업을 이어 가게 하는 명령이다
/goal 열려 있는 모든 이슈를 각각 별도의 브랜치에서 구현하고, 이슈마다 열린 PR 하나를 만들어 줘. 병합하지 말고 이슈도 닫지 마. gh pr list로 확인했을 때 각 이슈에 대응하는 열린 PR이 하나씩 있으면 완료다.
마지막 문장이 완료 조건이다. 병합과 이슈 종료를 금지한 것은 리뷰가 다음 단계에 남아 있기 때문이다
완료 판정 방식을 알면 조건을 어떻게 써야 할지 보인다. 현재 문서 기준으로 매 턴이 끝날 때 작고 빠른 모델(Claude API에서는 기본 Haiku)이 조건이 충족됐는지 검사한다. 작업한 모델이 아닌 새 모델이 판정한다. 이 평가 모델은 도구가 없어 대화 기록에 나온 내용만 보고 판단한다(goal). 조건은 Claude의 출력으로 증명할 수 있어야 한다는 뜻이다. “각 이슈에 PR이 있으면 완료”보다 “gh pr list로 확인했을 때”라고 쓰면, Claude가 그 명령을 실행하고 출력이 기록에 남아 평가 모델이 대조할 수 있다
판정은 아직 아님, 충족, 불가능 셋 중 하나다. 실행 중에는 ◎ /goal active 표시와 경과 시간이 보인다. 인수 없이 /goal을 입력하면 조건, 경과 시간, 턴 수, 토큰, 마지막 판정 이유를 보여 주고, /goal clear로 해제한다. 세션당 목표는 하나이고 조건은 4,000자까지 쓸 수 있으며, 세션을 다시 열면 복원된다
강의에서는 gh 관련 명령에 “앞으로 다시 묻지 않기”를 골라 settings.local.json에 허용 규칙을 쌓았다. 목표 세션은 이슈를 읽고 브랜치를 나눠 작업한 뒤, 마지막 이슈에서 타입 검사와 빌드를 마치고 조건을 확인해 완료로 표시했다. 이 세션은 끝날 때까지 종료하지 않고, 다음 단계는 Agent View나 다른 터미널의 새 세션에서 시작한다
/loop는 세션이 열려 있는 동안 같은 프롬프트를 되풀이한다
목표가 PR을 올리는 동안 다른 세션에서 새로 올라온 PR을 단순화한다. /loop는 프롬프트나 슬래시 명령을 정해진 간격으로 반복 실행한다
/loop 2m gh pr list --state open으로 열린 PR을 확인해 줘. simplified 라벨이 없는 PR마다 simplify를 수행하고, 끝나면 simplified 라벨을 붙여 줘.
이 프롬프트의 핵심은 라벨이다. 2분마다 같은 요청이 돌기 때문에 처리한 PR을 표시하지 않으면 다음 실행에서 같은 PR을 또 단순화한다. simplified 라벨은 “이미 처리함” 표시이자 다음 실행의 필터다. 강의에서는 처음 실행에서 열린 PR 세 개를 찾았고, 라벨이 없으면 먼저 만든 뒤 붙였다. 나중에는 여덟 개 모두 라벨이 붙었고, 이후 실행은 새 대상이 없다고 보고했다
현재 문서 기준 /loop의 조건은 다음과 같다(scheduled-tasks)
- 간격 단위는
s,m,h,d이고 최소 1분이다.7m처럼 나누어떨어지지 않는 값은 가까운 간격으로 반올림된다 - 간격을 빼면 Claude가 1분에서 1시간 사이로 정한다
- 세션이 열려 있고 대기 중일 때만 실행된다. 터미널을 닫으면 멈춘다
- 반복 작업은 만든 지 7일이 지나면 자동으로 만료되고, 세션당 50개까지 만들 수 있다
/loop 20m /review-pr 1234처럼 슬래시 명령이나 스킬도 반복할 수 있다
goal과 loop는 이름이 비슷하지만 멈추는 방식이 다르다. goal은 조건이 충족되면 끝나고, loop는 조건과 무관하게 세션이 살아 있는 동안 계속 돈다
큰 병렬 작업은 동적 워크플로가 스크립트로 조율한다
PR 여덟 개를 하나씩 리뷰하면 오래 걸린다. 동적 워크플로(dynamic workflow)는 Claude가 JavaScript 스크립트를 작성해 여러 서브에이전트를 동시에 조율하는 기능이다(workflows). 비유가 아니라 실제로 실행되는 코드다. 강의는 프롬프트에 ultracode 키워드를 넣어 워크플로를 요청했다. “워크플로를 사용해 줘”라고 직접 말해도 된다
ultracode simplified 라벨이 있는 열린 PR마다 코드 리뷰를 수행하도록 여러 에이전트를 실행해 줘. 각 PR의 리뷰 결과는 해당 PR에 댓글 하나로 남겨 줘. 병합은 하지 마.
워크플로는 PR 목록을 찾고, PR마다 리뷰 에이전트를 하나씩 띄워 결과를 댓글로 남겼다. 워크플로 화면에서 에이전트를 고르고 Enter를 누르면 각 에이전트에게 전달된 프롬프트와 진행 상황이 보인다. 강의 화면에서는 PR마다 Fable 5 에이전트가 붙었다
/workflows에서 실행을 고르고 소문자 s를 누르면 저장한다. 프로젝트용은 .claude/workflows/, 개인용은 ~/.claude/workflows/에 들어가고, 이후 /<이름>으로 다시 실행한다. 강의는 review-simplified-prs라는 이름으로 저장했고, 파일을 열어 보니 리뷰 작업을 조율하는 JavaScript 코드였다. 프로젝트에 커밋해 팀이 같은 워크플로를 쓸 수 있다. 기본 제공 워크플로로는 여러 에이전트가 검색하고 서로의 결과를 교차 검증하는 /deep-research가 있다
비용을 먼저 확인한다. 문서는 워크플로가 “상당히 많은 토큰”을 쓸 수 있다고 적고, 에이전트가 25개를 넘거나 예상 토큰이 150만을 넘으면 경고를 띄운다. 기본 동시 실행은 에이전트 16개, 한 번 실행의 상한은 1,000개다. Pro 요금제에서는 /config에서 켜야 쓸 수 있다. 강의에서도 여덟 개 중 두 개가 끝났을 때 이미 사용 한도가 줄어드는 것이 보였다. Esc로 중단할 수 있다
리뷰가 끝나자 모든 PR에 단순화 라벨과 리뷰 댓글이 달렸다. 강사는 리뷰를 마쳤다는 라벨까지 붙였으면 좋았겠다고 했다. loop에서 본 처리 표시가 워크플로에도 필요하다는 뜻이다. 표시가 없으면 워크플로를 다시 실행했을 때 이미 리뷰한 PR에 댓글이 또 달린다
공장의 각 단계는 멈추는 조건이 다르다
강의의 공장을 단계별 멈추는 조건과 처리 표시로 다시 그리면 다음과 같다
[진단 요청] 문제 찾기 → GitHub 이슈 8개 (한 번 실행하고 끝)
│
▼
[/goal] 이슈마다 브랜치 + PR 멈춤: 평가 모델이 "각 이슈에 PR 1개" 확인
│
▼
[/loop 2m] simplified 없는 PR 단순화 멈춤: 세션 종료 또는 7일 만료
│ 표시: simplified 라벨
▼
[워크플로] PR마다 리뷰 에이전트 → 댓글 멈춤: 스크립트 종료
│ 표시: (강의에서는 없음)
▼
[사람] PR과 리뷰 읽기 → 병합 판단
그림에서 사람이 맡은 칸은 마지막 하나다. 강사는 리뷰를 확인하고 괜찮으면 병합하는 루프도 구상할 수 있다고 했고, 그러면 사람의 역할이 이슈 작성과 코드 검토에 집중된다고 말했다. 병합 루프를 만든다면 그 루프의 완료 조건과 처리 표시, 그리고 “무엇이면 병합하지 않는가”를 먼저 적어야 한다. 앞 단계들이 모두 AI가 만든 결과를 AI가 검토하는 구조이므로, 3편의 브라우저 확인 같은 실행 검증 없이 병합까지 자동화하면 검증되지 않은 코드가 기본 브랜치에 들어간다
| 도구 | 멈추는 조건 | 맞는 작업 |
|---|---|---|
스킬 (/ship) | 절차가 끝나면 | 사람이 부르는 정해진 절차 |
/goal | 평가 모델이 조건 충족을 확인하면 | 범위가 정해진 작업을 끝까지 |
/loop | 세션 종료, 7일 만료 | 새로 생기는 대상을 주기적으로 처리 |
| 동적 워크플로 | 스크립트가 끝나면 | 대상이 많은 작업을 병렬로 한 번에 |
결과를 비개발자와 나눌 때는 아티팩트를 쓴다
PR 목록과 리뷰 댓글은 개발자에게는 충분하지만 CEO에게 보여 주기에는 어렵다. 아티팩트(artifact)는 Claude Code가 만든 HTML이나 마크다운 페이지를 claude.ai의 비공개 URL로 게시하는 기능이다. 기본은 비공개이고 페이지의 Share 버튼으로 공유한다. /artifacts로 목록을 본다(artifacts)
강의에서는 “PR 28의 코드와 변경의 영향 범위를 설명하는 아티팩트를 만들어 줘”라고 요청했다. 영향 범위(blast radius)는 수정이 어느 기능과 코드까지 영향을 미치는지를 뜻한다. 이어서 웹 페이지에서 작성자 표시를 선택하고 “작성자를 제거해 줘”라고 남기자, 그 요청이 터미널 세션으로 전달되어 Claude가 페이지를 고쳐 다시 게시했다
이 댓글 흐름은 조건이 있다. 현재 문서 기준으로 Pro·Max·Team·Enterprise 요금제에 claude.ai 계정 로그인이 필요하고, 댓글은 Team과 Enterprise에서 조직 안에 공유한 아티팩트에만 달 수 있다. 공개 아티팩트에는 댓글을 받을 수 없다. 편집 권한이 있는 사람이 Send to Claude를 눌러야 세션으로 전달된다(artifacts). 개인 요금제에서는 강의의 수정 요청 시연을 그대로 재현할 수 없다
훅은 이벤트에 명령을 걸지만 다른 경로는 막지 못한다
훅(hook)은 Claude가 도구를 쓰기 전이나 후, 세션을 시작하거나 끝낼 때처럼 정해진 이벤트에 셸 명령을 연결한다. 개념과 이벤트 목록은 예전에 쓴 훅 글에서 다뤘으므로, 여기서는 강의의 두 예제와 그 한계만 본다
강의의 첫 예제는 스키마 파일을 고치면 마이그레이션 파일을 자동으로 생성하는 PostToolUse 훅이다. 두 번째는 .env 편집을 막는 PreToolUse 훅이다. 강사는 두 번째 훅의 설정이 틀렸다고 시연 중에 인정했고, 음성에는 설정 전체가 낭독되지 않았다. 아래는 공식 문서의 형식에 맞춰 새로 쓴 설정이다. .claude/settings.json에 넣는다
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-env.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/drizzle-generate.sh" }
]
}
]
}
}
두 훅 모두 Edit과 Write 도구에 반응한다. 실제 판단은 스크립트가 한다
.env 계열 파일 편집을 막는 스크립트다
#!/usr/bin/env bash
# PreToolUse 훅: Edit/Write 대상이 .env 계열이면 차단한다
file_path=$(jq -r '.tool_input.file_path // empty')
case "$(basename "$file_path")" in
.env|.env.*)
echo "차단: $file_path 는 편집할 수 없다" >&2
exit 2
;;
esac
exit 0
훅은 표준 입력으로 도구 호출 정보를 JSON으로 받는다. 스크립트는 jq로 편집 대상 경로를 꺼내 파일 이름이 .env이거나 .env.로 시작하면 이유를 표준 오류에 쓰고 종료 코드 2로 끝낸다. PreToolUse에서 종료 코드 2는 도구 호출을 막고, 표준 오류의 내용이 Claude에게 전달된다(hooks). 이 패턴은 .env.example도 막으므로 필요하면 예외를 추가한다
이 글을 준비하면서 Claude 세션에 스크립트만 따로 실행하게 해 분기를 확인했다. 입력 JSON을 흉내 내 세 경로를 넣었다
for p in /app/.env /app/.env.local /app/src/schema.ts; do
echo "{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"$p\"}}" | ./block-env.sh
echo "exit=$? ($p)"
done
차단: /app/.env 는 편집할 수 없다 exit=2 (/app/.env) 차단: /app/.env.local 는 편집할 수 없다 exit=2 (/app/.env.local) exit=0 (/app/src/schema.ts)
.env와 .env.local은 종료 코드 2로 막히고, 스키마 파일은 0으로 통과한다. 이 결과는 스크립트의 분기만 확인한 것이다. Claude Code 세션에 연결해 실제로 편집이 막히는지는 확인하지 않았다
마이그레이션 생성 스크립트는 같은 방식으로 경로를 보고, 스키마 파일일 때만 생성 명령을 실행한다
#!/usr/bin/env bash
# PostToolUse 훅: 스키마 파일이 바뀌면 마이그레이션 파일을 생성한다
file_path=$(jq -r '.tool_input.file_path // empty')
case "$file_path" in
*/src/db/schema.ts)
cd "$CLAUDE_PROJECT_DIR" && npx drizzle-kit generate >&2
;;
esac
exit 0
src/db/schema.ts는 예시 경로이므로 프로젝트에 맞게 바꾼다. 이 훅은 마이그레이션 파일을 만들 뿐 데이터베이스에 적용하지 않는다. 같은 방식으로 npx를 가짜 명령으로 바꿔 실행하게 했을 때 스키마 파일에서만 생성 명령이 호출되고 다른 파일에서는 호출되지 않았다. 강의에서는 미용사 테이블에 예시 컬럼을 추가하자 마이그레이션이 자동으로 생성됐다. 강사는 스키마를 조금 바꿀 때마다 생성하는 방식을 권하지는 않는다고 덧붙였다. 여러 수정을 모은 뒤 한 번에 만들고 싶은 경우가 많기 때문이다
한계는 강의에서 Claude가 먼저 지적했다. 이 훅은 Edit과 Write 도구만 감시한다. Claude가 Bash로 sed를 실행하거나 Python 스크립트를 만들어 .env를 고치면 훅이 발동하지 않는다. 문서도 "Edit|Write" matcher는 Bash를 쓸 때가 아니라 두 도구를 쓸 때만 발동한다고 명시한다. 대안으로 matcher에 Bash를 추가하거나 FileChanged·Stop 훅을 함께 쓰는 방법을 제시한다(hooks-guide). 2편의 Read(./.env) 같은 deny 규칙은 cat, sed 같은 인식 가능한 Bash 명령까지 덮지만 스크립트가 파일을 직접 여는 경우는 못 막는다. 모든 프로세스를 막아야 한다면 샌드박스를 켜거나, 비밀 파일을 작업 디렉터리에 두지 않는다. CLAUDE.md에 “건드리지 마”라고 적는 방법은 1편에서 본 대로 강제가 아니다
자동화의 각 단계에 무엇을 보면 끝났다고 판단할지와 이미 처리한 대상을 어떻게 표시할지를 먼저 적고, 그다음 goal·loop·워크플로 중 그 조건에 맞는 도구를 고른다
이 시리즈의 글: Claude Code로 바이브 코딩 프로젝트 넘겨받기
- Claude Code의 CLAUDE.md는 짧게 두고 나머지는 필요할 때 불러온다
- Claude Code에게 코드를 맡기기 전에 탐색·계획·권한부터 정한다
- Claude Code가 고친 코드는 diff·리뷰·브라우저 확인을 거쳐 커밋한다
- Claude Code 병렬 작업은 worktree로 파일을, 세션으로 대화를 나눈다
- Claude Code의 goal·loop·workflow는 멈추는 조건으로 골라 쓴다 (이 글)
출처
참고 자료
- Skills — Claude Code Docs
- Settings reference — Claude Code Docs
- Changelog — Claude Code Docs
- Connect Claude Code to tools via MCP — Claude Code Docs
- Cloudflare MCP servers — Cloudflare Docs
- Goal — Claude Code Docs
- Scheduled tasks — Claude Code Docs
- Dynamic workflows — Claude Code Docs
- Introducing dynamic workflows in Claude Code — Claude Blog
- Artifacts — Claude Code Docs
- Hooks reference — Claude Code Docs
- Hooks guide — Claude Code Docs
- cloudflare/skills — GitHub