OpenCode et le PATH incomplet : pourquoi vos outils n'étaient pas dans le shell de l'agent
Pubblicato il 21 April 2026
- La scena: un agente cieco in un mondo di strumenti
- Anatomia del bug: due shell, due mondi
- I colpevoli : .zshrc vs .zshenv
- Diagnostica passo a passo
- La soluzione : .zshenv + i percorsi corretti
- Il caso NVM: quando il gestore di versioni dimentica di lasciare una traccia
- Tabella riassuntiva dei percorsi
- Trappole e mitigazioni
- Lezioni apprese
- Verifica finale
- Link
Avvii OpenCode nel tuo terminale, tutto funziona. L’agente tenta un`gh`— introvabile. Un`java -version`— assente. Un`node`— da nessuna parte. Questi strumenti sono proprio lì nel vostro shell. Il problema ? OpenCode lancia dei shellnon interattiviche non leggono mai il tuo`.zshrc`. Ecco come diagnosticare e correggere correttamente.
La scena: un agente cieco in un mondo di strumenti
Era un martedì sera. Avevo appena installatohttps://opencode.ai[OpenCode], l’agente IA che prometteva di trasformare il mio modo di codificare. Primo test: chiedergli di elencare i miei repository GitHub.
$ opencode
> Utilise gh pour lister mes repos
❌ bash: gh: command not found
Strano.`gh`funzionava perfettamente nel mio terminale. Sto provando altro :
> Vérifie la version de Java
❌ bash: java: command not found
Poi :
> Lance le build Gradle
❌ bash: gradle: command not found
Java, Gradle, Node,gh— tutto era invisibile per l’agente. Il mio terminale, invece, vedeva tutto. Come se l’agente e io vivessimo in due mondi paralleli.
Ho messo ore a capire. Ore di`echo $PATH`, de which java, di frustrazione crescente. Ho finito per capire che il problema non erano gli strumenti — era ilguscio. OpenCode, come qualsiasi agente che avvia sottoprocessi, lavora nei shellnon interattivi. E Zsh, in questi shell, ignora semplicemente il tuo`.zshrc`.
Quello che segue è il resoconto completo di questa diagnosi, della risoluzione, e della lezione che ho tirata. Se utilizzi un agente IA — OpenCode, Aider, Cursor, o anche degli script cron — questo problema ti riguarderà un giorno.
Anatomia del bug: due shell, due mondi
Quando apri un terminale, Zsh lo tratta come un shellinterattivo. Carica`.zshrc`, che inizializza tutto: SDKMAN, NVM, pnpm, gli alias, il tuo bel prompt. Il tuo ambiente è completo.
Ma quando OpenCode esegue un comando, non avvia un terminale interattivo. Avvia una shellnon interattivo— un shell di attività, senza umano dietro lo schermo. E Zsh, in questo contesto, salta`.zshrc`. Legge solo`.zshenv`.
Perché esiste questa distinzione? Perché una shell non interattiva è progettata per eseguire script, non per servire un utente umano. Caricare gli alias, il prompt e le completazioni in uno script che gira in background sarebbe uno spreco. Il problema è che il tuo PATH — l’informazione più critica per trovare gli eseguibili — viene spesso inizializzato in`.zshrc`, non in`.zshenv`.
@startuml skinparam backgroundColor #FEFEFE actor "Tu" as user actor "OpenCode\n(agent)" as agent participant "Shell\ninterattivo\n(.zshrc caricato)" as ishell participant "Shell\nnon interattivo\n(.zshrc ignorato)" as nshell user -> ishell : gh, java, node ✅ agent -> nshell : gh, java, node ❌ note right of ishell Sorgente .zshrc : - SDKMAN init - NVM init - ~/apps in PATH - pnpm in PATH end note note right of nshell .zshrc ignorato ! PATH = /usr/bin:/bin + alcune directory predefinite end note @enduml
La radice del problema :Zsh esegue il source di .zshrc solo per le shell interattive. OpenCode, come ogni agente IA che lancia sottoprocessi, utilizza shell non interattivi. Questi shell leggono solo`.zshenv`.
I colpevoli : .zshrc vs .zshenv
Zsh dispone diquattro file di inizializzazione, chacun avec un rôle précis. C’est un design élégant — mais c’est aussi le nœud du problème :
| File | sourcing quando | Ruolo | Modifica il PATH ? |
|---|---|---|---|
|
Sempre(interattivo + non interattivo + login) |
Variabili d’ambiente essenziali, PATH |
✅ Sì — è il posto di SA |
|
Solo le shell di login |
Comandi lenti (una volta per session) |
Possibile |
|
Solo shell interattive |
Alias, prompt, completamento, strumenti interattivi |
❌ Non per le variabili critiche |
|
Shells di login (dopo .zshrc) |
Messaggi di benvenuto, finalizzazione |
raramente |
La tabella fornisce un indizio, ma bisogna comprenderlo in profondità. Pensaci come le stanze di una casa :
-
`.zshenv`è ilatrio d’ingresso— tutti passano di lì, visitatori o residenti. Se mettete qualcosa qui, ogni shell potrà vederlo.
-
`.zshrc`è ilsoggiorno— solo i residenti (shell interattivi) vi entrano. I visitatori (shell non interattivi) rimangono nella hall.
-
.zprofileet `.zlogin`sono pezzi specializzati per le shell di login (come quando ti connetti via SSH).
Il dramma è che la maggior parte di noi mette il PATH nel salotto. E l’agente IA, lui, non ha mai il diritto di entrarci.
Il problema in un diagramma :
@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
Quando apri un terminale, Zsh è interattivo: legge`.zshrc`, tutto funziona. Quando OpenCode avvia un shell per eseguire un comando, Zsh è non interattivo: salta`.zshrc`, legge solo`.zshenv`. Et si `.zshenv`non esiste o non contiene il PATH — è il deserto.
|
Perché Zsh lo fa?È una scelta di progettazione ereditata da Unix. Un shell non interattivo deve essereveloce et riproducibile. Caricare gli alias, i prompt colorati e le inizializzazioni pesanti di SDKMAN in uno script cron o in un agente IA sarebbe lento e fragile. La separazione è quindi logica :`.zshenv`per l’essenziale (variables, PATH)`.zshrc`per il comfort (aliases, prompt, completiamo). Il problema si verifica quando si mette l’essenziale nel comfort. |
Diagnostica passo a passo
Prima di correggere, è necessario capire esattamente cosa manca. Ecco un metodo diagnostico riproducibile — tienilo nei preferiti se lavori con agenti IA.
Verificare il PATH dell’agente
Si confronta ciò che vede il terminale interattivo con ciò che vede una shell non interattiva — ovvero ciò che vede OpenCode.
# Dans votre terminal interactif (tout fonctionne)
echo $PATH | tr ':' '\n' | grep -v '^/usr' | sort
Risultato tipico:
/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
Ora, simuliamo ciò che vede l’agente — una shell non interattiva :
# Shell non-interactif : pas de .zshrc
zsh -c 'echo $PATH' | tr ':' '\n' | grep -v '^/usr' | sort
Risultato :
/home/cheroliv/.local/bin
Cinque percorsi su sei sono scomparsi.L’agente è amputato dell'83% del suo ambiente. È come se ti chiedessero di cucinare senza la metà dei tuoi utensili — potresti far bollire dell’acqua, ma nient’altro.
|
Il comando`zsh -c 'echo $PATH'`è tuostrumento di diagnostico numero 1. Se uno strumento che usi quotidianamente è assente dal risultato, il tuo agente IA non potrà vederlo neanche. Provala prima e dopo ogni modifica di`.zshenv`. |
2. Identificare ciò che è in .zshrc ma non è in .zshenv
Ora stiamo cercando il colpevole in`.zshrc`. Si filtrano le righe che menzionano i nostri strumenti:
grep -n 'apps\|SDKMAN\|NVM\|PNPM\|PATH' ~/.zshrc
Si trova tipicamente:
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"
Tutto ciò èinvisibileper OpenCode. E il`PATH`della riga 119 non è nemmeno`export`Non lascia mai la shell corrente. È un dettaglio tecnico cruciale: una variabile senza`export`rimane locale alla shell che la definisce. I sottoshell — come quelli avviati da OpenCode — non la ereditano mai.
3. Verificare se .zshenv esiste
cat ~/.zshenv 2>/dev/null || echo "FICHIER ABSENT"
Se la risposta è « FICHIER ABSENT », è lì che si decide tutto.
La soluzione : .zshenv + i percorsi corretti
La strategia è semplice, ma richiede precisione: inserire in`.zshenv` soloi percorsi essenziali, senza caricare gli script di inizializzazione pesanti. Non spostiamo`.zshrc`in`.zshenv`— si estrae l’essenziale.
Creare .zshenv con tutti i percorsi essenziali
`.zshenv`è ilsolo fileche Zsh garantisce di eseguire lo source intuttii contesti. È lì che deve andare il 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"
|
Si usa`$HOME/.nvm/current/bin`e non`$HOME/.nvm/versions/node/v22.19.0/bin`. La ragione: le versioni di Node cambiano. Un percorso hardcoded diventa falso al prossimo`nvm install`. Ulteriori dettagli nella sezione successiva. |
Perché non source sdkman-init.sh in .zshenv ?
L’approccio più allettante sarebbe semplicemente di riprodurre in`.zshenv`quello che facciamo in`.zshrc`— source gli script di inizializzazione. Sipotrebbeessere tentato di fare :
# ❌ MAUVAISE IDÉE
[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"
Problemi :
-
Lento(empty)`sdkman-init.sh`effettua risoluzioni di rete e verifiche a ogni shell. In una shell non interattiva, è un costo inutile.
-
Fragile: SDKMAN si aspetta un contesto interattivo. La sua inizializzazione può fallire silenziosamente in una pipe o in un sotto-shell.
-
Inutile: SDKMAN colloca i suoi candidati in`~/.sdkman/candidates/<tool>/current/bin`— collegamenti simbolici stabili che puntano alla versione attiva. Possiamo usarlidirettamente.
L’approccio corretto : aggirare l’inizializzazione di SDKMAN e puntare direttamente sui symlink`current`. È la chiave di tutta la soluzione :utilizzare la struttura dei file come contratto, anziché il codice di inizializzazione.
@startuml
skinparam backgroundColor #FEFEFE
rectangle "Approche LENTE\n(source sdkman-init.sh)" as slow {
card ".zshenv source sdkman-init.sh\n2-3 secondes par shell\nVérifications réseau\nRisque d'échec" as s1 #FDEDEC
}
rectangle "Approche RAPIDE\n(symlinks directs)" as fast {
card ".zshenv export PATH=...current/bin\n0 ms\nPas de réseau\nPas d'initialisation" as s2 #E8F8E8
}
slow --> fast : Même résultat final\nLe PATH pointe sur current/bin\ndans les deux cas
@enduml
Il caso NVM: quando il gestore di versioni dimentica di lasciare una traccia
Il problema NVM
È qui che l’indagine mi ha portato più lontano. SDKMAN ha un design elegante: quando si fa`sdk install java 25.0.2-tem`, crea un symlink:
~/.sdkman/candidates/java/current -> ~/.sdkman/candidates/java/25.0.2-tem
Questo collegamento simbolico èsempre aggiornato. Indicarlo dentro`.zshenv`e sei coperto, qualunque sia la shell. Beautiful.
NVM, lui, non creanientedi tale. Nessun symlink`current`. Nessun punto di ancoraggio stabile. Dobbiamo puntare su un percorso versionato fisso, che sembra così:
~/.nvm/versions/node/v22.19.0/bin/node
al prossimo`nvm install 24`, questo sentiero è morto. Il tuo`.zshenv`punta verso una versione che non è più la versione attiva. È una bomba a orologeria.
|
Perché NVM fa così ?NVM funziona modificando dinamicamente il PATH ad ogni`nvm use`. È progettato per sviluppatori che cambiano versione spesso, in una shell interattiva. Il symlink`current`non era nelle specifiche iniziali — è un’omissione di progettazione che correggeremo noi stessi. |
@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 ❌ (prima della correzione)" 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
Nessun symlink current!
.zshenv deve puntare in modo statico
Si romperà al prossimo nvm install
end note
}
package "NVM ✅ (dopo la correzione)" 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
La soluzione: creare il symlink current per NVM
Poiché NVM non lo fa, lo facciamo noi stessi. Il principio è lo stesso di SDKMAN — un symlink`current`che punta sempre alla versione attiva. Lo creiamo una volta, poi lo automatizziamo affinché si aggiorni da solo.
# Créer le symlink initial
ln -sfn "$HOME/.nvm/versions/node/v22.19.0" "$HOME/.nvm/current"
Adesso, in`.zshenv`, si utilizza :
$HOME/.nvm/current/bin
piuttosto che :
# ❌ Chemin en dur — cassé au prochain changement de version
$HOME/.nvm/versions/node/v22.19.0/bin
Automatizzare l’aggiornamento del symlink
Un symlink non vale nulla se non si aggiorna. Il symlink`current`deve aggiornarsi quando facciamo`nvm use` ou nvm install. La soluzione : uninvolucro— una funzione che avvolge il vero comando`nvm`e aggiorna il link simbolico dopo ogni chiamata.
# À 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'
Come funziona, nel dettaglio :
@startuml skinparam backgroundColor #FEFEFE actor Développeur participant "wrapper nvm\n(_nvm)" as wrapper participant "nvm reale\n(comando 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 Sempre aggiornato! Utilizzato da `.zshenv` → accessibile tramite OpenCode end note @enduml
Il wrapper`_nvm`chiama il vero comando`nvm`, poi aggiorna il collegamento simbolico. L’alias`nvm='_nvm'`fa in modo che digitando`nvm`, si passa attraverso il wrapper. E all’inizializzazione della shell, facciamo lo stesso dopo`nvm use --lts`.
|
`nvm_version_path`è una funzione interna di NVM che risolve il percorso completo di una versione. Evita di ricostruire manualmente il percorso. |
Tabella riassuntiva dei percorsi
Prima di passare alle trappole, un riepilogo visivo della trasformazione. A sinistra, ciò che avevi (tutto nel`.zshrc`, invisibile per l’agente). A destra, ciò che hai ora (i percorsi essenziali in`.zshenv`, visibili ovunque).
| Strumento | Prima (.zshrc solo) | Dopo (.zshenv + symlink) | Visibile da OpenCode ? |
|---|---|---|---|
|
PATH non esportato in .zshrc |
`$HOME/apps`in .zshenv |
✅ |
SDKMAN Java |
`sdkman-init.sh`source in .zshrc |
`$HOME/.sdkman/candidates/java/current/bin`in .zshenv |
✅ |
SDKMAN Gradle |
`sdkman-init.sh`sorgente in .zshrc |
`$HOME/.sdkman/candidates/gradle/current/bin`in .zshenv |
✅ |
Nodo NVM |
`nvm use --lts`in .zshrc |
`$HOME/.nvm/current/bin`in .zshenv (link simbolico) |
✅ |
pnpm |
`$PNPM_HOME`nel .zshrc |
`$PNPM_HOME`in .zshenv |
✅ |
JetBrains Toolbox |
Aggiunto automaticamente da Toolbox in .zshrc |
`$HOME/.local/share/JetBrains/Toolbox/scripts`in .zshenv |
✅ |
Python 3 |
`/usr/bin/python3`in PATH .zshrc |
Esplicitamente in .zshenv |
✅ |
Trappole e mitigazioni
Non tutto è perfetto con questo approccio. Ecco i problemi che ho incontrato, e come aggirarli.
| trappola | Descrizione | Mitigazione |
|---|---|---|
PATH duplicato |
Se .zshenv e .zshrc aggiungono lo stesso percorso, appare due volte |
`.zshenv`è ottenutoprima.zshrc. Entrambi vengono letti in una shell interattiva. Il PATH può essere duplicato. È solo estetico, non funzionale. Per evitarlo: mettere in .zshenv solo i percorsi assenti dal PATH predefinito. |
NVM : versione hardcoded in .zshenv |
Mettere`~/.nvm/versions/node/v22.19.0/bin`in duro diventa falso al prossimo`nvm install` |
Utilizzare il symlink`$HOME/.nvm/current/bin`+ il wrapper`_nvm`in .zshrc |
SDKMAN : source sdkman-init.sh in .zshenv |
Lento, fragile, inutile in una shell non interattiva |
Usare i symlinks`candidates/<tool>/current/bin`direttamente |
Alias mancante dopo la modifica |
Il wrapper`nvm='_nvm'`nel .zshrc ha effetto solo dopo il ricaricamento |
Riavvia la shell o`source ~/.zshrc` |
OpenCode non vede le modifiche |
L’agente ha già lanciato i suoi sottoshell con il vecchio .zshenv |
Riavviare OpenCode dopo la modifica di .zshenv |
.zshenv troppo carico |
Mettere funzioni pesanti o inizializzazioni interattive in .zshenv |
.zshenv = variabili d’ambiente + solo PATH. Non`source`, nessuna funzione pesante, nessun prompt. |
Lezioni apprese
-
.zshrc` è interattivo,
.zshenvè universale— Se una variabile deve esistere in tutti i shell (agenti IA, cron, script, IDE), essa va in`.zshenv`. Il soggiorno è confortevole, ma l’ingresso è l’unico posto dove passa tutto il mondo. -
I gestori di versioni non sono uguali— SDKMAN crea symlink`current`per design. NVM no. Bisogna colmare questa lacuna manualmente. È una lezione importante: prima di configurare il tuo PATH, verifica se il tuo gestore offre un punto di ancoraggio stabile.
-
Non caricare gli script di init in .zshenv—
sdkman-init.shet `nvm.sh`Sono progettati per una shell interattiva. Sono lenti e fragili in un contesto non interattivo. I symlink`current`sono sufficienti e sono istantanei. -
Testare sempre in shell non interattiva—`zsh -c 'echo $PATH'`Simula esattamente ciò che vede un agente. È il test di validazione. Senza questo test, non sai se la tua configurazione funziona per gli agenti.
-
Il wrapper pattern è riutilizzabile— Lo stesso pattern`_nvm`(empty)`alias nvm='_nvm'`Si applica a qualsiasi strumento che modifica il PATH dinamicamente senza lasciare una traccia stabile. È uno strumento in più nella tua cassetta delle idee.
@startuml
skinparam backgroundColor #FEFEFE
rectangle "PRIMA" as avant {
card "Shell interattiva: ✅\nShell non interattiva: ❌\nOpenCode: ❌\nCron: ❌\nScript: ❌" as av1 #FDEDEC
}
rectangle "DOPO" as apres {
card "Shell interattiva: ✅\nShell non interattiva: ✅\nOpenCode: ✅\nCron: ✅\nScript: ✅" as ap1 #E8F8E8
}
avant --> apres : .zshenv +\nsymlink ~/.nvm/current
@enduml
Verifica finale
Dopo aver creato`.zshenv`e il symlink NVM, verifica che tutto funzioni — in entrambi i contesti :
# Shell interactif (votre terminal)
gh --version && java -version && node --version
# Shell non-interactif (simulation OpenCode)
zsh -c 'gh --version && java -version && node --version'
Entrambi devono riuscire. Se sì, il tuo agente IA vedrà gli stessi strumenti che tu. Se no, torna alla diagnosi passo a passo — probabilmente hai dimenticato un percorso o il symlink NVM non è aggiornato.
|
Automatizza questo test.Aggiungi questa verifica in uno script di healthcheck che esegui dopo ogni aggiornamento dei tuoi strumenti. Un`zsh -c 'which java && which node && which gh'`in CI, è un’assicurazione contro le sorprese. |
_ Una shell non interattiva è come un ospite silenzioso: legge solo ciò che è visualizzato sulla porta d’ingresso. Se il PATH si trova nel soggiorno, non lo vedrà mai. _
Link
documentazione ufficiale
-
Documentazione Zsh : Startup Files— Il riferimento ufficiale sui file di inizializzazione di Zsh. È lì che tutto è spiegato, anche se spesso lo si dimentica.
-
OpenCode : configurazione— Come configurare OpenCode e il suo ambiente di esecuzione.
Strumenti menzionati
-
NVM su GitHub— Node Version Manager. Gestore delle versioni Node.js.
-
SDKMAN — sito ufficiale— SDKMAN! Il gestore di versioni per la JVM (Java, Kotlin, Gradle…)
-
SDKMAN : installazione— Guida all’installazione di SDKMAN.
-
gh` — GitHub CLI— Lo strumento da riga di comando GitHub, indispensabile per ogni sviluppatore.
-
Gradle — sito ufficialeIl sistema di build che installiamo tramite SDKMAN.
-
pnpm — sito ufficiale— Il gestore di pacchetti Node.js veloce ed efficiente nello spazio.
-
JetBrains Toolbox— Il gestore di IDE JetBrains, che aggiunge i suoi script al PATH.
Per andare più lontano
-
Zsh dotfiles: una guida completa— Articolo approfondito sulla gestione dei dotfiles Zsh.
-
Zsh su ArchWiki— Una delle migliori documentazioni della comunità su Zsh.
-
NVM problemi GitHub— Per vedere le discussioni attorno al symlink`current`e problemi di PATH.