본문으로 건너뛰기
Koding

Claude Code의 Plan 모드를 루프 엔지니어링으로 편하게 만들기

Jaewoo KimJaewoo Kim · 조회 0 · 6분 읽기
목차

들어가며

Claude Code의 Plan 모드를 사람의 수고를 덜면서 다루기 위한 루프 엔지니어링을 해 봤어요. 이 글을 읽으면 hook을 써서 Plan 모드에 "질문으로 요건 다듬기", "구현의 서브에이전트 위임", "계획의 HTML 확인"을 자동으로 넣는 방법을 알 수 있습니다.

목표

Plan 모드 운영에 다음 세 가지를 넣습니다. 이 중 1과 3의 일부(reject 시 재생성 사이클)가 반복 = 루프이고, 2는 모델/세션 할당 설계(이른바 하네스에 가까운 부분)입니다.

  1. Plan 모드의 첫 프롬프트 이후 타이핑 부하를 AskUserQuestion으로 낮춘다 (모호한 점이 없어질 때까지 질문을 반복하는 반복 구조)
  2. 작업 효율화를 자동 오케스트레이션으로 실현한다. 동시에 세션 컨텍스트 오염을 방지하고 토큰 사용량을 줄인다 (Sonnet 활용)
  3. Plan의 출력을 시각적인 HTML로 렌더링해 브라우저에서 확인한다 (reject 시에는 plan 수정 → 재생성 → 재확인 루프에 들어간다)

1과 3은 그럴싸하기만 한 계획이 나오는 걸 방지합니다. 계획 승인 후에는 기본적으로 auto mode에 들어가므로, 계획을 제대로 잡지 않으면 나중에 수정량이 어마어마해지기 때문이에요. 3은 공식 권장은 아니라고 생각하지만, 꽤 유행하는 확인 방법이라고 봅니다. 2는 흔히 말하는 "생각하는 부분은 Fable이나 Opus로, 구현은 Sonnet으로"와 세션 분리를 동시에 하는 방법입니다. 이것도 Plan 모드 같은 대규모 태스크에서는 공식에서 권장하고 있어요.

전체 흐름

구축이 끝난 Plan 모드는 이런 흐름으로 진행됩니다.

  1. Plan 모드에 들어가면 hook이 운영 규칙을 컨텍스트에 주입한다. Claude는 이 규칙에 따라 움직인다
    • AskUserQuestion으로 모호한 점이 없어질 때까지 인터뷰한다
    • plan md 확정 시, 끝에 마커 행을 쓴다
    • 승인 후의 구현은 메인 세션에서 하지 않고, 모델을 명시한 서브에이전트나 claude --bg에 위임한다
  2. 마커를 감지한 hook이 headless claude -p로 플랜을 HTML로 변환한다
  3. Claude가 HTML을 Claude Artifact로 게시하고, 그 URL을 브라우저로 자동으로 연 다음 ExitPlanMode를 호출한다
    • reject 하면 Claude가 plan md를 수정하고, HTML이 자동으로 재생성·재게시되어 다시 ExitPlanMode가 온다
    • 이후 구현은 1의 규칙에 따라 위임으로 진행되고, 메인 세션은 지시와 검수에만 집중한다

1. Plan 모드 한정으로 운영 규칙을 Hook으로 주입한다

여기서는 hook을 사용해서, Plan 모드의 첫 프롬프트를 입력했을 때 세션 공통의 운영 규칙을 끼워 넣습니다. 다음 두 가지는 이 hook에 고정해 두므로 매번 지시하지 않아도 됩니다.

  1. Plan 모드의 첫 프롬프트 이후 타이핑 부하를 AskUserQuestion으로 낮춘다
  2. 작업 효율화를 자동 오케스트레이션으로 실현한다. 동시에 세션 컨텍스트 오염을 방지하고 토큰 사용량을 줄인다

모든 hook은 stdin의 JSON으로 permission_mode를 받습니다(Plan 모드일 때의 값은 "plan"). 이걸 쓰면 "Plan 모드일 때만 작동하는 지시"를 UserPromptSubmit hook으로 주입할 수 있어요. ~/.claude/settings.json에는 이렇게 등록합니다.

"UserPromptSubmit": [
  {
    "hooks": [
      { "type": "command",
        "command": "/Users/you/.claude/hooks/plan-mode-workflow.sh" }
    ]
  }
]

hook 스크립트 본체는 접힌 블록으로 실어 둡니다. 하는 일은 다음 두 가지예요.

  • 요건의 모호한 점이 없어질 때까지 AskUserQuestion으로 반복해서 질문한다
  • 실행은 claude --bgclaude -p 그리고 서브에이전트에 위임한다. 모델은 공식 권장인 Sonnet을 베이스로 한다. 이번에는 다루지 않지만, 에이전트의 기본 모델을 Opus로 해 두면 복잡한 처리는 Opus에게 맡길 수 있다

plan-mode-workflow.sh (클릭해서 펼치기)

#!/bin/bash
# ~/.claude/hooks/plan-mode-workflow.sh
# UserPromptSubmit hook: permission_mode가 plan일 때만 Plan 모드 운영 지시를 주입한다
input=$(cat)
mode=$(printf '%s' "$input" | jq -r '.permission_mode // empty')
[ "$mode" = "plan" ] || exit 0

context=$(cat <<'EOF'
# Plan 모드 운영 규칙 (이 세션에서 반드시 적용한다)
1. 인터뷰: 첫 프롬프트 이후, 요건의 모호한 점이 없어질 때까지 AskUserQuestion으로 반복해서 질문한다. 1라운드로 끝내지 말고, 답변에서 새로운 모호점이 생기면 추가로 질문한다.
2. plan 확정 시그널: plan 파일이 확정되면, ExitPlanMode를 호출하기 전에 plan md 끝에 마커 행 <!-- render -->를 추가해 저장한다. 그 직후에 도착하는 시스템 알림의 절차를 따르고, 그것이 완료될 때까지 ExitPlanMode를 호출하지 않는다.
3. 실행 위임: ExitPlanMode 승인 후, 메인 세션에서는 파일 편집·구현 커맨드를 직접 실행하지 않는다. 일반 태스크는 Agent 툴(model은 4의 기준으로 다룬다), 장시간·독립성 높음·별도 디렉터리 작업은 claude --bg --model <model>, 단발 비대화 처리는 claude -p --model <model>에 위임한다. 메인 세션의 역할은 태스크 분할·지시·결과 검수·통합·보고뿐이다. 예외: 대화 전체의 컨텍스트를 파악하고 있어야만 수행할 수 있는 태스크(plan 전체와의 정합이 필요한 통합·최종 조정 등)에 한해 메인 세션에서 수행해도 된다. 그 외의 구현 작업을 메인 세션에서 하는 것은 불가.
4. 모델 배분: 범용 에이전트(general-purpose 등)는 model을 명시하고, 기본은 sonnet, 설계 판단·복잡한 디버깅·광범위한 변경 등 고난도 태스크는 opus를 지정한다. plan 수립을 위한 조사·설계 서브에이전트(Explore / Plan)에는 opus를 쓴다. 커스텀 에이전트는 model을 넘기지 말고 frontmatter 정의에 맡긴다. 덮어써도 되는 것은 고난도 태스크에서 opus로 끌어올리는 경우뿐이며, sonnet으로 끌어내리는 덮어쓰기는 하지 않는다.
EOF
)
jq -n --arg ctx "$context" '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $ctx}}'

일반 모드 세션에서는 아무것도 출력하지 않고 끝나므로 부작용은 0입니다. CLAUDE.md에 쓰지 않기 때문에 환경을 오염시키지 않아요.

2. Plan 모드에서 HTML을 만든다

HTML 출력과 문제점

Plan 모드는 계획이 굳어지면 최종 확인으로 만리장성보다 기이인 마크다운과 ExitPlanMode 승인이 마지막에 출력됩니다. 도해 하나 없는 마크다운을 읽는 건 꽤 고역이니 HTML로 확인하고 싶어집니다.

그런데 여기서 문제가 생깁니다. Plan 모드는 파일 작성이 불가능한 모드라서 HTML 파일도 못 만듭니다. 참고로 이번에 쓸 Claude Artifacts도 일단 로컬에 HTML 파일을 만들어야 하는 사양이라, 로컬에 만들지 않고 확인할 수는 없습니다.

대책

처음엔 Plan 모드에서 한 번 빠져나오는 것 말고는 방법이 없어 보였는데, 사실 Plan 모드가 만드는 md 파일은 내부적으로 Write / Edit 툴이 /Users/you/.claude/plans/xxxxx.md에 써넣는 형태로 만들어지고 있었어요.

그래서 PostToolUse hook을 써서, plans/xxxxx.md에 대한 쓰기를 감지해 headless claude -p에 변환시키는 방법으로 대처했습니다. hook이 기동하는 건 별도 프로세스(별도 세션)이므로, Plan 모드의 "파일을 만들 수 없다"는 제약에 걸리지 않아요. ~/.claude/settings.json 등록은 이렇습니다(matcherWrite|Edit에 반응시키고, 변환의 동기 실행에 대비해 timeout을 늘려 둡니다).

"PostToolUse": [
  {
    "matcher": "Write|Edit",
    "hooks": [
      { "type": "command",
        "command": "/Users/you/.claude/hooks/plan-html-render.sh",
        "timeout": 240,
        "statusMessage": "plan HTML 생성 중..." }
    ]
  }
]

Plan 모드의 계획 파일이 ~/.claude/plans/ 아래에 저장된다는 것은 필자가 직접 관찰한 내부 동작으로, Claude Code 버전에 따라 경로나 방식이 달라질 수 있습니다. 적용 전에 자기 환경에서 실제 저장 경로를 확인하는 것을 권합니다.

hook 스크립트 본체는 접힌 블록으로 실어 둡니다. Claude에게는 "확정되면 <!-- render -->를 끝에 써라"라고만 가르쳐 두고(이 지시는 1장의 hook에 들어 있습니다), hook은 마커가 있을 때만 돌아갑니다.

plan-html-render.sh (클릭해서 펼치기)

#!/bin/bash
# ~/.claude/hooks/plan-html-render.sh
# PostToolUse hook (Write|Edit): Plan 모드 중에 ~/.claude/plans/의 plan md에
# 확정 마커 <!-- render -->가 쓰이면, claude -p로 확인용 HTML을 동기 생성한다
input=$(cat)
mode=$(printf '%s' "$input" | jq -r '.permission_mode // empty')
[ "$mode" = "plan" ] || exit 0

file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
case "$file" in
  /Users/you/.claude/plans/*.md) ;;
  *) exit 0 ;;
esac
[ -f "$file" ] || exit 0
grep -q '<!-- render -->' "$file" || exit 0

# ~/.claude/ 아래는 설정 디렉터리 보호로 headless의 Write가 기계적으로 차단되므로 밖에 둔다
html_dir="/Users/you/plan-html"
mkdir -p "$html_dir"
base=$(basename "$file" .md)
html="$html_dir/$base.html"

prompt="다음 플랜 md를 리뷰용 self-contained HTML로 변환해 주세요. 이 HTML은 Claude Artifact로 게시하는 페이지이므로, Artifact의 디자인 가이던스(design fundamentals)에 따라 플랜 내용을 도해를 곁들여 시각화할 것.
입력: $file 을 Read 한다 (끝의 마커 행 <!-- render --> 는 출력에 포함하지 않는다).
출력: $html 에 Write 한다.
요건:
- DOCTYPE / html / head / body 태그는 쓰지 않는다. <title> 과 <style> 과 콘텐츠를 직접 쓴다.
- 외부 리소스(CDN·외부 폰트·외부 이미지)는 일절 쓰지 않는다. 인라인 CSS / SVG만.
- 색은 :root 의 CSS 커스텀 프로퍼티로 정의하고, prefers-color-scheme 과 :root[data-theme=\"dark\"] / :root[data-theme=\"light\"] 양쪽에 대응해 라이트/다크 두 테마에서 읽을 수 있게 한다.
- 절차·플로·구성·스케줄 등 순서나 관계가 본질인 내용은 그림(CSS 스텝 플로 / 인라인 SVG 박스+화살표 / CSS 간트)으로 시각화한다. 비교·나열은 표로 만든다. 장식 목적의 그림은 만들지 않는다.
- 제목 계층·여백·타이포그래피를 다듬어 한국어로 읽기 쉽게 한다. 가로로 넓은 요소는 overflow-x: auto 컨테이너에 넣는다.
Artifact 게시는 메인 세션이 한다. 당신은 파일을 Write 하기만 하고, 게시는 하지 말 것.
변환해서 Write 했으면 done 이라고만 출력하고 종료할 것."

if claude -p "$prompt" --model sonnet --allowedTools "Read,Write" --disallowedTools "Artifact" > /dev/null 2>&1 && [ -f "$html" ] && [ "$html" -nt "$file" ]; then
  ctx="plan 확인용 HTML을 자동 생성했다: $html
(1) Artifact 툴로 $html 을 게시하고, 그 URL을 Bash의 「open <URL>」로 브라우저에서 연 다음 ExitPlanMode로 진행한다. AskUserQuestion으로의 확인은 끼우지 않는다 (승인/수정은 사용자가 ExitPlanMode 다이얼로그에서 한다. 같은 경로로 재게시하면 URL은 유지된다. 재게시 시에는 브라우저 쪽 리로드로 반영되므로 다시 open 하지 않아도 된다)
(2) reject로 수정 지시가 돌아오면 plan md를 수정한다. 마커 <!-- render --> 는 남기고, 수정은 가능한 한 1회의 Edit로 모은다 (Edit 할 때마다 재변환이 돌기 때문)
(3) Artifact는 소유자만 볼 수 있는 비공개 상태로 다룰 것. 공유 링크 발행·public 공개·제3자 공유 설정은 툴 사양상 가능하더라도 일절 하지 않는다"
else
  ctx="plan HTML 자동 생성에 실패했다 ($html 이 갱신되지 않음). 사용자에게 그 사실을 전하고, plan md를 그대로 Artifact 툴로 게시해 확인을 받을 것 (Artifact는 비공개 상태로 다루고, 공유 링크 발행이나 public 공개는 하지 않는다)."
fi
jq -n --arg ctx "$ctx" '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $ctx}}'

마킹 방식 덕분에 plan md가 편집될 때마다 변환이 매번 돌지 않게 만들어 두었습니다. 마킹 후의 쓰기에서 hook이 돌고, stdin 정보로 Plan 모드의 Claude Code가 Write / Edit 한 파일을 특정하고, 마커를 체크해서 이벤트 구동으로 claude -p를 실행합니다.

claude -p에 넘기는 프롬프트($prompt)는 headless 세션 쪽에서 Skill의 description이 발동되도록 써 주세요. 다만 built-in인 artifact-design은 headless 세션에는 스킬 목록으로 제시되지 않아서, 키워드를 궁리해도 발동하지 않습니다(사양인지 버그인지는 미확인). 도해 의도나 디자인 원칙까지 넣고 싶다면 자작 Skill을 준비하세요(이 글에서는 plan-artifact라는 자작 Skill에 그림 구분 기준·컬러 토큰·인라인 CSS / SVG 구현 패턴 같은 artifact-design의 요점을 통째로 담아 두었습니다).

3. Claude Artifact로 게시한다

HTML로 변환하면 hook 스크립트의 마지막 행(다음 1행)이 결과를 Plan 모드 쪽 메인 세션으로 되돌려 줍니다. 여기서부터는 Plan 쪽이 태스크를 이어받습니다.

jq -n --arg ctx "$ctx" '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $ctx}}'

태스크를 이어받으면 Claude Artifacts(브라우저)가 열립니다.

plan을 도해한 HTML이 Claude Artifact로 브라우저에서 열린 화면

Artifact의 URL을 open으로 자동으로 열려면 allow 규칙이 필요합니다. Plan 모드 중의 Bash는 권한 프롬프트 대상이라, permissions.allow"Bash(open https://claude.ai/*)"를 넣어 두지 않으면 열 때마다 승인을 요구받게 됩니다.

터미널 쪽은 평소와 아무것도 달라지지 않고, 마크다운이 출력되고 ExitPlanMode가 호출됩니다.

터미널 쪽에서는 평소처럼 plan 마크다운과 ExitPlanMode 승인 다이얼로그가 표시된다

정리

다음 세 가지를 끼워 넣은 덕분에 꽤 편해지지 않을까 싶습니다.

  1. Plan 모드의 첫 프롬프트 이후 타이핑 부하를 AskUserQuestion으로 낮춘다
  2. 작업 효율화를 자동 오케스트레이션으로 실현한다. 동시에 세션 컨텍스트 오염을 방지하고 토큰 사용량을 줄인다
  3. Plan의 출력을 시각적인 HTML로 렌더링해 브라우저에서 확인한다

이 글이 도움이 됐다면 추천해 주세요

관련 글

댓글 0