※この記事は Zennの記事 からの転載です。
ターミナルで手で打つと動くコマンドが、Claude Code や Codex などの AI エージェントに実行させると「command not found」になる。cron で定時に動かすと失敗する。サイトでは動くプログラムが、SSH から動かすとエラーになる。
どれも「さっきは動いたのに」と感じる問題ですが、原因はたいてい1つです。1台のパソコンやサーバーの中に、実行環境が何種類もあり、それぞれ PATH が違うことです。
この記事では、実行環境がどう決まるのか、どう確かめるのか、どう固定すればいいのかを整理します。
呼び出す場所ごとに、使われるプログラムが違う
php や python3 や node と打ったとき、どのプログラムが動くかは、呼び出した場所ごとに決まります。
| 呼び出した場所 | 決まり方 |
|---|---|
| Web サーバー(PHP-FPM など) | サーバーの設定。共用サーバーでは管理画面でドメインごとに選んだ版 |
| 手で開いたターミナル | ログイン時に読み込まれる設定ファイル(.bashrc・.zshrc など)で作られた PATH |
SSH で1行だけ送ったコマンド(ssh host "cmd") |
対話しないシェル。.bashrc の別名(alias)や一部の設定が読まれないことがある |
| cron | ほぼ空の環境。PATH は /usr/bin:/bin 程度 |
| CI・コンテナ | イメージに入っている版 |
| AI エージェント(Claude Code・Codex など)のコマンド実行 | 多くは対話しないシェル。.zshrc の関数・別名が読まれないことがある。サンドボックスで外への通信や書ける場所が制限されることもある |
| エディタの中のターミナル | エディタを起動したときの環境を引き継ぐ |
手で打って動いたのは、手で開いたターミナルの環境で動いたというだけです。
仕組み:PATH と shebang
PATH は「探す順番」
php と打つと、シェルは環境変数 PATH に並んだフォルダを左から順に探し、最初に見つかった php を動かします。
$ echo $PATH
/home/me/bin:/opt/php-8.3/bin:/usr/bin:/bin
$ which php
/opt/php-8.3/bin/php
同じパソコンやサーバーでも、PATH の中身が違えば、別の php が見つかります。cron の PATH に /opt/php-8.3/bin が無ければ、/usr/bin/php(古い版)が動きます。
shebang は「このファイルを何で動かすか」
スクリプトの1行目の #! を shebang と呼びます。
#!/usr/bin/env php
/usr/bin/env php は「PATH で最初に見つかった php で動かす」という意味です。スクリプト自体に PHP の版は書かれていません。呼び出した場所の PATH しだいで、版が変わります。
#!/usr/bin/php のように絶対パスで書いてあれば、どこから呼んでも同じ版で動きます。そのかわり、その場所の版が古ければ古いままです。
AI エージェントで特に起きやすい理由
AI エージェントにコマンドを任せると、この問題によく当たります。
- エージェントのコマンド実行は、対話しないシェルで動くことが多い。人がターミナルで使っている
.zshrcの関数や別名(alias)は、そのままでは使えないことがある - 人は「いつも打っているコマンド」を頼むが、それが自分で作った関数や別名だと、エージェントからは存在しない
- エージェント用のサンドボックスでは、外へのネットワークや書き込める場所が絞られていることがある。「動かない」の原因が PATH ではなく、サンドボックスの制限のこともある
対策は、人の手で打つ前提をやめることです。
# × 自分で作った関数(対話シェルでしか読まれない)
myjob prompt.txt
# ○ エージェントにはスクリプトの絶対パスで渡す
/Users/me/bin/myjob.sh prompt.txt
# どうしても対話シェルの設定が要るなら、明示的に読ませる
zsh -ic 'myjob prompt.txt'
エージェントに渡す手順書(CLAUDE.md・AGENTS.md など)には、別名ではなく実体のパスを書いておくと、毎回の失敗が減ります。
確かめ方:失敗する場所の中で、版を出す
推測より、失敗する場所の中で版を出すのが確実です。
# cron に一時的に足す
* * * * * { date; echo "$PATH"; which php; php -v; } >> /tmp/env_check.log 2>&1
# SSH の1行コマンドで
ssh host 'echo "$PATH"; which php; php -v; head -1 "$(which wp)"'
<?php // Web 側
echo PHP_VERSION, ' ', PHP_BINARY;
WordPress なら、管理画面の「ツール → サイトヘルス → 情報 → サーバー」に、サイトで動いている PHP の版が出ます。
よくある取り違えは、ターミナルで php -v を打って、サイトの版を知った気になることです。逆も同じで、サイトの版が新しくても、コマンドは古い版のままのことがあります。
直し方:呼び出す側で版を固定する
1. 絶対パスで呼ぶ
いちばん確実です。
/opt/php-8.3/bin/php /usr/bin/wp cache flush
2. スクリプトの中で1か所に持つ
同じコマンドを何か所でも呼ぶなら、パスを定数にして1か所で変えられるようにします。
readonly WP="/opt/php-8.3/bin/php /usr/bin/wp"
$WP cache flush
WP_CLI = "/opt/php-8.3/bin/php /usr/bin/wp"
3. cron では PATH を先頭で決める
PATH=/opt/php-8.3/bin:/usr/local/bin:/usr/bin:/bin
0 4 * * * /home/me/bin/job.sh >> /home/me/log/job.log 2>&1
避けたほうがいいもの
- alias で済ませる:手で開いたターミナルでしか効きません。cron や SSH の1行コマンドでは読まれないことがあります
- 動かないほうに合わせて古くする:ライブラリやサイトの版を下げると、直された不具合や脆弱性が戻ります
- エラーの検査を止める:版の確認を消しても、古い版で動かないコードが動くようにはなりません
一度直したら、全部の呼び出し元を探す
1か所直して終わりにすると、別の呼び出し元が古いまま残ります。呼び出し元は、言語をまたいで散らばっていることが多いです。
# シェル・JavaScript・Python・Ruby をまとめて探す
grep -rnE '(^|[ ;&"`(])wp (eval|post|cache|option|plugin)' \
--include='*.sh' --include='*.js' --include='*.py' --include='*.rb' .
例:プラグインの更新で、コマンドだけが落ちた
私のサーバーでは、問い合わせフォームのプラグインを更新したあと、WordPress のコマンド(WP-CLI)だけが「PHP 8.3 以上が必要」というエラーで落ちるようになりました。
Composer detected issues in your platform:
Your Composer dependencies require a PHP version ">= 8.3.0". You are running 8.0.30.
サイトは管理画面で 8.3 にしてあったので動いていました。WP-CLI は #!/usr/bin/env php で、SSH の既定の PHP(8.0)で動いていました。直したのは、絶対パスで呼ぶことです。
最初はシェルと JavaScript だけを探して直し、Python の2本を見落としました。毎日の定時処理がそこから呼んでいて、失敗の通知メールが届いたことで気づきました。
同じ環境で、私がターミナルで使っている自作の関数(別の AI を起動するもの)を AI エージェントに実行させると「command not found」になる、ということも起きていました。エージェントの手順書には zsh -ic '...' で呼ぶと書いて解決しています。
まとめ
| 何が起きるか | 手で打つと動くのに、AI エージェント・cron・SSH の1行コマンド・CI・Web では動かない、または違う版が動く |
| なぜか | どのプログラムが動くかは、呼び出した場所の PATH と shebang で決まる |
| 確かめ方 | 失敗する場所の中で echo $PATH; which X; X -v を出す |
| 直し方 | 絶対パスで呼ぶ。スクリプトでは定数で1か所に持つ。cron は PATH を先頭で決める |
| 忘れやすいこと | 呼び出し元を全部探す。言語をまたいで grep する |
「さっきは動いたのに」と思ったら、まず「どの実行環境で動いたのか」を1行で出してみてください。