iPhoneのiSHでClaude Codeは動くのか試してみた(結果:起動はするが使えない)
発端は、Anthropicが2026年10月12日に出した「AIモデルはデータセンターなしでも自己複製できるかもしれない」というレポートのニュースです。30B以下の小型オープンウェイトモデルを、55ドルのAndroid TVボックス(メモリ4GB)15台に分散させて動かしたとか、隔離環境では GLM-5.2 が8台の仮想機器に自分のコピーを広げられたとか、そういう内容でした。
- AnthropicのFrontier Red Teamが「AIモデルの自己複製」に関するレポートを公開していたので読んでみた(DevelopersIO)
- 小型AIモデル、IoT機器上で自己複製する可能性 Anthropicが警鐘(ITmedia)
- AIは自己複製できるのか。Anthropicが実験(ギズモード・ジャパン)
LLMの推論そのものが数千円のTVボックスで動くなら、推論はサーバー側でやるClaude Codeのクライアントくらい、手元のiPhoneでも動くんじゃないか。ちょうどiPhoneのiSHにSSHで入れるようにしたところだったので、調子に乗って「iSHの中でClaude Codeそのものを動かせないか」を試しました。結果から書くと、Node 18を動かすところまでは成功し、Claude Codeも--helpや認証前の-pまでは動いたのですが、ログインして実際にAPIを呼ぶ段階でiSHごと落ちて終わりました。半日かけて無理だと分かった記録として残しておきます。
環境は以下です。
- iPhone、iSH 1.3.2(App Store版、中身はAlpine Linux 3.14)
- PCはMac。作業は全部MacからSSH越しにやりました
- iSHは32bitのx86(i686)エミュレータ
最初の壁:iSHのAlpineが古くてNode 14しか入らない
iSHのパッケージリポジトリはiSH側で固定されていて(/etc/apk/repositoriesを見るとapk.ish.app/v3.14-2023-05-19になっています)、apk search nodejsで出てくるのは14と16だけです。Claude Codeは最近の版だとNode 22以上を要求するので話になりません。
リポジトリをv3.21に書き換えてapk upgradeする手もありますが、普段使っている環境が壊れるのが怖かったので、別の方法を取りました。
chrootで新しいAlpineを隔離して入れる
Alpineは公式サイトでalpine-minirootfs-3.21.8-x86.tar.gzのような3MB程度の最小ルートファイルシステムを配布しています。これをiSHのホームの下に展開してchrootすれば、iSH本体の環境に触らずに新しいAlpineが使えます。
cd /root
curl -sLO https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/x86/alpine-minirootfs-3.21.8-x86.tar.gz
mkdir alpine321
tar -xzf alpine-minirootfs-3.21.8-x86.tar.gz -C alpine321
cp /etc/resolv.conf alpine321/etc/
chroot alpine321 /bin/sh
これが普通に動いたときはちょっと感動しました。chrootの中でapk add nodejs npmも通ります。
ただしminirootfsには/devの中身がほぼ無いので、/dev/nullや/dev/urandomはmknodで作る必要があります。あとiSHのchrootには癖があって、中でpwdを打つと/root/alpine321/rootのように外側から見たパスが返ってきます。この癖のせいで後で一手間増えました。
Node 22と20は落ちる、Node 18は動く
Alpine 3.21でNode 22を入れてnode -e 1を打ったら即死でした。
Bad system call
iSHが実装していないシステムコールをNode 22が起動時に呼んでいるようです。UV_USE_IO_URING=0や--jitlessを付けても変わりませんでした。
Alpine 3.20のNode 20はnode -e 1は通るのですが、npm --versionを打つと今度はIllegal instructionで落ちます。V8のJITがiSHにない命令を吐いているんだと思います。
Alpine 3.18のNode 18は普通に動きました。node -eの起動が0.9秒、npmも使えます。というわけで3つのバージョンを試して、Node 18だけが生き残りました。
| Alpine | Node | 結果 |
|---|---|---|
| 3.21 | 22.23 | Bad system callで起動すらしない |
| 3.20 | 20.15 | nodeは動くがnpmがIllegal instruction |
| 3.18 | 18.20 | 動く |
最新のClaude Codeはそもそも入らない
ここで調べて分かったのですが、Claude Codeのnpmパッケージは2.1.113以降、中身がただのインストーラーになっています。npm installするとOSとCPUに合ったネイティブバイナリをoptionalDependenciesから持ってくる仕組みで、対応しているのはmacOS、Linux x64/arm64、Windowsだけ。32bitのx86用は存在しないので、どうあがいても最新版は入りません。
JSのCLIを同梱していた最後の版は2.1.112で、これはNode 18以上で動く仕様でした。なのでこれを狙います。
chroot alpine3.18 /bin/sh
npm install -g --ignore-scripts @anthropic-ai/claude-code@2.1.112
--ignore-scriptsを付けているのは、さっきのchrootのパス漏れのせいでpostinstallが自分のファイルを見失って落ちるからです。
chrootを使わずにNodeを起動する
さらにchrootのパス漏れはClaude Code本体にも効いてきて、作業ディレクトリの認識がおかしくなります。回避策として、chrootせずにmuslのローダーを直接叩いてchroot内のNodeを動かすことにしました。
/root/alpine3.18/lib/ld-musl-i386.so.1 \
--library-path /root/alpine3.18/usr/lib:/root/alpine3.18/lib \
/root/alpine3.18/usr/bin/node --version
これだとpwdも正しく、ライブラリだけchroot側のものを使えます。ただしICUのデータファイルを探しに行く先がずれるのでICU_DATA=/root/alpine3.18/usr/share/icu/73.2を指定する必要がありました(指定しないとlocaleCompareがIcu errorで死にます)。
claude --versionは18秒で動いた
ここまででclaude --versionが通りました。18秒かかりますが、ちゃんと2.1.112 (Claude Code)と出ます。
しかし--helpや-pを打つと、V8がこう言って落ちました。
# Fatal error in , line 0
# unreachable code
切り分けたところ、空のWebAssemblyモジュールを作るだけで同じ落ち方をします。iSHの上ではV8のWebAssemblyエンジンが動きません。
WebAssemblyを避けるために3つ差し替える
対策は--jitlessでJITごとWebAssemblyを切ることなのですが、そうするとNode 18内蔵のfetch(undici)がWebAssemblyに依存しているのでWebAssembly is not definedで落ちます。なので内蔵fetchを--no-experimental-fetchで外して、純JSのnode-fetchとformdata-nodeを--requireで先に読み込ませてグローバルに生やしました。
// /root/fetch-polyfill.cjs
const nf = require("/root/alpine3.18/usr/local/lib/node_modules/node-fetch");
globalThis.fetch = nf; globalThis.Headers = nf.Headers;
globalThis.Request = nf.Request; globalThis.Response = nf.Response;
const fd = require("/root/alpine3.18/usr/local/lib/node_modules/formdata-node");
globalThis.FormData = fd.FormData; globalThis.File = fd.File;
最終的なラッパーはこうなりました。
#!/bin/sh
R=/root/alpine3.18
export USE_BUILTIN_RIPGREP=0 DISABLE_AUTOUPDATER=1
export ICU_DATA="$R/usr/share/icu/73.2"
export NODE_OPTIONS="--jitless --no-experimental-fetch --max-old-space-size=768 --require /root/fetch-polyfill.cjs"
exec "$R/lib/ld-musl-i386.so.1" --library-path "$R/usr/lib:$R/lib" "$R/usr/bin/node" \
"$R/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js" "$@"
USE_BUILTIN_RIPGREP=0は、同梱のripgrepがx64用で動かないのでapkで入れたrgを使わせるためです。DISABLE_AUTOUPDATER=1は、うっかりアップデートされると最新版(ネイティブバイナリ)に置き換わって二度と動かなくなるからです。
これでclaude --helpが通り、認証なしのclaude --bare -p "pong"も40秒で「Not logged in · Please run /login」まで正常に走りました。認証以外は全部動いている状態です。
認証を通したらiSHごと落ちた
ここからが駄目でした。
まず対話モードでclaudeを起動して/loginしようとしたら、初回設定のテーマ選択画面が描画された直後にiSHアプリごと落ちます。3回やって3回落ちました。エラーが画面に出るのではなくアプリが消えるので、iOSにメモリで止められているか、iSH内部でクラッシュしているかのどちらかです。
対話モードが駄目なら、と、Macでログイン済みの資格情報を~/.claude/.credentials.jsonとしてiPhoneにコピーして-pで実行してみました。これもAPIを呼ぶ直前で落ちます。資格情報を置く前は同じコマンドが正常に終わっていたので、認証を通過した先の処理が引き金なのは確実です。子プロセスの起動を省く--bareにCLAUDE_CODE_OAUTH_TOKENでトークンを渡す形も試しましたが同じでした。
| 試したこと | 結果 |
|---|---|
claude --version | 動く(18秒) |
claude --help | 動く |
-p 認証なし | 動く。「Not logged in」まで正常終了 |
-p 資格情報ファイルあり | iSHごと落ちる |
--bare -p + CLAUDE_CODE_OAUTH_TOKEN | iSHごと落ちる |
| 対話モード | 初回設定画面の直後にiSHごと落ちる |
落ちるときにログが残らないのでこれ以上は絞れず、ここで諦めました。
分かったこと
- iSHで新しいAlpineを使いたいときは、minirootfsを展開してchrootするのが安全で手軽。ただし
/devは自分で作る、pwdが外のパスになる、の2点に注意 - iSHで動くNodeは18まで。20と22はV8がiSHにない命令やシステムコールを使って落ちる
- iSHではV8のWebAssemblyが動かない。
--jitlessで回避できるが、Node内蔵のfetchが道連れになる - Claude Codeの最新版はネイティブバイナリ配布なので32bit環境には入らない。JS版の最終は2.1.112
- 2.1.112は上記の細工で起動まではするが、認証後の処理でiSHが落ちる
Node 18が動く環境としてはこのchrootは便利なので残してあります。Claude CodeをiPhoneから使いたいなら、素直にMac側で動かしてiPhoneからSSHやRemote Controlで操作するのが正解です。iSHにSSHで入れるようにしておけば、Mac側のClaude CodeからiPhoneのファイルを触ることはできるので、方向を逆にすれば用は足ります。
まとめ
- iSHのAlpineは古いので、minirootfs + chrootで新しいAlpineを隔離して使う
- Node 18は動く。20と22は落ちる
- Claude Code 2.1.112なら
--jitless、内蔵fetchの差し替え、ICU_DATAの指定で--helpまで動く - 認証を通した先でiSHごと落ちるので実用にはならない
- iPhoneからClaude Codeを使うならMacで動かして遠隔操作するのが正解