Claude Code 헤드리스 모드(claude -p): 출력 형식과 종료 코드

게시 읽는 시간 약 11분 자동화 워크플로

--bare를 붙이고 인증 없이 claude -p를 돌려 보면 종료 코드는 1인데 stderr는 비어 있습니다. 실패 이유는 stdout의 JSON 안에 있고, 그 JSON의 subtype은 "success"예요. 스크립트가 실패를 성공으로 읽는 자리가 바로 여기입니다.

그래서 저는 claude -p "프롬프트" --output-format json으로 돌리고, 성패는 종료 코드(0이면 성공)와 JSON의 is_error로 가르길 권합니다. 플래그가 틀리면 실행 전에 stderr로, 실행 중 실패는 stdout으로 나온다는 것도 알아 두면 좋고요.

Windows라면 하나가 더 있습니다. 셸 명령이 2.1.283에서는 PowerShell 도구로, 2.1.296에서는 Bash 도구로 돌아서 맞는 허용 규칙이 갈렸어요. 그래서 Bash(git commit *)와 PowerShell(git commit *)를 둘 다 적어 두는데, 규칙이 안 맞아 거부돼도 종료 코드는 0이더라고요.

2026년 9월 20일 Ubuntu 24.04 · Claude Code 2.1.278에서, 9월 30일 Windows 11 · PowerShell 5.1 · 2.1.283에서, 10월 13일 Windows 11 · 2.1.296에서 직접 실행한 출력과 같은 날 확인한 공식 문서를 근거로 했습니다. 호출을 많이 반복할 때만 드러나는 문제는 공개 이슈를 인용했습니다.

확인한 버전·환경

  • Claude Code 2.1.296 · Windows 11 · Windows PowerShell 5.1 · 확인일 2026-10-13
  • Claude Code 2.1.278 · Ubuntu 24.04 · 확인일 2026-09-20

claude -p는 한 번 돌고 끝난다

-p(--print)는 대화형 화면 없이 한 번 실행해 응답을 표준 출력으로 내보내고 끝내는 플래그입니다. 프롬프트는 인자로 줘도 되고 파이프로 흘려 넣어도 됩니다.

claude -p "Find and fix the bug in auth.py" --allowedTools "Read,Edit,Bash"
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt

파이프 입력은 10MB까지이고, 넘으면 분명한 오류와 함께 0이 아닌 코드로 끝납니다. 더 큰 입력은 파일로 두고 프롬프트에 경로를 적으세요.

모든 옵션이 -p와 같이 쓰이지는 않습니다. --bg와 작업 설명을 붙인 --cloud는 거부되고, 충돌하는 쪽을 이름으로 알려 줘요. 직접 실행하니 stderr에 이 한 줄이 나오고 종료 코드는 1이었습니다.

--bg and --print conflict: --print never starts the interactive session that `claude agents` attaches to, so the job would be unattachable. The prompt is the positional — drop --print: `claude --bg '<task>'`.

백그라운드로 돌리고 싶었던 거라면 --bg가 아니라 --print를 빼라는 뜻입니다.

여러 번 호출을 이어 붙이려면 세션 ID를 잡아 둔다

가장 최근 대화를 이으려면 --continue, 특정 세션이면 --resume <세션 ID>를 씁니다.

session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"

v2.1.223부터는 같은 컴퓨터의 어느 프로젝트에서든 ID로 세션을 찾습니다. 그 전에는 같은 프로젝트 폴더(와 그 git 워크트리)에서 돌려야 했어요. 세션 ID 대신 트랜스크립트 .jsonl 파일의 절대 경로를 넘겨도 됩니다.

끝나는 시점은 생각보다 늦을 수 있다

  • SIGTERM으로 멈추면 종료 코드 143으로 끝나고, 돌던 턴의 결과는 기록되지 않습니다. 턴을 마무리시키려면 먼저 SIGINT를 보내세요. 재개하면 남은 턴을 이어 갑니다.
  • 백그라운드 Bash 작업이 떠 있었다면 최종 결과가 나오고 표준 입력이 닫힌 뒤 약 5초 뒤에 그 셸이 끝납니다.
  • 백그라운드 서브에이전트나 워크플로가 떠 있으면 그 결과가 최종 출력의 일부라 기다리고, 연속 대기 10분에서 멈춘 뒤 부분 결과를 버립니다. CI 시간 제한을 잡을 때 참고할 값입니다.

스크립트에는 json 형식이 편하다

형식 받는 것 쓸 때
text(기본) 응답 본문만 파일로 바로 저장
json 결과·세션 ID·사용량·비용 한 객체 스크립트에서 필드를 읽을 때
stream-json 줄 단위 JSON 진행 상황을 실시간으로 그릴 때

한 줄짜리 프롬프트를 json으로 돌리면 이런 필드가 옵니다(일부 도구를 끈 상태, 실행마다 달라지는 값은 뺐습니다).

"result": "hello"
"is_error": false
"subtype": "success"
"num_turns": 1
"usage": { "input_tokens": 2, "cache_creation_input_tokens": 36120, "output_tokens": 4 }

제가 눈여겨본 건 사용량입니다. 프롬프트 입력은 2토큰인데 캐시 쓰기는 36,120토큰이었어요. 호출 한 번이 프롬프트 길이만큼의 비용이라고 가정하면 크게 어긋납니다.

플랜 한도가 어떤 단위로 줄어드는지는 사용량 한도에서 무엇이 세어지는지 정리한 글에 있습니다. total_cost_usd와 모델별 내역도 들어 있지만 클라이언트 쪽 추정치라 실제 청구와 다를 수 있으니, 정산보다 추세를 보는 데 쓰세요.

stream-json으로 토큰이 생성되는 대로 받으려면 --verbose·--include-partial-messages를 함께 붙입니다. 부분 메시지 없이 --verbose만 붙여 직접 돌리고 줄마다 type과 subtype만 찍어 보니 이 순서였습니다.

active_goal
autocompact_state
system | commands_changed
rate_limit_event
system | init
assistant
result | success

주의: 마지막 줄은 늘 result지만, 첫 줄이 늘 system/init인 것은 아닙니다. 시작 이벤트가 앞에 올 수 있으니 파싱할 때는 type으로 고르세요.

PowerShell에서는 jq 대신 ConvertFrom-Json

Windows에는 jq가 기본으로 없습니다. ConvertFrom-Json으로 바로 필드를 꺼내고, 종료 코드는 $LASTEXITCODE에서 보면 됩니다. Windows 11에서 직접 실행한 결과입니다.

$r = claude -p "Reply with exactly: hello" --output-format json --max-turns 1 | ConvertFrom-Json
$r.result; $r.is_error; $r.subtype; $LASTEXITCODE
hello
False
success
0

세션 이어 붙이기도 그대로 됐습니다. 첫 실행에서 PLUM-7이라는 단어를 기억시키고 --resume $sid로 두 번째 실행에서 물으니 PLUM-7이라고 정확히 답하더라고요.

성패는 종료 코드와 is_error로 가른다

종료 코드는 성공이면 0, 실패면 0이 아닌 값입니다. 실패 내용이 어디로 나오는지는 언제 실패했느냐에 따라 갈려요.

claude -p 실행 흐름도: 플래그 검증에 실패하면 턴이 시작되기 전에 stderr로 오류가 나가고 stdout은 비며, 실행이 시작된 뒤에 실패하면 stderr가 아니라 stdout으로 나오고 종료 코드가 0이 아니게 되지만 result 필드는 실패 종류에 따라 문구일 수도 null일 수도 있으며, 성공하면 stdout으로 응답이 나오고 종료 코드가 0이 된다.
플래그 오류는 실행 전에 stderr로, 실행 중 실패는 stdout으로 나옵니다.

직접 세 가지 실패를 만들어 봤습니다.

상황 나온 곳 종료 코드 result
플래그 값 오류(--output-format yaml) stderr 1 없음(stdout 비어 있음)
인증 없이 --bare 실행 stdout 1 오류 문구, subtype은 "success"
--max-turns 1 상한에 걸림 stdout 1 null, subtype은 "error_max_turns"

두 번째 줄이 제일 위험합니다. subtype 문자열로 성패를 가르는 스크립트는 이 실패를 성공으로 읽어요. 셋째 줄처럼 result가 null일 때도 있으니, jq -r '.result'로 곧장 꺼내 쓰기 전에 is_error부터 보세요.

error: option '--output-format <format>' argument 'yaml' is invalid

error: option '--output-format <format>' argument 'yaml' is invalid. Allowed choices are text, json, stream-json.

플래그 값이 허용 목록에 없어서 모델 호출 전에 끝난 경우입니다. 메시지가 허용값을 알려 줘요. 스크립트라면 stderr를 버리지 말고 로그에 남겨야 이 줄을 볼 수 있습니다.

Windows PowerShell 5.1에서 2>&1로 받으면 이 줄은 문자열이 아니라 ErrorRecord 객체가 됩니다. 직접 해 보니 $out[0].GetType().Name이 ErrorRecord였어요. 문자열로 비교하거나 로그에 남기려면 "$($out[0])"처럼 바꾸세요.

Authentication error · This may be a temporary network issue, please try again

종료 코드는 0이 아닌데 stderr는 비어 있고, JSON의 result에 이 문구가 들어 있습니다. --bare를 붙이고 ANTHROPIC_API_KEY를 두지 않아 생긴 상태예요. 바 모드라면 ANTHROPIC_API_KEY를 두거나 --settings로 apiKeyHelper를 주고, 아니라면 로그인 상태를 확인합니다.

CI에서는 권한을 명령줄에서 미리 정한다

물어볼 사람이 없으니 승인 기준을 미리 정해야 합니다. 개별 도구만 열려면 --allowedTools를 씁니다.

claude -p "Look at my staged changes and create an appropriate commit" \
  --allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)"

여기서 별표 앞의 공백이 중요합니다. Bash(git diff *)는 git diff로 시작하는 명령만 허용하지만, 공백 없는 Bash(git diff*)는 git diff-index까지 걸려요. 규칙 문법은 allow·deny 규칙이 어디서 새는지 정리한 글에 있습니다.

세션 전체의 기준은 권한 모드로 정합니다. 모드를 어디서도 정하지 않으면 내장 시작 모드를 따르는데, 보통은 수동(default)이지만 2.1.285부터 서드파티 제공자를 쓰거나 텔레메트리를 끈 -p 세션은 auto로 시작합니다(2026-10-02 확인). 직접 넘겨 두면 영향을 받지 않으니, CI에서는 넘겨 두는 편이 낫습니다.

모드 묻지 않고 하는 것 쓸 때
auto 분류기가 대부분 대신 검토 사람 대신 판단을 맡길 때
acceptEdits 파일 쓰기, mkdir·mv·cp 같은 흔한 명령 편집 위주 작업
dontAsk 원래 물었을 호출은 전부 거부 잠가 둔 CI

acceptEdits라도 다른 셸 명령과 네트워크 요청은 허용 규칙이 있어야 하고, dontAsk라도 원래 승인이 필요 없던 동작(작업 폴더 읽기, 읽기 전용 명령)과 허용 규칙이 덮는 동작은 그대로 돕니다. 여섯 모드 전체는 권한 모드를 언제 무엇으로 고르는지에 있습니다.

권한 프롬프트에 답할 주체 자체를 없애는 --permission-prompts none은 v2.1.259 이상이 필요하고, 그 전 버전은 알 수 없는 옵션으로 거부하니 오래된 러너에서는 실행이 시작되지 않습니다.

기계마다 같은 결과가 필요하면 --bare

-p라서 가볍게 돌 것 같지만 그렇지 않습니다. --bare 없이 돌리면 -p도 대화형 세션과 같은 컨텍스트를 읽어요.

작업 폴더와 ~/.claude의 설정을 포함해, 프로젝트 .claude/settings.json의 훅을 실행하고 .mcp.json의 서버에 접속합니다. -p에는 신뢰 대화상자도 서버별 승인도 뜨지 않으니 한 번도 신뢰한 적 없는 폴더에서도 그렇습니다.

--bare는 훅, 스킬, 커스텀 명령, 서브에이전트, 플러그인, MCP 서버, 자동 메모리, CLAUDE.md의 자동 탐색을 끕니다. 스크립트와 SDK 호출에 권장되는 모드이고, 앞으로 -p의 기본값이 될 예정이에요. 쓸 때 걸리는 점이 있습니다.

  • --add-dir로 준 폴더는 부분 예외입니다. 그 폴더의 .claude/skills/는 읽고, .claude/commands/·.claude/agents/는 건너뜁니다.
  • OAuth 자격 증명과 시스템 키체인을 읽지 않습니다. Anthropic API라면 ANTHROPIC_API_KEY나 --settings의 apiKeyHelper가 필요하고, Amazon Bedrock·Google Cloud의 Agent Platform·Microsoft Foundry는 평소대로 자기 자격 증명을 읽습니다.

Windows에서는 버전마다 맞는 허용 규칙이 달랐다

Windows 11에서 가장 크게 달랐던 자리입니다. 빈 git 저장소에서 “git commit --allow-empty -m probe로 커밋하라”고 시키고, 허용 규칙을 바꿔 가며 커밋이 실제로 생겼는지 셌습니다.

--allowedTools 커밋 수 거부 기록(permission_denials)
"Bash(git commit *)" 1 → 1 PowerShell 도구의 git commit
"PowerShell(git commit *)" 1 → 2 없음
지정 안 함 2 → 2 PowerShell 도구의 git commit

세 실행 모두 종료 코드는 0이었습니다. 이때는 명령이 Bash가 아니라 PowerShell 도구로 돌아서, 문서 예시의 Bash(...) 규칙이 맞지 않아 거부되고 거부돼도 종료 코드로는 드러나지 않습니다.

10월 13일 Claude Code 2.1.296에서 같은 실험을 수동 모드(--permission-mode default)로 다시 돌렸더니 결과가 정반대였습니다.

--allowedTools 커밋 수 거부 기록(permission_denials)
"Bash(git commit *)" 4 → 5 없음
"PowerShell(git commit *)" 5 → 5 Bash 도구의 git commit
지정 안 함 5 → 5 Bash 도구의 git commit

이번에는 같은 명령이 Bash 도구로 돌았어요. 같은 Windows인데 셸 명령이 어느 도구로 도는지가 2.1.283과 2.1.296 두 실행에서 갈린 거예요. 종료 코드는 이번에도 세 번 모두 0이었습니다.

그러니 Windows 러너에서는 Bash(git commit *)와 PowerShell(git commit *)를 둘 다 적고, JSON의 permission_denials가 비었는지 함께 보세요. 결과에 한글이 섞여 깨진다면 AI CLI 한글 깨짐 글을 보세요.

다른 사람들이 겪은 문제

다른 사람이 공개한 보고입니다. 세 건 모두 anthropics/claude-code 이슈이고 Windows에서 보고됐습니다. 반복 호출에서만 드러나는 문제라, 일괄 작업을 짜기 전에 한 번 호출로 미리 재 보는 게 좋습니다.

호출마다 고정 비용이 붙는다

Istuification(2026-09-17, 이슈 #95129, Claude Code 2.1.274, Windows 11)은 claude -p --output-format json --model sonnet을 --resume 없이 매번 새 프로세스로 띄웠더니, 콜드 스타트 호출의 cache_creation_input_tokens가 약 14,857~16,550이었고 계획한 2,640회 중 104회 만에 구독 한도가 소진됐다고 보고했습니다.

보고자는 원인을 단정하지 않고, 전체 시스템 프롬프트와 도구 정의 없이 호출할 방법이 있는지 질문으로 남겼습니다(9월 20일 확인 시 열려 있음).

위에서 잰 한 줄짜리 호출의 캐시 쓰기 36,120토큰도 같은 방향입니다.

--bare는 자동 탐색을 끄고 쓰는 도구를 Bash·파일 읽기·파일 편집으로 줄이며, 기본 시스템 프롬프트를 바꾸는 --system-prompt도 있습니다. 일괄 작업을 짜기 전에 자기 조건에서 usage부터 재 보세요.

Failed to refresh OAuth token: another Claude Code process is refreshing it or exited mid-refresh

davidfi01(2026-09-17, 이슈 #95236)은 헤드리스 호출 묶음(실행당 약 48회, 동시 약 3개)이 전부 이 메시지로 실패했고, 180초를 기다려 재시도해도 같았다고 보고했습니다.

그 시점에 %USERPROFILE%\.claude\.oauth_refresh.lock이 빈 폴더로 남아 있었고, 이를 손으로 지우자 다음 호출이 3.3초 만에 성공해 이후 약 200회 동안 재발하지 않았다고 적었습니다.

보고자는 60초가 지난 잠금은 회수됐어야 한다고 봤지만, 회수되지 않은 이유는 단정하지 않았습니다. 다른 Claude Code 프로세스를 닫고 그 폴더를 지우고, 그래도 안 되면 claude auth login을 다시 하라는 것이 보고자의 우회법입니다.

인자 순서에 따라 프롬프트가 무시됐다

calmond(2026-09-19, 이슈 #95585)는 -p를 --append-system-prompt보다 앞에 둬야 프롬프트가 따라졌다고 보고했습니다.

다만 재현에 쓴 것은 Anthropic 호스팅 모델이 아니라 로컬 호환 엔드포인트와 커스텀 모델이었고, 원인이 어디에 있는지 판단할 수 없다고 보고자 스스로 밝혔습니다. 같은 증상을 실제로 보기 전에 스크립트를 고칠 이유는 없다고 봅니다.

마치며

claude -p를 직접 돌려 보며 가장 크게 남은 건, 실패가 조용하다는 점이었습니다. 인증 실패는 subtype이 "success"인 채로 stdout에 나오고, Windows에서 막힌 커밋은 종료 코드 0으로 끝났어요.

그래서 형식은 json으로 받고, 성패는 종료 코드와 is_error로 가르고, Windows 러너라면 permission_denials까지 보는 것을 권합니다. CI에서는 권한 모드를 직접 넘기고, 같은 결과가 필요하면 --bare를 붙이세요.

지금 쓰는 스크립트에 subtype으로 성패를 가르는 줄이 있다면 그것부터 is_error와 종료 코드로 바꿔 보세요. 사람 없이 돌리기 전에 무엇을 갖춰야 하는지는 반복 작업을 에이전트에 넘기는 네 단계에 이어서 정리해 두었습니다.

참고한 공식 자료

  • Run Claude Code programmatically — -p 사용법, 출력 형식, 종료 코드와 실패 위치, --bare, 권한 모드, 세션 이어 붙이기 (2026-09-20 확인, 시작 모드는 2026-10-02 확인)
  • CLI reference — --max-turns를 비롯한 플래그와 제약 (2026-09-20 확인)
  • Tools reference — Windows에서 PowerShell 도구가 주 셸이 되는 조건 (2026-09-30 확인)

인용한 공개 사례

변경 이력

  • 2026-10-14 — 도입부가 여전히 Windows에서는 PowerShell 도구로 돈다고 단정하던 것을, 2.1.283과 2.1.296 두 실행에서 갈렸으니 두 규칙을 함께 적으라는 본문 결론에 맞췄습니다.
  • 2026-10-13 — Claude Code 2.1.296에서 Windows 허용 규칙 실험을 다시 해, 이번에는 셸 명령이 Bash 도구로 돌아 Bash(...) 규칙만 통과하고 PowerShell(...) 규칙은 거부되는 반대 결과가 나왔습니다. 두 규칙을 함께 적으라는 권고로 고쳤습니다.
  • 2026-10-02 — 헤드리스 문서가 -p의 시작 모드를 수동으로 고정해 적던 문장을 auto일 수 있다는 설명으로 바꿔, 인용과 조건 설명을 현재 문서에 맞췄습니다.
  • 2026-10-01 — Claude Code 2.1.285에서 일부 조건의 `-p` 세션 시작 모드가 auto로 바뀌어, 권한 모드를 넘기는 문단에 그 조건을 덧붙였습니다.
  • 2026-09-30 — Windows 11 · PowerShell 5.1에서 직접 실행한 절을 더했습니다. jq 없이 JSON을 읽는 법, 2>&1로 받은 stderr가 오류 객체가 되는 점, 세션 이어 붙이기, 그리고 Windows에서는 명령이 PowerShell 도구로 돌아 Bash(...) 허용 규칙이 듣지 않는 점을 다룹니다.