This guide collects the practices that make vex easier to operate in real projects, especially for teams, monorepos, CI, and Python workflows.
The core rule is simple:
- use version files for tool versions
- use lockfiles for reproducibility
- use
.vex.tomlfor project behavior - keep supported tool home/cache/bin state inside
~/.vex - use the shell hook for interactive work and
vex execor CI steps for automation
Use each file for one job:
.tool-versions- checked-in project tool versions
.tool-versions.lock- exact reproducible toolchain state with checksums
.vex.toml- project tasks, env vars, repo-local behavior, mirrors, and network overrides
vex-config.toml- shared team baseline used explicitly with
--from
- shared team baseline used explicitly with
Do not overload .tool-versions with environment variables, tasks, comments that carry policy, or conventions that only one shell script understands.
For shared repositories, prefer exact tool versions instead of floating aliases.
Good:
node 20.11.0
python 3.12.8
Riskier for teams:
node lts
python latest
Aliases are convenient for local experiments, but exact versions make bug reports, CI failures, and onboarding much easier to reproduce.
If a repository is meant to build the same way across machines, commit the lockfile:
vex lock
git add .tool-versions .tool-versions.lockThen restore with:
vex sync --frozenThis is the safest default for:
- CI
- release branches
- repositories with multiple contributors
- demos and workshops
Keep project-local behavior in .vex.toml, not in shell-specific startup snippets.
Good uses for .vex.toml:
- shared commands via
vex run - project env vars
- repo-local mirrors
- stricter retry or timeout settings for one repository
Example:
[env]
RUST_LOG = "debug"
[commands]
test = "cargo test"
lint = "cargo clippy --all-targets --all-features -- -D warnings"This keeps project behavior visible, reviewable, and portable across shells.
For most teams, this is the cleanest flow:
- Commit
.tool-versions. - Commit
.tool-versions.lockif reproducibility matters. - Add shared tasks to
.vex.toml. - Use
vex runfor routine commands. - Use
vex outdatedandvex upgradeintentionally instead of allowing drift to accumulate.
When you upgrade versions:
vex outdated
vex upgrade node
vex lockThen review and commit both version files.
Use vex-config.toml when a team needs a shared baseline across repositories, but not when a checked-in .tool-versions file would already solve the problem.
Good fit:
- internal starter repositories
- centralized baseline recommendations
- org-wide bootstrapping for new repos
Less useful:
- hiding repo-specific choices that should be explicit in the repo itself
Remember:
vex-config.tomlis only used when you pass--from- local
.tool-versionsentries override matching tools from that baseline
vex works best in monorepos when version ownership is easy to read from the directory tree.
Use one root .tool-versions when most projects share the same toolchain.
repo/
.tool-versions
.vex.toml
service-a/
service-b/
This is the simplest setup when every service can use the same Node, Go, Java, Rust, or Python versions.
Use a root .tool-versions for shared defaults and add child .tool-versions files only where a service diverges.
repo/
.tool-versions
services/
legacy-api/
.tool-versions
web/
This works well because vex resolves parent directories and lets child .tool-versions entries override matching tools while keeping the parent values for everything else.
Put .vex.toml at the project root that owns the commands.
repo/
.tool-versions
services/
api/
.vex.toml
frontend/
.vex.toml
vex run executes from the directory that contains the nearest .vex.toml, so nested subdirectories still run the correct project command from the correct project root.
For GitHub Actions on macOS, prefer the official action:
- uses: imnotnoahhh/vex@v1
with:
auto-install: trueOr request exact tools:
- uses: imnotnoahhh/vex@v1
with:
tools: node@20.11.0 go@1.24.0The action:
- installs the published
vexrelease - restores
~/.vex/cacheand~/.vex/toolchainswhen caching is enabled - re-activates the restored tools so later steps can use
~/.vex/bin
CI recommendations:
- use either
toolsorauto-install: true, not both - commit
.tool-versions.lockand runvex sync --frozenwhen reproducibility is important - prefer
vex execor normal workflow steps over interactive shell-hook assumptions - keep version files in the repository so cache keys reflect real tool changes
- use
vex repair migrate-homeafter onboarding to pull supported legacy home state into~/.vex
Install project tools into node_modules and commit the package-manager lockfile. When Node is active, vex puts the nearest node_modules/.bin before shared npm globals in shell hooks, vex exec, and vex run.
That means direct commands such as vite, eslint, and tsc resolve to the project-installed versions first. Use npm install -g for user-level CLIs only; those go into the shared npm globals prefix at ~/.vex/npm/prefix/bin, shared across vex-managed Node versions. npm's official user config is redirected to ~/.vex/npm/npmrc.
Use vex globals --verbose when debugging command resolution. It shows the global CLI path, source kind, and active version source for npm, Python base/user, Go, Cargo, Maven, and Gradle entries.
vex manages the active JDK and JAVA_HOME. When user-state capture is enabled,
it also points Maven's local repository and Gradle's user home into ~/.vex and
neutralizes global JVM option variables such as JAVA_TOOL_OPTIONS.
Maven and Gradle themselves remain project or system tools. Prefer mvnw and
gradlew inside projects so the build tool version is pinned with the repository.
vex globals java and vex doctor report external mvn/gradle binaries plus ~/.m2 and ~/.gradle state so you can see when Java build-tool state lives outside ~/.vex.
vex resolves Rust pins from .tool-versions, .rust-toolchain,
rust-toolchain, .rust-toolchain.toml, and rust-toolchain.toml. For TOML
files, it reads [toolchain].channel, matching rustup's common project format.
For Rust projects that need official extensions, keep them in vex instead of falling back to a second toolchain manager:
vex rust target add aarch64-apple-ios aarch64-apple-ios-sim
vex rust component add rust-srcExample:
- uses: imnotnoahhh/vex@v1
with:
version: latest
- run: vex sync --frozen
- run: vex exec -- node -vUse the shell hook for interactive development, but use explicit commands for automation.
Good for scripts:
vex exec -- cargo test
vex exec -- python -m pytest
vex run testLess reliable for scripts:
- assuming an interactive shell hook already ran
- assuming an old shell session has the right tool selected
- calling
vex usein helper scripts when process-local activation would be enough
Explicit activation makes scripts easier to debug and easier to move into CI later.
For Python repositories, vex works best with a checked-in interpreter version and a project-local virtual environment.
Recommended flow:
vex install python@3.12
vex local python@3.12.8
vex python init
vex python freezeCommit:
.tool-versionsrequirements.lock
Do not commit:
.venv/
With the shell hook installed, vex auto-activates .venv when you enter the project and deactivates it when you leave.
Use the Python base environment for user-level CLI tools that are not project dependencies:
vex use python@3.12
vex python base pip install kaggleThat installs into ~/.vex/python/base/<version>, not into the interpreter toolchain. pip's official --user base is redirected to ~/.vex/python/user for users who explicitly use that mode. When no project .venv is active, the shell hook exposes the base and user bin directories so commands such as kaggle are available. When a project .venv is active, vex hides those global Python CLI directories so they do not affect project dependency resolution.
Version managers are easiest to operate when only one of them owns the front of PATH.
Recommended:
~/.vex/bincomes before old manager paths- old manager init lines are removed after migration
If commands still resolve to old locations:
which node
which python
hash -rThen run:
vex doctorWhen something feels inconsistent, use the same short checklist every time:
- Run
vex doctor. - Run
vex current. - Inspect the relevant version file.
- Check
which <tool>. - Clear shell caches with
hash -r. - Confirm only one version manager is active in your shell startup files.
This catches most migration and PATH problems quickly.
If you are starting fresh, this is a strong default:
- commit a root
.tool-versions - add
.tool-versions.lockwhen reproducibility matters - add
.vex.tomlonly when the repo has real shared tasks or env vars - use exact versions in team repos
- use
vex runfor shared commands - use the official GitHub Action in CI on macOS
That gives you a setup that is explicit, reviewable, and easy to move between laptops and CI runners.