OpenCode e o PATH incompleto : por que suas ferramentas não estavam no shell do agente
Publié le 21 April 2026
- A cena: um agente cego num mundo de ferramentas
- Anatomia do bug: dois shells, dois mundos
- Os culpados: .zshrc vs .zshenv
- Diagnóstico passo a passo
- A solução: .zshenv + os caminhos corretos
- O caso NVM: quando o gerenciador de versões esquece de deixar uma marca
- Tabela resumo dos caminhos
- Armadilhas e mitigações
- Lições aprendidas
- Verificação final
- Links
Você inicia o OpenCode no seu terminal, tudo funciona. O agente tenta um`gh`— não encontrado. Um`java -version`— ausente. Um`node`— em nenhum lugar. Porém, essas ferramentas estão realmente aqui no votre shell. O problema? OpenCode lança shells.não interativosque nunca leem seu`.zshrc`. Como diagnosticar e corrigir isso corretamente.
- toc
-
[]
A cena: um agente cego num mundo de ferramentas
Era uma terça-feira à noite. Eu acabara de instalarhttps://opencode.ai[OpenCode], o agente IA que prometia transformar a minha forma de codar. Primeiro teste: pedir a ele para listar os meus repositórios do GitHub.
$ opencode
> Utilise gh pour lister mes repos
❌ bash: gh: command not found
Estranho.`gh`funcionava perfeitamente no meu terminal. Estou tentando outra coisa:
> Vérifie la version de Java
❌ bash: java: command not found
Então:
> Lance le build Gradle
❌ bash: gradle: command not found
Java, Gradle, Node,gh— tudo era invisível para o agente. Meu terminal, porém, via tudo. Como se o agente e eu vivêssemos em dois mundos paralelos.
Levei horas para entender. Horas de`echo $PATH`, de which java, de crescente frustração. Finalmente percebi que o problema não eram as ferramentas — era oshell. OpenCode, assim como todo agente que inicia subprocessos, trabalha em shellsnão interativos. E o Zsh, nesses shells, ignora simplesmente seu`.zshrc`.
O que se segue é o relato completo deste diagnóstico, da resolução e da lição que tirei. Se você utiliza um agente de IA — OpenCode, Aider, Cursor ou mesmo scripts cron — esse problema o afetará um dia.
Anatomia do bug: dois shells, dois mundos
Quando você abre um terminal, o Zsh o trata como um shellinterativo. Ele carrega`.zshrc`, que inicializa tudo: SDKMAN, NVM, pnpm, os aliases, o seu prompt bonito. O seu ambiente está completo.
Mas quando o OpenCode executa um comando, ele não inicia um terminal interativo. Ele inicia um shellnão interativo— um shell de tarefa, sem humano atrás da tela. E Zsh, neste contexto, salta`.zshrc`. Ele lê apenas`.zshenv`.
Por que essa distinção existe? Porque um shell não-interativo foi projetado para executar scripts, não para atender a um usuário humano. Carregar os aliases, o prompt e as completions em um script que roda em segundo plano seria um desperdício. O problema é que o seu PATH — a informação mais crítica para localizar os executáveis — costuma ser inicializado em`.zshrc`, não em`.zshenv`.
Failed to generate image: PlantUML preprocessing failed: [From <input> (line 5) ] @startuml skinparam backgroundColor #FEFEFE actor "Você" as user actor "OpenCode ^^^^^ Syntax Error? (Assumed diagram type: sequence) @startuml skinparam backgroundColor #FEFEFE actor "Você" as user actor "OpenCode (agente)" as agent participant "Shell interativo (.zshrc carregado)" as ishell participant "Shell não interativo (.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
A raiz do problema:Zsh apenas carrega .zshrc para shells interativos. OpenCode, como todo agente de IA que inicia subprocessos, utiliza shells não-interativos. Esses shells leem apenas`.zshenv`.
Os culpados: .zshrc vs .zshenv
Zsh dispõe dequatro arquivos de inicialização, cada um com um papel preciso. É um design elegante — mas isso também é o nó do problema:
arquivo |
Obtido quando |
papel |
Altera o PATH ? |
|
sempre(interativo + não interativo + login) |
Variáveis de ambiente essenciais, PATH |
✅ Sim — é o lugar dele/ela |
|
Apenas shells de login |
Comandos lentos (uma vez por sessão) |
Possível |
|
Shells interativos apenas |
Alias, prompt, completão, ferramentas interativas |
❌ Não para as variáveis críticas |
|
Shells de login (depois .zshrc) |
Mensagens de boas-vindas, finalização |
raramente |
A tabela dá uma indicação, mas é preciso compreendê-la em profundidade. Pense nisso como os cómodos de uma casa:
-
`.zshenv`é ohall de entrada— todos passam por lá, visitante ou residente. Se você colocar algo aqui, qualquer shell poderá vê-lo.
-
`.zshrc`é osala— apenas os residentes (shells interativos) entram ali. Os visitantes (shells não interativos) permanecem no hall.
-
.zprofileet `.zlogin`são peças especializadas para shells de login (como quando você se conecta via SSH).
O drama é que a maioria de nós colocamos o PATH na sala. E o agente de IA, ele, nunca tem o direito de entrar lá.
O problema em um diagrama:
Quando você abre um terminal, o Zsh é interativo: ele lê`.zshrc`, tudo funciona. Quando o OpenCode lança um shell para executar um comando, o Zsh é não-interativo: ele pula`.zshrc`, só lê`.zshenv`. Et si `.zshenv`não existe ou não contém o PATH — é o deserto.
|
Por que o Zsh faz isso?É uma escolha de design herdada do Unix. Um shell não interativo deve serrápido et reprodutível. Carregar os aliases, os prompts coloridos e as inicializações pesadas do SDKMAN em um script de cron ou um agente de IA seria lento e frágil. A separação, portanto, é lógica :`.zshenv`para o essencial (variáveis, PATH)`.zshrc`para o conforto (aliases, prompt, completamos). O problema ocorre quando colocamos o essencial no conforto. |
Diagnóstico passo a passo
Antes de corrigir, é preciso entender exatamente o que falta. Este é um método de diagnóstico reprodutível — guarde-a nos favoritos se você trabalha com agentes de IA.
Verificar o PATH do agente
Comparamos o que o seu terminal interativo vê com o que um shell não interativo vê — ou seja, o que o OpenCode vê.
# 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
Agora, vamos simular o que o agente vê — um shell não-interativo :
# Shell non-interactif : pas de .zshrc
zsh -c 'echo $PATH' | tr ':' '\n' | grep -v '^/usr' | sort
Resultado :
/home/cheroliv/.local/bin
Cinco caminhos de seis desapareceram.O agente está amputado de 83% do seu ambiente. É como se você fosse solicitado a cozinhar sem metade dos seus utensílios — você poderia ferver água, mas pouco mais.
|
Encomenda`zsh -c 'echo $PATH'`é seuferramenta de diagnóstico número 1. Se uma ferramenta que você utiliza diariamente estiver ausente do resultado, seu agente de IA também não poderá vê-la. Teste-a antes e depois de cada modificação de`.zshenv`. |
2. Identifique o que está em .zshrc mas não está em `.zshenv
Agora, estamos procurando o culpado em`.zshrc`. Filtrar as linhas que mencionam nossas ferramentas :
grep -n 'apps\|SDKMAN\|NVM\|PNPM\|PATH' ~/.zshrc
Costuma-se encontrar :
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"
Tudo isso éinvisívelpara OpenCode. E o`PATH`da linha 119 nem mesmo é`export`é — ele nunca sai do shell atual. É um detalhe técnico crucial: uma variável sem`export`Permanece local ao shell que a define. Os sub-shells — como aqueles lançados pelo OpenCode — nunca a herdam.
3. Verificar se o .zshenv existe
cat ~/.zshenv 2>/dev/null || echo "FICHIER ABSENT"
Se a resposta é « ARQUIVO AUSENTE », é aí que tudo se decide.
A solução: .zshenv + os caminhos corretos
A estratégia é simples, mas exige precisão: colocar dentro`.zshenv` apenascaminhos essenciais, sem executar os scripts de inicialização pesados. não movemos`.zshrc`em`.zshenv`— extraímos o essencial.
Criar .zshenv com todos os caminhos essenciais
`.zshenv`é oarquivo únicoque Zsh garante de sourcear emtodosos contextos. É aqui que o PATH deve ir.
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"
|
Utiliza-se`$HOME/.nvm/current/bin`e não`$HOME/.nvm/versions/node/v22.19.0/bin`. A razão : as versões do Node mudam. Um caminho fixo se torna falso no próximo`nvm install`. Mais detalhes na seção seguinte. |
Por que não source sdkman-init.sh no .zshenv?
A abordagem mais tentadora seria simplesmente reproduzir em`.zshenv`o que fazemos em`.zshrc`— carregar os scripts de inicialização. Nóspoderiaestar tentado de fazer:
# ❌ MAUVAISE IDÉE
[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"
Problemas :
-
Lento : `sdkman-init.sh`faz resoluções de rede e verificações a cada shell. Em um shell não interativo, é um custo desnecessário.
-
frágil: SDKMAN espera um contexto interativo. Sua inicialização pode falhar silenciosamente em um pipe ou em um subshell.
-
inútil: SDKMAN coloca seus candidatos em`~/.sdkman/candidates/<tool>/current/bin`— links simbólicos estáveis apontando para a versão ativa. Pode‑se utilizá‑losdiretamente.
A abordagem correta: contornar a inicialização do SDKMAN e apontar diretamente para os symlinks`current`. É a chave de toda a solução :usar a estrutura de arquivos como contrato, em vez do código de inicialização.
Failed to generate image: PlantUML preprocessing failed: [From <input> (line 6) ]
@startuml
skinparam backgroundColor #FEFEFE
rectangle "Abordagem Lenta\n(source sdkman-init.sh)" as slow {
card ".zshenv source sdkman-init.sh
2-3 segundos por shell
^^^^^
Syntax Error? (Assumed diagram type: activity)
@startuml
skinparam backgroundColor #FEFEFE
rectangle "Abordagem Lenta\n(source sdkman-init.sh)" as slow {
card ".zshenv source sdkman-init.sh
2-3 segundos por shell
Verificações de rede
Risco de falha" as s1 #FDEDEC
}
rectangle "Abordagem rápida
(symlinks diretos)" as fast {
card ".zshenv export PATH=...current/bin
0 ms
Sem rede
Sem inicialização" as s2 #E8F8E8
}
slow --> fast : Même résultat final\nLe PATH pointe sur current/bin\ndans les deux cas
@enduml
O caso NVM: quando o gerenciador de versões esquece de deixar uma marca
O problema NVM
É aqui que a investigação me levou mais longe. SDKMAN tem um design elegante: quando se faz`sdk install java 25.0.2-tem`, ele cria um symlink :
~/.sdkman/candidates/java/current -> ~/.sdkman/candidates/java/25.0.2-tem
Este link simbólico ésempre atualizadoApontem-no em`.zshenv`e você está coberto, não importa qual seja o shell. Bonito.
NVM, ele, não crianadade tal. Sem symlink`current`. Não há ponto de ancoragem estável. É necessário apontar para um caminho versionado embutido, que se parece com isso :
~/.nvm/versions/node/v22.19.0/bin/node
No próximo`nvm install 24`, este caminho está morto. Seu`.zshenv`apontará para uma versão que não é mais a versão ativa. É uma bomba-relógio.
|
Por que o NVM faz isso?NVM funciona modificando dinamicamente o PATH a cada`nvm use`. Foi projetado para desenvolvedores que mudam de versão frequentemente, em um shell interativo. O symlink`current`Não estava nas especificações iniciais — é um esquecimento de design que vamos corrigir nós mesmos. |
A solução: criar o symlink current para NVM
Como o NVM não faz isso, nós o fazemos nós mesmos. O princípio é o mesmo que o SDKMAN — um symlink`current`que sempre aponta para a versão ativa. Cria-se uma vez, então automatiza-se para que se atualize sozinho.
# Créer le symlink initial
ln -sfn "$HOME/.nvm/versions/node/v22.19.0" "$HOME/.nvm/current"
Agora, em`.zshenv`, utiliza :
$HOME/.nvm/current/bin
Em vez de :
# ❌ Chemin en dur — cassé au prochain changement de version
$HOME/.nvm/versions/node/v22.19.0/bin
Automatizar a atualização do symlink
Um symlink não vale nada se não se atualiza. O symlink`current`deve ser atualizado quando fazemos`nvm use` ou nvm install. A solução : umembrulho— uma função que envolve o comando verdadeiro`nvm`e atualiza o symlink após cada chamada
# À 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'
Como funciona, em detalhe:
Failed to generate image: PlantUML preprocessing failed: [From <input> (line 5) ] @startuml skinparam backgroundColor #FEFEFE actor Développeur participant "nvm envolvedor ^^^^^ Syntax Error? (Assumed diagram type: sequence) @startuml skinparam backgroundColor #FEFEFE actor Développeur participant "nvm envolvedor (_nvm)" as wrapper participant "nvm real\n(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
O wrapper`_nvm`chama o comando verdadeiro`nvm`, então atualiza o symlink. O alias`nvm='_nvm'`certifique-se de que ao digitar`nvm`, passamos pelo wrapper. E na inicialização do shell, fazemos o mesmo depois`nvm use --lts`.
|
`nvm_version_path`é uma função interna do NVM que resolve o caminho completo de uma versão. Isso evita de reconstruir o caminho manualmente. |
Tabela resumo dos caminhos
Antes de passar para as armadilhas, um resumo visual da transformação. À esquerda, o que você tinha (tudo em`.zshrc`, invisível para o agente). à direita, o que você tem agora (os caminhos essenciais em`.zshenv`, visíveis em todo lugar).
| ferramenta | Antes (.zshrc apenas) | Depois (.zshenv + symlink) | Visível pelo OpenCode? |
|---|---|---|---|
|
PATH não exportado no .zshrc |
`$HOME/apps`em .zshenv |
✅ |
SDKMAN Java |
`sdkman-init.sh`carregado no .zshrc |
`$HOME/.sdkman/candidates/java/current/bin`no .zshenv |
(No output, as there is no French text provided to translate) |
SDKMAN Gradle |
`sdkman-init.sh`carregado no .zshrc |
`$HOME/.sdkman/candidates/gradle/current/bin`em .zshenv |
�✅ |
NVM Node |
`nvm use --lts`no .zshrc |
`$HOME/.nvm/current/bin`em .zshenv (link simbólico) |
�✅ |
pnpm |
`$PNPM_HOME`no .zshrc |
`$PNPM_HOME`no .zshenv |
✅ |
JetBrains Toolbox |
Auto-adicionado pelo Toolbox em .zshrc |
`$HOME/.local/share/JetBrains/Toolbox/scripts`no .zshenv |
�✅ |
Python 3 |
`/usr/bin/python3`no PATH .zshrc |
Explícitamente em .zshenv |
�✅ |
Armadilhas e mitigações
Nem tudo é perfeito com essa abordagem. Aqui estão os problemas que encontrei, e como contorná-los.
| armadilha | descrição | mitigação |
|---|---|---|
Caminho duplicado |
Se .zshenv e .zshrc adicionam o mesmo caminho, aparece duas vezes |
`.zshenv`é fonteantes.zshrc. Os dois são lidos em um shell interativo. O PATH pode duplicar. É estético, não funcional. Para evitá‑lo: coloque apenas no .zshenv os caminhos ausentes do PATH padrão. |
NVM : versão fixa no .zshenv |
colocar`~/.nvm/versions/node/v22.19.0/bin`hard-coded torna-se falso no próximo`nvm install` |
Usar o symlink`$HOME/.nvm/current/bin`+ o wrapper`_nvm`no .zshrc |
SDKMAN: source sdkman-init.sh no .zshenv |
Lento, frágil, inútil em um shell não interativo |
Usar os symlinks`candidates/<tool>/current/bin`diretamente |
Alias ausente após modificação |
O wrapper`nvm='_nvm'`no .zshrc só tem efeito após recarregar |
Reiniciar o shell ou`source ~/.zshrc` |
OpenCode não vê as mudanças |
O agente já lançou seus subshells com o .zshenv antigo |
Reinicie o OpenCode após a modificação de .zshenv |
.zshenv demasiado carregado |
Colocar funções pesadas ou inicializações interativas no .zshenv |
.zshenv = variáveis de ambiente + PATH apenas. Não`source`, sem funções pesadas, sem prompts. |
Lições aprendidas
-
.zshrc` é interativo,
.zshenvé universal— Se uma variável deve existir em todos os shells (agentes IA, cron, scripts, IDE), ela vai para`.zshenv`. A sala é confortável, mas o hall de entrada é o único lugar onde todos passam. -
Os gerentes de versões não são iguais— SDKMAN cria symlinks`current`por design. NVM não. É preciso preencher essa lacuna manualmente. É uma lição importante: antes de configurar seu PATH, verifique se o gerenciador oferece um ponto de ancoragem estável.
-
Não source os init scripts em .zshenv—
sdkman-init.shet `nvm.sh`são projetados para um shell interativo. Eles são lentos e frágeis em um contexto não-interativo. Os symlinks`current`são suficientes e são instantâneos -
Sempre testar em shell não-interativo—`zsh -c 'echo $PATH'`simula exatamente o que um agente vê. Este é o teste de validação. Sem este teste, você não sabe se sua configuração funciona para os agentes.
-
O padrão wrapper é reutilizável— O mesmo padrão`_nvm`+`alias nvm='_nvm'`Aplica-se a qualquer ferramenta que modifique o PATH dinamicamente sem deixar um rastro estável. É uma ferramenta a mais na sua caixa de ideias.
Failed to generate image: PlantUML preprocessing failed: [From <input> (line 6) ]
@startuml
skinparam backgroundColor #FEFEFE
rectangle "ANTES" as avant {
card "Shell interativo : ✅
Shell não interativo : ❌
^^^^^
Syntax Error? (Assumed diagram type: activity)
@startuml
skinparam backgroundColor #FEFEFE
rectangle "ANTES" as avant {
card "Shell interativo : ✅
Shell não interativo : ❌
OpenCode : ❌
Cron : ❌
Scripts : ❌" as av1 #FDEDEC
}
rectangle "DEPOIS" as apres {
card "Shell interativo : ✅\nShell não interativo : ✅\nOpenCode : ✅\nCron : ✅\nScripts : ✅" as ap1 #E8F8E8
}
avant --> apres : .zshenv +\nsymlink ~/.nvm/current
@enduml
Verificação final
Depois de ter criado`.zshenv`e o symlink NVM, verifique que tudo funciona — nos dois 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 devem ter sucesso. Se sim, seu agente de IA verá as mesmas ferramentas que você. Se não, volte ao diagnóstico passo a passo — você provavelmente esqueceu um caminho ou o symlink NVM não está atualizado.
|
Automatize este teste.Adicione esta verificação em um script de healthcheck que você executa após cada atualização das suas ferramentas. Un`zsh -c 'which java && which node && which gh'`Em CI, é uma garantia contra as surpresas. |
</think> (No text provided to translate; output remains empty.) Um shell não-interativo é como um convidado silencioso: ele lê apenas o que está exibido na porta de entrada. Se o PATH estiver na sala de estar, ele nunca o verá. __
Links
Documentação oficial
-
Documentação do Zsh: Arquivos de inicialização— A referência oficial sobre os arquivos de inicialização do Zsh. É lá que tudo é explicado, mesmo que se esqueça frequentemente.
-
OpenCode : configuração— Como configurar OpenCode e seu ambiente de execução
Ferramentas mencionadas
-
NVM no GitHub— Node Version Manager. Gerenciador de versões Node.js.
-
SDKMAN — site oficial— SDKMAN! O gerenciador de versões para a JVM (Java, Kotlin, Gradle…)
-
SDKMAN : instalação— Guia de instalação do SDKMAN.
-
gh` — GitHub CLI— A ferramenta de linha de comando GitHub, essencial para todo desenvolvedor.
-
Gradle — site oficial— O sistema de build que instalamos via SDKMAN.
-
pnpm — site oficial— O gerenciador de pacotes Node.js rápido e econômico em espaço.
-
JetBrains Toolbox— O gerenciador de IDE JetBrains, que adiciona seus scripts ao PATH.
Para ir além
-
Dotfiles do Zsh : um guia completo— Artigo aprofundado sobre a gestão de dotfiles Zsh.
-
Zsh no ArchWiki— Uma das melhores documentações comunitárias sobre Zsh.
-
NVM problemas GitHub— Para ver as discussões ao redor do symlink`current`e problemas de PATH.