OpenCode y el PATH incompleto: ¿por qué tus herramientas no estaban en el shell del agente?
Publié le 21 April 2026
- La escena: un agente ciego en un mundo de herramientas
- Anatomía del bug: dos shells, dos mundos
- Los culpables : .zshrc vs .zshenv
- Diagnóstico paso a paso
- La solución: .zshenv + los buenos caminos
- El caso NVM: cuando el gestor de versiones olvida dejar una huella
- Tabla resumen de los caminos
- Trampas y mitigaciones
- Lecciones aprendidas
- Verificación final
- Enlaces
Lanzas OpenCode en tu terminal, todo funciona. El agente intenta un`gh`— no encontrado. Un`java -version`— ausente. un`node`— en ninguna parte. Sin embargo, estas herramientas están realmente allí en tu shell. ¿El problema? OpenCode lanza shellsno interactivosque nunca leen su`.zshrc`. Aquí tienes cómo diagnosticar y corregir esto correctamente.
- toc
-
[]
La escena: un agente ciego en un mundo de herramientas
Era un martes por la noche. Acababa de instalarhttps://opencode.ai[OpenCode], el agente IA que prometía transformar mi forma de programar. Primera prueba: pedirle que liste mis repositorios de GitHub.
$ opencode
> Utilise gh pour lister mes repos
❌ bash: gh: command not found
Extraño.`gh`funcionaba perfectamente en mi terminal. Estoy probando otra cosa :
> Vérifie la version de Java
❌ bash: java: command not found
Entonces :
> Lance le build Gradle
❌ bash: gradle: command not found
Java, Gradle, Node,gh— todo era invisible para el agente. Mi terminal, él, veía todo. Como si el agente y yo viviéramos en dos mundos paralelos.
Me llevó horas entender. Horas de`echo $PATH`, de which java, de frustración creciente. Por fin entendí que el problema no eran las herramientas — erashell. OpenCode, como cualquier agente que lance subprocesos, trabaja en shellsno interactivos.Y Zsh, en esos shells, ignora pura y simplemente su`.zshrc`.
Lo que sigue es el relato completo de este diagnóstico, de la resolución y de la lección que he aprendido. Si utilizas un agente de IA — OpenCode, Aider, Cursor o incluso scripts cron — este problema te afectará algún día.
Anatomía del bug: dos shells, dos mundos
Cuando abre un terminal, Zsh lo trata como un shellinteractivo. Él carga`.zshrc`, que inicializa todo: SDKMAN, NVM, pnpm, los alias, tu bonito prompt. Tu entorno está completo.
Pero cuando OpenCode ejecuta un comando, no inicia una terminal interactiva. Inicia una shellno interactivo— un shell de tarea, sin humano detrás de la pantalla. Y Zsh, en este contexto, salta`.zshrc`. Solo lee`.zshenv`.
¿Por qué existe esta distinción? Porque un shell no interactivo está diseñado para ejecutar scripts, no para servir a un usuario humano. Cargar los alias, el prompt y las completaciones en un script que se ejecuta en segundo plano sería un desperdicio. El problema es que tu PATH — la información más crítica para encontrar los ejecutables — suele inicializarse en`.zshrc`, no en`.zshenv`.
Failed to generate image: PlantUML preprocessing failed: [From <input> (line 7) ] @startuml skinparam backgroundColor #FEFEFE actor "Usted" as user actor "OpenCode\n(agente)" as agent participant "Shell\ninteractivo\n(.zshrc cargado)" as ishell participant "Shell ^^^^^ Syntax Error? (Assumed diagram type: sequence) @startuml skinparam backgroundColor #FEFEFE actor "Usted" as user actor "OpenCode\n(agente)" as agent participant "Shell\ninteractivo\n(.zshrc cargado)" as ishell participant "Shell no interactivo (.zshrc ignorado)" 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
La raíz del problema :Zsh solo source .zshrc para los shells interactivos. OpenCode, como cualquier agente de IA que lanza subprocesos, utiliza shells no interactivos. Estos shells solo leen`.zshenv`.
Los culpables : .zshrc vs .zshenv
Zsh dispone decuatro archivos de inicialización, cada uno con un papel preciso. Es un diseño elegante — pero también es el nudo del problema:
Archivo |
obtenido cuando |
Papel |
¿Modifica el PATH? |
|
siempre(interactivo + no interactivo + inicio de sesión) |
Variables de entorno esenciales, PATH |
✅ Sí — es SA lugar |
|
Shells de inicio de sesión únicamente |
Comandos lentos (una vez por sesión) |
Posible |
|
Sólo shells interactivos |
Alias, prompt, completado, herramientas interactivas |
No para las variables críticas |
|
Shells de inicio de sesión (después de .zshrc) |
Mensajes de bienvenida, finalización |
Rara vez |
La tabla da una pista, pero hay que comprenderla en profundidad. Piensa en ella como las piezas de una casa:
-
`.zshenv`es elhall de entrada— todos pasan por aquí, sea visitante o residente. Si pones algo aquí, cualquier shell podrá verlo.
-
`.zshrc`es elsalón— solo los residentes (shells interactivos) entran. Los visitantes (shells no interactivos) permanecen en el vestíbulo.
-
.zprofileet `.zlogin`son piezas especializadas para shells de inicio de sesión (como cuando te conectas por SSH).
El drama es que la mayoría de nosotros ponemos el PATH en la sala. Y el agente de IA, él, nunca tiene derecho a entrar.
El problema en un diagrama:
Cuando abre una terminal, Zsh es interactivo: lee`.zshrc`, todo funciona. Cuando OpenCode lanza un shell para ejecutar un comando, Zsh es no interactivo: salta`.zshrc`, solo lee`.zshenv`. Et si `.zshenv`no existe o no contiene el PATH — es el desierto.
|
¿Por qué Zsh hace eso?es una elección de diseño heredada de Unix. Un shell no interactivo debe serrápido et reproducible. Cargar los alias, los prompts coloreados y las inicializaciones pesadas de SDKMAN en un script de cron o un agente IA sería lento y frágil. La separación, por lo tanto, tiene sentido :`.zshenv`en esencia (variables, PATH)`.zshrc`Para la comodidad (aliases, prompt, completamos). El problema surge cuando se pone lo esencial en la comodidad. |
Diagnóstico paso a paso
Antes de corregir, es necesario comprender exactamente qué falta. Aquí tienes un método de diagnóstico reproducible — guárdalo en favoritos si trabajas con agentes de IA.
Comprobar el PATH del agente
Se compara lo que ve su terminal interactivo con lo que ve un shell no interactivo — es decir, lo que ve OpenCode.
# Dans votre terminal interactif (tout fonctionne)
echo $PATH | tr ':' '\n' | grep -v '^/usr' | sort
Resultado típico :
/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
Ahora, simulemos lo que ve el agente — un shell no interactivo:
# Shell non-interactif : pas de .zshrc
zsh -c 'echo $PATH' | tr ':' '\n' | grep -v '^/usr' | sort
Resultado :
/home/cheroliv/.local/bin
Cinco de seis caminos han desaparecido.El agente está amputado del 83% de su entorno. Es como si te pidieran cocinar sin la mitad de tus utensilios — podrías hervir agua, pero poco más.
|
El pedido`zsh -c 'echo $PATH'`es suherramienta de diagnóstico número 1. Si una herramienta que utilizas diariamente no aparece en el resultado, tu agente de IA tampoco podrá verla. Pruébala antes y después de cada modificación de`.zshenv`. |
Identificar lo que está en .zshrc pero no está en .zshenv
Ahora, estamos buscando al culpable en`.zshrc`. Filtramos las líneas que mencionan nuestras herramientas :
grep -n 'apps\|SDKMAN\|NVM\|PNPM\|PATH' ~/.zshrc
Se encuentra típicamente :
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"
Todo eso esinvisiblepara OpenCode. Y el`PATH`de la línea 119 ni siquiera`export`é — él nunca abandona el shell actual. Es un detalle técnico crucial: una variable sin`export`permanece local al shell que lo define. Los sub-shells — como los lanzados por OpenCode — nunca lo heredan.
3. Verificar si .zshenv existe
cat ~/.zshenv 2>/dev/null || echo "FICHIER ABSENT"
Si la respuesta es « FICHIER ABSENT », ahí es donde se juega todo.
La solución: .zshenv + los buenos caminos
La estrategia es simple, pero requiere precisión: poner en`.zshenv` sololos caminos esenciales, sin cargar los scripts de inicialización pesados. No lo movemos`.zshrc`en`.zshenv`— se extrae lo esencial.
Crear .zshenv con todos los caminos esenciales
`.zshenv`es elúnico archivoque Zsh garantiza de sourcear entodoslos contextes. Es ahí donde debe ir el 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"
|
Se utiliza`$HOME/.nvm/current/bin`y no`$HOME/.nvm/versions/node/v22.19.0/bin`. La razón: las versiones de Node cambian. Un camino fijo se vuelve falso en el próximo`nvm install`. Más detalles en la sección siguiente. |
¿Por qué no source sdkman-init.sh en .zshenv?
El enfoque más tentador sería simplemente reproducir en`.zshenv`lo que hacemos en`.zshrc`— sourcer los scripts de inicialización. Nosotrospodríaser tentado de hacer :
# ❌ MAUVAISE IDÉE
[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"
Problemas :
-
Cuaresma:`sdkman-init.sh`Hace resoluciones de red y verificaciones en cada shell. En un shell no interactivo, es un costo innecesario.
-
Frágil: SDKMAN espera un contexto interactivo. Su inicialización puede fallar silenciosamente en una tubería o un sub-shell.
-
Inútil: SDKMAN coloca a sus candidatos en`~/.sdkman/candidates/<tool>/current/bin`— enlaces simbólicos estables que apuntan a la versión activa. Se pueden utilizardirectamente.
La buena aproximación: evitar la inicialización de SDKMAN y apuntar directamente a los symlinks`current`. Es la clave de toda la solución :utilizar la estructura de archivos como contrato, en lugar del código de inicialización.
Failed to generate image: PlantUML preprocessing failed: [From <input> (line 5) ]
@startuml
skinparam backgroundColor #FEFEFE
rectangle "Enfoque LENTO
(source sdkman-init.sh)" as slow {
^^^^^
Syntax Error? (Assumed diagram type: activity)
@startuml
skinparam backgroundColor #FEFEFE
rectangle "Enfoque LENTO
(source sdkman-init.sh)" as slow {
card ".zshenv source sdkman-init.sh
2-3 segundos por shell
Comprobaciones de red
Riesgo de fallo" as s1 #FDEDEC
}
rectangle "Enfoque rápido
(enlaces simbólicos directos)" as fast {
card ".zshenv export PATH=...current/bin\n0 ms\nSin red\nSin inicialización" as s2 #E8F8E8
}
slow --> fast : Même résultat final\nLe PATH pointe sur current/bin\ndans les deux cas
@enduml
El caso NVM: cuando el gestor de versiones olvida dejar una huella
El problema NVM
Este es el lugar donde la investigación me ha llevado más lejos. SDKMAN tiene un diseño elegante: cuando se hace`sdk install java 25.0.2-tem`, crea un enlace simbólico:
~/.sdkman/candidates/java/current -> ~/.sdkman/candidates/java/25.0.2-tem
Este enlace simbólico essiempre al día. Apúntalo encima en`.zshenv`y estás cubierto, sea cual sea el shell. Hermoso.
NVM, él, no creanadade tel. No hay enlace simbólico`current`. No hay un punto de anclaje estable. Debemos apuntar a un camino versionado de forma fija, que se parece a esto:
~/.nvm/versions/node/v22.19.0/bin/node
Al próximo`nvm install 24`, este camino está muerto. Su`.zshenv`apuntará a una versión que ya no es la versión activa. Es una bomba de relojería.
|
¿Por qué NVM hace eso?NVM funciona modificando dinámicamente el PATH en cada`nvm use`. Esto está diseñado para desarrolladores que cambian de versión a menudo, en un shell interactivo. El symlink`current`no estaba en las especificaciones iniciales — es un olvido de diseño que vamos a corregir nosotros mismos. |
La solución: crear el symlink current para NVM
Como NVM no lo hace, nosotros lo hacemos nosotros mismos. El principio es el mismo que SDKMAN — un enlace simbólico`current`siempre apunta a la versión activa. Lo creamos una vez y luego lo automatizamos para que se actualice solo.
# Créer le symlink initial
ln -sfn "$HOME/.nvm/versions/node/v22.19.0" "$HOME/.nvm/current"
Ahora, en`.zshenv`, se utiliza :
$HOME/.nvm/current/bin
En lugar de :
# ❌ Chemin en dur — cassé au prochain changement de version
$HOME/.nvm/versions/node/v22.19.0/bin
Automatizar la actualización del enlace simbólico
Un enlace simbólico no vale nada si no se actualiza. El enlace simbólico`current`debe actualizarse cuando se hace`nvm use` ou nvm install. La solución: unenvoltorio— una función que envuelve el verdadero comando`nvm`y actualiza el enlace simbólico después de cada llamada.
# À 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'
¿Cómo funciona, en detalle :
Failed to generate image: PlantUML preprocessing failed: [From <input> (line 5) ] @startuml skinparam backgroundColor #FEFEFE actor Développeur participant "nvm wrapper ^^^^^ Syntax Error? (Assumed diagram type: sequence) @startuml skinparam backgroundColor #FEFEFE actor Développeur participant "nvm wrapper (_nvm)" as wrapper participant "nvm real (command 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
El wrapper`_nvm`llama al verdadero comando`nvm`, luego actualiza el enlace simbólico. El alias`nvm='_nvm'`hace que al teclear`nvm`, se pasa por el wrapper. Y al iniciar el shell, hacemos lo mismo después`nvm use --lts`.
|
`nvm_version_path`es una función interna de NVM que resuelve el camino completo de una versión. Evita tener que reconstruir el camino manualmente. |
Tabla resumen de los caminos
Antes de pasar a las trampas, un resumen visual de la transformación. A la izquierda, lo que tenías (todo en`.zshrc`, invisible para el agente). A la derecha, lo que tienes ahora (los caminos esenciales en`.zshenv`, visibles por todas partes).
| Herramienta | Antes (.zshrc solo) | Después (.zshenv + symlink) | Visible por OpenCode? |
|---|---|---|---|
|
PATH no exportado en .zshrc |
`$HOME/apps`en .zshenv |
�✅ |
SDKMAN Java |
`sdkman-init.sh`sourced en .zshrc |
`$HOME/.sdkman/candidates/java/current/bin`en .zshenv |
�✅ |
SDKMAN Gradle |
`sdkman-init.sh`cargado en .zshrc |
`$HOME/.sdkman/candidates/gradle/current/bin`en .zshenv |
(blank) |
NVM Node |
`nvm use --lts`en .zshrc |
`$HOME/.nvm/current/bin`en .zshenv (symlink) |
✅ |
pnpm |
`$PNPM_HOME`en .zshrc |
`$PNPM_HOME`en .zshenv |
(No output, as no French source text was provided for translation beyond the instructions themselves, which are not to be translated.) |
JetBrains Toolbox |
Añadido automáticamente por Toolbox en .zshrc |
`$HOME/.local/share/JetBrains/Toolbox/scripts`en .zshenv |
✅ |
Python 3 |
`/usr/bin/python3`en PATH .zshrc |
Explícitamente en .zshenv |
✅ |
Trampas y mitigaciones
No todo es perfecto con este enfoque. Aquí están los problemas que he encontrado, y cómo sortearlos.
| trampa | Descripción | Mitigación |
|---|---|---|
PATH duplicado |
Si .zshenv y .zshrc añaden el mismo camino, aparece dos veces |
`.zshenv`está obtenidoantes.zshrc. Los dos se leen en un shell interactivo. El PATH puede duplicarse. Es cosmético, no funcional. Para evitarlo: solo pon en .zshenv los caminos ausentes del PATH por defecto. |
NVM : versión hardcodeada en .zshenv |
Poner`~/.nvm/versions/node/v22.19.0/bin`en duro se vuelve falso al próximo`nvm install` |
Utilizar el symlink`$HOME/.nvm/current/bin`+ el wrapper`_nvm`en .zshrc |
SDKMAN: source sdkman-init.sh en .zshenv |
Lento, frágil, inútil en un shell no interactivo |
Utilizar los symlinks`candidates/<tool>/current/bin`directamente |
Alias faltante después de la modificación |
El wrapper`nvm='_nvm'`en .zshrc solo tiene efecto después de recargar |
Reiniciar el shell o`source ~/.zshrc` |
OpenCode no ve los cambios |
El agente ya ha lanzado sus sub-shells con el antiguo .zshenv |
Reiniciar OpenCode después de modificar .zshenv |
.zshenv demasiado cargado |
Poner funciones pesadas o inicializaciones interactivas en .zshenv |
.zshenv = variables de entorno + solo PATH. No`source`, sin funciones pesadas, sin prompts. |
Lecciones aprendidas
-
.zshrc` es interactivo,
.zshenves universal— Si una variable debe existir en todos los shells (agentes IA, cron, scripts, IDE), va a ir en`.zshenv`. El salón es cómodo, pero el vestíbulo es el único lugar donde todo el mundo pasa. -
Los gestores de versiones no son iguales— SDKMAN crea enlaces simbólicos`current`Por diseño. NVM no. Hay que cubrir esta falta manualmente. Es una lección importante: antes de configurar su PATH, verifique si su gestor ofrece un punto de anclaje estable.
-
No sourcer los init scripts en .zshenv—
sdkman-init.shet `nvm.sh`están diseñados para un shell interactivo. Son lentos y frágiles en un contexto no interactivo. Los symlinks`current`son suficientes y son instantáneos. -
Siempre probar en un shell no interactivo—`zsh -c 'echo $PATH'`Simula exactamente lo que ve un agente. Es la prueba de validación. Sin esta prueba, no sabes si tu configuración funciona para los agentes.
-
El patrón wrapper es reutilizable— El mismo patrón`_nvm`+`alias nvm='_nvm'`se aplica a cualquier herramienta que modifica el PATH de forma dinámica sin dejar una traza estable. Es una herramienta más en tu caja de ideas.
Failed to generate image: PlantUML preprocessing failed: [From <input> (line 6) ]
@startuml
skinparam backgroundColor #FEFEFE
rectangle "antes" as avant {
card "Shell interactivo : ✅
Shell no interactivo : ❌
^^^^^
Syntax Error? (Assumed diagram type: activity)
@startuml
skinparam backgroundColor #FEFEFE
rectangle "antes" as avant {
card "Shell interactivo : ✅
Shell no interactivo : ❌
OpenCode : ❌
Cron : ❌
Scripts : ❌" as av1 #FDEDEC
}
rectangle "DESPUÉS" as apres {
card "Shell interactivo : ✅\nShell no interactivo : ✅\nOpenCode : ✅\nCron : ✅\nScripts : ✅" as ap1 #E8F8E8
}
avant --> apres : .zshenv +\nsymlink ~/.nvm/current
@enduml
Verificación final
Después de haber creado`.zshenv`y el symlink NVM, verifica que todo funcione — en ambos contextos :
# Shell interactif (votre terminal)
gh --version && java -version && node --version
# Shell non-interactif (simulation OpenCode)
zsh -c 'gh --version && java -version && node --version'
Ambos deben funcionar. Si es así, su agente de IA verá las mismas herramientas que usted. Si no, vuelva al diagnóstico paso a paso — probablemente haya olvidado una ruta o el enlace simbólico NVM no esté actualizado.
|
Automatiza este test.Agregue esta verificación en un script de healthcheck que usted ejecuta después de cada actualización de sus herramientas. Uno`zsh -c 'which java && which node && which gh'`en CI, es un seguro contra las sorpresas. |
_ Un shell no interactivo es como un invitado silencioso: solo lee lo que se muestra en la puerta de entrada. Si el PATH está en el salón, nunca lo verá. _
Enlaces
Documentación oficial
-
Documentación de Zsh : Archivos de inicio— La referencia oficial sobre los archivos de inicialización de Zsh. Aquí es donde se explica todo, incluso si se olvida a menudo.
-
OpenCode : configuración— Cómo configurar OpenCode y su entorno de ejecución
Herramientas mencionadas
-
NVM en GitHub— Node Version Manager. Administrador de versiones Node.js.
-
SDKMAN — sitio oficial— SDKMAN! El gestor de versiones para la JVM (Java, Kotlin, Gradle…)
-
SDKMAN : instalación— Guía de instalación de SDKMAN.
-
gh` — CLI de GitHub— La herramienta de línea de comandos de GitHub, indispensable para todo desarrollador.
-
Gradle — sitio oficial— El sistema de compilación que instalamos vía SDKMAN
-
pnpm — sitio oficial— El gestor de paquetes de Node.js rápido y que ahorra espacio.
-
JetBrains Toolbox— El gestor de IDE de JetBrains, que agrega sus scripts al PATH.
Para ir más lejos
-
Zsh dotfiles: una guía completa— Artículo detallado sobre la gestión de los dotfiles Zsh.
-
Zsh en ArchWiki— Una de las mejores documentaciones comunitarias sobre Zsh.
-
NVM problemas GitHub— Para ver las discusiones alrededor del symlink`current`y problemas de PATH.