토큰은 유효했습니다. 하지만 제 헤드리스 에이전트는 401 오류를 냈습니다.
요약
Claude Code 사용자가 API 키 대신 OAuth 인증을 사용하는 환경에서 겪은 401 오류와 그 해결 과정을 다룹니다. API를 직접 호출하는 대신 이미 인증된 CLI 세션을 활용하는 방식을 제안합니다.
핵심 포인트
- Claude Code의 OAuth 인증과 Messages API의 API 키 인증은 서로 다름
- 환경 변수에 잘못된 값이 있으면 KeyError 없이 401 오류가 발생할 수 있음
- 헤드리스/CI 환경에서는 인증 실패가 로그에 묻혀 발견하기 어려움
- API 직접 호출 대신 'claude -p' CLI 명령어를 통해 인증 세션을 재사용 가능
이 저장소(repo)에는 Claude를 프로그래밍 방식으로 호출하는 두 개의 작은 도구가 있습니다. 하나는 커밋 메시지 생성기이고, 다른 하나는 프로필 업데이트 스크립트입니다. 두 도구 모두 동일한 방식으로 시작되었습니다. 환경 변수에서 ANTHROPIC_API_KEY를 가져와 Anthropic API로 원시 HTTPS 요청(raw HTTPS request)을 보내고, JSON을 다시 파싱하는 방식입니다. 교과서적인 방법이죠. 하지만 저에게는 이 방식이 작동하지 않았고, 특히 왜 그런지 이해하기까지 "하지만 토큰이 저기 있는데"라는 혼란스러운 과정을 거쳐야 했습니다.
멀쩡해 보이지만 그렇지 않은 설정
import os, urllib.request, json
def ask_claude(prompt):
...
이것은 올바른 코드입니다. 콘솔에서 실제 ANTHROPIC_API_KEY를 프로비저닝(provision)하고 내보내기(export)한 사람이라면 문제없이 작동할 것입니다. 하지만 저에게는 매번 401 오류와 함께 실패했습니다. 정말 미칠 노릇이었던 점은 os.environ["ANTHROPIC_API_KEY"]가 KeyError조차 발생시키지 않았다는 것입니다. 테스트 중에 언젠가 그 이름으로 _무언가_를 설정해 두었던 것입니다. 빈 문자열이거나 오래된 값이었겠죠. 그래서 요청은 기술적으로는 존재하지만 기능적으로는 유효하지 않은 자격 증명(credential)과 함께 전송되었습니다. 키가 아예 없는 경우와 동일한 실패 모드인 401, "invalid x-api-key" 오류가 발생했습니다.
실제 근본 원인: 두 가지 서로 다른 인증 시스템
저에게는 Anthropic API 키가 없습니다. 저는 구독을 통해 Claude Code를 사용하며, claude CLI를 통한 OAuth로 인증합니다. 이는 Messages API가 기대하는 원시 x-api-key 헤더와는 완전히 별개의 자격 증명 시스템입니다. 제 환경에는 작동해야 할 ANTHROPIC_API_KEY가 존재하지 않습니다. 그리고 폴백(fallback) 환경 변수가 있다고 가정하는 스크립트는 잘못된 열쇠로 잘못된 문을 확인하고 있다는 사실을 조용히 덮어버립니다.
해결책은 "API 호출을 수정하는 것"이 아니었습니다. 그것은 "API를 직접 호출하는 것을 멈추고" 대신 이미 유효한 OAuth 세션을 보유하고 있는 CLI를 통하는 것이었습니다.
import subprocess
def ask_claude(prompt):
...
claude -p는 제 대화형 터미널이 사용하는 것과 동일한 인증된 세션을 통해 일회성 프롬프트(one-shot prompt)를 실행합니다. API 키도, 헤더도, 헤드리스 스크립트에서 만료될 토큰도 필요 없습니다. CLI가 이미 신뢰하고 있는 세션을 그대로 이용하기 때문입니다.
헤드리스(Headless) / CI 환경에서 이것이 더 중요한 이유
대화형(Interactively) 환경에서는 이러한 종류의 버그가 즉각적으로 드러납니다. 명령어를 실행하면 401 오류가 발생하고, 사용자는 즉시 키를 확인하러 갈 것입니다. 하지만 헤드리스(Headless) 환경에서는 비용이 많이 듭니다. 크론 잡(cron job), 프리 커밋 훅(pre-commit hook), 에이전트를 호출하는 CI 단계에는 첫 번째 실패를 지켜보는 사람이 없습니다. 이는 조용히 실패하거나, 며칠 뒤 누군가 왜 자동 생성된 커밋 메시지가 더 이상 나타나지 않는지 물어볼 때까지 아무도 읽지 않는 로그 속에 묻혀버립니다.
여기서 발생하는 일반화 가능한 실패는 Anthropic에 국한된 문제가 아닙니다. 이는 "실제 환경에는 두 가지 인증 메커니즘이 있음에도 단일 인증 메커니즘을 가정했고, 기본값으로 잘못된 것을 선택했다"는 문제입니다. 저는 다음과 같은 유사한 사례들을 보았습니다:
- 실제 실행 컨텍스트는 수명이 짧은 OIDC 토큰을 통해 인증함에도 불구하고, 개인 액세스 토큰(personal access token)만 확인하는 도구.
- 실제 자격 증명 경로가 인스턴스 역할(instance role)이나 SSO 세션임에도 불구하고,
AWS_ACCESS_KEY_ID를 직접 읽는 스크립트가 누군가의 로컬 테스트에서 남겨진 환경 변수 값을 우연히 찾아내는 경우.
이 모든 사례의 공통적인 징후는 동일합니다. 토큰이 존재하며, 기본적인 검사를 통과할 만큼 형식이 잘 갖춰져 있기 때문에 요청이 나가기 전에는 아무것도 이를 잡아내지 못합니다. 따라서 실패는 "완전히 잘못된 인증 메커니즘"이 아니라, 마치
- 통합(integration) 코드를 작성하기 전에 '이 환경은 현재 실제로 어떻게 인증하는지, 대화형으로 물어보라'고 질문하세요. 단순히 '이 API에 인증하는 문서화된 방법은 무엇인가요?'라고 묻는 것은 아닙니다. 이 둘은 때때로 다르고, 구독(subscription) 또는 OAuth 기반의 도구 접근 방식이 바로 그 차이가 발생하는 경우입니다.
os.environ.get(...)의 참(truthiness) 값을 자격 증명(credential)이 유효하다는 증거로 신뢰하지 마세요. 존재하지만 오래된 값은 누락된 값과 동일하게 실패하며, 오류 메시지는 어느 쪽인지 알려주지 않을 것입니다.- 헤드리스 스크립트가 '나'의 역할을 해야 할 때, 인증을 재구현하기보다는 이미 대화형으로 사용하는 CLI(Command Line Interface)를 호출하는 것을 선호하세요. CLI의 유지 관리자들은 이미 토큰 새로 고침(token refresh), 세션 처리(session handling), 만료 등의 문제를 해결했습니다. 신선한
urllib호출은 훨씬 더 나쁜 출발점에서 이 모든 것을 재구현하게 만듭니다. - '유효한 키를 가져야 할 것 같은데도' 401 오류가 스크립트에서 나타나면, 요청 코드를 건드리기 전에 자격 증명 경로(credential path)를 확인하세요. 버그는 거의 HTTP 호출에 있는 것이 아닙니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기