время чтения : 13 minutes

Вы запускаете 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?

.zshenv

Всегда(интерактивный + неинтерактивный + login)

Основные переменные среды, PATH

✅ Да — это SA место

.zprofile

Только входные оболочки

Команды, работающие медленно (один раз за сеанс)

возможный

.zshrc

Только интерактивные оболочки

Alias, prompt, автодополнение, интерактивные инструменты

❌ Не для критичных переменных

.zlogin

Оболочки входа в систему (после .zshrc)

Приветственные сообщения, завершение

Редко

Таблица даёт подсказку, но её нужно понять глубоко. Подумайте об этом как о комнатах дома:

  • `.zshenv`являетсяприхожая— каждый проходит мимо, будь то посетитель или резидент. Если вы положите что-то здесь, любой shell сможет это увидеть.

  • `.zshrc`являетсясалон— только резиденты (интерактивные оболочки) заходят туда. Посетители (неинтерактивные оболочки) остаются в вестибюле.

  • .zprofile et `.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?

~/apps(gh, vscode)

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`, нет тяжёлых функций, нет подсказок.

Извлеченные уроки

  1. .zshrc` интерактивен, .zshenv универсален— Если переменная должна существовать во всех оболочках (ИИ-агенты, cron, скрипты, IDE), она помещается в`.zshenv`. Гостиная удобна, но прихожая — единственное место, где проходит каждый.

  2. Системы управления версиями не одинаковы— SDKMAN создаёт символические ссылки`current`По дизайну. NVM нет. Нужно заполнить этот пробел вручную. Это важный урок: прежде чем настраивать ваш PATH, проверьте, предоставляет ли ваш менеджер стабильную точку привязки.

  3. Не следует выполнять source скриптов инициализации в .zshenv—sdkman-init.sh et `nvm.sh`предназначены для интерактивной оболочки. Они медленные и хрупкие в неинтерактивном контексте. Символические ссылки`current`достаточно и мгновенны.

  4. Всегда тестировать в неинтерактивной оболочке—`zsh -c 'echo $PATH'`точно симулирует то, что видит агент. Это тест проверки. Без этого теста вы не знаете, работает ли ваша конфигурация для агентов.

  5. Шаблон обёртки повторно используется— Тот же шаблон`_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 находится в гостиной, он её никогда не увидит. __

Ссылки

Официальная документация

Упомянутые инструменты

Для более глубокого изучения

Похожие статьи