OpenCode et le PATH incomplet : pourquoi vos outils n'étaient pas dans le shell de l'agent
Опубликовано 21 April 2026
- Сцена: слепой агент в мире инструментов
- Анатомия ошибки: две оболочки, два мира
- Виновные: .zshrc vs .zshenv
- Пошаговая диагностика
- Решение: .zshenv + правильные пути
- Случай NVM: когда менеджер версий забыл оставить след
- Сводная таблица путей
- Ловушки и меры по смягчению
- Извлеченные уроки
- Финальная проверка
- Ссылки
Вы запускаете OpenCode в своём терминале, всё работает. Агент пытается`gh`— не найдено. Один`java -version`— отсутствует. Один`node`— нигде. Тем не менее, эти инструменты действительно находятся в votre оболочке. Проблема? OpenCode запускает оболочкиnon-interactifsкоторые никогда не читают ваш`.zshrc`. Вот как правильно диагностировать и исправить это.
Сцена: слепой агент в мире инструментов
Это был вторник вечером. Я только что установилhttps://opencode.ai[OpenCode], ИИ-агент, который обещал изменить мой способ писать код. Первый тест: попросить его перечислить мои репозитории GitHub.
$ opencode
> Utilise gh pour lister mes repos
❌ bash: gh: command not found
Странно.`gh`работал идеально в моём терминале. Пробую что-то другое :
> Vérifie la version de Java
❌ bash: java: command not found
Затем :
> Lance le build Gradle
❌ bash: gradle: command not found
Java, Gradle, Node,gh— всё было невидимо для агента. Мой терминал же видел всё. Как будто агент и я жили в двух параллельных мирах.
Я потратил часы, пытаясь понять. Часы`echo $PATH`, de which java, растущей фрустрации. Я в конце концов понял, что проблема не в инструментах — это былоболочка. OpenCode, как и любой агент, запускающий подпроцессы, работает в оболочкахнеинтерактивные. И Zsh, в этих оболочках, просто игнорирует ваш`.zshrc`.
Вот полный рассказ о том, как я поставил диагноз, как решил проблему и какие уроки извлёк. Если вы используете ИИ‑агент — OpenCode, Aider, Cursor, или даже скрипты cron — эта проблема когда‑нибудь коснётся вас.
Анатомия ошибки: две оболочки, два мира
Когда вы открываете терминал, Zsh обрабатывает его как оболочкуинтерактивный. Он заряжает`.zshrc`, который инициализирует всё: SDKMAN, NVM, pnpm, псевдонимы, ваш красивый промпт. Ваше окружение полностью готово.
Но когда OpenCode выполняет команду, он не запускает интерактивный терминал. Он запускает оболочкунеинтерактивный— оболочка задачи, без человека за экраном. И Zsh, в этом контексте, прыгает`.zshrc`. Он читает только`.zshenv`.
Почему существует это различие? Потому что неинтерактивный оболочка предназначена для выполнения скриптов, а не для обслуживания человека. Загрузка псевдонимов, приглашения и автодополнения в скрипт, работающий в фоновом режиме, была бы тратой ресурсов. Проблема заключается в том, что ваш PATH — наиболее важная информация для нахождения исполняемых файлов — часто инициализируется в`.zshrc`, не в`.zshenv`.
@startuml skinparam backgroundColor #FEFEFE actor "Вы" as user actor "OpenCode\n(агент)" as agent participant "Интерактивный\nShell\n(.zshrc загружен)" as ishell participant "Неинтерактивный\nShell\n(.zshrc игнорируется)" as nshell user -> ishell : gh, java, node ✅ agent -> nshell : gh, java, node ❌ note right of ishell Исходный .zshrc : - инициализация SDKMAN - инициализация NVM - ~/apps в PATH - pnpm в PATH end note note right of nshell .zshrc игнорируется ! PATH = /usr/bin:/bin + несколько каталогов по умолчанию end note @enduml
Корень проблемы:Zsh подгружает .zshrc только для интерактивных оболочек. OpenCode, как любой ИИ-агент, запускающий подпроцессы, использует неинтерактивные оболочки. Эти оболочки читают только`.zshenv`.
Виновные: .zshrc vs .zshenv
Zsh имеетчетыре файла инициализации, каждый с конкретной ролью. Это элегантный дизайн — но это также узел проблемы :
| Файл | получено когда | Роль | Изменить PATH? |
|---|---|---|---|
|
Всегда(интерактивный + неинтерактивный + login) |
Основные переменные среды, PATH |
✅ Да — это SA место |
|
Только входные оболочки |
Команды, работающие медленно (один раз за сеанс) |
возможный |
|
Только интерактивные оболочки |
Alias, prompt, автодополнение, интерактивные инструменты |
❌ Не для критичных переменных |
|
Оболочки входа в систему (после .zshrc) |
Приветственные сообщения, завершение |
Редко |
Таблица даёт подсказку, но её нужно понять глубоко. Подумайте об этом как о комнатах дома:
-
`.zshenv`являетсяприхожая— каждый проходит мимо, будь то посетитель или резидент. Если вы положите что-то здесь, любой shell сможет это увидеть.
-
`.zshrc`являетсясалон— только резиденты (интерактивные оболочки) заходят туда. Посетители (неинтерактивные оболочки) остаются в вестибюле.
-
.zprofileet `.zlogin`являются специализированными компонентами для оболочек входа (например, когда вы подключаетесь через SSH).
Трагедия в том, что большинство из нас помещают PATH в гостиную. И у ИИ-агента, его, никогда нет права войти туда.
Проблема в виде диаграммы :
@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
Когда вы открываете терминал, Zsh является интерактивным: он читает`.zshrc`, всё работает. Когда OpenCode запускает оболочку для выполнения команды, Zsh является неинтерактивным: он пропускает`.zshrc`, только читает`.zshenv`. Et si `.zshenv`не существует или не содержит PATH — это пустыня.
|
Почему Zsh делает это?Это наследуемый от Unix выбор дизайна. Неинтерактивная оболочка должна бытьбыстро et воспроизводимый. Загрузить псевдонимы, цветные подсказки и тяжелые инициализации SDKMAN в скрипт cron или в агент ИИ было бы медленно и хрупко. Поэтому разделение логично :`.zshenv`по сути (переменные, PATH),`.zshrc`для комфорта (aliases, prompt, мы дополняем). Проблема возникает, когда мы помещаем суть в комфорт. |
Пошаговая диагностика
Прежде чем исправлять, нужно понять точно, чего не хватает. Вот воспроизводимый метод диагностики — сохраняйте его в закладках, если работаете с ИИ‑агентами.
1. Проверить PATH агента
Сравнивают то, что видит ваш интерактивный терминал, с тем, что видит неинтерактивный шелл — то есть то, что видит OpenCode.
# Dans votre terminal interactif (tout fonctionne)
echo $PATH | tr ':' '\n' | grep -v '^/usr' | sort
Типичный результат :
/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
Теперь смоделируем то, что видит агент — неинтерактивный шелл:
# Shell non-interactif : pas de .zshrc
zsh -c 'echo $PATH' | tr ':' '\n' | grep -v '^/usr' | sort
Результат :
/home/cheroliv/.local/bin
Пять дорог из шести исчезли.Агент лишён 83% своей среды. Это как если бы вам сказали готовить без половины ваших принадлежностей — вы могли бы вскипятить воду, но не могли бы сделать ничего другого.
|
Команда`zsh -c 'echo $PATH'`вашинструмент диагностики №1. Если инструмент, которым вы ежедневно пользуетесь, отсутствует в результате, ваш ИИ-агент также не сможет его увидеть. Протестируйте его до и после каждого изменения`.zshenv`. |
2. Идентифицировать то, что находится в .zshrc, но не в .zshenv
Теперь мы ищем виновного в`.zshrc`. Мы фильтруем строки, упоминающие наши инструменты :
grep -n 'apps\|SDKMAN\|NVM\|PNPM\|PATH' ~/.zshrc
Обычно встречается:
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"
Все этоневидимыйpour OpenCode. И`PATH`строки 119 даже не`export`é — Он никогда не покидает текущей оболочки. Это важная техническая деталь : переменная без`export`остаётся локальной для оболочки, которая её определяет. Подоболочки — такие, как те, которые запущены OpenCode — никогда её не наследуют.
3. Проверить, существует ли .zshenv
cat ~/.zshenv 2>/dev/null || echo "FICHIER ABSENT"
Если ответ « ФАЙЛ ОТСУТСТВУЕТ », то именно здесь решается всё.
Решение: .zshenv + правильные пути
Стратегия простая, но требует точности: поместить в`.zshenv` толькоосновные пути, не подгружая тяжелые скрипты инициализации. Мы не перемещаем`.zshrc`в`.zshenv`— извлекаютсьть.
Создать .zshenv со всеми основными путями
`.zshenv`являетсяединственный файлчто Zsh гарантирует source ввсеВ контекстах. Вот куда должен идти 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"
|
Используют`$HOME/.nvm/current/bin`et non`$HOME/.nvm/versions/node/v22.19.0/bin`. Причина: версии Node меняются. Жёстко прописанный путь становится неправильным в следующем`nvm install`. Подробнее в следующем разделе. |
Почему бы не source sdkman-init.sh в .zshenv?
Самый заманчивый подход — просто воспроизвести в`.zshenv`что мы делаем в`.zshrc`— выполнить скрипты инициализации. Onмог быбыть искушённым сделать :
# ❌ MAUVAISE IDÉE
[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"
Проблемы:
-
медленный : `sdkman-init.sh`делает сетевые разрешения и проверки при каждой оболочке. В неинтерактивной оболочке это ненужные расходы.
-
Хрупкий: SDKMAN ожидает интерактивного контекста. Его инициализация может завершиться неудачей без вывода в пайпе или подшеле.
-
Бесполезно: SDKMAN помещает своих кандидатов в`~/.sdkman/candidates/<tool>/current/bin`— стабильные символические ссылки, указывающие на активную версию. Их можно использоватьпрямо.
Хороший подход: обойти инициализацию SDKMAN и указать напрямую на символические ссылки`current`. Это ключ ко всему решению :использовать структуру файлов как контракт, а не код инициализации.
@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
Случай NVM: когда менеджер версий забыл оставить след
Проблема NVM
Это именно здесь расследование привело меня дальше всего. SDKMAN имеет элегантный дизайн : когда ты делаешь`sdk install java 25.0.2-tem`, он создаёт символическую ссылку:
~/.sdkman/candidates/java/current -> ~/.sdkman/candidates/java/25.0.2-tem
Эта символическая ссылкавсегда актуально. Наведите на него в`.zshenv`И вы защищены, независимо от оболочки. Прекрасно.
NVM, он не создаётничегот. нет симлинка`current`. Нет стабильной точки привязки. Нам нужно указать на жёстко прописанный версионированный путь, который выглядит так:
~/.nvm/versions/node/v22.19.0/bin/node
на следующем`nvm install 24`, этот путь мёртв. Ваш`.zshenv`указывает на версию, которая больше не является активной. Это бомба замедленного действия.
|
Почему NVM делает это?NVM работает, динамически изменяя PATH при каждом`nvm use`. Это предназначено для разработчиков, которые часто меняют версии, в интерактивной оболочке. Символическая ссылка`current`Он не входил в исходные спецификации — это упущение в дизайне, которое мы исправим сами. |
@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 ❌ (до исправления)" 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
Нет симлинка current!
.zshenv должен указывать напрямую
Сломается при следующем nvm install
end note
}
package "NVM ✅ (после исправления)" 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
Решение: создать симлинк current для NVM
Поскольку NVM не делает этого, мы делаем это сами. Принцип такой же, как у SDKMAN — символическая ссылка`current`который всегда указывает на активную версию. Мы создаем его один раз, затем автоматизируем его, чтобы он обновлялся сам.
# Créer le symlink initial
ln -sfn "$HOME/.nvm/versions/node/v22.19.0" "$HOME/.nvm/current"
Теперь, в`.zshenv`, используют :
$HOME/.nvm/current/bin
Вместо того чтобы:
# ❌ Chemin en dur — cassé au prochain changement de version
$HOME/.nvm/versions/node/v22.19.0/bin
Автоматизировать обновление символической ссылки
Символическая ссылка ничего не стоит, если она не обновляется. Символическая ссылка`current`должен обновляться, когда мы делаем`nvm use` ou nvm install. Решение: одинобёртка— функция, которая оборачивает настоящую команду`nvm`et обновляет символическую ссылку после каждого вызова.
# À 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'
Как это работает, подробно:
@startuml skinparam backgroundColor #FEFEFE actor Développeur participant "обертка nvm\n(_nvm)" as wrapper participant "реальный nvm\n(команда nvm)" as realnvm participant "~/.nvm/current\n(символическая ссылка)" 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 Всегда актуально! Используется в `.zshenv` → доступно через OpenCode end note @enduml
обёртка`_nvm`вызывает настоящую команду`nvm`, затем обновляет символическую ссылку. Псевдоним`nvm='_nvm'`делает так, что при вводе`nvm`, мы проходим через обёртку. И при инициализации оболочки, мы делаем то же самое после`nvm use --lts`.
|
`nvm_version_path`Это внутренняя функция NVM, которая разрешает полный путь к версии. Это избавляет от необходимости вручную перестраивать путь. |
Сводная таблица путей
Прежде чем перейти к ловушкам, визуальное резюме преобразования. Слева, что у вас было (всё внутри`.zshrc`, невидимый для агента). Справа, что у вас сейчас (основные пути в`.zshenv`, видны везде).
| Инструмент | До (.zshrc только) | После (.zshenv + symlink) | Видно OpenCode? |
|---|---|---|---|
|
PATH не экспортирован в .zshrc |
`$HOME/apps`в .zshenv |
✅ |
SDKMAN Java |
`sdkman-init.sh`взято из .zshrc |
`$HOME/.sdkman/candidates/java/current/bin`в .zshenv |
✅ |
SDKMAN Gradle |
`sdkman-init.sh`выполнен в .zshrc |
`$HOME/.sdkman/candidates/gradle/current/bin`в .zshenv |
✅ |
NVM Node |
`nvm use --lts`в .zshrc |
`$HOME/.nvm/current/bin`в .zshenv (символическая ссылка) |
✅ |
pnpm |
`$PNPM_HOME`в .zshrc |
`$PNPM_HOME`в .zshenv |
✅ |
JetBrains Toolbox |
Автоматически добавлено Toolbox в .zshrc |
`$HOME/.local/share/JetBrains/Toolbox/scripts`в .zshenv |
✅ |
Python 3 |
`/usr/bin/python3`в PATH .zshrc |
Явно в .zshenv |
✅ |
Ловушки и меры по смягчению
Не всё идеально с этим подходом. Вот проблемы, с которыми я столкнулся, и как их обойти.
| Ловушка | Описание | Смягчение |
|---|---|---|
Дублированный PATH |
Если .zshenv и .zshrc добавляют один и тот же путь, он появляется дважды |
`.zshenv`снабжён источникомперед.zshrc. Оба читаются в интерактивной оболочке. PATH может дублироваться. Это косметично, не функционально. Чтобы избежать этого: помещать в .zshenv только те пути отсутствующие в PATH по умолчанию. |
NVM : жёстко закодированная версия в .zshenv |
Положить`~/.nvm/versions/node/v22.19.0/bin`из твёрдого материала становится ложно в следующем`nvm install` |
Использовать симлинк`$HOME/.nvm/current/bin`+ обёртка`_nvm`в .zshrc |
SDKMAN: source sdkman-init.sh в .zshenv |
Медленный, хрупкий, бесполезный в неинтерактивной оболочке |
Использовать symlinks`candidates/<tool>/current/bin`непосредственно |
Отсутствует псевдоним после изменения |
обёртка`nvm='_nvm'`в .zshrc вступает в силу только после перезагрузки |
Перезапустить оболочку или`source ~/.zshrc` |
OpenCode не видит изменений |
Агент уже запустил свои под-shells со старым .zshenv |
Перезапустить OpenCode после изменения .zshenv |
.zshenv слишком загружен |
Размещать тяжелые функции или интерактивные инициализации в .zshenv |
.zshenv = переменные среды + PATH только. Нет`source`, нет тяжёлых функций, нет подсказок. |
Извлеченные уроки
-
.zshrc` интерактивен,
.zshenvуниверсален— Если переменная должна существовать во всех оболочках (ИИ-агенты, cron, скрипты, IDE), она помещается в`.zshenv`. Гостиная удобна, но прихожая — единственное место, где проходит каждый. -
Системы управления версиями не одинаковы— SDKMAN создаёт символические ссылки`current`По дизайну. NVM нет. Нужно заполнить этот пробел вручную. Это важный урок: прежде чем настраивать ваш PATH, проверьте, предоставляет ли ваш менеджер стабильную точку привязки.
-
Не следует выполнять source скриптов инициализации в .zshenv—
sdkman-init.shet `nvm.sh`предназначены для интерактивной оболочки. Они медленные и хрупкие в неинтерактивном контексте. Символические ссылки`current`достаточно и мгновенны. -
Всегда тестировать в неинтерактивной оболочке—`zsh -c 'echo $PATH'`точно симулирует то, что видит агент. Это тест проверки. Без этого теста вы не знаете, работает ли ваша конфигурация для агентов.
-
Шаблон обёртки повторно используется— Тот же шаблон`_nvm` + `alias nvm='_nvm'`применяется к любому инструменту, который динамически изменяет PATH без оставления стабильного следа. Это ещё один инструмент в вашем ящике с идеями.
@startuml
skinparam backgroundColor #FEFEFE
rectangle "ДО" as avant {
card "Интерактивная оболочка: ✅\nНеинтерактивная оболочка: ❌\nOpenCode: ❌\nCron: ❌\nСкрипты: ❌" as av1 #FDEDEC
}
rectangle "ПОСЛЕ" as apres {
card "Интерактивная оболочка: ✅\nНеинтерактивная оболочка: ✅\nOpenCode: ✅\nCron: ✅\nСкрипты: ✅" as ap1 #E8F8E8
}
avant --> apres : .zshenv +\nsymlink ~/.nvm/current
@enduml
Финальная проверка
После создания`.zshenv`и символическая ссылка NVM, проверьте, что всё работает—в обоих контекстах:
# Shell interactif (votre terminal)
gh --version && java -version && node --version
# Shell non-interactif (simulation OpenCode)
zsh -c 'gh --version && java -version && node --version'
Оба должны успешно пройти. Если да, ваш ИИ-агент увидит те же инструменты, что и у вас. Если нет, вернитесь к пошаговой диагностике — вы, вероятно, забыли путь или символическая ссылка NVM не актуальна.
|
Автоматизируйте этот тест.Добавьте эту проверку в скрипт healthcheck, который вы запускаете после каждого обновления ваших инструментов. Один`zsh -c 'which java && which node && which gh'`в CI, это защита от неожиданностей. |
</think> Неинтерактивный оболочка подобен молчаливу гостю: он читает лишь то, что отображено на входной двери. Если переменная PATH находится в гостиной, он её никогда не увидит. __
Ссылки
Официальная документация
-
Документация Zsh : файлы запуска— Официальный справочник по файлам инициализации Zsh. Вот где всё объяснено, даже если это часто забывают.
-
OpenCode: конфигурация— Как настроить OpenCode и его среду выполнения.
Упомянутые инструменты
-
NVM на GitHub— Node Version Manager. Менеджер версий Node.js.
-
SDKMAN — официальный сайт— SDKMAN! Менеджер версий для JVM (Java, Kotlin, Gradle…)
-
SDKMAN : установка— Руководство по установке SDKMAN.
-
gh` — GitHub CLI— Инструмент командной строки GitHub, незаменимый для любого разработчика.
-
Gradle — официальный сайт— Система сборки, которую мы устанавливаем через SDKMAN.
-
pnpm — официальный сайт— Быстрый и экономичный по месту менеджер пакетов Node.js.
-
JetBrains Toolbox— Менеджер IDE JetBrains, который добавляет свои скрипты в PATH.
Для более глубокого изучения
-
Zsh dotfiles : полное руководство— Подробная статья о управлении dotfiles Zsh.
-
Zsh на ArchWiki— Одна из лучших документаций сообщества по Zsh.
-
Проблемы NVM на GitHub— Чтобы увидеть обсуждения вокруг symlink`current`et проблемы с PATH.