vmn
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:
Pick an environment in fzf. vmn changes into that environment's project directory when one is known, then activates the venv in your current shell.
Features
- 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.
Requirements
- 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.
Install
From crates.io:
From a local checkout:
Verify:
Shell Setup
Initialize local state and print shell integration for your shell:
Append the printed snippet to your shell config:
zsh: ~/.zshrc
bash: ~/.bashrc
Then restart your shell:
# or
The 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:
Quick Start
Create a managed environment:
Create a project-local environment:
Scan existing project venvs:
Open the picker:
Activate directly without the picker or shell integration:
Deactivate:
or use Python venv's standard function:
Managed vs Scanned Environments
vmn 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.
Commands
vmn pythons
List Python interpreters discovered on PATH.
Use this before creating a venv when multiple Python versions are installed.
vmn init
Create config/data directories, initialize the SQLite registry, and print shell integration.
vmn list
List registered environments.
--fzf prints tab-separated rows for shell integration:
<id> <name> <status> <path> <python_version> <last_used_at>
vmn create
Create and register a new venv.
By 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.
vmn scan
Discover existing venvs under one or more directories.
Scanning detects venv roots with pyvenv.cfg and an activation script. Permission-denied paths are skipped and counted instead of crashing the scan.
vmn info
Show cached metadata for one environment.
Default info reads SQLite only. Use --live to probe Python/pip before printing.
vmn refresh
Refresh Python, pip, and package metadata.
Package capture can be slower than registry reads, so vmn does it explicitly through refresh or info --live.
vmn path
Print only the venv root path.
vmn project-dir
Print the associated project directory when known. Managed envs without a project print nothing.
vmn activate
Activate an environment by selector.
With 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.
vmn deactivate
Deactivate the current environment when shell integration is installed.
Without shell integration, leave a standalone activated subshell with exit.
vmn activate-path
Print only the activation script path and update last_used_at.
The shell integration uses this internally. Prefer vmn activate <selector> for normal interactive use.
vmn doctor
Check local health.
Checks include writable config/data directories, database availability, Python, optional fzf availability, missing active paths, activation scripts, and duplicate names.
vmn remove
Remove an environment from active use.
Without --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.
vmn prune
Delete stale registry records.
Selectors
Most 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.
Data Locations
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:
Reset all local VMN state:
This deletes VMN-managed environments under ~/.local/share/vmn/envs/. It does not delete project-local .venv directories elsewhere.
Troubleshooting
fzf is missing
fzf is optional for the CLI and required for the v picker. Install fzf if you want interactive selection, then rerun:
Without fzf, use direct activation:
v is not found
Restart your shell:
# or
Then check:
The picker opens but activation does not stick
Activation 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.
pip reports externally-managed-environment
That message means system Python/pip is being used instead of the venv. Activate with:
If shell integration is not installed, this opens a child shell. Install packages inside that shell, then type exit when done.
I selected an env but it did not cd where I expected
Check the project directory:
Managed envs usually have no project directory. Project-local envs created with --here or discovered by scan should have one.
A path is missing
If a venv is deleted manually, vmn marks it missing instead of silently deleting it.
A selector is ambiguous
Use the id prefix shown by:
Package metadata looks stale
Refresh it:
Development
Run checks:
Install locally:
Package check:
License
MIT. See LICENSE.