← nacyot / artifacts

CLAUDE CODE, 패키징

Opus 5가 출시됐지만, 로컬의 “최신” 클로드 코드에는 4.8밖에 보이지 않았다

새 모델을 쓰려고 claude update를 돌렸다. 답은 “Claude Code is up to date”. 그런데 모델 목록에는 여전히 4.8까지만 있었다. 업데이트 명령이 거짓말을 한 건 아니다 — 자기가 보는 채널에서는 최신이 맞았다. 그 이유와, 같은 함정을 피하는 정리법을 정리한다.

2026-07-25 · 버전 번호와 명령 출력은 docker 컨테이너(ubuntu:24.04, node:22, python:3.12 / linux-arm64)에서 그날 실제로 실행한 결과다. 호스트에는 아무것도 설치·변경하지 않았다.

결론부터: 무엇을 어디까지 올려야 하나

필요한 버전 (2026-07-25 기준)확인
Claude Code CLI 2.1.220latest 채널. stable은 2.1.212에 머물러 있다claude doctorAuto-update channel
# 채널을 명시해서 다시 설치한다
curl -fsSL https://claude.ai/install.sh | bash -s -- latest

“로컬 claude를 최신으로 올렸는데 왜 안 되지?”의 답은 한 줄이다. 그 “최신”이 stable 채널의 최신이었다.

모델 가용성에 대한 단서

“어느 버전부터 어떤 모델이 보이는가”는 계정·구독·조직 설정·지역 롤아웃에 걸쳐 결정되므로 인증 없는 컨테이너에서는 재현할 수 없다. 이 글에서 그 부분은 내 계정에서 관찰된 사례다 — 2.1.212에서는 Opus 4.8까지 보였고, 2.1.220으로 올린 뒤 Opus 5가 보였다. 버전 번호·채널·경로·오류 메시지 등 그 밖의 모든 출력은 컨테이너 실측이다.

1증상: 업데이트는 최신이라는데 모델이 없다

순서는 이랬다. Opus 5가 나왔다는 소식을 듣고 모델을 바꾸려 했는데 목록에 없었다. 버전이 낮아서겠거니 하고 claude update를 실행했다. 결과는 “Claude Code is up to date (2.1.212)”. 최신이라니 그런가 보다 하고 넘어갈 뻔했다.

그런데 그 시점의 최신 릴리스는 2.1.220이었다. 업데이트 명령이 최신이라 말하는데 최신이 아니다. 버그처럼 보이지만 버그가 아니고, 이 문장에는 생략된 주어가 하나 있었다. “(stable 채널에서는) 최신이다.”

2원인: “최신”은 내 채널의 최신이었다

Claude Code는 하나의 버전 번호 체계 위에서 두 개의 채널로 배포된다. latest는 나오는 대로, stable은 한 박자 뒤에서 따라간다. npm의 dist-tag와 네이티브 배포 서버 양쪽이 같은 상태를 보여 준다.

# docker run --rm node:22 …
$ npm view @anthropic-ai/claude-code dist-tags
{ stable: '2.1.212', latest: '2.1.220', next: '2.1.220' }

$ curl -fsSL https://downloads.claude.ai/claude-code-releases/stable
2.1.212
$ curl -fsSL https://downloads.claude.ai/claude-code-releases/latest
2.1.220

npm에 찍힌 발행 시각(npm view … time)으로 보면 stable이 가리키던 2.1.212는 7월 16일 릴리스였고, latest는 7월 24일에 나온 2.1.220이었다. 여드레, 그 사이 여덟 개 릴리스가 stable 밖에 있었다.

그림 1. 2026-07-25 시점의 두 채널. 점 하나가 npm에 발행된 릴리스 하나다(간격은 균등 배치). claude update는 자기 채널의 화살표 끝만 본다.

stable latest 8일 · 릴리스 8개 차이 2.1.212 2.1.220 07-16 발행 07-24 발행 그 사이의 릴리스들

공식 인스톨러에 stable을 주고 설치한 뒤 그대로 claude update를 돌리면, 내가 본 화면이 컨테이너에서 그대로 재현된다.

# ubuntu:24.04 컨테이너, 일반 사용자
$ bash install.sh stable
  Version: 2.1.212

$ claude update
Current version: 2.1.212
Checking for updates to stable version...
Claude Code is up to date (2.1.212)

힌트는 이미 화면에 있었다. Checking for updates to stable version… — 어느 채널을 보고 있는지 한 줄로 알려 준다. 무심코 지나치기 쉬운 줄이고, 나도 지나쳤다.

claude update는 “세상의 최신”이 아니라 “내 채널의 최신”을 확인한다. stable에 묶여 있으면 latest에 무엇이 올라오든 영원히 up to date다.

확인하고 바꾸기

지금 어느 채널인지는 claude doctor 한 줄에 나온다. 값이 저장되는 자리는 ~/.claude/settings.jsonautoUpdatesChannel이고, 그 파일만 고쳐도 다음 업데이트부터 새 채널을 본다.

$ claude doctor
Running: native (2.1.212)
Path: ~/.local/share/claude/versions/2.1.212
Config install method: native
Auto-updates: enabled
Auto-update channel: stable

$ cat ~/.claude/settings.json
{ "autoUpdatesChannel": "stable" }

# latest로 고쳐 쓰고 다시 update
$ claude update
Checking for updates to latest version...
Updating to 2.1.220...
Successfully updated from 2.1.212 to version 2.1.220

표 1. 채널을 바꾸는 세 가지 방법(모두 실측).

방법명령결과
인스톨러 인자curl -fsSL https://claude.ai/install.sh | bash -s -- latest해당 채널의 최신을 설치하고 autoUpdatesChannel도 같이 바뀜
설정 파일~/.claude/settings.jsonautoUpdatesChannel 수정다음 claude update부터 새 채널을 봄
버전 직접 지정… install.sh | bash -s -- 2.1.212그 버전으로 고정(롤백에 사용)

덧붙일 것 하나. 인자 없이 새로 설치한 컨테이너에서는 기본값이 latest였다(npm 전역 설치도 마찬가지였다). 즉 stable 고착은 “요즘 기본값” 때문이 아니라 예전에 그렇게 깔렸거나 누군가 그렇게 지정해 둔 설정이 남아 있는 것이다. 기본값이 바뀌어도 기존 설치의 설정은 따라 바뀌지 않는다. 오래된 머신일수록 한 번은 확인할 값이다.

물론 stable은 이름값을 한다. 여러 사람이 같은 버전을 써야 하는 팀이나 CI라면 stable에 두거나 버전을 아예 고정하는 편이 낫다. 문제는 채널 선택이 아니라, 내가 어느 채널에 있는지 모른 채 “up to date”를 최신의 뜻으로 읽는 것이다.

네이티브 인스톨러가 하는 일 (구조 · 인자 · sudo 거부)

스크립트는 플랫폼(linux-x64, linux-arm64, musl 여부, darwin-*)을 판별해 매니페스트에서 SHA-256을 꺼내고, 바이너리를 받아 체크섬을 대조한 뒤 claude install로 넘긴다. 설치 결과에서 ~/.local/bin/claude는 실행 파일이 아니라 버전 디렉터리를 가리키는 심볼릭 링크다.

$ ls -l ~/.local/bin/claude
lrwxrwxrwx … ~/.local/bin/claude -> ~/.local/share/claude/versions/2.1.220

# 인자는 stable | latest | X.Y.Z 셋뿐. 버전을 주면 그 버전으로 롤백된다
$ bash install.sh 2.1.212 && ls ~/.local/share/claude/versions/
2.1.212
2.1.220
$ du -sh ~/.local/share/claude/versions/
509M	# 버전마다 260MB짜리 바이너리가 통째로 쌓인다

$ bash install.sh 2.1
Usage: install.sh [stable|latest|VERSION]

습관적으로 sudo를 붙이는 사람(나다)을 위한 안전장치도 있다. 설치 경로가 홈 디렉터리라 sudo로 돌리면 root의 홈에 깔리고 정작 본인 셸에서는 claude를 못 찾게 되는데, 스크립트가 그 상황을 먼저 막는다. 조건은 “root이면서 SUDO_USER가 일반 사용자일 때”라, 원래부터 root인 컨테이너·CI는 걸리지 않는다.

$ curl -fsSL https://claude.ai/install.sh | sudo bash
Error: do not run this installer with sudo.

Claude Code installs into your home directory and does not need root access.
With sudo, the installation would go into root's home directory instead of
yours, and the 'claude' command would not work from your own shell.

3곁가지: 로컬에 claude가 여러 벌일 때

채널을 고치려다 알게 된 게 하나 더 있다. 머신에 claude가 한 벌이 아니었다. node를 여러 벌 쓰는 환경에서는 npm i -g가 각각 다른 전역 디렉터리에 설치되고, 결국 이름이 같은 실행 파일이 PATH 위 여러 곳에 놓인다. 그러면 실행되는 버전은 오직 PATH 순서로 결정된다. 컨테이너에서 apt node(18)와 tarball node(22)를 함께 두고 재현한 결과다.

$ which -a claude
/opt/node22/bin/claude
/usr/local/bin/claude

$ PATH=/opt/node22/bin:/usr/local/bin:… claude --version
2.1.212 (Claude Code)
$ PATH=/usr/local/bin:/opt/node22/bin:… claude --version
2.1.195 (Claude Code)

같은 머신, 같은 명령, 다른 버전. 네이티브 설치와 npm 전역이 섞여도 마찬가지여서, 네이티브로 2.1.220을 깔아 둬도 PATH 앞의 npm 전역 2.1.195가 이긴다. 점검할 때 주의할 것은 npm ls -g 역시 지금 PATH에서 잡히는 npm의 전역 디렉터리만 본다는 점이다. node가 여러 벌이면 npm마다 경로를 박아 물어봐야 전모가 보인다.

$ /usr/bin/npm ls -g --depth=0
/usr/local/lib
`-- @anthropic-ai/[email protected]

$ /opt/node22/bin/npm ls -g --depth=0
/opt/node22/lib
+-- @anthropic-ai/[email protected]

버전 매니저를 쓴다면: shim 잔재

중복을 정리하려고 npm 전역을 지우는 과정에서 mise 사용자라면 이 오류를 만날 수 있다. mise(및 asdf 계열)는 PATH에 실제 실행 파일 대신 shim을 놓는데, shim의 수명이 실제 패키지와 따로 놀기 때문이다.

컨테이너에서 경우를 갈라 보니 갈리는 지점이 분명했다. mise exec -- npm rm -g …mise uninstall node@…처럼 매니저를 통해 지우면 자동 reshim으로 shim도 함께 사라져 깨끗했다. 남는 건 패키지는 그대로인데 버전 해석만 끊긴 경우다. node는 설치돼 있는데 설정(~/.config/mise/config.toml)에서 그 도구가 빠지면, shim 파일은 남고 가리킬 곳만 없어진다.

$ claude --version
mise ERROR No version is set for shim: claude
Set a global default version with one of the following:
mise use -g [email protected]

메시지가 친절해서 오히려 헷갈린다. “node 버전을 정하라”고 안내하지만, 하려던 일은 node를 되살리는 게 아니라 claude를 네이티브로 옮기는 것이었다. 이 경우 필요한 건 shim 파일을 지우는 것이다. 참고로 도구가 하나도 설정되지 않은 상태에서 mise reshim을 돌려도 남은 shim들은 지워지지 않았다. reshim은 현재 설치된 도구로 shim을 만드는 명령이지 청소해 주는 명령이 아니다.

4점검과 정리, 한 장

결국 확인할 것은 둘이다. “지금 실행되는 게 뭐냐”“그게 어느 채널을 보고 있냐”.

# 진단
which -a claude && claude --version
claude doctor                    # Running / Path / Auto-update channel
cat ~/.claude/settings.json      # autoUpdatesChannel

# 정리 — npm 전역을 npm마다 제거하고, 남은 shim은 파일을 직접 지운다
npm rm -g @anthropic-ai/claude-code
/usr/bin/npm rm -g @anthropic-ai/claude-code
rm -f ~/.local/share/mise/shims/claude

# 네이티브 한 벌로 정리 (채널 명시)
curl -fsSL https://claude.ai/install.sh | bash -s -- latest
hash -r                          # 셸의 실행 파일 경로 캐시를 비운다
which -a claude && claude --version && claude doctor

# PATH에 ~/.local/bin 이 없으면 (인스톨러도 이 문구로 안내한다)
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc

hash -r를 빼먹지 말자. bash는 한 번 찾은 실행 파일 경로를 캐시해서, 파일을 지운 뒤에도 같은 셸에서는 옛 경로를 계속 부른다. 그리고 옛 버전 바이너리를 정리할 땐 readlink -f ~/.local/bin/claude가 가리키는 것만은 남긴다(한 벌에 260MB다).

5마치며

Opus 5를 쓰기까지의 고비는 결국 한 문장이었다. claude update의 “up to date”는 이 채널에서 최신이라는 뜻이었다. 도구가 거짓말을 한 게 아니라, 생략된 주어를 내가 잘못 채운 것이다.

새 모델이 안 보일 때 확인할 순서는 그래서 짧다. claude doctor로 채널과 버전을 본다. 나머지는 대체로 그다음 문제다.

부록: 이 문서의 재현 명령 요약 (전부 컨테이너 안에서 실행)
# 1) 채널 확인
docker run --rm node:22 npm view @anthropic-ai/claude-code dist-tags
docker run --rm node:22 curl -fsSL https://downloads.claude.ai/claude-code-releases/stable

# 2) stable 고착 재현 (ubuntu:24.04, 일반 사용자)
curl -fsSL https://claude.ai/install.sh -o install.sh
bash install.sh stable && claude update      # -> up to date (2.1.212)
bash install.sh latest && claude --version    # -> 2.1.220

# 3) 중복 설치 재현 (배포판 node + tarball node)
apt-get install -y nodejs npm
tar -xJf node-v22.14.0-linux-arm64.tar.xz -C /opt/node22 --strip-components=1
npm i -g @anthropic-ai/[email protected]
/opt/node22/bin/npm i -g @anthropic-ai/[email protected]
which -a claude

# 4) mise shim 잔재 재현
curl -fsSL https://mise.run | sh
mise use -g node@22 && mise exec -- npm i -g @anthropic-ai/claude-code@stable && mise reshim
sed -i '/node/d' ~/.config/mise/config.toml
claude --version                              # -> No version is set for shim: claude

# 5) SDK 번들 확인
npm pack @anthropic-ai/claude-agent-sdk@latest
npm i @anthropic-ai/[email protected]
./node_modules/@anthropic-ai/claude-agent-sdk-linux-*/claude --version
pip install claude-agent-sdk && python -c "from claude_agent_sdk._cli_version import __cli_version__ as v; print(v)"

별록: Agent SDK를 쓴다면 — “내” claude를 쓰지 않는다

여기까지는 터미널의 claude 이야기였다. 그런데 코드에서 Claude를 부르는 경로가 하나 더 있다. Claude Agent SDK(npm @anthropic-ai/claude-agent-sdk, PyPI claude-agent-sdk)다. SDK는 내부적으로 Claude Code CLI를 프로세스로 띄워 쓴다. 그러면 로컬 claude를 올리면 SDK 앱도 따라 올라갈까?

아니다. SDK는 자기 몫의 CLI를 패키지 안에 넣어 다닌다. package.json에 그대로 적혀 있다 — claudeCodeVersion 필드와, 플랫폼별 바이너리를 정확한 버전에 고정한 optionalDependencies다.

$ npm pack @anthropic-ai/claude-agent-sdk@latest && tar -xzf *.tgz
$ cat package/package.json
{
  "name": "@anthropic-ai/claude-agent-sdk",
  "version": "0.3.220",
  "optionalDependencies": {
    "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.220",
    "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.220",
    …
  },
  "claudeCodeVersion": "2.1.220"
}

버전 번호도 우연이 아니다. SDK 0.3.N은 CLI 2.1.N과 짝이고, npm 발행 시각이 2초 차이다. 같은 파이프라인에서 함께 나간다. 그러니 본문의 기준에 맞추면, CLI 2.1.220을 번들한 SDK는 npm 0.3.220, Python 0.2.128이다.

# npm view … time --json 에서 발췌
claude-agent-sdk 0.3.220 : 2026-07-24T23:11:19.727Z
claude-code      2.1.220 : 2026-07-24T23:11:21.821Z

실제로 설치하면 260MB짜리 CLI 바이너리가 node_modules 안으로 따라 들어온다. SDK 버전을 내리면 딸려 오는 CLI도 같이 내려가고, 시스템에 무엇이 깔려 있든 상관하지 않는다.

$ npm i @anthropic-ai/[email protected]
$ ./node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64/claude --version
2.1.212 (Claude Code)

$ npm i @anthropic-ai/claude-agent-sdk@latest
$ ./node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64/claude --version
2.1.220 (Claude Code)

# 시스템 claude를 2.1.195로 깔아 둬도 SDK 쪽은 꿈쩍하지 않는다
$ npm i -g @anthropic-ai/[email protected] && claude --version
2.1.195 (Claude Code)
$ ./node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64/claude --version
2.1.220 (Claude Code)

파이썬도 구조가 같다. 휠 안에 _bundled/claude가 들어 있고(휠 84MB, 풀면 260MB), 어느 CLI를 물고 있는지 모듈로 노출한다. 그리고 탐색 순서가 코드에 명시돼 있다 — 번들이 먼저고 PATH는 폴백이다.

$ pip install claude-agent-sdk
$ cat …/site-packages/claude_agent_sdk/_cli_version.py
__cli_version__ = "2.1.220"

# claude_agent_sdk/_internal/transport/subprocess_cli.py
    def _find_cli(self) -> str:
        # First, check for bundled CLI
        bundled_cli = self._find_bundled_cli()
        if bundled_cli:
            return bundled_cli
        # Fall back to system-wide search
        if cli := shutil.which("claude"):

즉 SDK 앱에서는 which claude가 무엇을 가리키든 상관이 없다. 번들 파일이 있는 한 그쪽이 이긴다. 타입스크립트 SDK의 타입 정의도 같은 말을 한다 — pathToClaudeCodeExecutable?: string에 달린 주석이 “Uses the built-in executable if not specified.”다.

그림 2. 두 경로는 서로 다른 것을 실행한다. 터미널은 PATH 순서로 하나를 고르고, SDK 앱은 PATH를 보지 않는다.

터미널 PATH 앞에서부터 첫 번째로 걸리는 것 ~/.local/share/mise/shims/claude ~/.npm-global/bin/claude /usr/local/bin/claude ~/.local/bin/claude (네이티브) 앞의 하나만 실행된다나머지는 조용히 가려짐 SDK 앱 node_modules/…claude-agent-sdk-<plat>/claudeSDK 버전에 고정 · PATH를 보지 않음 번들 바이너리 실행SDK를 올려야 바뀐다

정리하면, SDK 앱이 쓰는 CLI 버전은 설치된 SDK 패키지 버전이 정한다. 로컬 claude를 올려도, 네이티브로 옮겨도, 채널을 바꿔도 바뀌지 않는다. 바꾸려면 SDK를 올려야 하고, lockfile로 고정해 뒀다면 그것도 같이 풀어야 한다. 굳이 시스템 CLI를 쓰게 하려면 경로를 명시한다(TS는 pathToClaudeCodeExecutable, 파이썬은 ClaudeAgentOptions(cli_path=…)). 다만 SDK와 CLI는 짝으로 릴리스되니 임의 조합은 스스로 검증할 몫이다.