콘텐츠로 이동

Claude 프로바이더

Claude는 현재 구현된 유일한 프로바이더입니다. 그 아래의 모든 것이 두 번째 프로바이더가 대조될 기준이 됩니다.

import { Claude, Command } from 'activecli';
const command = Command.open(Claude, { workingDir: '/proj' });
await command.setMessage('fix the failing test').send();

프로바이더 자체는 아홉 줄 — 이름 하나와 팩토리 하나입니다. 실제 작업은 Adapter/Claude/ 아래 네 클래스에 있습니다.

클래스 무엇을 소유하는가
ClaudeAdapter 어느 플래그가 세션을 재개하는지, 언제 spawn에 Job Object가 필요한지, 출력을 파싱 가능하게 유지하는 환경
ClaudeArgv 인자 벡터. 프로세스를 시작하지 않고도 플래그 구성을 단언할 수 있도록 객체로 구축
ClaudePermissionFlag PermissionMode와 CLI 고유 플래그 이름 사이의 양방향 번역
ClaudeAuthEnv 상속된 자격 증명 중 spawn된 CLI에 도달해서는 안 되는 것

ClaudeAdapter는 구현하기보다 조합합니다. argv는 ClaudeArgv에서, PATH는 AugmentedPath에서, 고아 프로세스 봉쇄는 JobSpawner에서, 출력 분류는 EventParser에서 옵니다. 남는 것은 진짜로 Claude 고유인 부분입니다.

ClaudeArgv.STREAM_FLAGS는 상수입니다 — 모든 spawn이 이 전부를 싣고 갑니다. CLI를 터미널 UI가 아니라 제어 가능한 스트림으로 만드는 플래그들입니다.

private static readonly STREAM_FLAGS = [
'-p',
'--output-format',
'stream-json',
'--input-format',
'stream-json',
'--verbose',
'--include-partial-messages',
'--permission-prompt-tool',
'stdio',
];
플래그 없으면 무엇이 깨지는가
-p CLI가 구동되는 프로세스로 실행되는 대신 대화형 터미널 UI를 그린다
--output-format stream-json 출력이 파서가 분류할 수 있는 엔트리가 아니라 사람이 읽을 산문으로 도착한다
--input-format stream-json stdin이 열린 채 유지되지 않아, 세션이 이어지는 대신 답변 하나로 끝난다 — 대화를 양방향으로 만드는 것이 이 플래그다
--verbose 호스트가 턴을 따라가는 데 필요한 엔트리가 아예 방출되지 않는다
--include-partial-messages 텍스트가 턴이 완료될 때에만 도착하므로, 생성되는 대로 사용자에게 스트리밍할 수 없다
--permission-prompt-tool stdio 승인 요청이 호스트가 듣고 있는 채널로 라우팅되지 않아, 답할 방법이 없다

마지막 한 쌍이 권한 처리를 애초에 가능하게 합니다. --input-format stream-json이 호스트에게 말할 채널을 주고, --permission-prompt-tool stdio가 CLI의 승인 요청을 그 채널로 향하게 합니다.

세 가지가 더 있으며, 무언가가 요청했을 때에만 덧붙습니다. 각각의 생략이 하나의 결정입니다.

플래그 언제 붙는가 왜 조건부인가
--session-id / --resume 항상 둘 중 하나 새 대화를 시작하는 것과 기존 대화를 이어가는 것은 다른 플래그를 쓰며, 어느 쪽인지는 호출자만 안다
--no-session-persistence ephemeral 실행 프로브는 사용자의 히스토리에 나타나서는 안 된다. 사용자가 요청한 턴이 아니며, 그런 것으로 가득한 트랜스크립트는 사용자가 대응할 수 없는 잡음이다
--permission-mode 모드가 확립되었을 때 부재는 아직 아무것도 모드를 확립하지 않았다는 뜻이므로, CLI가 자신의 설정된 기본값을 읽도록 남겨 둔다
--model 모델이 고정되었고 그것이 default가 아닐 때 default는 “고정된 모델 없음”을 뜻하는 CLI 자신의 별칭이므로, 넘긴다면 선택으로 위장한 무동작이 된다

이 중 둘은 명시해 둘 만한 규칙을 지니며, 둘 다 틀리기 쉽습니다.

규칙 이유
호출자가 선호를 표현하지 않았다면 플래그를 통째로 생략한다 CLI가 쓰는 “기본값”이라는 단어를 넘기는 것은 사용자의 설정에 양보하는 것이 아니라 그것을 덮어씁니다. --permission-mode default는 특정 모드를 지칭할 뿐 “사용자 설정을 따르라”를 뜻하지 않습니다.
모델이 선택되었다면 명시적으로 고정한다 컨트롤 채널을 통한 모델 재지정은 살아 있는 프로세스에만 닿습니다. 이 플래그가 없으면 유휴 상태에서 고른 모델이 다음 spawn에서 사라집니다.

ClaudeAdapter는 플래그 두 개를 들고 그중 하나를 고릅니다.

private static readonly RESUME_FLAG = '--resume';
private static readonly NEW_SESSION_FLAG = '--session-id';

선택은 options.isResuming()에서 오며, 이는 resume && sessionId !== null입니다 — 단지 id가 존재한다는 사실에서 오지 않습니다.

모든 spawn은 id를 싣고 갑니다. 재개할 수 없는 대화는 세션이 아니기 때문입니다. buildArgv는 id 없이는 throw합니다. 생략이 옳은 경우는 없습니다.

private static readonly PROTOCOL_ENV: NodeJS.ProcessEnv = {
TERM: 'dumb',
CI: 'true',
CLAUDECODE: undefined,
};
변수 이유
TERM=dumb 자신이 유능한 터미널에 붙어 있다고 여기는 CLI는 출력을 ANSI 이스케이프로 장식하고, 그것이 프로토콜과 같은 스트림에 떨어집니다. JSON 줄 앞에 \x1b[1 q가 붙으면 파싱이 실패합니다 — 다른 도구들이 로그인 셸을 통해 에이전트를 띄울 때 겪은 바로 그 고장입니다.
CI=true 같은 이유로 비대화형 동작을 요청합니다. 여기에는 터미널에 그려진 프롬프트에 답할 주체가 없습니다.
CLAUDECODE 제거 이 변수는 “너는 Claude Code 안에서 실행 중이다”를 표시합니다. 실제로 그랬던 부모로부터 상속받으면, 중첩 실행이 아닌데도 CLI가 중첩 실행처럼 동작합니다.

이것들을 병합할 때는 순서가 중요합니다. 프로토콜 환경이 바닥으로 먼저 오고, 그다음 호출자 자신의 것 — 라이브러리가 모르는 무언가를 알고 있을 수 있습니다 — 이 오며, 자격 증명 제거가 마지막입니다. 누가 물려주었든 상속된 토큰은 CLI를 갱신할 수 없는 것에 고정시키기 때문입니다.

ClaudeAuthEnv — 이것이 막는 401 루프

섹션 제목: “ClaudeAuthEnv — 이것이 막는 401 루프”

호스트가 자신을 띄운 무언가 — 데스크톱 앱, IDE, 오래된 셸 — 로부터 OAuth 토큰을 상속받았을 때 그것을 그대로 넘기면 CLI가 그 토큰에 고정됩니다. CLI는 명시적으로 주어진 토큰을 최종으로 받아들이고 키체인 기반 갱신을 결코 수행하지 않으므로, 상속된 토큰이 만료되는 순간 모든 호출이 401을 반환하고 계속 그렇게 반환합니다. 이를 제거하면 CLI가 갱신할 수 있는 인증으로 되돌아갑니다.

private static readonly STRIPPABLE = [
'CLAUDE_CODE_OAUTH_TOKEN',
'CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR',
] as const;

이 목록에서 빠진 두 가지는 의도적입니다.

ANTHROPIC_API_KEY는 그대로 둡니다. 만료되지 않고 갱신 흐름도 없으므로 위의 루프를 일으킬 수 없습니다 — 무언가를 제거할 유일한 이유가 그것입니다. 이것을 제거했더니 정확히 한 부류의 사용자가 깨졌습니다. 설정에 고정하는 대신 셸에서 export하여 인증하는 사용자들입니다. claude CLI는 이 변수를 직접 존중하므로, CLI를 구동하는 쪽도 그래야 합니다. 그러지 않으면 터미널에서 되는 그 명령이 GUI 아래에서는 실패합니다.

사용자 자신의 Claude 설정에 놓인 키가 이깁니다. 거기에 놓인 토큰은 의도적인 재정의 — 일부러 고른 개발용·스테이징용 자격 증명입니다. 그것을 제거하는 것은 사용자를 덮어쓰는 일이므로, 거기 존재하는 키는 그대로 둡니다.

변수는 { KEY: undefined }를 병합해 제거합니다. child_process가 “이 변수를 생략하라”로 취급하는 것이 그 형태이기 때문입니다. 빈 문자열은 여전히 CLI가 읽고 신뢰하는 값입니다.

PermissionMode는 공유 어휘이고, CLI는 자신만의 철자를 가집니다.

PermissionMode Claude의 플래그
PLAN plan
ASK_BEFORE_EDIT default
AUTO_EDIT acceptEdits
AUTO auto
BYPASS bypassPermissions

역방향 맵은 정방향에서 파생되므로 둘이 어긋날 수 없습니다.

두 방향 모두 필요합니다. 라이브러리는 플래그로 spawn하고, CLI는 같은 어휘로 자신의 모드를 알립니다 — spawn 시점의 system/init에서, 그리고 CLI가 스스로 모드를 바꿀 때 system/status에서 다시. 그것이 승인된 plan이 plan 모드를 벗어나는 것을 tool call을 들여다보지 않고도 관측 가능하게 만들며, Session.modeSession.requiresRestartFor()를 동작하게 합니다.

ASK_BEFORE_EDIT가 문자 그대로 default라는 이름의 플래그로 매핑된다는 점에 주목할 만합니다. 플래그를 생략하는 것과 넘기는 것이 같지 않은 이유가 바로 이것입니다. --permission-mode default는 편집 전 확인 모드를 선택하고, 생략은 사용자가 설정한 것에 양보합니다.

win32에서는 spawn이 Job Object를 거칩니다. 오래 사는 세션이야말로 git-bash 워커가 떨어져 나가 트리 종료에서 살아남는 바로 그 경우이기 때문입니다. 그 외의 환경에서는 CLI를 detached로 spawn하여 프로세스 그룹의 리더가 되게 하고, 그래야 PosixProcessTree가 그룹 전체에 시그널을 보낼 수 있습니다.

작업 디렉토리는 isSpawnableAsCwd()가 cwd가 될 수 있다고 답할 때에만 건네집니다. 이를 거부하는 WSL UNC 경로는 디렉토리 이름을 담은 ENOENT로 spawn을 실패시키는데, 그것은 실행 파일이 없는 것처럼 읽힙니다 — 그래서 통째로 빼 버립니다.