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가 읽고 신뢰하는 값입니다.
ClaudePermissionFlag — 양방향
섹션 제목: “ClaudePermissionFlag — 양방향”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.mode와 Session.requiresRestartFor()를 동작하게 합니다.
ASK_BEFORE_EDIT가 문자 그대로 default라는 이름의 플래그로 매핑된다는 점에 주목할 만합니다. 플래그를 생략하는 것과 넘기는 것이 같지 않은 이유가 바로 이것입니다. --permission-mode default는 편집 전 확인 모드를 선택하고, 생략은 사용자가 설정한 것에 양보합니다.
spawn이 향하는 곳
섹션 제목: “spawn이 향하는 곳”win32에서는 spawn이 Job Object를 거칩니다. 오래 사는 세션이야말로 git-bash 워커가 떨어져 나가 트리 종료에서 살아남는 바로 그 경우이기 때문입니다. 그 외의 환경에서는 CLI를 detached로 spawn하여 프로세스 그룹의 리더가 되게 하고, 그래야 PosixProcessTree가 그룹 전체에 시그널을 보낼 수 있습니다.
작업 디렉토리는 isSpawnableAsCwd()가 cwd가 될 수 있다고 답할 때에만 건네집니다. 이를 거부하는 WSL UNC 경로는 디렉토리 이름을 담은 ENOENT로 spawn을 실패시키는데, 그것은 실행 파일이 없는 것처럼 읽힙니다 — 그래서 통째로 빼 버립니다.
다음으로 볼 것
섹션 제목: “다음으로 볼 것”- 프로바이더 작성하기 — 이 형태를 다른 CLI로 옮기기.
- AbstractProvider — 이것이 구현하는 계약과 그것이 아직 작은 이유.
- Command 레퍼런스 — 호출자가 실제로 작성하는 표면.