waktu membaca : 13 minutes

Anda menjalankan OpenCode di terminal Anda, semuanya berfungsi. Agen mencoba sebuah`gh`— tidak ditemukan. Satu`java -version`— tidak hadir. Satu`node`— tidak ada di mana-mana. Namun, alat-alat ini memang ada di votre shell. Masalah? OpenCode menjalankan shell.non-interaktifyang tidak pernah membaca Anda`.zshrc`. Berikut cara mendiagnosis dan memperbaikinya dengan rapi.

tok

[]

Pemandangan: seorang agen buta di dunia alat

Itu adalah malam Selasa. Saya baru saja menginstal.https://opencode.ai[OpenCode], agen AI yang menjanjikan untuk mengubah cara saya coding. Pertama tes : meminta agar menampilkan repositori GitHub saya.

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

Aneh`gh`Berfungsi dengan sempurna di terminal saya. Saya mencoba hal lain :

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

Kemudian :

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

Java, Gradle, Node,gh— semuanya tak terlihat bagi agen. Terminalku, justru, melihat semuanya. Seolah-olah agen dan aku hidup di dua dunia yang paralel.

Saya menghabiskan berjam-jam untuk memahaminya. Berjam-jam dari`echo $PATH`, de which java, dari frustrasi yang semakin meningkat. Saya akhirnya menyadari bahwa masalah bukan alat-alat — itu adalahkerang. OpenCode, seperti agen apa pun yang meluncurkan subprocess, bekerja dalam shelltidak interaktif. Dan Zsh, di shell-shell tersebut, sekadar mengabaikan Anda`.zshrc`.

Yang berikut adalah cerita lengkap dari diagnosis tersebut, penyelesaiannya, dan pelajaran yang saya pelajari. Jika Anda menggunakan agen AI — OpenCode, Aider, Cursor, atau bahkan skrip cron — masalah ini akan memengaruhi Anda suatu hari.

Anatomi bug : dua shell, dua dunia

Ketika Anda membuka sebuah terminal, Zsh memperlakukannya seperti sebuah shell.interaktif. Dia memuat`.zshrc`, yang menginisialisasi semuanya : SDKMAN, NVM, pnpm, alias, prompt yang cantik Anda. Lingkungan Anda lengkap.

Tetapi ketika OpenCode mengeksekusi perintah, ia tidak meluncurkan terminal interaktif. Ia meluncurkan sebuah shelltidak interaktif— sebuah shell tugas, tanpa manusia di balik layar. Dan Zsh, dalam konteks ini, melompat..zshrc. Dia hanya membaca`.zshenv`.

Mengapa perbedaan ini ada? Karena shell non-interaktif dirancang untuk menjalankan skrip, bukan untuk melayani pengguna manusia. Memuat alias, prompt, dan pengisian dalam skrip yang berjalan di latar belakang akan sia-sia. Masalahnya adalah PATH Anda — informasi paling kritis untuk menemukan executable — sering diinisialisasi di`.zshrc`, tidak dalam`.zshenv`.

@startuml
skinparam backgroundColor #FEFEFE

actor "Anda" as user
actor "OpenCode
(agen)" as agent
participant "Shell\ninteraktif\n(.zshrc di-source)" as ishell
participant "Shell\nnon-interaktif\n(.zshrc diabaikan)" 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

Akar masalah :Zsh hanya men-source .zshrc untuk shell interaktif. OpenCode, seperti agen AI lain yang meluncurkan subproses, menggunakan shell non-interaktif. Shell-shell ini hanya membaca`.zshenv`.

Pelaku : .zshrc vs .zshenv

Zsh memilikiempat file inisialisasi, masing-masing dengan peran yang spesifik. Ini sebuah desain yang elegan—tapi juga inti masalah:

berkas

Diperoleh ketika

Peran

Ubah PATH ?

.zshenv

Selalu(interaktif + non-interaktif + login)

Variabel lingkungan penting, PATH

✅ Ya — itu adalah tempat SA

.zprofile

Shell login saja

Perintah lambat (sekali per sesi)

Mungkin

.zshrc

Hanya shell interaktif

Alias, prompt, penyelesaian, alat interaktif

❌ Bukan untuk variabel kritis

.zlogin

Shell login (setelah .zshrc)

Pesan sambutan, penyelesaian

jarang

Tabel memberi sebuah petunjuk, tapi harus dipahami secara mendalam. Pikirkan itu seperti bagian-bagian rumah:

  • `.zshenv`adalahaula masuk— semua orang lewat di sini, pengunjung atau warga. Jika Anda menaruh sesuatu di sini, semua shell akan dapat melihatnya.

  • `.zshrc`adalahruang tamu— hanya penghuni (shell interaktif) yang masuk. Pengunjung (shell non-interaktif) tetap berada di aula.

  • .zprofile et `.zlogin`adalah bagian khusus untuk login shell (seperti saat Anda login melalui SSH)

Tragisnya, kebanyakan dari kita menaruh PATH di ruang tamu. Dan agen AI, ia, tidak pernah berhak masuk ke sana.

Masalah dalam satu diagram :

@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

Ketika Anda membuka terminal, Zsh bersifat interaktif: ia membaca`.zshrc`, semuanya berfungsi. Ketika OpenCode meluncurkan shell untuk menjalankan perintah, Zsh adalah non-interaktif: ia melompat`.zshrc`, hanya membaca`.zshenv`. Et si `.zshenv`tidak ada atau tidak mengandung PATH — ini adalah gurun.

Mengapa Zsh melakukan itu ?Ini adalah pilihan desain yang diwarisi dari Unix. Shell non-interaktif haruscepat et dapat direproduksi. Memuat alias, prompt berwarna, dan inisialisasi berat dari SDKMAN dalam skrip cron atau agen AI akan lambat dan rapuh. Oleh karena itu, pemisahan itu logis :`.zshenv`utamanya (variabel, PATH)`.zshrc`untuk kenyamanan (aliases, prompt, complétons). Masalah terjadi ketika kita menempatkan yang penting dalam kenyamanan.

Diagnostik langkah demi langkah

Sebelum memperbaiki, kamu harus memahami persis apa yang hilang. Berikut adalah metode diagnostik yang dapat direproduksi — simpan sebagai favorit jika Anda bekerja dengan agen AI.

Memeriksa PATH agen

Kita membandingkan apa yang dilihat oleh terminal interaktif Anda dengan apa yang dilihat oleh shell non-interaktif — yaitu apa yang dilihat oleh OpenCode.

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

Hasil tipikal :

/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

Sekarang, mari kita mensimulasikan apa yang dilihat agen—sebuah shell non-interaktif:

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

Hasil :

/home/cheroliv/.local/bin

Lima dari enam jalur telah menghilang.Agen dipotong 83% dari lingkungannya. Seolah-olah diminta untuk memasak tanpa setengah dari peralatan Anda — Anda bisa merebus air, tetapi tidak banyak lagi.

Perintah`zsh -c 'echo $PATH'`adalah Andaalat diagnostik nomor 1. Jika alat yang Anda gunakan sehari-hari tidak ada dalam hasil, agen AI Anda juga tidak akan dapat melihatnya. Uji sebelum dan setelah setiap perubahan`.zshenv`.

2. Identifikasi apa yang ada di .zshrc tetapi tidak ada di .zshenv

Sekarang, kami mencari pelaku di`.zshrc`. Kita memfilter baris yang menyebutkan alat kita :

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

Seringkali ditemukan :

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"

Semua ini adalahtidak terlihatuntuk OpenCode. Dan le`PATH`tidak bahkan dari baris 119`export`é — dia tidak pernah keluar dari shell saat ini. Ini adalah sebuah rincian teknis krusial : sebuah variabel tanpa`export`Tetap lokal untuk shell yang mendefinisikannya. Sub-shell — seperti yang diluncurkan oleh OpenCode — tidak pernah mewarisinya.

3. Memeriksa jika .zshenv ada

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

Jika jawabannya adalah « FICHIER ABSENT », itulah tempatnya semuanya terjadi.

Solusi: .zshenv + jalur yang tepat

Strategi ini sederhana, tapi membutuhkan ketelitian: menaruh di dalam`.zshenv` hanyajalan penting, tanpa melakukan source pada skrip inisialisasi yang berat. Kita tidak memindahkan`.zshrc`dalam`.zshenv`— inti diambil.

Membuat .zshenv dengan semua jalur esensial

`.zshenv`adalahfile tunggalbahwa Zsh menjamin untuk menyource disemuales konteks. Itu lah tempat PATH harus pergi.

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"

Kita menggunakan`$HOME/.nvm/current/bin`dan tidak`$HOME/.nvm/versions/node/v22.19.0/bin`. Alasan: versi Node berubah. Jalur hard-coded menjadi salah pada versi berikutnya.nvm install. Lebih banyak detail di bagian berikutnya.

Mengapa tidak source sdkman-init.sh di .zshenv?

Pendekatan yang paling menggoda akan hanya mereproduksi di`.zshenv`apa yang kita lakukan di`.zshrc`— meng-source skrip inisialisasi. Onmungkintergoda untuk melakukan :

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

Masalah :

  • lambat:`sdkman-init.sh`Membuat resolusi jaringan dan verifikasi di setiap shell. Dalam shell non-interaktif, ini adalah biaya yang tidak perlu.

  • rapuh: SDKMAN mengharapkan konteks interaktif. Inisialisasinya dapat gagal secara diam-diam dalam pipa atau subshell.

  • tidak berguna: SDKMAN menempatkan calon-calonya di`~/.sdkman/candidates/<tool>/current/bin`— symlink stabil yang menunjuk ke versi aktif. Mereka dapat digunakan.langsung.

Pendekatan yang baik: menghindari inisialisasi SDKMAN dan menunjuk langsung ke symlink`current`. Ini adalah kunci dari seluruh solusi :menggunakan struktur file sebagai kontrak, bukan kode inisialisasi.

@startuml
skinparam backgroundColor #FEFEFE

rectangle "Pendekatan LAMBAT\n(source sdkman-init.sh)" as slow {
  card ".zshenv source sdkman-init.sh
2-3 detik per shell
Pemeriksaan jaringan
Risiko kegagalan" as s1 #FDEDEC
}

rectangle "Pendekatan cepat
(symlinks langsung)" as fast {
  card ".zshenv export PATH=...current/bin\n0 ms\nTidak ada jaringan\nTidak ada inisialisasi" as s2 #E8F8E8
}

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

@enduml

Kasus NVM: ketika manajer versi melupakan untuk meninggalkan jejak

Masalah NVM

Ini adalah tempat di mana penelitian telah membawa saya sejauh ini. SDKMAN memiliki desain yang elegan: ketika seseorang melakukan`sdk install java 25.0.2-tem`, ia membuat sebuah symlink:

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

Ini symlink adalahselalu terbaru. Arahkan ke dalam`.zshenv`Anda terlindungi, apa pun shell-nya. Indah.

NVM, dia, tidak membuattidak adaseperti itu. Tidak ada symlink`current`. Tidak ada titik ancrage yang stabil. Kita harus menunjuk ke jalur versi yang ditetapkan secara keras, yang terlihat seperti ini:

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

Berikutnya`nvm install 24`, jalan ini mati. Anda`.zshenv`Akan menunjuk ke versi yang tidak lagi versi aktif. Ini adalah bom waktu.

Mengapa NVM melakukan itu?NVM berfungsi dengan mengubah secara dinamis variabel PATH setiap`nvm use`. Ini dirancang untuk pengembang yang sering berganti versi, dalam shell interaktif. Symlink`current`tidak ada dalam spesifikasi awal — ini adalah kesalahan desain yang akan kita perbaiki sendiri.

@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 ❌ (sebelum perbaikan)" 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 ✅ (setelah koreksi)" 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

Karena NVM tidak melakukannya, kami melakukannya sendiri. Prinsipnya sama dengan SDKMAN — sebuah symlink.`current`yang selalu menunjuk ke versi aktif. Kita membuatnya sekali, lalu mengotomatiskannya agar ia dapat memperbarui dirinya sendiri.

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

Sekarang, dalam`.zshenv`, kita menggunakan :

$HOME/.nvm/current/bin

Daripada:

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

Symlink tidak berguna jika tidak diperbarui. Symlink`current`harus diperbarui ketika kita melakukan`nvm use` ou nvm install. Solusi: sebuahbungkus— sebuah fungsi yang meliputi perintah yang sebenarnya`nvm`dan memperbarui symlink setelah setiap panggilan.

# À 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'

Bagaimana cara kerjanya, secara detail :

@startuml
skinparam backgroundColor #FEFEFE

actor Développeur
participant "nvm wrapper
(_nvm)" as wrapper
participant "nvm nyata\n(perintah nvm)" as realnvm
participant "~/.nvm/current\n(symlink)" 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

Pembungkus`_nvm`panggil perintah yang sebenarnya`nvm`, lalu memperbarui symlink. Alias`nvm='_nvm'`memastikan bahwa saat mengetik`nvm`, lewat wrapper. Dan saat inisialisasi shell, kita melakukan hal yang sama setelahnya`nvm use --lts`.

`nvm_version_path`adalah fungsi internal NVM yang menyelesaikan jalur lengkap sebuah versi. Ini menghindari rekonstruksi jalur secara manual.

Tabel ringkasan jalur

Sebelum beralih ke jebakan, sebuah ringkasan visual dari transformasi. Di sebelah kiri, apa yang Anda miliki (semua di`.zshrc`, tidak terlihat bagi agen). Di sebelah kanan, apa yang Anda miliki sekarang (jalan-jalan penting di`.zshenv`, terlihat di mana-mana).

alat Sebelum (.zshrc hanya) Sesudah (.zshenv + symlink) Terlihat oleh OpenCode?

~/apps(gh, vscode)

PATH tidak diekspor di .zshrc

`$HOME/apps`dalam .zshenv

✅

SDKMAN Java

`sdkman-init.sh`diambil dalam .zshrc

`$HOME/.sdkman/candidates/java/current/bin`di .zshenv

(Empty)

SDKMAN Gradle

`sdkman-init.sh`disumberkan dalam .zshrc

`$HOME/.sdkman/candidates/gradle/current/bin`dalam .zshenv

✅

NVM Node

`nvm use --lts`di .zshrc

`$HOME/.nvm/current/bin`dalam .zshenv (symlink)

✅

pnpm

`$PNPM_HOME`dalam .zshrc

`$PNPM_HOME`dalam .zshenv

✅

JetBrains Toolbox

Ditambahkan secara otomatis oleh Toolbox di .zshrc

`$HOME/.local/share/JetBrains/Toolbox/scripts`dalam .zshenv

✅

Python 3

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

Secara eksplisit dalam .zshenv

✅

Jebakan dan mitigasi

Tidak semua sempurna dengan pendekatan ini. Berikut masalah yang saya temui, dan cara mengatasinya.

jebakan Deskripsi Mitigasi

PATH duplikat

Jika .zshenv dan .zshrc menambahkan jalur yang sama, jalur tersebut muncul dua kali

`.zshenv`adalah bersumbersebelum.zshrc. Kedua dibaca dalam shell interaktif. PATH dapat duplikat. Ini hanya kosmetik, tidak fungsional. Untuk menghindarinya: jangan menaruh di .zshenv kecuali jalur absents dari PATH default.

NVM : versi tetap dalam .zshenv

Menaruh`~/.nvm/versions/node/v22.19.0/bin`beton menjadi salah pada berikutnya`nvm install`

Menggunakan symlink`$HOME/.nvm/current/bin`+ pembungkus`_nvm`dalam .zshrc

SDKMAN : source sdkman-init.sh di .zshenv

Lambat, rapuh, tidak berguna dalam shell non-interaktif

Menggunakan symlink`candidates/<tool>/current/bin`secara langsung

Alias hilang setelah modifikasi

Bungkus`nvm='_nvm'`.zshrc hanya berlaku setelah dimuat ulang

Mulai ulang shell atau`source ~/.zshrc`

OpenCode tidak melihat perubahan

Agent telah meluncurkan sub-shell-nya dengan .zshenv lama

Mulai ulang OpenCode setelah modifikasi .zshenv

.zshenv terlalu banyak

Letakkan fungsi berat atau inisialisasi interaktif di .zshenv

.zshenv = variabel lingkungan + PATH hanya. Tidak ada`source`, tidak ada fungsi berat, tidak ada prompt.

Pelajaran yang dipelajari

  1. zshrc` adalah interaktif, .zshenv adalah universal— Jika variabel harus ada di semua shell (agen AI, cron, skrip, IDE), ia pergi ke`.zshenv`. Ruang tamu nyaman, tetapi aula masuk adalah satu-satunya tempat di mana semua orang berlalu.

  2. Manajer versi tidak sama— SDKMAN membuat symlink`current`secara sengaja. NVM tidak. Kita harus mengisi kekurangan ini secara manual. Ini adalah pelajaran penting: sebelum mengatur PATH Anda, periksa apakah manajer Anda menawarkan titik ancuran yang stabil.

  3. Jangan menyource skrip inisialisasi di .zshenv—sdkman-init.sh et `nvm.sh`dirancang untuk shell interaktif. Mereka lambat dan rapuh dalam konteks non-interaktif. Symlink`current`cukup dan terjadi secara instan.

  4. Selalu menguji dalam shell non-interaktif—`zsh -c 'echo $PATH'`Simulasi tepat apa yang dilihat oleh agen. Ini adalah tes validasi. Tanpa tes ini, Anda tidak tahu apakah konfigurasi Anda berfungsi untuk agen.

  5. Polah wrapper dapat dipakai ulang.— Pola yang sama`_nvm`+`alias nvm='_nvm'`berlaku untuk setiap alat yang mengubah PATH secara dinamis tanpa meninggalkan jejak yang stabil. Ini adalah alat tambahan di kotak ide Anda.

@startuml
skinparam backgroundColor #FEFEFE

rectangle "sebelum" as avant {
  card "Shell interaktif : ✅
Shell non-interaktif : ❌
OpenCode : ❌
Cron : ❌
Scripts : ❌" as av1 #FDEDEC
}

rectangle "setelah" as apres {
  card "Shell interaktif : ✅
Shell non-interaktif : ✅
OpenCode : ✅
Cron : ✅
Skrip : ✅" as ap1 #E8F8E8
}

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

@enduml

Verifikasi akhir

Setelah membuat`.zshenv`dan symlink NVM, pastikan bahwa semuanya berfungsi — dalam kedua konteks:

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

Kedua harus berhasil. Jika ya, agen AI Anda akan melihat alat yang sama dengan Anda. Jika tidak, kembali ke diagnosa langkah demi langkah — Anda mungkin telah melupakan jalur atau symlink NVM tidak diperbarui.

Otomatisasi tes ini.Tambahkan pemeriksaan ini dalam skrip healthcheck yang Anda jalankan setelah setiap pembaruan alat Anda. Sebuah`zsh -c 'which java && which node && which gh'`di CI, ini adalah jaminan terhadap kejutan.

_ Shell non-interaktif seperti tamu yang diam: ia hanya membaca apa yang ditampilkan di pintu masuk. Jika PATH ada di ruang tamu, ia tidak akan pernah melihatnya. _

Tautan

Dokumentasi resmi

Alat yang disebutkan

Untuk pergi lebih jauh

Artikel terkait