🐈‍⬛

asdf の shim が消えない問題とその設計上の理由

に公開

先にまとめ

方針

  • asdf 管理下での npm i -g などのグローバルインストールは避けて native でのインストールに寄せる

asdf の挙動

  • asdf の shim は作成時に全バージョンを走査し、実行時に現在のバージョンを参照する
  • この非対称性により、古いバージョンにインストールしたパッケージの shim が残り続ける
  • 対処するには、全バージョンからアンインストールして reshim を実行する

バージョン

この記事は以下のバージョンで検証しています。

asdf 0.18.0

事象

ある時 claude code を実行しようとしたら、以下のエラーが出ました。

> claude  

No version is set for command claude
Consider adding one of the following versions in your config file at /Users/kokoichi206/ghq/github.com/kokoichi206/zenn-content/.tool-versions
nodejs 22.20.0
nodejs 20.18.1

asdf で複数の Node.js バージョンを管理していたのですが、現在 global に設定しているバージョンとは別のバージョンに npm i -g でインストールしたパッケージが残っていたのが原因でした。

対処

各 asdf の nodejs plugin のバージョンからアンインストールして reshim を実行しました。

# どの nodejs に入ってるのか特定がだるいので、全部から消す。
for v in $(asdf list nodejs | tr -d ' '); do
  echo "=== Node $v ==="
  ASDF_NODEJS_VERSION="$v" asdf exec npm -g uninstall @anthropic-ai/claude-code
  # uninstall でうまくいかないケースがあったため、念のため実態からも削除する。
  rm -rf "$dir/lib/node_modules/@anthropic-ai/claude-code"
  rm -rf "$dir/lib/node_modules/@anthropic-ai/claude"
  rm -rf "$dir/lib/node_modules/claude"
  rm -f  "$dir/bin/claude"
done

asdf reshim nodejs

その後 native にバイナリを導入し直しました。
(claude も現在ではその方法が推奨されています。)

# mac
curl -fsSL https://claude.ai/install.sh | bash

asdf shim の挙動について

shim の挙動が気になったので少し調べてみました。

shim とは、各々の plugin にインストールされている実行プログラムが、現在の環境で利用できるようにするための仕組みです。

実体は exec asdf exec をするだけの、シンプルなラッパーとなっています。

> which claude       
/Users/kokoichi206/.asdf/shims/claude

> cat /Users/kokoichi206/.asdf/shims/claude                 
#!/usr/bin/env bash
# asdf-plugin: nodejs 22.20.0
# asdf-plugin: nodejs 20.18.1
exec asdf exec "claude" "$@"%

shim 作成時

asdf reshim, asdf reshim nodejs を実行すると、全バージョンのグローバルパッケージを走査して shim を生成します。

reshim 作業での成果物は、先ほど確認した ~/.asdf/shims/claude という 1 つのファイルとなります。

その際に asdf-plugin という形で、コメントに実行バイナリのインストールされたプラグインのバージョンを記載します。

# asdf-plugin: nodejs 22.20.0
# asdf-plugin: nodejs 20.18.1

shim 実行時

~/.asdf/shims/claude が実行されると、先ほど確認した通り exec asdf exec "claude" "$@"% が実行されます。

この shim が実行されると、以下のステップで実際の実行ファイルを探します。

  1. shim ファイルに記載されている plugin のバージョンを取得
  2. そのバージョンに対応する実行ファイルを探索
  3. resolve.Resolve でカレントディレクトリから親へ .tool-versions を再帰的に探す + global で設定されたバージョンの .tool-versions を確認

つまり、実行時は global および現在のディレクトリの .tool-versions に基づいてバージョンが決定されます。

shim 作成時と実行時の非対称性

作成時と実行時で参照するバージョンが異なります。

参照するバージョン
作成時 全てのバージョン
実行時 現在の .tool-versions で指定されたバージョン

本来、PATH に登録されるコマンドは『今使えるもの』だけであるべきで、作成時と実行時に同じバージョンを参照するのが理想的に思えます。

しかし asdf の設計上 reshim のタイミングが不定であり、確実にバイナリを登録できるかが保証できません。
そのため、「どこかのバージョンで使える可能性があるもの」を全て登録しておく設計になっています。

この設計の副作用として、今回のように「もう使わないパッケージの shim が残り続ける」という問題が起きます。
古いバージョンにグローバルインストールしたパッケージがあると、現在のバージョンで使っていなくても shim が生成され続けます。

GitHubで編集を提案

Discussion