読了時間 : 13 minutes

あなたはターミナルで OpenCode を起動し、すべてが機能します。エージェントは`gh`— 見つからない。一つ`java -version`不在. 一つ`node`— どこにもない。 しかし、これらのツールは あなたの シェルに確かに存在します。問題? OpenCode はシェルを起動します。非インタラクティブあなたを決して読まない`.zshrc`. ここで診断し、適切に修正する方法を示します。

コン

[]

シーン:ツールの世界の中の盲目のエージェント

それは火曜日の夜だった。私はそれをインストールしたばかりだった。https://opencode.ai[オープンコード], そのAIエージェントは私のコードの書き方を変えると約束していた。最初のテスト:彼にGitHubのリポジトリをリストするように頼む。

$ opencode
> Utilise gh pour lister mes repos
❌ bash: gh: command not found

変だ。`gh`端末で正常に動作していました。他のものを試してみます:

> Vérifie la version de Java
❌ bash: java: command not found

それから:

> Lance le build Gradle
❌ bash: gradle: command not found

Java, Gradle, Node,gh— エージェントには何も見えなかった。 私の端末は、すべてを見ていた。 まるでエージェントと私が二つの並行世界に住んでいるかのようだ。

理解するのに何時間も費やした。何時間`echo $PATH`, de which java,苛立ちが増すなか、私はついに気付いた、問題はツールではなく — それはシェル。OpenCodeは、サブプロセスを起動するようなエージェントのように、シェルで作業する非インタラクティブ.そしてZshは、これらのシェルでは、純粋かつ単純にあなたを無視します`.zshrc`.

以下は、この診断、解決策、そして私が学んだ教訓の完全な物語です。AIエージェントを使用している場合 — OpenCode、Aider、Cursor、あるいはcronスクリプト — この問題はいずれあなたに関わってくるでしょう。

バグの解剖:2つのシェル、2つの世界

ターミナルを開くと、Zshはそれをシェルのように扱います。インタラクティブロードする`.zshrc`, すべてを初期化します:SDKMAN, NVM, pnpm, エイリアス, 素敵なプロンプト。あなたの環境は完成しました。

しかし、OpenCode がコマンドを実行するとき、インタラクティブなターミナルを起動しません。シェルを起動します。非インタラクティブ— 人間が画面の後ろにいないタスクシェル。そしてZshはこの文脈ではジャンプする`.zshrc`. 彼はただ読むだけ`.zshenv`。

なぜこの区別が存在するのでしょうか?なぜなら、非インタラクティブなシェルはスクリプトを実行するために設計されており、人間のユーザーにサービスを提供するためではないからです。バックグラウンドで動作するスクリプトにエイリアス、プロンプト、および補完をロードするのは無駄です。問題は、あなたの PATH — 実行可能ファイルを見つけるための最も重要な情報 — がしばしば初期化される場所にあるということです。.zshrc, 中にない`.zshenv`。

@startuml
skinparam backgroundColor #FEFEFE

actor "あなた" as user
actor "OpenCode
(エージェント)" as agent
participant "シェル
対話型
(.zshrc がソースされました)" as ishell
participant "シェル
非対話的
(.zshrc 無視)" as nshell

user -> ishell : gh, java, node ✅
agent -> nshell : gh, java, node ❌

note right of ishell
  Source .zshrc :
  - SDKMAN init
  - NVM init
  - ~/apps dans PATH
  - pnpm dans PATH
end note

note right of nshell
  .zshrc ignoré !
  PATH = /usr/bin:/bin
  + quelques répertoires défaut
end note

@enduml

問題の根源:Zsh は .zshrc を対話型シェルのみに対してソースします. OpenCode, すべてのIAエージェントがサブプロセスを起動するように、非インタラクティブシェルを使用します。これらのシェルはただ読み取るだけ`.zshenv`。

犯人 : .zshrc vs `.zshenv

Zshは4つの初期化ファイル、それぞれに特定の役割がある。エレガントなデザインだが、それが問題の核心でもある:

ファイル

取得されたとき

役割

PATHを変更しますか?

.zshenv

常に(インタラクティブ + ノンインタラクティブ + ログイン)

必須の環境変数、PATH

✅ はい — これは SA 場所

.zprofile

ログインシェルのみ

遅いコマンド(セッションごとに一度)

可能

.zshrc

インタラクティブシェルのみ

エイリアス, プロンプト, 補完, インタラクティブツール

�❌ 重要な変数には使用しないでください

.zlogin

ログインシェル(.zshrc の後)

ウェルカムメッセージ、最終化

めったに

表は手がかりを与えるが、それを深く理解する必要がある。家の部屋のように考えてみてください:

  • `.zshenv`はエントランスホール— 誰もがそこに通る、訪問者でも住民でも。ここで何かを置けば、どのシェルでもそれを見ることができる。

  • `.zshrc`である居間— ただ、インタラクティブなシェル(居住者)だけがそこに入る。訪問者(非インタラクティブなシェル)はロビーにとどまる。

  • .zprofile et `.zlogin`ログインシェル用の専用パーツです(SSHで接続するときなど)。

問題は、ほとんどの人が PATH をリビングに置いてしまうことだ。そして AI エージェントは、決してそこに入る権限を持たない。

問題を図に示す:

@startuml
skinparam backgroundColor #FEFEFE

start

if (Shell interactif ?) then (Oui)
  :Source .zshenv;
  :Source .zprofile;
  :Source .zshrc;
  :Source .zlogin;
  note right
    SDKMAN ✅
    NVM ✅
    ~/apps ✅
    pnpm ✅
  end note
else (Non — shell non-interactif)
  :Source .zshenv uniquement;
  note right
    SDKMAN ❌
    NVM ❌
    ~/apps ❌
    pnpm ❌
  end note
endif

stop

@enduml

ターミナルを開くと、Zshは対話的です:それは読みます`.zshrc`, すべてが機能しています。 OpenCode がコマンドを実行するためにシェルを起動するとき、Zsh は非インタラクティブです:それはスキップします。.zshrc, 読むのは`.zshenv`. Et si .zshenv 存在しないまたは PATHを含まない — そこは砂漠だ。

なぜ Zsh はそれをするのですか?これはUnixから継承された設計上の選択です。非インタラクティブなシェルはなければならない速い et 再現可能エイリアス、カラープロンプトとSDKMANの重い初期化をcronスクリプトまたはAIエージェントに読み込むと、遅くてもろいでしょう。したがって、分離は論理的:.zshenv`主に (変数, PATH),.zshrc`� 快適さのために (aliases, prompt, 完了する)。問題は、本質を快適さの中に置くと発生します。

ステップバイステップ診断

修正する前に、何が足りないのか正確に理解する必要があります。これが再現可能な診断方法です — AIエージェントと作業している場合は、これをお気に入りに保存してください。

エージェントのPATHを確認する

あなたは対話型ターミナルが見ているものと、非対話型シェルが見ているもの—つまり OpenCode が見ているもの—を比較します。

# Dans votre terminal interactif (tout fonctionne)
echo $PATH | tr ':' '\n' | grep -v '^/usr' | sort

典型的な結果 :

/home/cheroliv/apps
/home/cheroliv/.nvm/versions/node/v22.19.0/bin
/home/cheroliv/.sdkman/candidates/java/current/bin
/home/cheroliv/.sdkman/candidates/gradle/current/bin
/home/cheroliv/.local/share/pnpm
/home/cheroliv/.local/bin

では、エージェントが見ているものをシミュレートしましょう — 対話型でないシェル :

# Shell non-interactif : pas de .zshrc
zsh -c 'echo $PATH' | tr ':' '\n' | grep -v '^/usr' | sort

結果:

/home/cheroliv/.local/bin

六つの道のうち五つが消えてしまった。エージェントは環境の83%を失っている。それは、半分の調理器具しかない状態で料理をしろと言われているようなものです。水を沸かすことはできますが、それ以外はあまりできません。

コマンド`zsh -c 'echo $PATH'`あなたの�診断ツール番号1. あなたが日常的に使用しているツールが結果に含まれていない場合、あなたのAIエージェントもそれを見ることはできません。 これを各変更の前後でテストしてください`.zshenv`。

2. .zshrcにあるが.zshenvにないものを特定する

今、私たちは犯人を探している 中`.zshrc`. 私たちのツールを参照している行をフィルタリングします:

grep -n 'apps\|SDKMAN\|NVM\|PNPM\|PATH' ~/.zshrc

典型的には:

119: PATH="/usr/bin/python3:$HOME/apps:$PATH"           # ← pas exporté !
171: export NVM_DIR="$HOME/.nvm"
172: [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"   # ← NVM init
176: nvm use --lts --silent                              # ← active une version
179: export PNPM_HOME="/home/cheroliv/.local/share/pnpm"
187: export SDKMAN_DIR="$HOME/.sdkman"
188: [[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"

それは全部です不可視OpenCodeのため。そして`PATH`行 119 のものでもない`export`é — 彼は決して現在のシェルを離れません。これは重要な技術的詳細です:変数がない`export`シェルが定義している場所にローカルのままです。 サブシェルは — OpenCodeによって起動されたもののように — それを決して継承しません。

3. .zshenv が存在するか確認する

cat ~/.zshenv 2>/dev/null || echo "FICHIER ABSENT"

回答が「ファイルが見つからない」ならば、そこで勝負が決まる。

解決策: .zshenv + 正しいパス

戦略はシンプルだが、精度が求められる:その中に入れる`.zshenv` のみ� 必須のパスを、重い初期化スクリプトをソースせずに。移動しない`.zshrc`中に`.zshenv`— 本質を抽出します。

すべての必須パスを含む .zshenv を作成する

`.zshenv`は唯一のファイルZshがソースすることを保証する中でみんなコンテキスト。ここでPATHを設定すべきです。

export SDKMAN_DIR="$HOME/.sdkman"
export NVM_DIR="$HOME/.nvm"
export PNPM_HOME="$HOME/.local/share/pnpm"
export PATH="$HOME/apps:$HOME/.sdkman/candidates/java/current/bin:$HOME/.sdkman/candidates/gradle/current/bin:$HOME/.nvm/current/bin:$PNPM_HOME:$HOME/.local/bin:$HOME/.local/share/JetBrains/Toolbox/scripts:/usr/bin/python3:$PATH"

使用します`$HOME/.nvm/current/bin`そしていいえ`$HOME/.nvm/versions/node/v22.19.0/bin`理由:Node のバージョンが変わります。ハードコーディングされたパスは次ので偽になります`nvm install`. 以下のセクションで詳しく説明します。

.zshenv で source sdkman-init.sh を使わないのはなぜですか?

最も誘惑的なアプローチは、単純に再現するで`.zshenv`そこで私たちがすること`.zshrc`— ソースする 初期化スクリプト。 Onできるしたくなる :

# ❌ MAUVAISE IDÉE
[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"

問題 :

  • 遅い:`sdkman-init.sh`ネットワークの解決と各シェルでの検証を行います。非インタラクティブなシェルでは、これは無駄なコストです。

  • �壊れやすいSDKMAN は対話的なコンテキストを期待します。その初期化はパイプやサブシェルでは無音で失敗する可能性があります。

  • 無駄な: SDKMANはその候補者を配置します`~/.sdkman/candidates/<tool>/current/bin`— アクティブなバージョンを指す安定したシンボリックリンクです。 これらを使用できます直接的に。

良いアプローチ:SDKMANの初期化を回避し、シンボリックリンクを直接指す`current`. これはすべての解決策の鍵 :初期化コードではなく、ファイル構造を契約として使用する。

@startuml
skinparam backgroundColor #FEFEFE

rectangle "ゆっくりとしたアプローチ
(source sdkman-init.sh)" as slow {
  card ".zshenv source sdkman-init.sh
シェルごとに2-3秒
ネットワークチェック
失敗のリスク" as s1 #FDEDEC
}

rectangle "�迅速なアプローチ
(直接シンボリックリンク)" as fast {
  card ".zshenv export PATH=...current/bin
0 ms
ネットワークなし
初期化なし" as s2 #E8F8E8
}

slow --> fast : Même résultat final\nLe PATH pointe sur current/bin\ndans les deux cas

@enduml

NVMケース: バージョンマネージャーが痕跡を残すのを忘れたとき

NVMの問題

調査が私を最も遠くまで導いたのはここだ。SDKMANはエレガントなデザインを持っています:すると`sdk install java 25.0.2-tem`, シンボリックリンクを作成します :

~/.sdkman/candidates/java/current -> ~/.sdkman/candidates/java/25.0.2-tem

このシンボリックリンクは常に最新その中を指してください`.zshenv`そしてあなたはカバーされています、どのシェルでも。 美しい。

NVM、彼、作らない何もそのような。シンボリックリンクはありません`current`. 安定したアンカーポイントはありません。 ハードコードされたバージョン管理されたパスを指す必要があり、これがその見た目です :

~/.nvm/versions/node/v22.19.0/bin/node

次の`nvm install 24`, この道は死んでいます。 あなたの`.zshenv`アクティブでなくなったバージョンを指します。これは時限爆弾です。

NVMはなぜそれをするのですか?NVMはPATHを動的に変更することで動作する毎`nvm use`. これは、バージョンを頻繁に変更する開発者向けに設計されています。インタラクティブなシェルで。シンボリックリンク`current`初期の仕様には含まれていませんでした — これは設計の見落としであり、自分たちで修正します。

@startuml
skinparam backgroundColor #FEFEFE

package "SDKMAN ✅" {
  [~/.sdkman/candidates/java/current] as sdk_current
  [~/.sdkman/candidates/java/25.0.2-tem/] as java_25
  [~/.sdkman/candidates/java/21.0.7-tem/] as java_21

  sdk_current --> java_25 : symlink
}

package "NVM ❌ (修正前)" as nvm_before {
  [~/.nvm/versions/node/v22.19.0/] as node_22
  [~/.nvm/versions/node/v20.16.0/] as node_20
  note right of node_22
    Pas de symlink current !
    .zshenv doit pointer en dur
    Cassé au prochain nvm install
  end note
}

package "NVM ✅ (修正後)" as nvm_after {
  [~/.nvm/current] as nvm_current
  [~/.nvm/versions/node/v22.19.0/] as node_22b
  [~/.nvm/versions/node/v20.16.0/] as node_20b

  nvm_current --> node_22b : symlink\n(mis à jour auto)
}

@enduml

解決策:NVM用にシンボリックリンク current を作成する

NVMがそれをしないので、私たちが自分たちでやります。原則は`SDKMAN`と同じ — シンボリックリンクです`current`常にアクティブなバージョンを指している。これを一度作成し、それから自動化して自分で更新されるようにする。

# Créer le symlink initial
ln -sfn "$HOME/.nvm/versions/node/v22.19.0" "$HOME/.nvm/current"

今、中に`.zshenv`、使用します:

$HOME/.nvm/current/bin

よりも:

# ❌ Chemin en dur — cassé au prochain changement de version
$HOME/.nvm/versions/node/v22.19.0/bin

シンボリックリンクのアップデートを自動化する

シンボリックリンクは、アップデートしない場合、何の価値もありません。シンボリックリンク`current`それを行うときにアップデートする必要がある`nvm use` ou nvm install. 解決策:1ラッパー— 本当のコマンドをラップする関数`nvm`そして、各呼び出し後にシンボリックリンクを更新します。

# À la fin de .zshrc, APRÈS le chargement de NVM
nvm use --lts --silent
ln -sfn "$(nvm_version_path "$(nvm current)")" "$NVM_DIR/current"

_nvm() {
  command nvm "$@"
  local rc=$?
  ln -sfn "$(nvm_version_path "$(nvm current)")" "$NVM_DIR/current"
  return $rc
}
alias nvm='_nvm'

詳しく説明すると、どのように機能するか:

@startuml
skinparam backgroundColor #FEFEFE

actor Développeur
participant "nvm wrapper
(_nvm)" as wrapper
participant "nvm 実
(コマンド nvm)" as realnvm
participant "~/.nvm/current\n(シンボリックリンク)" as symlink

Développeur -> wrapper : nvm use 20
wrapper -> realnvm : command nvm use 20
realnvm --> wrapper : version activée
wrapper -> symlink : ln -sfn .../v20.16.0 ~/.nvm/current
wrapper --> Développeur : retour

note right of symlink
  Toujours à jour !
  Utilisé par .zshenv
  → accessible par OpenCode
end note

@enduml

ワラッパー`_nvm`本物のコマンドを呼び出す`nvm`, その後シンボリックリンクを更新します。エイリアス`nvm='_nvm'`入力するときにそうなるようにする`nvm`, wrapperを経由します。 シェルの初期化時に同様にその後も行います。nvm use --lts。

`nvm_version_path`はNVMの内部関数で、バージョンのフルパスを解決します。これにより、パスを手動で再構築する必要がなくなります。

パスのサマリー表

罠に移る前に、変換のビジュアルサマリー。左側に、あなたが持っていたもの(すべて`.zshrc`, エージェントに見えない)。 右側、あなたが今持っているもの (必須のパスの中の`.zshenv`, どこでも見える)

ツール 前 (.zshrc だけ) その後 (.zshenv + symlink) OpenCodeによって表示されますか?

`~/apps`python s = input() print(s, end='')

.zshrcでPATHがエクスポートされていない

$HOME/apps.zshenvの中

�✅

SDKMAN Java

sdkman-init.sh.zshrc でソースされている

$HOME/.sdkman/candidates/java/current/bin.zshenv内

�✅

SDKMAN Gradle

sdkman-init.sh.zshrc でソースされている

$HOME/.sdkman/candidates/gradle/current/bin.zshenv内で

(This response is intentionally left blank.)

NVM ノード

nvm use --lts.zshrcの中に

$HOME/.nvm/current/bin.zshenv (symlink)の中に

✅

pnpm

$PNPM_HOME.zshrc に

$PNPM_HOME.zshenvの中で

✅

JetBrains ツールボックス

Toolbox によって .zshrc に自動的に追加されました

$HOME/.local/share/JetBrains/Toolbox/scripts.zshenv 内

✅

Python 3

`/usr/bin/python3`PATH .zshrc内

明示的に .zshenv で

✅

罠と対策

このアプローチではすべてが完璧ではない。私が遭遇した問題と、それを回避する方法を示します。

�罠 説明 軽減

PATH が重複

もし.zshenv と .zshrc が同じパスを追加すると、それが二回表示されます

`.zshenv`出典が明記されている前.zshrc. 両方ともインタラクティブシェルで読み込まれます。PATHが重複することがあります。これは見た目の問題で、機能的ではありません。これを避けるには、デフォルトのPATHに含まれていない_absents_なパスだけを.zshenvに記述してください。

NVM : .zshenv にハードコードされたバージョン

置く`~/.nvm/versions/node/v22.19.0/bin`ハードコードは次の段階で偽になる`nvm install`

シンボリックリンクを使用する`$HOME/.nvm/current/bin`+ ラッパー`_nvm`.zshrcの中

SDKMAN : .zshenv で sdkman-init.sh を source する

�遅い、脆い、非対話シェルでは役に立たない

シンボリックリンクを使用する`candidates/<tool>/current/bin`直接に

変更後のエイリアスが不足

ラッパー`nvm='_nvm'`.zshrcの変更は、再読み込み後にのみ反映されます

シェルを再起動するまたは`source ~/.zshrc`

OpenCode は変更を検出していません

エージェントはすでに古い .zshenv を使ってそのサブシェルを起動しています

OpenCodeの.zshenvを変更後に再起動する

.zshenv が重すぎる

.zshenv に重い関数や対話型の初期化を置く

.zshenv = 環境変数とPATH のみ。なし`source`, 重い関数はなく、プロンプトもありません。

学んだ教訓

  1. .zshrc` はインタラクティブです、.zshenv は普遍的です。— 変数が すべて のシェル(AIエージェント、cron、スクリプト、IDE)に存在する必要がある場合、それはに入ります`.zshenv`リビングは快適ですが、エントランスホールは誰もが通る唯一の場所です。

  2. バージョンマネージャーは等しくないSDKMANはシンボリックリンクを作成する`current`設計によるものです。 NVM ではない。 このギャップを手動で埋める必要があります。 これは重要な教訓です:PATHを設定する前に、あなたのマネージャーが安定した固定ポイントを提供しているか確認してください。

  3. .zshenv 内で init スクリプトをソースしないでください—sdkman-init.sh et `nvm.sh`インタラクティブなシェルのために設計されています。 非インタラクティブなコンテキストでは遅くて脆弱です。 シンボリックリンクは`current`十分で、即座です。

  4. 常に非インタラクティブなシェルでテストする—`zsh -c 'echo $PATH'`エージェントが見るものを正確にシミュレートします。 これは検証テストです。 このテストがなければ、エージェントに対して構成が機能しているかどうか分かりません。

  5. Wrapperパターンは再利用可能です— 同じパターン`_nvm`+`alias nvm='_nvm'`これは、PATHを動的に変更し、安定した痕跡を残さないすべてのツールに適用されます。 これはあなたのアイデアボックスに追加されるもう一つのツールです。

@startuml
skinparam backgroundColor #FEFEFE

rectangle "前" as avant {
  card "対話型シェル : ✅\n非対話型シェル : ❌\nOpenCode : ❌\nCron : ❌\nScripts : ❌" as av1 #FDEDEC
}

rectangle "後" as apres {
  card "対話型シェル : ✅
非対話型シェル : ✅
OpenCode : ✅
Cron : ✅
スクリプト : ✅" as ap1 #E8F8E8
}

avant --> apres : .zshenv +\nsymlink ~/.nvm/current

@enduml

最終確認

作成した後`.zshenv`そして NVM シンボリックリンク、すべてが正常に動作することを確認してください — 両方のコンテキストで:

# Shell interactif (votre terminal)
gh --version && java -version && node --version
# Shell non-interactif (simulation OpenCode)
zsh -c 'gh --version && java -version && node --version'

両方とも成功しなければなりません。はいの場合、あなたのAIエージェントはあなたと同じツールを見ることになります。いいえの場合、ステップバイステップの診断に戻ってください — おそらくパスを忘れたか、NVMのシンボリックリンクが古いです。

このテストを自動化してください。このチェックを、ツールを更新するたびに実行するヘルスチェックスクリプトに追加してください。Un`zsh -c 'which java && which node && which gh'`CIでは、これは驚きに対する保険です。

_ 非対話型シェルは、無言のゲストのようになります:それは玄関に表示されているものだけを読みます。PATH がリビングにある場合、それは決してそれを見ることはありません。 _

リンク

公式ドキュメント

言及されたツール

もっと進む

関連記事