време читања : 13 minutes

Pokrećete OpenCode u svom terminalu, sve radi. Agent pokušava`gh`— nepronađen. Jedan`java -version`— nedostaje. Jedan`node`— нигде. Ме intermediary, ови алатови су добро там у ваше шеллу. Проблем? OpenCode покреће шелове.ne-interaktivnikoji nikada ne čitaju vaš`.zshrc`. Ово је како да дијагностикујете и исправите то на правилан начин.

tak

[]

Scena: slep agent u svetu alata

Bio je utorak večer. Pres sam instaliraohttps://opencode.ai[OpenCode], AI agent koji je obećao da će promeniti moj način kodiranja. Prvi test: zamoliti ga da izlistati moje GitHub repozitorijume.

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

Čudno.gh fonctionnait parfaitement dans mon terminal. J’essaie autre chose :

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

Затим:

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

Java, Gradle, Node,gh— Sve je bilo nevidljivo agentu. Moj terminal, on, je vidio sve. Kao da je agent i ja živeli u dvama paralelnim svetima.

Potrošio sam sate da razumem. Sati`echo $PATH`, de which java, od rastuće frustracije. Konačno sam razumeo da problem nije u alati — bilo jeлушпа. OpenCode, kao svaki agent koji pokreće podprocesse, radi u shell-ovimaneinteraktivni. I Zsh, u ovim shell-ovima, jednostavno ignoriše vaše`.zshrc`.

Sledeći je potpuni izveštaj o ovom dijagnozu, o ovom rešenju i o ovoj lekciji koju sam naučila. Ako koristite AI agent — OpenCode, Aider, Cursor, ili čak i cron skripte — ovaj problem će vas jednog dana tići.

Anatomija greške: dva shella, dva sveta

Kada otvarate terminal, Zsh ga traktira kao shellинтерактиван. Он заређује`.zshrc`, koji inicijalizuje sve : SDKMAN, NVM, pnpm, alias, vaš leп prompt. Vaše okruženje je kompletno.

Но када OpenCode изврши команду, не покреће интерактивни терминал. Он покреће шелу.neinteraktivni— shell zadatka, bez čoveka iza ekrana. I Zsh, u ovom kontekstu, skače`.zshrc`. On samo čita`.zshenv`.

Zasto postoji ova razlika? Jer neinteraktivni shell je namenjen izvršavanju skriptova, a ne služenju ljudskog korisnika. Učitavanje aliasa, prompta i završetaka u skriptu koja radi u pozadini bila bi istrata. Problem jeste što vaš PATH — najvažnija informacija za pronalaženje izvršnih fajlova — se često inicijalizuje u`.zshrc`, nije u`.zshenv`.

@startuml
skinparam backgroundColor #FEFEFE

actor "Ви" as user
actor "OpenCode\n(agent)" as agent
participant "Shell
interaktivno
(.zshrc učitan)" as ishell
participant "Shell\nneinteraktivni\n(.zshrc je ignorisan)" 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

Koren problema:Zsh izvršava .zshrc samo za interaktivne shellove. OpenCode, kao i svaki AI agent koji pokreće podprocesse, koristi neinteraktivne shell-ove. Ovi shell-ovi ne čitaju`.zshenv`.

Krivci: .zshrc vs .zshenv

Zsh imaчетири фајла иницијализације, svaki sa specifičnom ulogom. To je elegantan dizajn — ali to je i čvor problema :

Fajl

изворен када

Улога

Modifikuj PATH?

.zshenv

Uvek(interaktivan + neinteraktivan + login)

Неопходне променљиве околине, PATH

✅ Да — то је SA mjesto

.zprofile

Само шељеве за логин

Sporе komande (jednom po sesiji)

Moguće

.zshrc

Само интерактивне лиуске

Alias, prompt, dovršavanje, interaktivni alati

❌ Nije za kritične promenljive

.zlogin

Shell-ovi za prijavu (posle .zshrc)

Поруке добро дошлица, завршетак

Ретако

Tabela daje pokazatelj, ali ga je potrebno duboko razumeti. Mislite na to kao na sobe kuće:

  • `.zshenv`jeulazni hol— svi prođu tamo, posetilac ili stanovnik. Ako stavite nešto ovde, svaki shell će moći da ga vidi.

  • `.zshrc`јеsalon— sami stanovnici (interaktivne ljuške) ulaze tamo. Posetioaci (neinteraktivne ljuške) ostaju u holu.

  • .zprofile et `.zlogin`Su specijalizovani delovi za login shellove (kao kada se povežete pomoću SSH).

Tragediya je da većina od nas stavimo PATH u dnevnu sobu. A AI agent, on, nikada nema pravo da uđe tamo.

Problem u dijagramu :

@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

Kada otvarate terminal, Zsh je interaktivan: čita`.zshrc`, sve radi. Kada OpenCode pokreće shell da izvrši komandu, Zsh je neinteraktivan : on preskače`.zshrc`, сам чита`.zshenv`. Et si `.zshenv`ne postoji ili ne sadrži PATH — to je pustinja.

Zasto Zsh to radi?Ovo je izbor dizajna nasleđen od Unix-a. Neinteraktivni shell mora bitibrz et репродуктиванUčitavanje aliasa, obojenih promptova i teških inicijalizacija SDKMAN-a u cron skriptu ili agente AI bi bilo sporo i křehko. Odvajanje je zato logično:`.zshenv`glavno (varijable, PATH)`.zshrc`za komfor (alias, prompt, kompletujemo). Problem se javlja kada stavimo najvažnije u komfor.

Korak po korak dijagnostika

Pre nego što ispravite, morate da razumete tačno šta nedostaje. Ovo je ponovljiva dijagnostička metoda — sačuvajte je u omiljenim ako radite sa AI agensima.

Проверити PATH агента

Poredimo ono što vidi vaš interaktivni terminal sa onim što vidi neinteraktivna shell — to jest, ono što vidi OpenCode.

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

Tipičan rezultat :

/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

Sada, simulirajmo šta agent vidi — neinteraktivna lupina:

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

Резултат :

/home/cheroliv/.local/bin

Pet od šest puteva je nestalo.Agent je amputovan na 83% svog okruženja. Kao da se vam pita da kuhate bez pola vaših posuđa — mogli biste provriti vodu, ali i baš malo više.

Naredba`zsh -c 'echo $PATH'`је вашalat dijagnostike broj 1. Ако алат који користите свакодневно није приступан у резултату, ваш ИА агент такође неће моћи да га види. Тестирајте ње пре и након сваке измене`.zshenv`.

2. Identifikati šta se nalazi u .zshrc, ali ne u .zshenv

Sada, tražimo krivca u`.zshrc`. Filtriramo redove koji spominju naša alat:

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

Tipično se nađe:

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"

Sve to jeневидимза OpenCode. И он`PATH`linije 119 nije ni`export`é — On nikada ne napušta trenutni shell. To je ključan tehnički detalj: varijabla bez`export`ostaje lokalno za shell koji ga definiše. Pod-šelovi — kao oni pokrenuti od strane OpenCode — ga nikad ne nasleđuje.

3. Проверити да ли .zshenv постоји

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

Ako je odgovor « FICHIER ABSENT », tu se sve odigrava.

Решение : .zshenv + правине путање

Strategija je jednostavna, ali zahteva preciznost: staviti u`.zshenv` самоosnovne putevi, bez učitavanja tehkih skriptova inicijalizacije. Ne pomeramo`.zshrc`у`.zshenv`— izvučemo suštinu.

Kreirati .zshenv sa svim neophodnim putanjama

`.zshenv`јеjedan fajlда Zsh гарантује да се извори уsviконтексти. Тамо треба да иде 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"

Korišćeno je`$HOME/.nvm/current/bin`i ne`$HOME/.nvm/versions/node/v22.19.0/bin`. Razlog: verzije Node se menjaju. Tvrdokodirana putanja postaje netačna u sledećem.nvm install. Više detalja u sledećoj sekciji.

Zašto ne source sdkman-init.sh u .zshenv?

Najprivlačniji pristup bi bio da se jednostavno reproducira u`.zshenv`šta radimo u`.zshrc`— izvršiti skripte inicijalizacije. Onmogao bibiti pokušen da uradi :

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

Проблеми :

  • sporan:`sdkman-init.sh`Pravi mrežne resolucije i proverke na svakom shell-u. U neinteraktivnom shell-u, to je nepotrebni trošak.

  • fragilan: SDKMAN očekuje interaktivni kontekst. Njegova inicijalizacija može tiho neuspeti u cevi ili pod-šeliju.

  • Bezvažno: SDKMAN postavlja svoje kandidate u`~/.sdkman/candidates/<tool>/current/bin`— stabilni simbolički linkovi koji pokazuju na aktivnu verziju. Možemo ih koristitidirektno.

Дobar приступ: облазити иницијалисање на SDKMAN и право показује на симлинкове`current`. Ово је кључ свега решења :koristiti strukturu fajlova kao ugovor, umesto koda inicijalizacije.

@startuml
skinparam backgroundColor #FEFEFE

rectangle "Spor pristup
(source sdkman-init.sh)" as slow {
  card ".zshenv source sdkman-init.sh
2-3 sekunde po shellu
Mrežne proverke
Rizik neuspeha" as s1 #FDEDEC
}

rectangle "Brzi pristup
(direktni simbolički linkovi)" as fast {
  card ".zshenv export PATH=...current/bin
0 ms
Nema mreže
Nema inicijalizacije" as s2 #E8F8E8
}

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

@enduml

Slucaj NVM: kada menadžer verzija zaboravi da ostavi trag

проблем NVM

Ovdje me je istraživanje doveo najdaleko. SDKMAN ima elegantan dizajn: kada uradimo`sdk install java 25.0.2-tem`, On pravi simbollink :

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

Ovaj symlink jeувек ажурирано. Ukažite na njega u`.zshenv`Јсте зашћени, без обзира на шелл. Прелепо.

Nikad, on ne kreiraништаod tel. Nema symlink`current`. Nema stalne tacke sidra. Moramo pokazivati na ugrađen (ukočen u kod) verzionisan put, koji izgleda ovako :

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

Na sledeći`nvm install 24`, Ovaj put je mrtav. Vaš`.zshenv`Pokazivač na verziju koja više nije aktivna verzija. Ovo je vremenska bomba.

Zašto NVM to radi?NVM radi dinamičkim modifikovanjem PATHa pri svakom`nvm use`. Namenjen je za developere koji često menjaju verziju, u interaktivnom shellu. Simbolička veza`current`Nije bio u početnim specifikacijama — ovo je greška u dizajnu koju ćemo sami ispraviti.

@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 ❌ (pre korekcije)" 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 ✅ (nakon korekcije)" 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

Posto NVM ne radi to, mi to radimo sami. Princip je isto kao SDKMAN — jedan symlink.`current`Kreiramo ga jednom, zatim ga automatizujemo da se ažurira sam.

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

Sada, u`.zshenv`, koristi se :

$HOME/.nvm/current/bin

Umesto :

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

Automatizuj ažuriranje symlinka

Симлинк није вредан ако се не ажурира. Симлинк`current`Treba da se ażurira kada se radi`nvm use` ou nvm install. Решење: једанomotač— funkcija koja omotava stvarnu komandu`nvm`i ažurira simboličku vezu nakon svakog poziva.

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

Kako to funkcionise, u detalju :

@startuml
skinparam backgroundColor #FEFEFE

actor Développeur
participant "nvm wrapper\n(_nvm)" as wrapper
participant "nvm реальан
(команда nvm)" as realnvm
participant "~/.nvm/current\n(simbolički link)" 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

Omotač`_nvm`zove pravi komanda`nvm`, zatim ažurira simlink. Alias`nvm='_nvm'`osigurava da prilikom unosa`nvm`, prolazimo kroz omotač. I pri inicijalizaciji shella, ponavljamo isto posle`nvm use --lts`.

`nvm_version_path`Ово је унутрашња функција NVM-а која решава комплетан пут до конкретне верзије. Ово спречава ручно постављање пута.

pregledna tabela putova

Пре него што пређеш на застане, визуелни сажетак трансформације. На левој страни, што ste imали (сав у`.zshrc`, nevidljivo za agenta). Desno, što sada imate (osnovni putevi u`.zshenv`, vidljivi svuda).

alat Pre (.zshrc samo) После (.zshenv + simblična veza) Vidljiv od OpenCode?

~/apps(gh, vscode)

PATH nije izvozen u .zshrc

`$HOME/apps`у .zshenv

✅

SDKMAN Java

`sdkman-init.sh`izvorljeno u .zshrc

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

✅

SDKMAN Gradle

`sdkman-init.sh`uvođen u .zshrc

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

✅

NVM čvor

`nvm use --lts`у .zshrc

`$HOME/.nvm/current/bin`u .zshenv (simbolička veza)

(No output, as there is no French text provided to translate)

pnpm

`$PNPM_HOME`у .zshrc

`$PNPM_HOME`у .zshenv

✅

JetBrains Toolbox

Automatski dodat od Toolbox u `.zshrc

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

✅

Python 3

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

Izričito u .zshenv

✅

Peštere i mitigations

Nije sve savršeno sa ovim pristupom. Evo problema koje sam naišao, i kako ih obistći.

уловка Opis umerenje

PATH duplikovan

Ako .zshenv i .zshrc dodaju istu putanju, pojavljuje se dva puta

`.zshenv`je izvornoпре.zshrc. Oba se čitaju u interaktivnoj ljusci. PATH se može duplirati. To je kosmetičko, ne funkcionalno. Da se izbegne: stavite u .zshenv samo putanje koje nisu prisutne u podrazumevanom PATH-u.

NVM : hardkodirana verzija u .zshenv

staviti`~/.nvm/versions/node/v22.19.0/bin`тврд постаје нетачан у следећем`nvm install`

Користити симлинк`$HOME/.nvm/current/bin`+ омотач`_nvm`у .zshrc

SDKMAN : izvršavanje sdkman-init.sh u .zshenv

Spor, ranljiv, bez korisnosti u neinteraktivnoj ljusci

Користите симлинкове`candidates/<tool>/current/bin`direktno

Nedostajući alias nakon izmene

Omot`nvm='_nvm'`у .zshrc утиче само после релоадовања

Покрени shell ponovno hoặc`source ~/.zshrc`

OpenCode ne vidi promene

Agent je već pokrenuo svoje potkornice sa starim .zshenv

Рестартуј OpenCode после измене .zshenv

.zshenv преоптерећен

Stavljati teške funkcije ili interaktivne inicijalizacije u `.zshenv

.zshenv = promenljive okruženja + PATH samo. Nema`source`, bez teških funkcija, bez promptova.

Научене лекcije

  1. .zshrc` je interaktivan, .zshenv je univerzalni— Ako jedna promenljiva mora da postoji u svim shell-ovima (agenti IA, cron, skripte, IDE), ona ide u`.zshenv`. Salon je udoban, ali ulazni hol je jedino mesto gde prolazi sve.

  2. Menadžeri verzija nisu jednaki— SDKMAN kreira simbolne veze`current`po dizajnu. NVM ne. Treba ručno popuniti ovaj nedostatak. Ovo je važna lekcija: pre podešavanja vašeg PATH, proverite da li vaš menadžer nudi stabilnu kotvicu.

  3. Ne učitati init skripte u .zshenv—sdkman-init.sh et `nvm.sh`Su dizajnirani za interaktivni shell. Oni su spori i krehki u neinteraktivnom kontekstu. Simbolički linkovi`current`Oni su dovoljni i instantani.

  4. Uvek testirati u neinteraktivnom shell-u—`zsh -c 'echo $PATH'`tačno simulira šta vidi agent. To je test validacije. Bez ovog testa, ne znate da li vaša konfiguracija funkcionise za agente.

  5. Wrapper pattern je ponovo upotrebljiv— Isti pattern`_nvm`+`alias nvm='_nvm'`Primena se na sve alate koji dinamički menjaju PATH bez ostavljanja stabilnog traga. Ovo je još jedan alat u vašoj kutiji ideja.

@startuml
skinparam backgroundColor #FEFEFE

rectangle "prije" as avant {
  card "Shell interaktivni : ✅
Shell neinteraktivni : ❌
OpenCode : ❌
Cron : ❌
Skripte : ❌" as av1 #FDEDEC
}

rectangle "nakon" as apres {
  card "Interaktivni shell : ✅\nNeinteraktivni shell : ✅\nOpenCode : ✅\nCron : ✅\nSkripte : ✅" as ap1 #E8F8E8
}

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

@enduml

Finalna proverka

Posle kreiranja`.zshenv`et symlink NVM, проверијте да све функционише — у два контекста :

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

Oba moraju da uspeju. Ako da, vaš agent AI će videti isti alat kao vi. Ako ne, vratite se na korak po korak dijagnozu — verovatno ste zaboravili putanju ili NVM symlink nije ažuriran.

Automate ovaj test.Dodajte ovu proveru u skriptu healthcheck koju pokrećete nakon svakog ažuriranja vaših alata.`zsh -c 'which java && which node && which gh'`u CI, to je osiguranje protiv iznenađenja.

Čuvajte sve blokove koda u backtick-ovima (…​) istačno — nikad ne menjajte sadržaj backtick-ova, razmake ili poziciju. Ovaj tekst može biti fragment većeg rečenice — prevedite fragment bez zahtevanja dodatnog konteksta. Izlazite samo prevedeni tekst — bez objašnjenja, komentarata, uvoda, alternativa ili opcija. Neinteraktivan shell je kao tiho gost: on čita samo ono što je prikazano na ulaznoj vrati. Ako je PATH u salašnom prostoru, on ga nikad neće videti. __

Linkovi

Službena dokumentacija

Navedeni alati

Za daljnje istraživanje

Повезани чланци