vmn is a fast local Python virtual environment manager for developers who work across many projects.
It keeps a SQLite registry of known virtual environments, gives you optional zsh/bash/fzf shell integration for switching between them, and can create, discover, inspect, refresh, remove, and prune environments from one CLI.
With shell integration installed, the daily workflow is:
vPick an environment in fzf. vmn changes into that environment's project directory when one is known, then activates the venv in your current shell.
- Create centrally managed venvs.
- Create project-local
./.venvenvironments. - Scan project folders for existing
.venvdirectories. - Activate environments from anywhere with optional zsh/bash/fzf integration.
- Inspect Python, pip, package, path, status, and source metadata.
- Mark missing environments instead of silently deleting registry records.
- Safely remove registry entries and optionally delete venv directories.
- Use scriptable output contracts for shell integration.
- Rust/Cargo for installation.
- Python 3 with the standard
venvmodule. - Optional:
fzffor the interactive shell picker. - Optional: zsh or bash for generated shell integration.
vmn targets Unix-like developer environments: Linux, macOS, and WSL2. The CLI commands work without shell integration or fzf. Shell integration is only needed for the v picker and for vmn activate/vmn deactivate to mutate the current parent shell. Without shell integration, vmn activate <selector> starts a child shell with the venv active.
From crates.io:
cargo install vmnFrom a local checkout:
cargo install --path .Verify:
vmn --version
vmn doctorInitialize local state and print shell integration for your shell:
vmn init --shell zsh
vmn init --shell bashAppend the printed snippet to your shell config:
zsh: ~/.zshrc
bash: ~/.bashrc
Then restart your shell:
exec zsh
# or
exec bashThe snippet defines:
v open the VMN fzf picker, cd to the project, activate the venv
vd deactivate the current Python venv
Ctrl-F open the VMN picker
Ctrl-Shift-F deactivate when supported by the terminal
Ctrl-X Ctrl-F deactivate fallback
Many terminals cannot distinguish Ctrl-F from Ctrl-Shift-F. When that happens, use vd or Ctrl-X Ctrl-F to deactivate.
If fzf is not installed, the CLI still works. Use:
vmn list
vmn activate <selector>Create a managed environment:
vmn create apiCreate a project-local environment:
cd ~/Projects/api
vmn create api --hereScan existing project venvs:
vmn scan ~/ProjectsOpen the picker:
vActivate directly without the picker or shell integration:
vmn activate api
exitDeactivate:
vdor use Python venv's standard function:
deactivatevmn supports two common styles.
Managed environments live under the VMN data directory:
~/.local/share/vmn/envs/<name>
Project-local environments live in your project:
~/Projects/my-app/.venv
Use managed envs when you want named environments independent of a project folder. Use --here or scan when each project owns its own .venv.
List Python interpreters discovered on PATH.
vmn pythons
vmn pythons --jsonUse this before creating a venv when multiple Python versions are installed.
Create config/data directories, initialize the SQLite registry, and print shell integration.
vmn init --shell zsh
vmn init --shell bashList registered environments.
vmn list
vmn list --fzf
vmn list --json
vmn list --include-missing
vmn list --all--fzf prints tab-separated rows for shell integration:
<id> <name> <status> <path> <python_version> <last_used_at>
Create and register a new venv.
vmn create api
vmn create api --here
vmn create api --path ~/scratch/api-venv
vmn create api --python 3.12
vmn create api --python python3.12
vmn create api --python /opt/homebrew/bin/python3.12By default, vmn create <name> creates a managed environment. --here creates ./.venv in the current directory. --python accepts an executable name, a full executable path, or a version selector such as 3.11 or 3.12.
Discover existing venvs under one or more directories.
vmn scan ~/Projects
vmn scan ~/Projects ~/Work --max-depth 5
vmn scan ~/Projects --dry-run
vmn scan ~/Projects --jsonScanning detects venv roots with pyvenv.cfg and an activation script. Permission-denied paths are skipped and counted instead of crashing the scan.
Show cached metadata for one environment.
vmn info api
vmn info api --packages
vmn info api --json
vmn info api --liveDefault info reads SQLite only. Use --live to probe Python/pip before printing.
Refresh Python, pip, and package metadata.
vmn refresh api
vmn refresh --allPackage capture can be slower than registry reads, so vmn does it explicitly through refresh or info --live.
Print only the venv root path.
vmn path apiPrint the associated project directory when known. Managed envs without a project print nothing.
vmn project-dir apiActivate an environment by selector.
vmn activate apiWith shell integration installed, this changes the current shell, changes into the associated project directory when one is known, and sources the venv activation script.
Without shell integration, vmn cannot modify its parent shell, so it starts a child shell with VIRTUAL_ENV and PATH configured for the selected venv. Type exit to return to the previous shell.
Deactivate the current environment when shell integration is installed.
vmn deactivateWithout shell integration, leave a standalone activated subshell with exit.
Print only the activation script path and update last_used_at.
source "$(vmn activate-path api)"The shell integration uses this internally. Prefer vmn activate <selector> for normal interactive use.
Check local health.
vmn doctorChecks include writable config/data directories, database availability, Python, optional fzf availability, missing active paths, activation scripts, and duplicate names.
Remove an environment from active use.
vmn remove api
vmn remove api --delete-files
vmn remove api --delete-files --yesWithout --delete-files, vmn marks the environment deleted in the registry and leaves files alone.
With --delete-files, vmn requires the target directory to look like a venv before deleting it. It refuses to delete paths without pyvenv.cfg and an activation script.
Delete stale registry records.
vmn prune --dry-run
vmn prune --missing
vmn prune --deleted
vmn prune --deleted --yesMost commands accept:
- a full environment id
- a unique id prefix
- a unique name
If a name or prefix is ambiguous, vmn fails and lists matching ids and paths.
On Linux and WSL2/XDG-style systems:
~/.config/vmn/config.toml
~/.local/share/vmn/vmn.db
~/.local/share/vmn/envs/
On macOS, vmn uses the platform-standard application support directory through Rust's directories crate, typically under:
~/Library/Application Support/vmn/
For tests or isolated usage, override locations:
export VMN_CONFIG_DIR=/tmp/vmn-config
export VMN_DATA_DIR=/tmp/vmn-dataReset all local VMN state:
rm -rf ~/.config/vmn ~/.local/share/vmnThis deletes VMN-managed environments under ~/.local/share/vmn/envs/. It does not delete project-local .venv directories elsewhere.
fzf is optional for the CLI and required for the v picker. Install fzf if you want interactive selection, then rerun:
vmn doctorWithout fzf, use direct activation:
vmn list
vmn activate <selector>Restart your shell:
exec zsh
# or
exec bashThen check:
type vActivation that persists in the current shell must happen through shell integration from vmn init --shell zsh or vmn init --shell bash. Without that integration, vmn activate <selector> intentionally opens an activated child shell instead.
That message means system Python/pip is being used instead of the venv. Activate with:
vmn activate <selector>If shell integration is not installed, this opens a child shell. Install packages inside that shell, then type exit when done.
Check the project directory:
vmn info <name-or-id>
vmn project-dir <name-or-id>Managed envs usually have no project directory. Project-local envs created with --here or discovered by scan should have one.
If a venv is deleted manually, vmn marks it missing instead of silently deleting it.
vmn doctor
vmn prune --missing --dry-run
vmn prune --missing --yesUse the id prefix shown by:
vmn list --allRefresh it:
vmn refresh <name-or-id>Run checks:
cargo fmt
cargo test --all-targets --all-features
cargo clippy --all-targets --all-features -- -D warningsInstall locally:
cargo install --path . --forcePackage check:
cargo package --list
cargo publish --dry-runMIT. See LICENSE.