lib.nvim · Foundation · vimdoc
:help lib.nvim-spawn_env
Subprocess spawn environment
doc/lib.nvim-spawn_env.txt — rendered from the plugin's own vimdoc
*lib.nvim-spawn_env.txt* *lib.nvim-spawn-env* Subprocess spawn environment Author: lib.nvim maintainers License: Same as Neovim
CONTENTS
1. Introduction ............................ |spawn_env-introduction| 2. Usage ................................... |spawn_env-usage| 3. API Reference ........................... |spawn_env-api| 4. Options ................................. |spawn_env-options| 5. Diagnosis ............................... |spawn_env-diagnosis| 6. Technical Notes ......................... |spawn_env-technical|
INTRODUCTION
lib.nvim.cross.run.envbuilds a deliberately completed environment table for subprocesses spawned from Neovim — a guaranteed-completePATHplus the session/keyring variables an authenticated CLI needs. A subprocess started withvim.system(),vim.uv.spawn()orjobstart()inherits Neovim's own process environment, not the environment an interactive login shell would have. That is standardfork+exec/CreateProcesssemantics on every OS, not a Neovim bug — but it bites in two independent ways:
PATH is short
Entries added by a shell hook (nvm,pyenv,rbenv,asdf,mise, Homebrew'sshellenv,~/.cargo/bin) only exist after.zshrc/.profileran. Start Neovim from a window-manager entry, a dock icon, a terminal that does not spawn a login shell, or a CI runner, and a spawned CLI is either missing entirely or resolves to the wrong version.
Session-bound auth is not an environment variable
gh/glabread their token from the OS keyring, and reaching the keyring depends on a variable the desktop session sets —DBUS_SESSION_BUS_ADDRESSfor gnome-keyring/kwallet via libsecret on Linux, the login-session binding on macOS, the user context on Windows. Without it the CLI reports "not logged in" while the token sits valid in the keyring.env -i <cmd>in a shell reproduces exactly this failure with no Neovim involved — a fast way to confirm the cause is the environment, not the spawn. Before this module, every plugin that cared hand-rolled its ownvim.tbl_extend("force", vim.fn.environ(), { ... }).lib.nvim.cross.run's shell-string runners (run/run_blocking, |lib.nvim-spawn-env-run-integration| below) already build on this module by default, so a plugin that only needs to run a shell command string gets the fix without calling anything here directly.
USAGE
local env = require("lib.nvim.cross.run.env")
-- or, via the aggregator: require("lib").spawn_env
-- The common case: hand a completed env to a spawn call
vim.system({ "gh", "api", "/user" }, { env = env.build() }, on_done)
-- Same, keeping other spawn options
vim.system({ "docker", "ps" }, env.apply({ cwd = root, text = true }), cb)
-- Only the PATH part
vim.env.PATH = env.path({ extra_paths = { "/opt/mytool/bin" }, mason = true })
-- Recover state Neovim's own process never received (POSIX, blocking)
local e = env.build({ login_shell = true })
*lib.nvim-spawn-env-run-integration*lib.nvim.cross.run'srun/run_blocking(shell-string runners) callenv.build()themselves before every spawn — no explicitenv.build()call needed for that path:
local run = require("lib.nvim.cross.run")
run.run_blocking("gh auth status") -- enriched by default
run.run_blocking("gh api /user", { env_opts = { login_shell = true } })
run.run_blocking("echo $PATH", { env = false }) -- opt out entirely
See:help lib.nvim.cross.run(lua/lib/nvim/cross/run/README.md) for the fullopts.env/opts.env_optscontract. Argv-based runners (cross.uv.spawn_capture/spawn_stream,cross.run_argv) are not wired to this by default — passenv.build()/env.apply()explicitly there.
API REFERENCE
env.build({opts}) *spawn_env.build()*
vim.fn.environ() (or opts.base), with PATH replaced by env.path(opts)
and every known session variable filled in wherever a value can be recovered.
Parameters:
{opts} Lib.Cross.Run.Env.Opts|nil - see |spawn_env-options|
Returns:
table<string,string> - environment table for a spawn call
Note:
PATHis written back under the key the base table already used — on Windows that isPath, and adding a secondPATHkey would hand the child two conflicting entries. env.path({opts}) *spawn_env.path()* APATHstring with every existing well-known binary directory guaranteed present. Nothing inherited is dropped or reordered; candidates are appended.
Precedence:
opts.extra_paths-> Masonbin(withmason = true) -> inheritedPATH-> login-shellPATH(withlogin_shell = true) ->candidate_dirs()
Returns:
string-;-separated on Windows,:-separated elsewhere; duplicates removed case- and separator-insensitively on Windows env.array({vars}) *spawn_env.array()* Build the completed environment as an array of"KEY=VALUE"strings, ready forlib.nvim.cross.uv.spawn_capture'sopts.env-- raw libuvuv.spawnwants an array wherebuild()returns a{ [key] = value }dict (the shapevim.system/jobstartwant).
Parameters:
{vars} table<string,string>|nil - explicit overrides, applied last
Returns:
string[]env.apply({spawn_opts}, {opts}) *spawn_env.apply()* A copy of {spawn_opts} withenvfilled in bybuild(), every other key untouched. Anenvalready present in {spawn_opts} is folded in as overrides, so caller-supplied variables survive. The input is not mutated.
Returns:
table- options table forvim.systemenv.candidate_dirs() *spawn_env.candidate_dirs()* The platform's well-known binary directories that actually exist (checked viauv.fs_stat), in priority order — version-manager shims first (they exist to shadow the system binary, and an interactive shell puts them in front too), then user bin dirs, then system ones. Cached for the session.
Returns:
string[]env.session_vars() *spawn_env.session_vars()* Known session/keyring/agent variable names for the current platform,SESSION_VARS.commonfirst.
Returns:
string[]env.SESSION_VARS *spawn_env.SESSION_VARS* The underlying table, keyed by the platform nameslib.nvim.cross.platform.is()reports:common,linux,wsl,macos,windows.wslaliaseslinux— a WSL userland needs the Linux session variables, not the Windows ones. env.missing() *spawn_env.missing()* Which known session variables are absent from Neovim's own environment.
Returns:
string[]env.login_shell_env({timeout_ms}) *spawn_env.login_shell_env()* The environment of$SHELL -lc env(falling back tosh) — what an interactive shell has after its profile ran, and the only way to recover state Neovim's own process never received.
Parameters:
{timeout_ms} integer|nil - default 3000
Returns:
table<string,string>|nil-nilon native Windows, or on failure
Note:
Blocking, but bounded, so a profile that blocks on a prompt or a slow
network mount cannot hang Neovim. Cached for the session.
env.clear() *spawn_env.clear()*
Drop the cached directory scan and login-shell probe. Call after installing
a toolchain or editing a shell profile inside a running session.
OPTIONS
All three ofbuild(),path()andapply()take the same option table: basetable<string,string>starting environment (defaultvim.fn.environ()) extra_pathsstring[]directories prepended toPATH, highest priority passthroughstring[]extra variable names to carry over fromvim.envwhen absent frombasevarstable<string,string>explicit overrides, applied last masonbooleanalso put Mason'sbindirectory onPATH(defaultfalse) login_shellbooleanfill gaps from a POSIX login shell (blocking, cached; no-op on Windows) timeout_msintegertimeout for thelogin_shellprobe (default3000)
DIAGNOSIS
build()cannot invent a value Neovim never received — that is its honest limit.missing()reports which known session variables are in that state, which is the explanation whenever a keyring-backed CLI fails from Neovim but works in the shell next to it:
for _, name in ipairs(require("lib").spawn_env.missing()) do
vim.print("not inherited by Neovim: " .. name)
end
A non-empty result is the cue to passlogin_shell = true, or to supply the value explicitly viavars.
TECHNICAL NOTES
Windows PATH key
Windows environment blocks are case-insensitive andvim.fn.environ()reportsPaththere.build()looks up the existing key case-insensitively and writes back under it, rather than introducing a secondPATHentry.
Caching
candidate_dirs()(a filesystem scan) andlogin_shell_env()(a subprocess) are each computed once per session.clear()resets both.
No login shell on Windows
login_shell_env()returnsnilon native Windows. There is no login-shell initialisation step there: a Windows process receives its environment from the user profile at creation time, which Neovim already inherited.
Related
|lib.nvim-modules| for the namespace overview;lua/lib/nvim/cross/run/env/README.mdfor the same content with examples;lib.nvim.cross.run(shell-string runners),lib.nvim.cross.run_argv(argv runners, no shell),lib.nvim.cross.executable(is the binary reachable at all?).