본문 바로가기
AI.IT

OpenClaw claude-cli 빌트인 백엔드 설정법, 텔레그램 스트리밍 패치까지

by bamsik 2026. 4. 15.
반응형

API 과금 없이 Claude를 쓰고 싶었다

OpenClaw에서 Claude를 쓰려면 원래 Anthropic API 키가 필요하다. 종량제 과금이라 크론잡이나 블로그 자동화처럼 하루에 수십 번 호출하는 용도로는 부담이 꽤 크다.

그런데 Claude Code를 Pro나 Max로 구독하고 있으면, 그 CLI를 OpenClaw 백엔드로 물려서 API 비용 0원에 Claude를 굴릴 수 있다. OpenClaw에 claude-cli라는 빌트인 백엔드가 이미 들어있거든.

설정 자체는 어렵지 않은데, 한 가지 함정이 있었다. 텔레그램에서 응답이 실시간으로 안 오고 한 번에 뭉텅이로 오는 문제. 이걸 해결하려면 dist 파일 패치가 필요하다.

오늘은 빌트인 claude-cli 백엔드 설정부터 텔레그램 스트리밍 패치까지, 실제로 적용한 과정을 정리해봤다.

 

빌트인 claude-cli 백엔드란

OpenClaw 2026.4.x 버전부터 claude-cli라는 CLI 백엔드가 내장되어 있다. 예전에는 커스텀 wrapper 스크립트를 만들어서 Claude Code CLI를 감싸야 했는데, 이제 그럴 필요가 없다.

빌트인이 자동으로 처리해주는 것들이 꽤 많다.

  • --output-format stream-json — 스트리밍 출력
  • --permission-mode bypassPermissions — 자동화용 권한 우회
  • bundleMcp: true — MCP 서버 자동 번들링
  • watchdog — 프로세스 hang 감지 (idle 120초, total 600초)
  • clearEnv: true — 환경변수 격리
  • serialize: true — 요청 순차 처리

직접 설정해야 하는 건 CLI 경로, 모델 매핑, 세션 모드 정도다.

 

설정 방법: 3가지만 하면 된다

전제 조건은 두 가지. Claude Code CLI가 설치되어 있어야 하고, claude login으로 구독 인증이 완료되어야 한다.

~/.openclaw/openclaw.jsonagents.defaults 안에 세 가지를 추가한다.

1. cliBackends 등록

"cliBackends": {
  "claude-cli": {
    "command": "/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js",
    "modelArg": "--model",
    "modelAliases": {
      "opus": "claude-opus-4-6",
      "sonnet": "claude-sonnet-4-6",
      "haiku": "claude-haiku-4-5"
    },
    "sessionMode": "always"
  }
}

command에는 Claude Code CLI의 절대 경로를 넣는다. macOS Homebrew 설치 기준이고, npm global이나 Linux라면 경로가 다르다. which claude로 확인하면 된다.

sessionMode: "always"는 매 대화에 세션 ID를 부여해서 맥락을 유지시켜준다. 크론잡이나 에이전트처럼 이어지는 대화가 필요한 경우 필수.

2. 모델 등록

"models": {
  "claude-cli/sonnet": { "alias": "cli-sonnet" },
  "claude-cli/haiku": { "alias": "cli-haiku" },
  "claude-cli/opus": { "alias": "cli-opus" }
}

3. 기본 모델 지정

"model": { "primary": "claude-cli/sonnet" }

이러면 에이전트, 크론잡, 블로그 파이프라인 전부 claude-cli/sonnet을 기본으로 쓰게 된다. 경량 작업에는 claude-cli/haiku, 고품질이 필요하면 claude-cli/opus를 지정하면 되고.

 

여기서 끝이 아니었다 — 텔레그램 스트리밍 문제

설정을 마치고 openclaw chat으로 테스트하면 잘 된다. 근데 텔레그램 봇에서 메시지를 보내보면 문제가 보인다.

응답이 실시간으로 타이핑되듯 오는 게 아니라, 전체 응답이 완성된 뒤에 한 번에 뭉텅이로 온다. 이건 사용 경험이 확 나빠지는 부분이다.

원인을 파봤더니, OpenClaw의 agent-runner.runtime 코드에서 CLI 백엔드 경로와 embedded(API) 경로의 처리가 달랐다.

  • embedded 경로: onPartialReply 콜백이 정상 연결됨 → 텔레그램에 실시간 전달
  • CLI 경로: runCliAgent 호출 시 params.opts.onPartialReply완전히 드랍

CLI가 내부적으로 emitAgentEvent({stream:"assistant"})로 이벤트 버스에 delta를 올리긴 하는데, 그걸 텔레그램의 onPartialReply 콜백으로 프록시하는 코드가 CLI 경로에는 없는 거다.

패치: 3곳만 고치면 된다

대상 파일은 OpenClaw dist 폴더의 agent-runner.runtime-*.js. 해시값이 버전마다 다르니까 패턴으로 찾는다.

find $(npm root -g)/openclaw/dist -name "agent-runner.runtime-*.js"

패치 전에 반드시 백업.

cp agent-runner.runtime-XXXXXXXX.js agent-runner.runtime-XXXXXXXX.js.bak

패치 1: import에 onAgentEvent 추가

파일 상단의 agent-events import 라인을 찾는다.

// 변경 전
import { i as emitAgentEvent, u as registerAgentRunContext } from "./agent-events-XXXXXXXX.js";

// 변경 후 — onAgentEvent 추가
import { i as emitAgentEvent, l as onAgentEvent, u as registerAgentRunContext } from "./agent-events-XXXXXXXX.js";

onAgentEvent의 export alias(l)는 버전마다 다를 수 있다. agent-events-*.js 파일에서 실제 export를 확인해야 한다.

패치 2: CLI 경로에 스트리밍 브릿지 삽입

runCliAgent 호출 직전, let lifecycleTerminalEmitted = false; 바로 아래에 추가한다.

const _unsubCliStream = params.opts?.onPartialReply ? onAgentEvent((evt) => {
  if (evt.runId === runId && evt.stream === "assistant" && evt.data?.text) {
    try { params.opts.onPartialReply({ text: evt.data.text }); } catch {}
  }
}) : null;

이 코드가 하는 일은 간단하다. CLI가 이벤트 버스에 올리는 assistant 스트림 이벤트를 구독해서, 텔레그램의 onPartialReply 콜백으로 전달하는 브릿지 역할이다.

패치 3: finally에서 구독 해제

같은 함수의 finally 블록 첫 줄에 한 줄 추가.

_unsubCliStream?.();

에러든 정상 종료든, 이벤트 구독을 반드시 해제해서 리소스 누수를 방지한다.

패치 검증과 재시작

패치 후 문법 검사부터.

node --check agent-runner.runtime-*.js && echo "syntax ok"

패치가 제대로 들어갔는지 확인.

grep -n "onAgentEvent\|_unsubCliStream" agent-runner.runtime-*.js

3곳이 나와야 한다. import 라인, 브릿지 생성, 구독 해제. 하나라도 빠지면 안 된다.

확인됐으면 Gateway를 재시작한다.

pm2 restart openclaw-gateway

텔레그램에서 메시지를 보내보면, 응답이 타이핑되듯 실시간으로 들어오는 걸 확인할 수 있다.

주의할 점 하나

OpenClaw을 업데이트하면 dist 파일이 덮어씌워진다. 즉 이 패치가 날아간다.

업데이트 후에는 항상 이걸 체크해야 한다.

grep "onAgentEvent" agent-runner.runtime-*.js

결과가 안 나오면 재패치. 백업 파일이 있으니 diff로 비교하면 빠르다.

솔직히 이 부분이 좀 아쉽긴 하다. OpenClaw 쪽에서 CLI 경로에도 onPartialReply를 제대로 연결해주면 패치가 필요 없을 텐데. 이슈로 올려볼까 싶기도 하고.

정리하면

OpenClaw 빌트인 claude-cli 백엔드를 쓰면 API 비용 없이 Claude를 에이전트, 크론잡, 블로그 자동화에 활용할 수 있다. 설정은 openclaw.json에 cliBackends + 모델 등록 + 기본 모델 지정 3가지.

다만 텔레그램 실시간 스트리밍이 필요하면 agent-runner.runtime 패치가 필수다. 이벤트 버스의 assistant 스트림을 onPartialReply로 프록시하는 브릿지 코드 3줄이 핵심.

예전에 커스텀 wrapper를 만들어서 쓰다가 빌트인으로 갈아탔는데, 유지보수 부담이 확 줄었다. wrapper 관리, 타임아웃 맞추기, stderr 에러 핸들링 같은 삽질이 사라진 게 제일 크다.


📌 함께 보면 좋은 글

반응형