Introduction
scoop is a centralized Python virtual environment manager powered by uv.
One scoop, endless envs — pyenv-style workflow with uv’s blazing speed.
What is scoop?
Think of it like running an ice cream parlor:
- The Freezer (
~/.scoop/) keeps all your flavors fresh - Flavors are your virtualenvs — mix once, serve anywhere
- One scoop is all you need to get the right env
| The Old Way | The scoop Way |
|---|---|
.venv scattered across projects | ~/.scoop/virtualenvs/ centralized |
Manual source .venv/bin/activate | Auto-activate on directory entry |
| pyenv-virtualenv is slow | uv-powered, 100x+ faster |
| Which Python? Which venv? Chaos. | scoop doctor checks everything |
Quick Example
# Install Python
scoop install 3.12
# Create a virtualenv
scoop create myproject 3.12
# Use it (auto-activates!)
scoop use myproject
(myproject) $ pip install -r requirements.txt
# Check what's available
scoop list
Features
- Fast — Powered by uv, virtualenv creation is nearly instant
- Centralized — All environments live in
~/.scoop/virtualenvs/ - Auto-activation — Enter a directory, environment activates automatically
- Shell integration — Works with bash, zsh, fish, and PowerShell
- IDE friendly —
scoop use --linkcreates.venvsymlink for IDE discovery - Health checks —
scoop doctordiagnoses your setup
Getting Started
Ready to scoop? Head to the Installation guide to get started.
Links
- GitHub Repository
- API Reference (docs.rs)
- Crates.io
- llms.txt — AI/LLM-friendly project reference
- llms-full.txt — Full API reference for AI tools
Installation
Prerequisites
| Dependency | Version | Install Command |
|---|---|---|
| uv | Latest | curl -LsSf https://astral.sh/uv/install.sh | sh |
| Rust | 1.85+ | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
Install via Cargo
cargo install scoop-uv
The binary is installed to ~/.cargo/bin/scoop.
Upgrade
To upgrade scoop to the latest version:
cargo install scoop-uv
This overwrites the existing binary in ~/.cargo/bin/scoop. Your virtual environments in ~/.scoop/ are preserved.
Verify the upgrade:
scoop --version
Verify Installation
scoop --version
# scoop 0.11.0
Troubleshooting
scoop: command not found
Ensure ~/.cargo/bin is in your PATH:
# Add to ~/.zshrc or ~/.bashrc
export PATH="$HOME/.cargo/bin:$PATH"
Then restart your terminal or run:
source ~/.zshrc # or ~/.bashrc
uv not found
scoop requires uv to be installed and available in PATH. Verify:
uv --version
If not installed, run:
curl -LsSf https://astral.sh/uv/install.sh | sh
Next Steps
After installation, set up Shell Integration to enable auto-activation and tab completion.
Quick Start
This guide walks you through the basic scoop workflow.
1. Set Up Shell Integration
Zsh (macOS default):
echo 'eval "$(scoop init zsh)"' >> ~/.zshrc
source ~/.zshrc
Bash:
echo 'eval "$(scoop init bash)"' >> ~/.bashrc
source ~/.bashrc
Fish:
echo 'eval (scoop init fish)' >> ~/.config/fish/config.fish
source ~/.config/fish/config.fish
PowerShell:
Add-Content -Path $PROFILE -Value 'Invoke-Expression (& scoop init powershell)'
. $PROFILE
2. Install Python
# Install latest Python
scoop install 3.12
# Verify installation
scoop list --pythons
3. Create a Virtual Environment
scoop create myproject 3.12
This creates a virtual environment at ~/.scoop/virtualenvs/myproject/.
If Python 3.12 isn’t installed yet, add --install-python to install it on demand:
scoop create myproject 3.12 --install-python
4. Use the Environment
cd ~/projects/myproject
scoop use myproject
This:
- Creates
.scoop-versionfile in the current directory - Activates the environment (prompt shows
(myproject))
5. Work With Your Environment
(myproject) $ pip install -r requirements.txt
# If the file is in a different location:
(myproject) $ pip install -r path/to/requirements.txt
# Verify installed packages
(myproject) $ pip list
6. Auto-Activation
Once configured, entering a directory with .scoop-version automatically activates the environment:
cd ~/projects/myproject
# (myproject) appears in prompt automatically
Common Commands
| Task | Command |
|---|---|
| List environments | scoop list |
| List Python versions | scoop list --pythons |
| Show environment info | scoop info myproject |
| Remove environment | scoop remove myproject |
| Check installation | scoop doctor |
IDE Integration
Create a .venv symlink for IDE compatibility:
scoop use myproject --link
This creates .venv pointing to the scoop environment, recognized by VS Code, PyCharm, etc.
Next Steps
- See Shell Integration for advanced configuration
- See Commands for full command reference
Shell Integration
scoop uses a shell wrapper pattern (like pyenv) where the CLI outputs shell code that gets evaluated by the shell.
How It Works
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User runs │ --> │ CLI outputs │ --> │ Shell evals │
│ scoop use │ │ export ... │ │ the output │
└─────────────┘ └─────────────┘ └─────────────┘
The scoop shell function wraps the CLI binary:
scoop() {
case "$1" in
use)
command scoop "$@"
local name=""
shift
for arg in "$@"; do
case "$arg" in
-*) ;;
*) name="$arg"; break ;;
esac
done
if [[ -n "$name" ]]; then
eval "$(command scoop activate "$name")"
fi
;;
activate|deactivate|shell)
eval "$(command scoop "$@")"
;;
*)
command scoop "$@"
;;
esac
}
Setup
Zsh
echo 'eval "$(scoop init zsh)"' >> ~/.zshrc
source ~/.zshrc
Bash
echo 'eval "$(scoop init bash)"' >> ~/.bashrc
source ~/.bashrc
Fish
echo 'eval (scoop init fish)' >> ~/.config/fish/config.fish
source ~/.config/fish/config.fish
PowerShell
# Add to $PROFILE
Add-Content $PROFILE 'Invoke-Expression (& scoop init powershell)'
# Restart PowerShell
Auto-Activation
When enabled, scoop automatically activates environments based on version files.
Zsh: Uses chpwd hook (runs on directory change)
autoload -Uz add-zsh-hook
add-zsh-hook chpwd _scoop_hook
Bash: Uses PROMPT_COMMAND
PROMPT_COMMAND="_scoop_hook;$PROMPT_COMMAND"
Fish: Uses --on-variable PWD event handler
function _scoop_hook --on-variable PWD
# Check for version file and activate/deactivate
end
The hook checks for version files and activates/deactivates accordingly.
Version Resolution Priority
scoop checks these sources in order (first match wins):
| Priority | Source | Set by |
|---|---|---|
| 1 | SCOOP_VERSION env var | scoop shell |
| 2 | .scoop-version file | scoop use (walks parent directories) |
| 3 | ~/.scoop/version file | scoop use --global |
The “system” Value
When any source contains the value system, scoop deactivates the current virtual environment and uses the system Python.
scoop use system # Write "system" to .scoop-version
scoop shell system # Set SCOOP_VERSION=system (this terminal only)
Environment Variables
| Variable | Description | Default |
|---|---|---|
SCOOP_HOME | Base directory | ~/.scoop |
SCOOP_VERSION | Override version (highest priority) | (unset) |
SCOOP_NO_AUTO | Disable auto-activation | (unset) |
SCOOP_ACTIVE | Currently active environment | (set by scoop) |
SCOOP_RESOLVE_MAX_DEPTH | Limit parent directory traversal | (unlimited) |
Disable Auto-Activation
export SCOOP_NO_AUTO=1
Temporary and Project-Scoped Control
Disable only in the current shell session (does not affect global settings):
export SCOOP_NO_AUTO=1
# ...work without auto-activation...
unset SCOOP_NO_AUTO
For one project directory, use local version files instead of global settings:
cd ~/project
# Keep auto-activation, but force system Python in this project only
scoop use system
# Or pin a specific environment for this project only
scoop use myproject
For temporary per-terminal overrides without changing files:
scoop shell system # this terminal only
# ...test...
scoop shell --unset # return to file-based behavior
Custom Home Directory
export SCOOP_HOME=/custom/path
Network Filesystem Optimization
For slow network filesystems (NFS, SSHFS), limit directory traversal depth:
# Only check current directory and up to 3 parents
export SCOOP_RESOLVE_MAX_DEPTH=3
# Only check current directory (fastest)
export SCOOP_RESOLVE_MAX_DEPTH=0
Using with pyenv
Add scoop after pyenv in your shell config:
# ~/.zshrc
eval "$(pyenv init -)" # 1. pyenv first
eval "$(scoop init zsh)" # 2. scoop second (takes precedence)
Tab Completion
Shell integration includes completion for:
- Commands and subcommands
- Environment names
- Python versions
- Command options
Completion is automatically enabled by scoop init.
Supported Shells
| Shell | Status |
|---|---|
| Zsh | Full support (auto-activation, completion) |
| Bash | Full support (auto-activation, completion) |
| Fish | Full support (auto-activation, completion) |
| PowerShell | Full support (auto-activation, completion) |
Python Management
scoop delegates all Python installation and discovery to uv. This page explains how Python versions are found, installed, and used with scoop.
How Python Discovery Works
When you run scoop create myenv 3.12, scoop asks uv to create a virtual environment with Python 3.12. uv searches for a matching Python in this order:
- uv-managed installations in
~/.local/share/uv/python/(installed viascoop installoruv python install) - System Python on PATH — executables named
python,python3, orpython3.x - Platform-specific locations — Windows registry, Microsoft Store (Windows only)
Key behavior: For managed Pythons, uv prefers the newest matching version. For system Pythons, uv uses the first compatible version found on PATH.
Installing Python Versions
Via scoop (recommended)
# Install latest Python
scoop install
# Install specific minor version (latest patch)
scoop install 3.12
# Install exact version
scoop install 3.12.3
# List installed versions
scoop list --pythons
Behind the scenes
scoop install 3.12 runs uv python install 3.12 internally. uv downloads a standalone Python build from the python-build-standalone project and stores it in ~/.local/share/uv/python/.
Using System Python
scoop can use Python versions already installed on your system (via Homebrew, apt, the OS, etc.) — no scoop install needed.
# Check what Python versions uv can find on your system
uv python list
# Example output:
# cpython-3.13.1 /opt/homebrew/bin/python3.13 (system)
# cpython-3.12.8 ~/.local/share/uv/python/... (managed)
# cpython-3.12.0 /usr/bin/python3 (system)
# cpython-3.11.5 /usr/bin/python3.11 (system)
# Create environment using system Python 3.13
# (uv finds it automatically — no scoop install needed)
scoop create myenv 3.13
If the version you request matches a system Python, uv will use it. You only need scoop install if the version is not already available on your system.
Using Custom Python Installations
If you have a custom-built Python or an alternative interpreter (PyPy, GraalPy) in a non-standard location, you can point scoop directly to the executable.
When the required version is not in default sources
If scoop install <version> and normal uv discovery do not provide the interpreter you need,
integrate your own Python using one of these patterns:
- Direct path (recommended):
scoop create <env> --python-path /path/to/python - PATH-based discovery: add your Python to
PATH, then runscoop create <env> <version>
Use –python-path (recommended)
The simplest approach is to pass the Python executable path directly:
# Custom Python built from source
scoop create debug-env --python-path /opt/python-debug/bin/python3
# PyPy interpreter
scoop create pypy-env --python-path /opt/pypy/bin/pypy3
# GraalPy
scoop create graal-env --python-path /opt/graalpy/bin/graalpy
scoop validates the path, auto-detects the version, and stores the custom path in metadata.
# Verify what was integrated
scoop info debug-env
# Name: debug-env
# Python: 3.13.0
# Python Path: /opt/python-debug/bin/python3
Metadata is stored in ~/.scoop/virtualenvs/<name>/.scoop-metadata.json (python_path field).
See create command for details.
Alternative: Add custom Python to PATH
You can also add the Python to your PATH so uv discovers it automatically:
# Example: custom Python built from source in /opt/python-debug/
export PATH="/opt/python-debug/bin:$PATH"
# Verify uv can find it
uv python list | grep python
# cpython-3.13.0 /opt/python-debug/bin/python3.13
# Now scoop can use it
scoop create debug-env 3.13
Use UV_PYTHON_INSTALL_DIR
For managed Python installations in a custom location:
# Store uv-managed Pythons in a custom directory
export UV_PYTHON_INSTALL_DIR=/opt/shared-pythons
# Install Python to the custom location
scoop install 3.12
# All team members can share the same Python installations
Python preference settings
Control whether uv prefers managed or system Python:
# Use only uv-managed Python (ignore system Python)
UV_PYTHON_PREFERENCE=only-managed scoop create myenv 3.12
# Use only system Python (ignore uv-managed)
UV_PYTHON_PREFERENCE=only-system scoop create myenv 3.12
# Prefer system Python over managed (default: managed first)
UV_PYTHON_PREFERENCE=system scoop create myenv 3.12
Migrating from Other Tools
If you have existing virtual environments in pyenv, conda, or virtualenvwrapper, scoop can migrate them:
# See what can be migrated
scoop migrate list
# Example output:
# pyenv-virtualenv:
# myproject (Python 3.12.0)
# webapp (Python 3.11.8)
# conda:
# ml-env (Python 3.10.4)
# Migrate a specific environment
scoop migrate @env myproject
# Migrate everything at once
scoop migrate all
The migration process:
- Discovers environments from pyenv (
~/.pyenv/versions/), conda (conda info --envs), or virtualenvwrapper ($WORKON_HOME) - Creates a new scoop environment with the same Python version
- Reinstalls packages using uv for improved performance
- Preserves originals by default (use
--delete-sourceto remove source envs after success)
scoop migrate all runs migrations in parallel across CPU cores via rayon — typically 4-8× faster than sequential. Single-env (migrate @env) and --dry-run stay sequential for predictable output.
See migrate command for details.
Troubleshooting
Python version not found
$ scoop create myenv 3.14
# Error: Python 3.14 not found
# Solution 1: Install it via scoop
scoop install 3.14
# Solution 2: Check what's available
uv python list
scoop list --pythons
Invalid custom Python path
# Example custom path flow
scoop create myenv --python-path /opt/custom/python3
# Verify the binary exists and runs
/opt/custom/python3 --version
If the path is invalid or not executable, provide a valid Python binary path and retry.
Verify custom integration end-to-end
# 1) Confirm uv can see your interpreter (PATH-based flow)
uv python list
# 2) Confirm scoop recorded the interpreter path
scoop info myenv
# 3) Diagnose broken links or metadata issues
scoop doctor -v
Using a different Python than expected
# Check which Python uv would select for a version
uv python find 3.12
# /opt/homebrew/bin/python3.12
# Check all available 3.12 installations
uv python list | grep 3.12
# cpython-3.12.8 /opt/homebrew/bin/python3.12 (system)
# cpython-3.12.7 ~/.local/share/uv/python/... (managed)
Verify environment’s Python
# Check what Python an environment uses
scoop info myenv
# Name: myenv
# Python: 3.12.8
# Path: ~/.scoop/virtualenvs/myenv
Removing Python Versions
Quick: Cascade removal (recommended)
Use --cascade to automatically remove all environments using a Python version:
# Remove Python 3.12 and all environments using it
scoop uninstall 3.12 --cascade
# Skip confirmation prompt
scoop uninstall 3.12 --cascade --force
Preview affected environments
Before uninstalling, you can check which environments would be affected:
# Filter environments by Python version
scoop list --python-version 3.12
# myproject 3.12.1
# webapp 3.12.0
Manual workflow
If you prefer manual control (without --cascade):
# 1. Identify environments using the target Python version
scoop list --python-version 3.12
# myproject 3.12.1
# webapp 3.12.1
# 2. Remove or recreate affected environments
scoop remove myproject --force
scoop remove webapp --force
# Or recreate with a different version:
# scoop remove myproject --force && scoop create myproject 3.13
# 3. Uninstall the Python version
scoop uninstall 3.12
# 4. Verify everything is clean
scoop list --pythons # Confirm Python removed
scoop doctor # Check for broken environments
Recovery from accidental uninstall
If you uninstalled Python without cleaning up environments:
# Detect broken environments
scoop doctor -v
# ⚠ Environment 'myproject': Python symlink broken
# Fix by reinstalling the Python version
scoop install 3.12
scoop doctor --fix
# Or remove the broken environments and start fresh
scoop remove myproject --force
See uninstall command and doctor command for details.
Summary
| Scenario | What to do |
|---|---|
| Standard Python version | scoop install 3.12 then scoop create myenv 3.12 |
| System Python (Homebrew, apt) | Just scoop create myenv 3.12 — uv finds it automatically |
| Custom Python executable | scoop create myenv --python-path /path/to/python |
| Custom Python in non-standard path | Add to PATH, then scoop create myenv <version> |
| PyPy or alternative interpreter | scoop create myenv --python-path /opt/pypy/bin/pypy3 |
| Existing pyenv/conda environments | scoop migrate all |
| Shared Python installations | Set UV_PYTHON_INSTALL_DIR |
| Force system-only Python | Set UV_PYTHON_PREFERENCE=only-system |
| Uninstall Python + cleanup envs | scoop uninstall 3.12 --cascade (or manual workflow) |
| Find envs using a Python version | scoop list --python-version 3.12 |
| Fix broken environments | scoop doctor --fix (after reinstalling the Python version) |
Frequently Asked Questions
What’s the difference between scoop and pyenv?
While both tools help you manage Python, they focus on different parts of the workflow:
pyenv is primarily a version manager. It focuses on:
- Installing multiple versions of the Python interpreter (e.g., 3.9.0, 3.12.1)
- Switching between them globally or per folder
scoop is an environment and workflow manager powered by uv. It focuses on:
- Creating and managing isolated virtual environments
- Fast project-specific environment workflows
Summary: You might use pyenv to install Python 3.11 on your machine, but you use scoop to actually build and run your application within a lightning-fast virtual environment using that Python version.
How do I set Python 3.11.0 as the global default for all new shells and environments?
Use this workflow:
# 1) Install Python 3.11.0 (skip if already available on your system)
scoop install 3.11.0
# 2) Create an environment that uses 3.11.0
scoop create py311 3.11.0
# 3) Make that environment the global default
scoop use py311 --global
Important details:
--globalstores an environment name in~/.scoop/version, not a raw version like3.11.0.- This global default is applied in new shells and directories without a local
.scoop-version. - Priority is:
SCOOP_VERSIONenv var > local.scoop-version> global~/.scoop/version.
To remove the global default later:
scoop use --unset --global
How do I create a new virtual environment for a project, explicitly specifying Python 3.9.5?
Use this end-to-end workflow:
# 1) Install Python 3.9.5 (skip if already available on your system)
scoop install 3.9.5
# 2) Create a new project environment with that exact version
scoop create myproject 3.9.5
# 3) Verify which Python the environment uses
scoop info myproject
If creation fails because 3.9.5 is not found, run:
uv python list
scoop list --pythons
Then install the exact version and retry:
scoop install 3.9.5
scoop create myproject 3.9.5
How do I uninstall a specific Python version and all its associated virtual environments managed by scoop?
Use --cascade to remove both the Python version and every environment that depends on it:
# 1) Optional: preview affected environments
scoop list --python-version 3.12
# 2) Remove Python 3.12 and all associated environments
scoop uninstall 3.12 --cascade
# 3) Verify cleanup
scoop list --pythons
scoop doctor
Useful variants:
- Non-interactive mode:
scoop uninstall 3.12 --cascade --force - JSON output for automation:
scoop uninstall 3.12 --cascade --json
Important detail:
- Without
--cascade, environments are not removed and can become broken.
Given Scoop-uv’s auto-activation feature, how would a developer temporarily disable or customize its behavior for a specific project or directory without affecting global settings?
Use one of these local or temporary patterns:
# Option 1) Disable auto-activation only in the current shell session
export SCOOP_NO_AUTO=1
# ...work here...
unset SCOOP_NO_AUTO
# Option 2) For one project directory, force system Python locally
cd ~/project
scoop use system
# Option 3) For one project directory, pin a specific environment locally
scoop use myproject
# Option 4) Temporary override in this terminal only (no file changes)
scoop shell system
# ...test...
scoop shell --unset
Notes:
- These approaches avoid
--global, so global defaults are unchanged. .scoop-versionchanges fromscoop use ...are local to the project directory (and inherited by subdirectories).scoop shell ...affects only the current terminal session.
Once a Scoop-uv environment is active, how would you install project dependencies from a requirements.txt file into it?
Run pip inside the active environment:
# Prompt shows active environment, e.g. (myproject)
pip install -r requirements.txt
Useful variants:
- Different file location:
pip install -r path/to/requirements.txt - Verify installed dependencies:
pip list
If requirements.txt is in the project root, run the command from that directory.
How can a developer list all Python versions and their associated virtual environments currently managed by Scoop-uv?
Use this sequence:
# 1) Show all managed Python versions
scoop list --pythons
# 2) Show all environments and their Python versions
scoop list
# 3) Show environments for one specific Python version
scoop list --python-version 3.12
For automation:
- Use
--jsonfor machine-readable output. - Use
--barefor name-only output in shell scripts.
Example script to iterate each Python version and print associated environments:
for v in $(scoop list --pythons --bare); do
echo "== Python $v =="
scoop list --python-version "$v" --bare
done
If no versions or environments exist yet, these commands simply return empty results.
If a project requires a Python version not directly available through Scoop-uv’s default sources, how could a developer integrate a custom or pre-existing Python installation into Scoop-uv’s management system?
Use one of these two approaches:
# Option 1) Recommended: point directly to a Python executable
scoop create myenv --python-path /opt/python-debug/bin/python3
# Option 2) Add custom Python to PATH, then use normal version selection
export PATH="/opt/python-debug/bin:$PATH"
scoop create myenv 3.13
Validation and diagnostics:
uv python list # confirm interpreter discovery
scoop info myenv # confirm selected Python + Python Path
scoop doctor -v # detect broken links/metadata issues
Where scoop stores this integration:
- Environment metadata file:
~/.scoop/virtualenvs/myenv/.scoop-metadata.json - Custom interpreter path is recorded in the
python_pathfield.
Can I use scoop with conda environments?
Not directly. They serve different purposes and operate independently:
conda is a package and environment manager. It handles:
- Its own binaries and non-Python dependencies
- Heavy data science libraries (MKL, CUDA, cuDNN, etc.)
scoop is a lightweight environment manager powered by uv. It:
- Leverages your existing Python installations
- Creates fast, portable virtual environments
When to use what: For heavy data science requiring non-Python libraries → conda. For almost everything else → scoop (significantly faster and more portable).
How do I uninstall scoop completely?
To remove scoop from your system:
1. Delete the data folder
rm -rf ~/.scoop
2. Remove the shell hook
Edit your shell config file and remove the scoop init line:
| Shell | Config File | Line to Remove |
|---|---|---|
| Bash | ~/.bashrc | eval "$(scoop init bash)" |
| Zsh | ~/.zshrc | eval "$(scoop init zsh)" |
| Fish | ~/.config/fish/config.fish | eval (scoop init fish) |
| PowerShell | $PROFILE | Invoke-Expression (& scoop init powershell) |
3. (Optional) Remove config
rm -f ~/.scoop/config.json
4. Restart your terminal
Does scoop work on Windows?
scoop supports PowerShell on Windows (both PowerShell Core 7.x+ and Windows PowerShell 5.1+). Shell integration including auto-activation and tab completion works fully.
# Add to $PROFILE
Invoke-Expression (& scoop init powershell)
Note: Command Prompt (cmd.exe) is not supported. Use PowerShell for the full scoop experience.
Can I use a custom or pre-existing Python with scoop?
Yes, in two ways:
Option 1: Use –python-path (recommended for custom builds)
Point directly to any Python executable:
# Custom-built Python
scoop create debug-env --python-path /opt/python-debug/bin/python3
# PyPy interpreter
scoop create pypy-env --python-path /opt/pypy/bin/pypy3
# GraalPy
scoop create graal-env --python-path /opt/graalpy/bin/graalpy
scoop validates the path, auto-detects the version, and stores it in metadata.
Option 2: System Python via uv discovery
scoop uses uv for Python discovery, which automatically finds Python installations on your system:
# Check what Python versions uv can discover
uv python list
# Example output:
# cpython-3.13.1 /opt/homebrew/bin/python3.13 (system)
# cpython-3.12.8 ~/.local/share/uv/python/... (managed)
# cpython-3.11.5 /usr/bin/python3.11 (system)
# Use a system-installed Python directly (no scoop install needed)
scoop create myenv 3.13
For a custom Python in a non-standard location, add it to your PATH:
export PATH="/opt/python-debug/bin:$PATH"
scoop create debug-env 3.13
See also: Python Management for the full guide on Python discovery, system Python, custom interpreters, and environment variables.
Can I migrate environments from pyenv or conda?
Yes. scoop can discover and migrate existing environments from pyenv-virtualenv, conda, and virtualenvwrapper:
# See what can be migrated
scoop migrate list
# pyenv-virtualenv:
# myproject (Python 3.12.0)
# conda:
# ml-env (Python 3.10.4)
# Migrate a specific environment
scoop migrate @env myproject
# Migrate everything at once
scoop migrate all
The original environments are preserved by default. Use --delete-source to remove source envs after successful migration. See migrate command for details.
Command Reference
Complete reference for all scoop commands.
Commands Overview
| Command | Aliases | Description |
|---|---|---|
scoop list | ls | List virtualenvs or Python versions |
scoop create | - | Create virtualenv |
scoop use | - | Set + activate environment |
scoop remove | rm, delete | Remove virtualenv |
scoop install | - | Install Python version |
scoop uninstall | - | Uninstall Python version |
scoop doctor | - | Diagnose installation |
scoop info | - | Show virtualenv details |
scoop status | - | Summarise the currently active env |
scoop which | - | Resolve an executable inside an env |
scoop run | - | Run a command inside an env without activating |
scoop sync | - | Apply .scoop.toml declaratively |
scoop export | - | Write a portable JSON snapshot of an env |
scoop import | - | Recreate an env from an export file (or stdin) |
scoop clone | - | Duplicate an env (with or without packages) |
scoop migrate | - | Migrate from pyenv/conda/venvwrapper |
scoop gc | - | Garbage-collect orphan virtualenvs |
scoop prune | - | Prune the uv cache |
scoop verify | - | Per-env health diagnosis (6 checks) |
scoop lang | - | Get/set display language |
scoop shell | - | Set shell-specific env (temporary) |
scoop init | - | Shell init script |
scoop completions | - | Completion script |
scoop man | - | Generate man pages (for distro packagers) |
Global Options
Available for all commands:
| Option | Description |
|---|---|
-q, --quiet | Suppress all output |
--no-color | Disable colored output |
-h, --help | Show help message |
-V, --version | Show version |
Environment Variables
| Variable | Description | Default |
|---|---|---|
SCOOP_HOME | Base directory for scoop | ~/.scoop |
SCOOP_NO_AUTO | Disable auto-activation | (unset) |
SCOOP_LANG | Display language (en, ko, ja, pt-BR) | System locale |
NO_COLOR | Disable colored output | (unset) |
Directory Layout
| Location | Purpose |
|---|---|
~/.scoop/virtualenvs/ | Virtual environments storage |
~/.scoop/version | Global default environment |
.scoop-version | Local environment preference |
.venv | Symlink to active environment (with --link) |
list
List all virtual environments or installed Python versions.
Aliases: ls
Usage
scoop list [options]
Options
| Option | Description |
|---|---|
--pythons | Show Python versions instead of virtualenvs |
--python-version <VERSION> | Filter environments by Python version (e.g., 3.12) |
--sort <MODE> | Sort order: name (default), created, last-used |
--bare | Output names only (for scripting) |
--json | Output as JSON |
Sort
--sort reorders the output without changing what’s shown:
| Mode | Order | Tie-break |
|---|---|---|
name | Alphabetical (default, back-compat) | — |
created | Newest created_at first | Name (asc) |
last-used | Most recently activated first | Name (asc) |
Envs missing the relevant timestamp (created_at / last_used) sort
to the end of the list, with name-order tie-break — so legacy or
never-activated envs don’t bury the interesting ones. last_used
populates when an env is actually activated: scoop activate,
shell-hook auto-activation triggered by scoop use, scoop run, or
scoop shell. scoop use on its own only writes the version file
and does not touch metadata; the touch fires when the shell wrapper
sources the activate script afterwards.
--sort is mutually exclusive with --pythons (which lists Python
installations, not environments).
Examples
scoop list # List all virtualenvs
scoop list --pythons # List installed Python versions
scoop list --bare # Names only, one per line
scoop list --json # JSON output
# Filter by Python version
scoop list --python-version 3.12 # Show only 3.12.x environments
scoop list --python-version 3 # Show all Python 3.x environments
scoop list --python-version 3.12.1 # Exact version match
# Sort
scoop list --sort created # Newest envs first
scoop list --sort last-used # Recently active envs first
List Python Versions with Associated Environments
Use this workflow to see both sides of the mapping:
# 1) List installed Python versions managed by scoop/uv
scoop list --pythons
# 2) List all virtual environments with their Python versions
scoop list
# 3) Show environments associated with a specific Python version
scoop list --python-version 3.12
For scripting, combine --bare with per-version filtering:
for v in $(scoop list --pythons --bare); do
echo "== Python $v =="
scoop list --python-version "$v" --bare
done
You can also use --json for machine-readable output:
scoop list --pythons --json
scoop list --json
Version Filtering
The --python-version option uses prefix matching to filter environments:
scoop list --python-version 3.12
# Output:
# myproject 3.12.1
# webapp 3.12.0
# (environments using 3.11 or 3.13 are not shown)
This is useful for identifying environments before uninstalling a Python version:
# See which environments will be affected
scoop list --python-version 3.12
# Then uninstall with cascade
scoop uninstall 3.12 --cascade
Note:
--python-versioncannot be combined with--pythons(which lists Python installations, not environments).
Empty Results
- If no Python versions are installed,
scoop list --pythonsshows no entries. - If no environments exist,
scoop listshows no entries. - If no environments match a filter,
scoop list --python-version <VERSION>shows no entries.
create
Create a new virtual environment.
Usage
scoop create <name> [python-version]
Arguments
| Argument | Required | Default | Description |
|---|---|---|---|
name | Yes | - | Name for the new virtualenv |
python-version | No | 3 (latest) | Python version (e.g., 3.12, 3.11.8) |
Options
| Option | Description |
|---|---|
--force, -f | Overwrite existing virtualenv |
--python-path <PATH> | Use a specific Python executable instead of version discovery |
--install-python | Install the requested Python version first if it’s not already available (conflicts with --python-path) |
Examples
scoop create myproject 3.12 # Create with Python 3.12
scoop create webapp # Create with latest Python
scoop create myenv 3.11 --force # Overwrite if exists
# Auto-install Python first if the version is missing
scoop create myenv 3.13 --install-python
# Use a specific Python executable
scoop create myenv --python-path /opt/python-debug/bin/python3
scoop create graal --python-path /opt/graalpy/bin/graalpy
Create a Project Environment with Python 3.9.5
# Install exact Python version (skip if already available)
scoop install 3.9.5
# Create a new project environment using that exact version
scoop create myproject 3.9.5
# Verify the environment uses Python 3.9.5
scoop info myproject
If 3.9.5 is not available, install it first with scoop install 3.9.5, then check discovery with
uv python list and scoop list --pythons.
Python Version Resolution
scoop delegates Python discovery to uv. The python-version argument is passed to uv venv --python, which searches for a match in:
- uv-managed Python installations
- System Python on
PATH(Homebrew, apt, pyenv, etc.) - Platform-specific locations (Windows only)
# Uses uv-managed Python 3.12 (if installed via scoop install)
scoop create myenv 3.12
# Also works with system Python — no scoop install needed
# (e.g., if Homebrew has python@3.13)
scoop create myenv 3.13
# Check what Python versions are available
uv python list
scoop list --pythons
Tip: If the version isn’t found, install it first with
scoop install 3.12. See Python Management for custom Python paths.
Custom Python Executable
Use --python-path to create a virtualenv with a specific Python binary. This is useful for:
- Custom-built Python (debug builds, optimized builds)
- Alternative interpreters (PyPy, GraalPy)
- Python installations in non-standard locations
# Debug build from source
scoop create debug-env --python-path /opt/python-debug/bin/python3
# PyPy interpreter
scoop create pypy-env --python-path /opt/pypy/bin/pypy3
# GraalPy
scoop create graal-env --python-path /opt/graalpy/bin/graalpy
The path must point to a valid, executable Python binary. scoop will:
- Validate the path (exists, is a file, is executable)
- Auto-detect the Python version from the binary
- Store the custom path in the environment’s metadata
You can verify the custom path with scoop info:
scoop info debug-env
# Name: debug-env
# Python: 3.13.0
# Python Path: /opt/python-debug/bin/python3
# Path: ~/.scoop/virtualenvs/debug-env
use
Set a virtual environment for the current directory and activate it.
Usage
scoop use <name> [options]
scoop use system [options]
scoop use --unset [options]
Arguments
| Argument | Required | Description |
|---|---|---|
name | No | Name of the virtualenv, or system for system Python |
Options
| Option | Description |
|---|---|
--unset | Remove version file (local or global) |
--global, -g | Set as global default |
--link | Create .venv symlink for IDE compatibility |
--no-link | Do not create .venv symlink (default) |
Behavior
- Creates
.scoop-versionfile in current directory - Immediately activates the environment (if shell hook installed)
- With
--global: writes to~/.scoop/version - With
--link: creates.venv -> ~/.scoop/virtualenvs/<name>
Special Value: system
Using system as the name tells scoop to use the system Python:
scoop use system # Use system Python in this directory
scoop use system --global # Use system Python as global default
This writes the literal string system to the version file, which the shell hook interprets as “deactivate any virtual environment.”
The --unset Flag
Removes the version file entirely:
scoop use --unset # Delete .scoop-version in current directory
scoop use --unset --global # Delete ~/.scoop/version
After unsetting, scoop falls back to the next priority level in version resolution.
Examples
# Use a virtual environment in this directory
scoop use myproject
# Also create .venv symlink (for IDE support)
scoop use myproject --link
# Set global default environment
scoop use myproject --global
# Use system Python in this directory
scoop use system
# Use system Python globally
scoop use system --global
# Remove local version setting
scoop use --unset
# Remove global version setting
scoop use --unset --global
Set Python 3.11.0 as Global Default
--global stores an environment name, not a raw Python version string.
Create an environment with Python 3.11.0, then set that environment globally:
scoop install 3.11.0
scoop create py311 3.11.0
scoop use py311 --global
This writes py311 to ~/.scoop/version, which is used in new shell sessions and
directories that do not have a local .scoop-version.
If a local .scoop-version file or SCOOP_VERSION environment variable is present,
it takes precedence over the global setting.
Version File Format
The .scoop-version file contains a single line with either:
- An environment name (e.g.,
myproject) - The literal string
system
$ cat .scoop-version
myproject
remove
Remove a virtual environment.
Aliases: rm, delete
Usage
scoop remove <name> [options]
Arguments
| Argument | Required | Description |
|---|---|---|
name | Yes | Name of the virtualenv to remove |
Options
| Option | Description |
|---|---|
--force, -f | Skip confirmation prompt |
Examples
scoop remove myproject # Remove with confirmation
scoop remove myproject --force # Remove without asking
scoop rm old-env -f # Using alias
Check Before Removing
To see details about an environment before removing it:
# Show environment details (Python version, path, packages)
scoop info myproject
# Output:
# Name: myproject
# Python: 3.12.1
# Path: ~/.scoop/virtualenvs/myproject
Removing All Environments for a Python Version
To remove all environments that use a specific Python version:
# List environments to identify which use Python 3.12
scoop list
# myproject 3.12.1
# webapp 3.12.1
# ml-env 3.11.8
# Remove each one
scoop remove myproject --force
scoop remove webapp --force
# Then optionally uninstall the Python version itself
scoop uninstall 3.12
See also: uninstall command for the complete workflow to uninstall a Python version and clean up associated environments.
install
Install a Python version.
Usage
scoop install [version] [options]
Arguments
| Argument | Required | Default | Description |
|---|---|---|---|
version | No | latest | Python version (e.g., 3.12, 3.11.8) |
Options
| Option | Description |
|---|---|
--latest | Install latest stable Python (default) |
--stable | Install oldest fully-supported Python (3.10) |
Version Resolution
- No argument or
--latest: installs latest Python 3.x --stable: installs Python 3.10 (oldest with active security support)3.12: installs latest 3.12.x patch3.12.3: installs exact version
Examples
scoop install # Install latest
scoop install --latest # Same as above
scoop install --stable # Install Python 3.10
scoop install 3.12 # Install latest 3.12.x
scoop install 3.12.3 # Install exact 3.12.3
Note: Python versions are managed by uv.
Python Discovery
You don’t always need scoop install. When you run scoop create, uv searches for a matching Python in this order:
- uv-managed — installed via
scoop installoruv python install - System PATH — Homebrew, apt, pyenv, or any Python on your
PATH - Platform-specific — Windows registry, Microsoft Store
# See all Python versions uv can find
uv python list
# Example output:
# cpython-3.13.1 /opt/homebrew/bin/python3.13 (system)
# cpython-3.12.8 ~/.local/share/uv/python/... (managed)
# cpython-3.11.5 /usr/bin/python3.11 (system)
# Use system Python directly — no scoop install needed
scoop create myenv 3.13
If the requested version isn’t found anywhere, scoop create will fail with an error. Use scoop install <version> to download it first.
See also: Python Management for custom Python paths, environment variables, and migration.
uninstall
Remove an installed Python version.
Usage
scoop uninstall <version>
Arguments
| Argument | Required | Description |
|---|---|---|
version | Yes | Python version to remove |
Options
| Option | Description |
|---|---|
--cascade | Also remove all virtual environments using this Python version |
--force, -f | Skip confirmation for cascade removal (requires --cascade) |
Examples
scoop uninstall 3.12 # Remove Python 3.12
scoop uninstall 3.11.8 # Remove specific version
# Remove Python and all environments using it
scoop uninstall 3.12 --cascade
# Remove without confirmation prompt
scoop uninstall 3.12 --cascade --force
Uninstall a Python Version and All Associated Environments
Recommended workflow for a full cleanup:
# 1) Optional: preview which environments would be removed
scoop list --python-version 3.12
# 2) Remove Python 3.12 and all environments using it
scoop uninstall 3.12 --cascade
# 3) Verify cleanup
scoop list --pythons
scoop doctor
For non-interactive scripts, skip the confirmation prompt:
scoop uninstall 3.12 --cascade --force
If the target version is not installed, check available versions first:
scoop list --pythons
Cascade Removal
The --cascade flag automatically removes all virtual environments that use the target Python version before uninstalling it. This replaces the manual multi-step workflow.
scoop uninstall 3.12 --cascade
# Finding environments using Python 3.12...
# Found 2 environments using Python 3.12:
# - myproject
# - webapp
# Remove these environments and uninstall Python 3.12? [y/N]
# Removing myproject...
# Removing webapp...
# Uninstalling Python 3.12...
# ✓ Python 3.12 uninstalled
With --force, the confirmation prompt is skipped:
scoop uninstall 3.12 --cascade --force
With --json, the output includes the list of removed environments:
scoop uninstall 3.12 --cascade --json
# {
# "status": "success",
# "command": "uninstall",
# "data": {
# "version": "3.12",
# "removed_envs": ["myproject", "webapp"]
# }
# }
Note: Without
--cascade, uninstalling a Python version does not remove virtual environments that were created with it. Those environments will become broken. Use--cascadeto handle this automatically, or follow the manual workflow below.
Manual Uninstall Workflow
If you prefer manual control (without --cascade):
Step 1: Identify affected environments
# List environments filtered by Python version
scoop list --python-version 3.12
# Output:
# myproject 3.12.1
# webapp 3.12.1
# Or use JSON for scripting
scoop list --json
Step 2: Handle affected environments
# Option A: Remove the environment entirely
scoop remove myproject --force
# Option B: Recreate with a different Python version
scoop remove myproject --force
scoop create myproject 3.13
# Option C: Keep it (will be broken until you reinstall that Python)
# Do nothing — scoop doctor can detect and help fix it later
Step 3: Uninstall the Python version
scoop uninstall 3.12
Step 4: Verify
# Confirm Python is removed
scoop list --pythons
# Check for broken environments
scoop doctor
# If any issues found:
scoop doctor --fix
Recovery
If you uninstalled a Python version without cleaning up environments first:
# Detect broken environments
scoop doctor -v
# Output:
# ⚠ Environment 'myproject': Python symlink broken
# Option 1: Reinstall the Python version
scoop install 3.12
scoop doctor --fix
# Option 2: Recreate affected environments with a new version
scoop remove myproject --force
scoop create myproject 3.13
doctor
Check scoop installation health and diagnose issues.
Usage
scoop doctor [options]
Options
| Option | Description |
|---|---|
-v, --verbose | Show more details (can repeat: -vv) |
--json | Output diagnostics as JSON |
--fix | Auto-fix issues where possible |
Checks Performed
| Check | What it verifies |
|---|---|
| uv installation | uv is installed and accessible |
| Shell integration | Shell hook is properly configured |
| Environment integrity | Python symlinks are valid, pyvenv.cfg exists |
| Path configuration | ~/.scoop/ directory structure is correct |
| Version file validity | .scoop-version files reference existing environments |
Examples
scoop doctor # Quick health check
scoop doctor -v # Verbose diagnostics
scoop doctor --fix # Fix what can be fixed
scoop doctor --json # JSON output for scripting
Environment Integrity
The doctor checks each virtual environment for:
- Python symlink — Does the
pythonbinary in the environment point to a valid Python installation? - pyvenv.cfg — Does the environment’s configuration file exist and reference a valid Python?
Environments can become broken when their underlying Python version is uninstalled. Use scoop doctor to detect these issues:
# After accidentally uninstalling Python 3.12:
scoop doctor -v
# Output:
# ✓ uv: installed (0.5.x)
# ✓ Shell: zsh integration active
# ⚠ Environment 'myproject': Python symlink broken
# ⚠ Environment 'webapp': Python symlink broken
# Auto-fix by recreating symlinks (requires Python to be reinstalled)
scoop install 3.12
scoop doctor --fix
# Output:
# ✓ Fixed 'myproject': Python symlink restored
# ✓ Fixed 'webapp': Python symlink restored
Tip: Run
scoop doctorperiodically or after uninstalling Python versions to catch broken environments early. See uninstall command for the safe uninstall workflow.
info
Show detailed information about a virtual environment — heavier
sibling of scoop status. Reads metadata, walks the
directory for size, and shells out to the venv’s own pip for a
package list.
Usage
scoop info <name>
Arguments
| Argument | Required | Description |
|---|---|---|
name | Yes | Name of the virtualenv |
Options
| Option | Description |
|---|---|
--all-packages | Show the full installed-package list (default: top 5) |
--no-size | Skip the directory-size walk |
--json | Output as JSON |
Human Output
Name: myproject
Python: 3.12.1
Path: ~/.scoop/virtualenvs/myproject
Active: yes
Created: 2026-05-29 12:34:56
Last used: 3 hours ago
Size: 45 MB
Packages: 8
requests==2.31.0
...
The Last used: row reads never for envs whose metadata exists but
have never been activated (scoop activate / scoop run /
scoop shell is what touches it), and is omitted entirely when there
is no on-disk metadata at all.
JSON Output
scoop info myproject --json
{
"status": "success",
"command": "info",
"data": {
"name": "myproject",
"python": "3.12.1",
"path": "/Users/me/.scoop/virtualenvs/myproject",
"active": true,
"created_at": "2026-05-29T12:34:56+00:00",
"last_used": "2026-06-02T09:00:00+00:00",
"size_bytes": 47185920,
"size_display": "45 MB",
"packages": { "total": 8, "items": [{"name": "requests", "version": "2.31.0"}], "truncated": true }
}
}
last_used (RFC 3339) is omitted when the env has never been
activated. size_bytes / size_display are omitted under --no-size.
Examples
scoop info myproject # Default top-5 packages
scoop info myproject --all-packages
scoop info myproject --no-size # Skip directory-size walk
scoop info myproject --json
status
Summarise the current environment in one shot — designed to be fast (no
package listing, no directory size walk). Use scoop info for the
heavier per-env view.
Usage
scoop status [--json]
States
status resolves to one of four states:
| State | Trigger |
|---|---|
active | $SCOOP_ACTIVE is set (shell-activated) |
configured | A .scoop-version file or ~/.scoop/version selects an env |
system | The configured env is the literal name system |
none | Nothing resolved |
$SCOOP_ACTIVE wins over version files because it reflects what the shell
actually activated.
Human Output
For a real env (active / configured):
Name: myenv
Source: scoop_active_env
Python: 3.12.1
Path: ~/.scoop/virtualenvs/myenv
Created: 2026-05-29 12:34:56
Last used:3 hours ago
The Last used: row reads never for envs that have metadata but
have not yet been activated (fresh scoop create, or envs whose
metadata predates the field). It’s omitted entirely when there’s no
metadata at all — that way “we don’t know” doesn’t get conflated with
“definitely never used”.
For system: a single line indicating system Python is in use.
For none: a hint pointing to scoop use <name>.
JSON Output
{
"status": "success",
"command": "status",
"data": {
"state": "active",
"name": "myenv",
"source": "scoop_active_env",
"path": "/Users/me/.scoop/virtualenvs/myenv",
"python": "3.12.1",
"created_at": "2026-05-29T12:34:56+00:00",
"last_used": "2026-06-02T09:00:00+00:00"
}
}
Fields are omitted (skip_serializing_if) when not applicable to the
state. last_used is RFC 3339 and absent in two distinct cases:
- No metadata at all (legacy env / metadata file removed) —
the timestamp is unknown. Human output omits the
Last used:row entirely. - Metadata present but never activated since the field landed —
the timestamp is known to be never. Human output renders
Last used: never.
JSON consumers therefore should NOT collapse “missing” to “never”;
combine the absence of last_used with the presence of created_at
to tell the two cases apart.
Examples
scoop status # human-readable
scoop status --json # machine-readable
which
Print the full path to an executable inside a scoop environment, the same way
pyenv which resolves binaries.
Usage
scoop which <exe> [--env <name>] [--json]
Arguments
| Argument | Required | Description |
|---|---|---|
exe | Yes | Executable name to locate (e.g. python, pip, pytest) |
Options
| Option | Description |
|---|---|
--env <name> | Look in this environment instead of the active one |
--json | Output as JSON |
Resolution Order
--env <name>if provided$SCOOP_ACTIVE(set byscoop activate/scoop shell).scoop-version(local → parents → global)
If none of those resolve to a real virtualenv (e.g. system Python or no
configuration), the command fails with No active environment.
On Windows, the lookup also probes .exe, .bat, and .cmd extensions.
Examples
scoop which python # active env's python
scoop which pytest --env myenv # explicit env
scoop which python --json # JSON: { exe, env, path }
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Path printed to stdout |
| 1 | No active env / env missing / executable not in env’s bin/ |
run
Run a command inside a virtualenv without activating it in the parent shell — useful for CI, one-shot scripts, and editor integrations.
Usage
scoop run <env> [--] <command> [args...]
The -- separator is optional but recommended when <command> accepts flags
that might collide with scoop’s own flags.
Arguments
| Argument | Required | Description |
|---|---|---|
env | Yes | Name of the virtualenv |
command | Yes | Program (and arguments) to execute |
Environment Wiring
The spawned child sees the same vars that scoop activate would set:
| Variable | Value |
|---|---|
VIRTUAL_ENV | Absolute path of the env |
SCOOP_ACTIVE | <env> |
PATH | Env’s bin/ prepended to inherited PATH |
PYTHONHOME | Removed |
A bare program name (no / or \) is looked up inside the env’s bin/
first — so scoop run env -- python always picks the env’s interpreter, not
a system one. An explicit path (/usr/bin/python3) is used verbatim.
Exit Codes
scoop run exits with the child’s exit code. On Unix, a child killed by a
signal exits as 128 + signum (matching what bash exposes via $?).
Examples
scoop run myenv -- python script.py
scoop run myenv -- pip install requests
scoop run myenv -- pytest -vv tests/
scoop run myenv -- which python # absolute path inside myenv
sync
Apply a project’s declarative .scoop.toml — create the env if needed (auto-
installing Python if missing) and reconcile its packages via uv pip.
Usage
scoop sync [--with <GROUP>]... [--dry-run] [--json]
Options
| Option | Description |
|---|---|
--with <GROUP> | Install an extra package group on top of default (repeatable) |
--dry-run | Print the resolved plan without creating env or installing packages |
--json | Output as JSON |
Manifest Resolution
scoop sync walks from the current directory up to the filesystem root looking
for .scoop.toml — the same model as .scoop-version. The first manifest it
finds wins. If none is found, the command fails with MANIFEST_NOT_FOUND and
points you at this doc.
.scoop.toml Format
[environment]
name = "myproject" # required — must pass `is_valid_env_name`
python = "3.12" # required — same specifier syntax as `scoop create`
[packages]
default = ["pytest", "black", "mypy"] # always installed
dev = ["ipython", "debugpy"] # opt-in via `--with dev`
docs = ["mkdocs"] # opt-in via `--with docs`
Field rules:
[environment].namefollows the same validation asscoop create <NAME>(letter-leading,[a-zA-Z][a-zA-Z0-9_-]*, not a reserved subcommand name).[environment].pythonis forwarded touv venv --pythonas-is, so any specifier uv accepts works (3.12,3.12.7,cpython@3.12,pypy@3.10).[packages]is optional;default = []is valid.- Any other key inside
[packages]becomes a named group selectable with--with <name>. - Top-level keys other than
[environment]and[packages]are rejected (deny_unknown_fields) so typo’d sections fail loudly instead of silently doing nothing.
Behaviour
| State | What scoop sync does |
|---|---|
| Env missing | Auto-installs the requested Python (if uv doesn’t have it), creates the env, then installs packages |
| Env exists, Python matches | Just installs packages (idempotent — pip resolves and skips already-satisfied entries) |
| Env exists, Python mismatch | Warns and proceeds. Recreating an env on a version change is destructive, so it stays explicit: scoop remove <name> then scoop sync |
Unknown --with <group> | Fails fast before any env work, lists available groups |
scoop sync does not uninstall packages that are present in the env but
missing from the manifest. That’s a deliberate scoping decision for v1 — full
lockstep reconciliation is left to a future --prune flag.
Examples
# In a project directory with .scoop.toml
scoop sync # default group only
scoop sync --with dev # default + dev
scoop sync --with dev --with docs # multiple groups
scoop sync --dry-run # preview, no side effects
scoop sync --with dev --dry-run --json # machine-readable plan
Sample dry-run output
• Dry-run plan:
manifest: /path/to/project/.scoop.toml
environment: myproject (Python 3.12)
groups: default, dev
action: create env + install packages
packages: 5 total
- pytest
- black
- mypy
- ipython
- debugpy
Sample JSON output
{
"status": "success",
"command": "sync",
"data": {
"manifest_path": "/path/to/.scoop.toml",
"environment": "myproject",
"python": "3.12",
"groups": ["default", "dev"],
"packages": ["pytest", "black", "mypy", "ipython", "debugpy"],
"env_created": true,
"dry_run": false
}
}
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Sync succeeded (or dry-run produced a plan) |
| 1 | Manifest not found, unknown group, parse error, or pip install failed |
Not Yet Supported (v1)
These fields are intentionally not parsed in this version:
[hooks](post-create,post-activate, …) — needs a separate threat model before scoop will execute arbitrary shell from a checked-in file.python_path— per-env custom interpreter overrides (parallel to the existing--python-pathflag onscoop create).- Lock file format.
The parser rejects unknown top-level keys, so a manifest using these will fail cleanly today and stop working when they ship in a future release without a silent behaviour change.
export
Write a portable JSON snapshot of an environment so another machine (or
another teammate) can recreate it with scoop import.
Usage
scoop export <name> [-o <PATH>]
Arguments
| Argument | Required | Description |
|---|---|---|
name | Yes | Name of the environment to export |
Options
| Option | Description |
|---|---|
-o, --output <PATH> | Write to this file instead of stdout |
When -o is omitted, the JSON document is written to stdout and status
messages stay on stderr. That keeps the command pipe-friendly:
scoop export myenv > myenv.json
scoop export myenv | jq '.packages | length'
Schema
The exported file is versioned (scoop_export_version) so a future format
change is detected cleanly rather than silently mis-parsed.
{
"scoop_export_version": "1",
"environment": {
"name": "myproject",
"python": "3.12.7",
"created_at": "2026-05-29T12:34:56+00:00"
},
"packages": [
{ "name": "pytest", "version": "8.0.0" },
{ "name": "black", "version": "24.1.0" }
]
}
Field notes:
environment.pythonis the resolved version recorded in the env’s metadata (e.g.3.12.7), not the original specifier you typed.environment.created_atis RFC 3339 and may be absent for hand-authored or pre-metadata exports.packagesis what the venv’s own pip reports — versions are pinned exactly so imports are reproducible.
Note (Unreleased / post-0.12.0): The export schema is still v1 and intentionally does not include the new
last_usedtimestamp.last_usedis local usage telemetry — it describes how you’ve been using this env, not what an importer needs to recreate it elsewhere. Imported envs start fresh with nolast_usedand the field populates the first time the new env is activated locally.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Export written successfully (or printed to stdout) |
| 1 | Env not found, or writing the destination file failed |
See Also
scoop import— reverse operationscoop sync— declarative manifest with looser pinning
import
Recreate an environment from a scoop export JSON file.
Usage
scoop import <PATH> [--name <NEW_NAME>] [--force] [--json]
scoop import - [--name <NEW_NAME>] # read from stdin
Arguments
| Argument | Required | Description |
|---|---|---|
path | Yes | Path to the export JSON, or - to read from stdin |
Options
| Option | Description |
|---|---|
--name <NAME> | Override the env name from the file (validated like scoop create) |
-f, --force | Overwrite an existing environment with the same name |
--json | Output as JSON |
Behaviour
- Reads and validates the export schema. Mismatched
scoop_export_versionproduces a clearEXPORT_UNSUPPORTED_VERSIONerror pointing at upgrade guidance instead of trying to limp on. - Applies
--nameoverride (if any) and validates the resulting name. - If the target env already exists: errors out unless
--forceis set, in which case the existing env is removed first. - Auto-installs the requested Python if it isn’t already available via uv
(matches the ergonomics of
scoop sync). - Creates the env, then
uv pip installs every pinned package (name==version) in one shot.
Examples
# Plain file -> recreates with the schema's original name
scoop import myenv.json
# Pipe from a sibling machine
ssh other 'scoop export myenv' | scoop import -
# Rename on the fly + overwrite if it already exists
scoop import myenv.json --name myenv-2 --force
# Machine-readable summary for CI
scoop import myenv.json --json
JSON Output
{
"status": "success",
"command": "import",
"data": {
"name": "myenv",
"python": "3.12.7",
"packages_installed": 42,
"source": "/path/to/myenv.json"
}
}
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Imported successfully |
| 1 | Invalid file, unsupported schema version, invalid name, or env existed without --force |
See Also
scoop export— produce the input filescoop sync— for declarative.scoop.toml-driven workflows
clone
Duplicate an environment — same Python version, same packages by default — without going through an export/import roundtrip.
Usage
scoop clone <SRC> <DST> [--no-packages] [--force] [--json]
Arguments
| Argument | Required | Description |
|---|---|---|
src | Yes | Name of the source environment |
dst | Yes | Name for the new environment |
Options
| Option | Description |
|---|---|
--no-packages | Skip package copy — create an empty env at the same Python version |
-f, --force | Overwrite the destination if it already exists |
--json | Output as JSON |
Behaviour
- Validates
<DST>(rejects reserved names likelist,clone, …). - Refuses a self-clone (
src == dst). - Resolves
<SRC>’s recorded Python version from its metadata; surfaces a clearCorruptedEnvironmenterror when metadata is missing so you know recreate-from-scratch is the right next step. - Creates
<DST>at the same Python version. - Unless
--no-packages, lists the source’s installed packages via the venv’s own pip and re-installs them pinned (name==version) into the destination.
Examples
# Full copy
scoop clone myenv myenv-experiment
# Just the shell, no packages
scoop clone myenv myenv-clean --no-packages
# Replace an existing clone
scoop clone myenv myenv-experiment --force
JSON Output
{
"status": "success",
"command": "clone",
"data": {
"src": "myenv",
"dst": "myenv-experiment",
"python": "3.12.7",
"path": "/Users/me/.scoop/virtualenvs/myenv-experiment",
"packages_copied": 12,
"packages_skipped": false
}
}
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Cloned successfully |
| 1 | Invalid dst name, self-clone, src missing, dst exists without --force, or src corrupted |
See Also
scoop export/scoop import— portable JSON for cross-machine duplicationscoop create --force— recreate from scratch with a specific Python version
migrate
Migrate virtual environments from other tools (pyenv-virtualenv, virtualenvwrapper, conda).
Usage
# List migratable environments
scoop migrate list
# Migrate a single environment
scoop migrate @env <name>
# Migrate all environments
scoop migrate all
Subcommands
| Subcommand | Description |
|---|---|
list | List environments available for migration |
@env <name> | Migrate a single environment by name |
all | Migrate all discovered environments |
Supported Sources
| Source | Detection |
|---|---|
| pyenv-virtualenv | ~/.pyenv/versions/ (non-system virtualenvs) |
| virtualenvwrapper | $WORKON_HOME or ~/.virtualenvs/ |
| conda | conda info --envs |
Options
| Option | Subcommand | Description |
|---|---|---|
--source <pyenv|virtualenvwrapper|conda> | all subcommands | Restrict to a single source tool |
--json | all subcommands | Machine-readable output (see JSON Output) |
--dry-run | @env, all | Preview without making changes |
--force | @env, all | Overwrite existing scoop env with the same name; bypass EOL Python guard |
--yes | @env, all | Skip the interactive confirmation prompt |
--strict | @env, all | Fail on the first package install error inside an env (default: keep going) |
--delete-source | @env, all | Remove the source env after successful migration |
--rename <new-name> | @env | Migrate under a different name |
--auto-rename | @env | On name conflict, append -<source> suffix automatically (conflicts with --force) |
Global flags (--quiet, --no-color) apply to all subcommands.
Exit codes
scoop migrate follows the layered exit-code contract. The mapping differs slightly between migrate all (which partitions envs into buckets before iterating) and the single-env paths (@env, list).
migrate all
| Code | Returned when |
|---|---|
0 | (a) all envs migrated, (b) no envs found but source tools are installed, or (c) only non-conflict skips occurred (EOL / corrupted envs in the skipped bucket, no preflight name conflicts, no per-env failures) |
2 | At least one per-env failure or at least one preflight name conflict without --force. Returned via MigrationBatchFailed |
3 | No source tool (pyenv / virtualenvwrapper / conda) is detected on the system. Returned via MigrationSourcesNotFound |
migrate @env <name>
| Code | Returned when |
|---|---|
0 | The env migrated successfully (or the user chose Skip at the interactive conflict prompt) |
2 | MigrationNameConflict (env exists in scoop home, --force not set, non-interactive context) or MigrationFailed (e.g. requested env’s Python is EOL and --force not set) |
3 | The named source env was not found in the requested source (PyenvEnvNotFound, VenvWrapperEnvNotFound, CondaEnvNotFound) or the source env is CorruptedEnvironment |
migrate list
Exit 0 is informational. list only fails when a discovery I/O error occurs (exit 1, via the catchall).
Notes:
- Before v0.14,
migrate allalways exited0regardless of per-env outcome. CI gates need0.14+to distinguish success from a batch where some envs failed. - When
migrate allreturnsMigrationBatchFailed, the human summary and JSON envelope are already on stdout/stderr;main.rssuppresses the globalerror:prefix to avoid duplicate noise.
–force vs –auto-rename (single env, @env)
The two flags are mutually exclusive (clap-enforced via
conflicts_with = "force"). For migrate all only --force is
available; conflicts without --force count toward the exit-2
contract.
--force (recommended for guaranteed resolution)
| Status of source env | With --force |
|---|---|
| Ready | Migrated normally |
| Name conflict with an existing scoop env | The existing scoop env is overwritten in place |
| EOL Python version (e.g. 2.7) | Migrated anyway (the EOL guard is intentionally bypassed) |
| Corrupted source env | Not bypassed — still returns CorruptedEnvironment (exit 3) |
--auto-rename (name-conflict only)
--auto-rename is a convenience for the pure name-conflict case:
when a scoop env with the same name already exists, the migration
proceeds under an auto-generated name. It does not override any
other status (EOL / corrupted), and it does not delete or overwrite
the existing scoop env.
Known limitations (tracked separately from this docs PR):
- The generated name is currently hard-coded as
<name>-pyenveven forvirtualenvwrapper/condasources; the doc-comment promise of<name>-<source>doesn’t yet match the code. - For envs whose status would be both “name conflict” and “EOL”,
conflict detection runs before EOL detection in
determine_status, so the EOL branch is never reached and--auto-renameproceeds with the EOL Python silently. Prefer--forceif you need explicit control over EOL semantics in mixed-status envs.
For deterministic conflict handling in scripts, prefer --force over
--auto-rename until these limitations are addressed.
Examples
List Migratable Environments
$ scoop migrate list
📦 Migratable Environments
pyenv-virtualenv:
• myproject (Python 3.12.0)
• webapp (Python 3.11.8)
conda:
• ml-env (Python 3.10.4)
Migrate Single Environment
$ scoop migrate @env myproject
✓ Migrated 'myproject' from pyenv-virtualenv
Source: ~/.pyenv/versions/myproject
Target: ~/.scoop/virtualenvs/myproject
Migrate All
$ scoop migrate all
✓ Migrated 3 environments
• myproject (pyenv-virtualenv)
• webapp (pyenv-virtualenv)
• ml-env (conda)
CI gate (fail the build on batch failure)
# Exits 2 if any env failed or a name conflict was skipped.
# Exits 3 if no source tool is installed on the runner.
scoop migrate all --yes
JSON Output
All three subcommands accept --json. The envelope follows scoop’s standard
shape: {status, command, data} on success; {status: "error", command, error: { code, message, ... }, data} on failure paths that already
rendered structured data.
migrate list --json
data carries the requested source filter string ("pyenv",
"virtualenvwrapper", "conda", or "all"), the full environments
array, and a summary bucketed by status.
{
"status": "success",
"command": "migrate list",
"data": {
"source": "all",
"environments": [
{
"name": "myproject",
"python_version": "3.12.0",
"path": "/home/u/.pyenv/versions/myproject",
"source_type": "pyenv",
"size_bytes": 12582912,
"status": "ready"
},
{
"name": "oldenv",
"python_version": "2.7.18",
"path": "/home/u/.pyenv/versions/oldenv",
"source_type": "pyenv",
"size_bytes": null,
"status": "python_eol",
"version": "2.7.18"
}
],
"summary": { "total": 2, "ready": 1, "conflict": 0, "eol": 1, "corrupted": 0 }
}
}
The status field is a serde-tagged enum (#[serde(tag = "status", rename_all = "snake_case")]) — "ready", "name_conflict"
(existing payload), "python_eol" (version payload), or
"corrupted" (reason payload). size_bytes is lazily computed and
may be null if not yet requested.
migrate all --json — success path
MigrateAllData carries five top-level data keys. conflicts[] (new in
0.14) is additive — name-conflict envs continue to appear in
skipped[] for backward compatibility, and summary.total == migrated.len() + failed.len() + skipped.len() still holds. conflicts[]
is a structured view so consumers can branch on the failure class
without parsing the localized reason string in skipped[].
{
"status": "success",
"command": "migrate all",
"data": {
"migrated": [
{
"name": "myproject",
"python_version": "3.12.0",
"packages_migrated": 42,
"packages_failed": [],
"dry_run": false,
"path": "/home/u/.scoop/virtualenvs/myproject",
"source_deleted": false,
"actual_python_version": "3.12.0"
}
],
"failed": [],
"skipped": [],
"conflicts": [],
"summary": { "total": 1, "success": 1, "failed": 0, "skipped": 0 }
}
}
actual_python_version may differ from python_version when uv
selected a compatible interpreter (e.g. requested 3.12, resolved to
3.12.4). source_deleted reflects whether --delete-source was
honored for this env. dry_run mirrors the flag the command was
invoked with.
migrate all --json — failure path (exit 2)
Returned when at least one per-env failure occurred, or at least one
preflight name conflict was detected without --force. The envelope
embeds the full data view so consumers don’t lose detail on the
failure side either.
{
"status": "error",
"command": "migrate all",
"error": {
"code": "MIGRATE_BATCH_FAILED",
"message": "Migration finished with 1 failure(s) and 1 name conflict(s)",
"failed_count": 1,
"conflict_count": 1
},
"data": {
"migrated": [],
"failed": [
{
"name": "webapp",
"source_type": "pyenv",
"error_code": "MIGRATE_EXTRACTION_FAILED",
"error": "Couldn't extract packages: pip not found at ..."
}
],
"skipped": [
{ "name": "myproj", "reason": "name conflict (use --force)" }
],
"conflicts": [
{
"name": "myproj",
"source_type": "pyenv",
"existing": "/home/u/.scoop/virtualenvs/myproj"
}
],
"summary": { "total": 2, "success": 0, "failed": 1, "skipped": 1 }
}
}
Per-env failure objects carry two additive fields:
source_type("pyenv","virtualenvwrapper","conda") — origin tool.error_code— the stableScoopError::code()constant (e.g."MIGRATE_EXTRACTION_FAILED","MIGRATE_NAME_CONFLICT","UV_COMMAND_FAILED"). Scripts branch on this instead of parsingerror(which is localized).
Exit-3 paths — no JSON envelope on stdout
Two distinct exit-3 cases exist; neither emits a JSON envelope on
stdout, even under --json. The localized error message and
install/lookup suggestion are written to stderr as plain text; stdout
stays empty. Detect via the exit code.
| Command | Error variant | Trigger |
|---|---|---|
migrate all | MigrationSourcesNotFound | No source tool detected at all (pyenv / virtualenvwrapper / conda) |
migrate @env <name> | PyenvEnvNotFound / VenvWrapperEnvNotFound / CondaEnvNotFound | The named env isn’t present in the requested (or any) source |
migrate @env <name> | CorruptedEnvironment | The named env exists but its layout is broken (missing python, broken pyvenv.cfg, etc) |
Script template:
if ! scoop migrate all --json > out.json; then
case $? in
2) echo "batch failure — read out.json for detail" ;;
3) echo "no source tool installed" ;;
esac
fi
The exit-2 path (batch failure) is the only migrate all failure path
that emits a structured envelope on stdout. Bridging the exit-3
asymmetry would require batch.rs (and single.rs) to call
Output::json_error before returning Err; tracked for a follow-up.
Migration Process
- Discovery: Scans configured source paths for virtual environments
- Extraction: Identifies Python version and installed packages
- Recreation: Creates new scoop environment with same Python version
- Package Install: Reinstalls packages using
uv pip install - Cleanup: Originals are preserved by default;
--delete-sourceremoves them after successful migration
Notes
- Original environments are preserved by default; use
--delete-sourceto remove sources after migration - Package versions are preserved where possible
- Migration creates fresh environments using
uvfor improved performance
Performance
scoop migrate all fans out across all CPU cores via rayon when migrating
more than one environment. The dominant cost (uv venv + pip install per env)
is I/O-bound on subprocesses, so wall-clock time scales close to linearly
with core count.
--dry-run stays sequential — preview output is more useful when ordered.
Progress lines may interleave when multiple envs finish close together. In
the JSON summary, the migrated[] and failed[] arrays are sorted
alphabetically by env name (so worker thread scheduling doesn’t leak into
the output). skipped[] and conflicts[] preserve the scan / partition
order — which itself is deterministic (source-type then name; see
scan_all_environments).
gc
Garbage-collect orphan virtual environments — directories under ~/.scoop/virtualenvs/ that no longer look like working environments, plus (optionally) environments that haven’t been activated in a while.
Usage
scoop gc # Preview orphans only (default)
scoop gc --yes # Actually remove orphans
scoop gc --aggressive # Also flag unused Python versions
scoop gc --aggressive --yes # Remove orphans + unused Pythons
scoop gc --older-than 30d # Also preview envs idle >30 days
scoop gc --older-than 6w --yes # Remove orphans + stale envs (≥6 weeks idle)
What counts as an orphan?
An environment directory is considered an orphan if either:
- It has no
.scoop-metadata.json(it wasn’t created by scoop, or the metadata was deleted), or - Its Python interpreter is missing (
bin/pythonon Unix /Scripts/python.exeon Windows) — typically because the Python version was uninstalled out from under it
Healthy environments are left untouched.
--aggressive
With --aggressive, gc also reports uv-managed Python versions that no surviving environment references. Pair with --yes to uninstall them via uv python uninstall.
Without --aggressive, Python versions are never touched — even ones that look unused — because manually installed interpreters might be intentionally kept around for ad-hoc use.
--older-than <DURATION>
Flag environments whose last_used timestamp is older than the given duration. Accepts <n>d (days), <n>w (weeks = 7d), and <n>y (years = 365d). Examples: 30d, 2w, 1y.
Months are deliberately rejected — m is ambiguous between “minute” and “month”, and calendar months would require timezone-aware arithmetic for a marginal gain in accuracy on a stale-env heuristic. Use 30d or 1y instead.
The maximum allowed value is 200 years (200y); larger values are rejected to keep the resulting cutoff inside chrono’s representable range.
Note on system clock: the cutoff is
Utc::now() - <duration>, so the threshold moves with the system clock. A host whose clock is wrong (NTP compromise, manualdateset, or hibernated VM that woke up with a stale time) can shift which envsgc --older-thanconsiders stale. This is a best-effort heuristic, not a security boundary — pair it with--yesonly when you trust the clock.
Conservative rules
Two cases are never flagged as stale, by design:
last_used = None— fresh envs that have never been activated since the field landed, and envs whose metadata predates the field. Either way we have no positive evidence the env is unused.- Corrupt metadata — if we can’t read the metadata, we don’t pretend to know its age.
If you want to clean up un-activated envs anyway, surface them with
scoop list --sort last-used — envs missing last_used always sort to
the bottom — and remove individual ones with scoop remove <name>.
For scripted enumeration:
scoop list --json | jq -r '.data.virtualenvs[] | select(.last_used == null) | .name'
(Note: scoop verify checks per-env health — metadata / interpreter /
manifest drift — and intentionally does NOT flag a healthy env just
because it has never been activated.)
TOCTOU guard
Between the --older-than scan and the actual delete, an env may be activated. Each candidate is re-checked just before removal:
SkippedRecentlyUsed— the env was touched after the scan;last_usedis now at-or-newer than the original cutoff, so it is no longer stale.SkippedNoData— metadata became unreadable or missing between scan and remove. We refuse to delete envs we can no longer reason about.
Both surface in the JSON envelope as outcome values so scripts can distinguish them from Removed / Failed.
Options
| Option | Description |
|---|---|
-y, --yes | Actually remove the candidates (default: preview only) |
--aggressive | Also remove uv-managed Python versions that no environment uses |
--older-than <DURATION> | Also flag envs idle past the cutoff (30d / 2w / 1y) |
--json | Output as JSON |
Examples
# See what would be removed
scoop gc
# Sample output:
# Orphan virtualenvs (2):
# - broken-env (Python interpreter missing) ~/.scoop/virtualenvs/broken-env
# - rogue-dir (no .scoop-metadata.json) ~/.scoop/virtualenvs/rogue-dir
# (dry run — pass `--yes` to actually remove)
# Actually clean up
scoop gc --yes
JSON output
scoop gc --json
scoop gc --older-than 30d --json
{
"status": "success",
"command": "gc",
"data": {
"dry_run": true,
"envs": [
{ "name": "broken-env", "path": "/Users/x/.scoop/virtualenvs/broken-env", "reason": "broken_python", "outcome": "pending" },
{ "name": "rogue-dir", "path": "/Users/x/.scoop/virtualenvs/rogue-dir", "reason": "missing_metadata", "outcome": "pending" },
{ "name": "old-poc", "path": "/Users/x/.scoop/virtualenvs/old-poc", "reason": "stale", "age_days": 62, "outcome": "pending" }
],
"pythons": []
}
}
reason stays a flat string for all variants — orphans use the
existing "missing_metadata" / "broken_python" values; stale
records add "stale" plus a sibling age_days integer. Old consumers
that match on reason keep working unchanged; the only additive
change is the new outcome values skipped_recently_used and
skipped_no_data.
See also
prune— clean the uv cachedoctor— diagnose without removingremove— delete a specific environment by name
prune
Prune the uv cache — deletes unused download archives, wheels, and source artifacts that uv has cached but no longer needs.
Usage
scoop prune
This is a thin wrapper around uv cache prune. uv decides what’s safe to delete; scoop just forwards the result so you don’t have to remember the exact invocation.
When to use
- After uninstalling Python versions you no longer need
- When disk space on
~/.cache/uv/is filling up - As part of regular cleanup, paired with
scoop gcfor orphan virtualenvs
Options
| Option | Description |
|---|---|
--json | Output the result as JSON |
Examples
# Standard cleanup
scoop prune
# Capture freed-bytes for a script
scoop prune --json | jq -r '.data.output'
See also
verify
Verify the health of one or all virtual environments. Where doctor checks system-wide setup (uv installed, shell wrapper wired up, etc.), verify looks inside each env directory and answers: “does Python actually work in here?”
Usage
scoop verify # Check every environment
scoop verify <NAME> # Check just one environment
scoop verify --json # Machine-readable output
scoop verify --strict # Exit 1 if any check has Fail status (default: always 0)
What gets checked
Six checks run per environment, in order:
| Check | Status | What it means |
|---|---|---|
metadata | Fail | .scoop-metadata.json is missing or unreadable |
python_binary | Fail | bin/python (Scripts/python.exe on Windows) is missing |
pyvenv_cfg | Fail | pyvenv.cfg venv marker is missing |
activate_script | Fail | bin/activate (Scripts/Activate.ps1 on Windows) is missing |
python_executes | Fail | python --version fails to run (Skip if the binary is already missing) |
manifest_match | Warn | env’s Python doesn’t match .scoop.toml if a manifest exists in the cwd hierarchy (Skip otherwise) |
A check returns one of:
- Pass — everything looks right
- Skip — irrelevant for this env (e.g. no
.scoop.toml, or the prerequisite already failed) - Warn — soft issue; env may still work
- Fail — hard breakage; env likely unusable
An env is considered healthy when every check is Pass or Skip.
Exit codes
By default, verify always exits 0 — even when checks fail. This matches doctor’s philosophy: surfacing information shouldn’t break CI just because someone wanted to look at the report. Pass --strict to opt into exit 1 when any env has at least one Fail check (Warn alone does not trigger the non-zero exit).
verify vs doctor vs gc
| Command | Scope | Action |
|---|---|---|
doctor | System (uv install, shell wrapper, paths) | Diagnose + optional --fix |
verify | Per-env (file presence, exec, manifest) | Diagnose only |
gc | All envs (orphan detection) | Diagnose + optional removal |
verify is the “what’s wrong with this specific env?” tool. gc is the “which envs are so broken they should just be removed?” tool. They share territory but differ in granularity and intent.
Examples
# Quick health check on every env
scoop verify
# Specific env, JSON for scripting
scoop verify myproject --json | jq '.data.envs[0].healthy'
# CI gate: fail the build if any env is broken
scoop verify --strict
JSON output
{
"status": "success",
"command": "verify",
"data": {
"envs": [
{
"name": "myenv",
"healthy": true,
"python": "3.12.0",
"checks": [
{ "name": "metadata", "status": "pass" },
{ "name": "python_binary", "status": "pass" },
{ "name": "pyvenv_cfg", "status": "pass" },
{ "name": "activate_script", "status": "pass" },
{ "name": "python_executes", "status": "pass" },
{ "name": "manifest_match", "status": "skip" }
]
}
],
"summary": { "total": 1, "healthy": 1, "issues": 0 }
}
}
See also
doctor— system-level diagnosticsgc— remove broken envs detected hereinfo— detailed view of a single env (without health checks)
diff
Compare two virtualenvs across Python version, installed packages, and metadata. Useful for spotting drift between teammates’ envs, between a dev env and an export, or between a baseline and a working env when something breaks.
Usage
scoop diff <env-a> <env-b> # human table, exit 0
scoop diff <env-a> <env-b> --json # machine-readable
scoop diff <env-a> <env-b> --strict # exit 1 if any diff
scoop diff <env-a> <env-b> --packages-only # skip metadata section
scoop diff <env-a> <env-b> --metadata-only # skip package enumeration
What gets compared
Three independent sections, all enabled by default:
| Section | Fields |
|---|---|
| Python | python_version from each env’s metadata |
| Packages | name==version set via uv pip list --format=json on each env |
| Metadata | python_version, created_at, last_used, uv_version |
Package matching uses PEP 503
canonical names (lowercase, -/_/. collapsed to single -), so
Requests vs requests and flask_sqlalchemy vs Flask-SQLAlchemy
are treated as the same package.
Options
| Option | Description |
|---|---|
--json | Output as JSON (see JSON Output) |
--strict | Exit 1 if any differences are detected (default: always exit 0) |
--packages-only | Skip the metadata section; package enumeration still runs |
--metadata-only | Skip package enumeration (no uv subprocess); metadata section only |
--packages-only and --metadata-only are mutually exclusive
(clap-enforced).
Global flags (--quiet, --no-color) apply.
Exit codes
scoop diff follows the layered exit-code contract:
| Code | Returned when |
|---|---|
0 | Default — diff reported (even if envs differ) |
1 | --strict was set AND at least one difference was detected (DiffMismatch). Same precedent as verify --strict. |
Operational failures (env not found, uv missing, corrupt metadata)
return the appropriate underlying ScoopError variant via the
catchall exit 1 path; --strict is not required for those.
Examples
Identical envs
$ scoop diff webapp webapp-mirror
Environments are identical
Mixed differences
$ scoop diff webapp webapp-mirror
Python
~ python a: 3.12.0 b: 3.11.9
Packages (3 differences)
- numpy==1.26.0
+ pandas==2.2.0
~ requests: 2.31.0 → 2.32.0
Metadata
python_version a: 3.12.0 b: 3.11.9
~ created_at a: 2025-01-10T00:00:00Z b: 2025-03-22T00:00:00Z
last_used a: - b: -
uv_version a: 0.5.14 b: 0.5.14
The ~ marker flags changed scalar fields; -/+ flag removed/added
packages; absent metadata values render as -.
CI gate (fail the build on drift)
scoop diff baseline production --strict
# exits 1 if baseline and production diverge
JSON output
--json emits one envelope to stdout. Success and failure envelopes
share the same data shape; only the top-level wrapper differs.
Success envelope (default exit 0)
{
"status": "success",
"command": "diff",
"data": {
"env_a": "webapp",
"env_b": "webapp-mirror",
"identical": false,
"python": { "a": "3.12.0", "b": "3.11.9", "changed": true },
"packages": {
"added": [{"name": "pandas", "version": "2.2.0", "display_name": "pandas"}],
"removed": [{"name": "numpy", "version": "1.26.0", "display_name": "numpy"}],
"changed": [{"name": "requests", "version_a": "2.31.0", "version_b": "2.32.0"}]
},
"metadata": {
"python_version": { "a": "3.12.0", "b": "3.11.9", "changed": true },
"created_at": { "a": "2025-01-10T00:00:00Z", "b": "2025-03-22T00:00:00Z", "changed": true },
"last_used": { "a": null, "b": null, "changed": false },
"uv_version": { "a": "0.5.14", "b": "0.5.14", "changed": false }
},
"summary": {
"differences": 5,
"python_changed": true,
"packages_added": 1,
"packages_removed": 1,
"packages_changed": 1,
"metadata_fields_changed": 2
}
}
}
Strict failure envelope (--strict with differences, exit 1)
{
"status": "error",
"command": "diff",
"error": {
"code": "DIFF_MISMATCH",
"message": "'webapp' and 'webapp-mirror' differ (5 difference(s))",
"env_a": "webapp",
"env_b": "webapp-mirror",
"differences": 5
},
"data": { "...": "same shape as success" }
}
Scalar diff fields — 2-state contract
Each metadata field in data.metadata is a ScalarDiff carrying both
sides as Option<T>:
a | b | changed |
|---|---|---|
"x" | "x" | false |
"x" | "y" | true |
"x" | null | true |
null | null | false |
The null side means the value is not observable on that side —
regardless of whether the metadata file was missing, the field was
absent, or the field was present-as-null in the JSON. Diff intentionally
does not surface the cause; if you need to know why, run scoop info
on the env directly. (The data.packages shape uses non-nullable
inner objects because pip never emits a “package present but no
version” state.)
Mode-suppressed sections
--packages-only→data.metadataisnull.--metadata-only→data.packagesisnull; nouvsubprocess is spawned.
data.python and data.summary are always present.
See also
verify— per-env health checks (different scope: one env at a time, not pairwise)info— detailed view of a single envlist— enumerate all envsapi.md— full process exit-code contract
lang
Get or set the display language for scoop CLI messages.
Usage
# Show current language
scoop lang
# Set language
scoop lang <code>
# List supported languages
scoop lang --list
# Reset to system default
scoop lang --reset
Arguments
| Argument | Description |
|---|---|
<code> | Language code to set (e.g., en, ko) |
Options
| Option | Description |
|---|---|
--list | List all supported languages |
--reset | Reset to system default language |
--json | Output as JSON |
Supported Languages
| Code | Language |
|---|---|
en | English (default) |
ko | 한국어 (Korean) |
ja | 日本語 (Japanese) |
pt-BR | Português (Brazilian Portuguese) |
Language Detection Priority
SCOOP_LANGenvironment variable~/.scoop/config.jsonsetting- System locale (via
sys-locale) - Default:
en
Examples
Show Current Language
$ scoop lang
Current language: en (English)
Set Korean
$ scoop lang ko
✓ Language set to Korean (한국어)
List Languages
$ scoop lang --list
Supported languages:
en - English
ko - 한국어 (Korean)
ja - 日本語 (Japanese)
pt-BR - Português (Brazilian Portuguese)
Reset to System Default
$ scoop lang --reset
✓ Language reset to system default
JSON Output
$ scoop lang --json
{
"status": "success",
"data": {
"current": "ko",
"name": "한국어",
"source": "config"
}
}
Configuration
Language preference is stored in:
~/.scoop/config.json
{"lang": "ko"}
Environment Variable Override
# Temporarily use English regardless of config
SCOOP_LANG=en scoop list
# Set for current session
export SCOOP_LANG=ko
Notes
- CLI help text (
--help) remains in English (industry standard) - JSON output keys remain in English (machine-readable)
- Error messages, success messages, and prompts are translated
init
Output shell initialization script.
Usage
scoop init <shell>
Arguments
| Argument | Required | Description |
|---|---|---|
shell | Yes | Shell type: bash, zsh, fish, powershell |
Setup
Add to your shell configuration:
# Bash (~/.bashrc)
eval "$(scoop init bash)"
# Zsh (~/.zshrc)
eval "$(scoop init zsh)"
# Fish (~/.config/fish/config.fish)
eval (scoop init fish)
# PowerShell ($PROFILE)
Invoke-Expression (& scoop init powershell)
Features Enabled
- Auto-activation when entering directories with
.scoop-version - Tab completion for commands, environments, and options
- Wrapper function for
activate/deactivate/use
Examples
scoop init bash # Output bash init script
scoop init zsh # Output zsh init script
scoop init fish # Output fish init script
scoop init powershell # Output PowerShell init script
shell
Set shell-specific environment (current shell session only).
Unlike scoop use which writes to a file, scoop shell sets the SCOOP_VERSION environment variable for the current shell session only.
Usage
eval "$(scoop shell <name>)" # Bash/Zsh
eval (scoop shell <name>) # Fish
Note: If you have shell integration set up (
scoop init), theevalis automatic:scoop shell myenv # Works directly
Arguments
| Argument | Required | Description |
|---|---|---|
name | No | Environment name or system |
Options
| Option | Description |
|---|---|
--unset | Clear shell-specific environment |
--shell <SHELL> | Target shell type (auto-detected if not specified) |
Behavior
- Sets
SCOOP_VERSIONenvironment variable - If
nameis an environment: also outputs activation script - If
nameissystem: also outputs deactivation script --unset: outputsunset SCOOP_VERSION
Priority
SCOOP_VERSION has the highest priority in version resolution:
1. SCOOP_VERSION env var <- scoop shell (highest)
2. .scoop-version file <- scoop use
3. ~/.scoop/version <- scoop use --global
This means scoop shell overrides any file-based settings until:
- You run
scoop shell --unset - You close the terminal
Examples
# Use a specific environment in this terminal
scoop shell myproject
# Use system Python in this terminal
scoop shell system
# Clear the shell setting (return to file-based resolution)
scoop shell --unset
# Explicit shell type
scoop shell --shell fish myenv
Use Cases
Temporary Testing
# Currently using myproject
scoop shell testenv # Switch to testenv temporarily
python test.py # Test something
scoop shell myproject # Switch back
Override Project Settings
cd ~/project # Has .scoop-version = projectenv
scoop shell system # Use system Python anyway
python --version # System Python
scoop shell --unset # Back to projectenv
completions
Generate shell completion script.
Usage
scoop completions <shell>
Arguments
| Argument | Required | Description |
|---|---|---|
shell | Yes | Shell type: bash, zsh, fish, powershell |
Examples
scoop completions bash # Output bash completions
scoop completions zsh # Output zsh completions
scoop completions fish # Output fish completions
Tip: Usually you don’t need this separately -
scoop initincludes completions.
man
Generate Unix man pages from scoop’s clap::Command tree. Because they’re rendered from the live CLI definition, the man pages always reflect the actual --help text — no separate documentation to keep in sync.
Usage
# Print the top-level scoop.1 to stdout
scoop man
# Preview with `man -l`
scoop man | man -l -
# Write scoop.1 + scoop-<sub>.1 (one per subcommand) into a directory
scoop man /tmp/scoop-man
Arguments
| Argument | Description |
|---|---|
[DIR] | Write scoop.1 + scoop-<sub>.1 files into this directory. Omit to print the top-level page to stdout. |
Options
| Option | Description |
|---|---|
--json | Output as JSON (only meaningful with DIR) |
Packager usage
Distro packagers can wire this into their build recipe:
# In your build script
mkdir -p $PKG_DIR/usr/share/man/man1
./scoop man $PKG_DIR/usr/share/man/man1
gzip -9 $PKG_DIR/usr/share/man/man1/*.1
Hidden subcommands (activate, deactivate, resolve — internal to the shell wrapper) are intentionally not rendered: they’re not user-facing.
See also
completions— shell completion scripts (also generated fromclap::Command)
Contributing
Guide for contributing to scoop development.
Prerequisites
Setup
# Clone the repository
git clone https://github.com/ai-screams/scoop-uv.git
cd scoop-uv
# Install prek (Rust-native pre-commit alternative)
uv tool install prek
# or: cargo install prek
# Install git hooks
prek install
# Build
cargo build
# Run tests
cargo test
Project Structure
src/
├── main.rs # Entry point
├── lib.rs # Library root
├── error.rs # Error types (ScoopError)
├── paths.rs # Path utilities
├── validate.rs # Name/version validation
├── uv/ # uv client wrapper
│ ├── mod.rs
│ └── client.rs
├── core/ # Business logic
│ ├── mod.rs
│ ├── virtualenv/ # VirtualenvService (mod.rs + tests.rs)
│ ├── version.rs # VersionService
│ ├── metadata.rs # Metadata structs
│ └── doctor.rs # Health diagnostics
├── cli/ # CLI layer
│ ├── mod.rs # Cli struct, Commands enum
│ └── commands/ # Command handlers
│ ├── mod.rs
│ ├── list.rs
│ ├── create.rs
│ ├── use_env/ # Use command (normal, system, unset, symlink)
│ ├── remove.rs
│ ├── install.rs
│ ├── doctor.rs
│ ├── migrate/ # Migration subcommands
│ └── ...
├── shell/ # Shell integration
│ ├── mod.rs
│ ├── common.rs # Shared utilities (version check macros)
│ ├── bash.rs
│ ├── zsh.rs
│ ├── fish.rs
│ └── powershell.rs
└── output/ # Output formatting
├── mod.rs
└── spinner.rs
docs/ # Public documentation
.docs/ # Internal technical docs
tests/ # Integration tests
Common Commands
Build and Run
cargo build # Debug build
cargo build --release # Release build (optimized)
cargo run -- --help # Show help
cargo run -- list # Run commands
cargo run -- doctor # Check setup health
Quick Quality Check
# All checks at once (recommended before commit)
cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings && cargo test
For detailed guides, see:
- Testing - Comprehensive testing guide
- Code Quality - Formatting, linting, pre-commit hooks
Architecture
Key Services
VirtualenvService (src/core/virtualenv/)
- Manages virtualenvs in
~/.scoop/virtualenvs/ - Wraps uv commands for venv creation
VersionService (src/core/version.rs)
- Manages
.scoop-versionfiles - Resolves current directory to active environment
Doctor (src/core/doctor.rs)
- Health diagnostics for scoop setup
- Checks uv, shell integration, paths, environments
UvClient (src/uv/client.rs)
- Wrapper for
uvCLI commands - Python version management
Shell Integration
Shell scripts are embedded in Rust code:
src/shell/bash.rs- Bash init scriptsrc/shell/zsh.rs- Zsh init script
Key components:
- Wrapper function - Intercepts
use/activate/deactivate - Hook function - Auto-activation on directory change
- Completion function - Tab completion
Adding a New Command
- Define command in
src/cli/mod.rs:
#![allow(unused)]
fn main() {
#[derive(Subcommand)]
pub enum Commands {
// ...
MyCommand {
#[arg(short, long)]
option: bool,
},
}
}
- Create handler in
src/cli/commands/my_command.rs:
#![allow(unused)]
fn main() {
pub fn execute(output: &Output, option: bool) -> Result<()> {
// Implementation
Ok(())
}
}
-
Export in
src/cli/commands/mod.rs -
Wire up in
src/main.rs -
Add shell completion in
src/shell/{bash,zsh}.rs
Testing
Unit Tests
Located within source files:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_something() {
// ...
}
}
}
Integration Tests
Located in tests/:
#![allow(unused)]
fn main() {
use assert_cmd::Command;
#[test]
fn test_cli_command() {
Command::cargo_bin("scoop")
.unwrap()
.args(["list"])
.assert()
.success();
}
}
Release Process
Releases are automated via release-plz:
- Create PR with changes
- Merge to main
- release-plz creates release PR
- Merge release PR to publish to crates.io
Internal Documentation
See .docs/ for internal technical references:
TECHNICAL_REFERENCE.md- Implementation detailsSHELL_GOTCHAS.md- Shell integration pitfallsIMPLEMENTATION_PLAN.md- Development roadmapbrand/brand.md- Brand guidelines
Code Style
- Follow Rust conventions
- Run
cargo fmtbefore committing - Keep functions small and focused
- Document public APIs with
///comments - Use
thiserrorfor error types - Translated error messages with solutions (en, ko, ja, pt-BR)
Translation Guide
This document provides guidelines for contributing translations to scoop.
Current Status
For the latest translation status, see:
- Issue #42: i18n Translation Tracking
- Run
scoop lang --listto see currently supported languages
Contribution Process
Step 1: Fork and Clone
git clone https://github.com/YOUR_USERNAME/scoop-uv.git
cd scoop-uv
Step 2: Add Translations
Edit locales/app.yml and add your language to every key:
create.success:
en: "Created '%{name}' environment"
ko: "'%{name}' 환경 생성됨"
pt-BR: "Ambiente '%{name}' criado"
{ lang }: "Your translation here" # Add your language code and translation
Important:
- Add translations to all ~115 keys
- Keep placeholder syntax exactly:
%{name},%{version}, etc. - Preserve special characters:
→, quotes, backticks
Step 3: Register Language
Edit src/i18n.rs and add your language to SUPPORTED_LANGS:
#![allow(unused)]
fn main() {
pub const SUPPORTED_LANGS: &[(&str, &str)] = &[
("en", "English"),
("ko", "한국어"),
("pt-BR", "Português (Brasil)"),
("{lang}", "Your Language Name"), // Add your language
];
}
Language Code Format:
- Use BCP 47 format
- Simple languages:
ja,fr,es,de,it - Regional variants:
pt-BR,zh-CN,zh-TW,es-MX
Step 4: Test Locally
# Build and test
cargo build
cargo test
# Test your language (replace {lang} with your language code)
SCOOP_LANG={lang} ./target/debug/scoop --help
SCOOP_LANG={lang} ./target/debug/scoop lang
Step 5: Create Pull Request
Required files in PR:
-
locales/app.yml- All 115 keys translated -
src/i18n.rs- Language registered in SUPPORTED_LANGS
PR Title Format:
docs(i18n): add {Language Name} translation
Style Guidelines
Philosophy: Your Language, Your Style
We trust translators. You know your language and community best.
- Word choice is yours — Pick terms that feel natural to native speakers
- Creativity welcome — Witty expressions are fine if they’re clear and widely understood
- Casual over formal — scoop is a friendly CLI tool, not enterprise software
General Principles
- Concise: CLI messages should be short and clear
- Natural: Use natural phrasing, not word-for-word translation
- Casual: Friendly, approachable tone — like talking to a colleague
- Clear: Wit is great, but clarity comes first
Tone Examples
# Too formal (avoid)
"The environment has been successfully created."
# Too robotic (avoid)
"Environment creation: complete."
# Good - casual and clear
"Created 'myenv' — ready to go!"
"'myenv' is ready"
Message Types
| Type | English Example | Guidance |
|---|---|---|
| Progress | “Installing…” | Use progressive/ongoing form |
| Success | “Created ‘myenv’” | Completion — feel free to add flair |
| Error | “Can’t find ‘myenv’” | Clear and actionable |
| Hint | “→ Create: scoop create…” | Helpful, not lecturing |
Translator’s Discretion
These decisions are up to you:
- Vocabulary: Choose words that resonate with your community
- Idioms: Use local expressions if they fit naturally
- Humor: Light wit is welcome (e.g., ice cream puns if appropriate)
- Formality level: Lean casual, but match your culture’s CLI norms
Only requirement: The meaning must be clear to users.
Technical Terms
For technical vocabulary:
- Check your community — What do Python developers in your language use?
- Consistency — Pick one term and stick with it throughout
- Loanwords OK — If your community uses English terms (e.g., “install”), that’s fine
Tip: Study existing translations in locales/app.yml for reference, but don’t feel bound by them.
Glossary
Do NOT Translate
These terms should remain in English in all languages:
| Term | Reason |
|---|---|
scoop | Brand name |
uv | Tool name |
pyenv | Tool name |
conda | Tool name |
virtualenv | Technical term |
virtualenvwrapper | Tool name |
Python | Language name |
shell | Technical term (bash, zsh, fish) |
JSON | Format name |
PATH | Environment variable |
pip | Tool name |
Commands - Never Translate
All commands and code examples must stay in English:
# WRONG - Command translated
hint: "→ Create: {translated_command} myenv 3.12"
# CORRECT - Only description translated
hint: "→ {translated_word}: scoop create myenv 3.12"
Common Terms to Translate
These are core concepts you’ll need to translate. Reference existing translations for consistency:
| English | What to look for |
|---|---|
| environment | Your language’s term for “environment” |
| create | Common verb for “make/create” |
| remove/delete | Common verb for “delete/remove” |
| install | Standard software installation term |
| uninstall | Standard software removal term |
| activate | Term for “enable/turn on” |
| deactivate | Term for “disable/turn off” |
| migrate | IT term for migration (often kept as loanword) |
| version | Your language’s term for “version” |
| path | Your language’s term for file path |
| error | Your language’s term for “error” |
| success | Your language’s term for “success” |
Tip: Check how these terms are translated in existing translations for reference.
Ice Cream Metaphor (README only)
scoop uses ice cream metaphors in documentation:
| Term | Meaning | Guidance |
|---|---|---|
| scoop | The tool | Always keep as “scoop” |
| flavor | virtualenv | Translate if the metaphor works in your language |
| freezer | ~/.scoop/ directory | Translate if the metaphor works |
Note: The metaphor is mainly in README.md, not in CLI messages (locales/app.yml).
File Structure
locales/app.yml
# Categories in order:
# 1. lang.* - Language command messages
# 2. create.* - Create command messages
# 3. remove.* - Remove command messages
# 4. list.* - List command messages
# 5. use.* - Use command messages
# 6. install.* - Install command messages
# 7. uninstall.* - Uninstall command messages
# 8. migrate.* - Migrate command messages
# 9. error.* - Error messages
# 10. suggestion.* - Suggestion/hint messages
src/i18n.rs
#![allow(unused)]
fn main() {
// Language detection priority:
// 1. SCOOP_LANG environment variable
// 2. Config file (~/.scoop/config.json)
// 3. System locale
// 4. Default: "en"
pub const SUPPORTED_LANGS: &[(&str, &str)] = &[
("en", "English"),
// ... existing languages
// Add new languages here
];
}
Common Mistakes
1. Missing SUPPORTED_LANGS Registration
Symptom: Translation exists but scoop lang {code} doesn’t work
Fix: Add language to src/i18n.rs SUPPORTED_LANGS
2. Broken Placeholders
# WRONG - Missing placeholder
error: "Cannot find environment"
# CORRECT - Placeholder preserved
error: "Cannot find '%{name}' environment"
3. Translating Commands
# WRONG - Command translated
hint: "→ List: {translated} list"
# CORRECT - Only label translated
hint: "→ {Translated Label}: scoop list"
4. Inconsistent Key Coverage
All languages must have ALL keys. Missing keys fall back to English.
Testing Checklist
Before submitting PR:
- All 115 keys translated
- All placeholders preserved (
%{name},%{version}, etc.) - Language registered in SUPPORTED_LANGS
-
cargo buildsucceeds -
cargo testpasses -
SCOOP_LANG={code} scoop langshows your language - Messages display correctly in terminal
Questions?
- Open an issue: GitHub Issues
- See existing translations for reference:
locales/app.yml
Documentation Translation (mdBook)
For translating CLI strings (the ~115 keys in
locales/app.ymlconsumed by the Rust binary), see translation instead. This page covers user-documentation translation only.
scoop’s user documentation lives in docs/src/*.md and is the
single English source of truth. Translations are layered on top
via gettext .po files
under docs/po/, processed by the
mdbook-i18n-helpers
preprocessor at render time. Untranslated strings automatically
fall back to English, so a partial translation is always
deployable.
URL layout
- English (canonical):
https://ai-screams.github.io/scoop-uv/ - Korean:
https://ai-screams.github.io/scoop-uv/ko/
The locale switcher in the top-right of every page jumps between the same page on each side.
Prerequisites
# macOS
brew install gettext # msginit / msgmerge / msgfmt
cargo install mdbook --version 0.5.3 --locked
cargo install mdbook-i18n-helpers --version 0.4.0 --locked
# Linux
sudo apt-get install -y gettext
# Then the same `cargo install` lines as above.
The cargo install step needs Rust 1.88 or newer (helpers’
upstream MSRV). The project’s rust-toolchain.toml pins 1.85
for the CRATE itself; for the docs tooling, use cargo +stable install ... from outside the repo, or just rely on whatever
stable toolchain is on ubuntu-latest in CI.
Workflow: updating an existing translation
When you edit a page in docs/src/, the existing translations
need to learn about the new / changed strings.
cd docs
# 1. Re-extract template (overwrites docs/po/messages.pot).
MDBOOK_OUTPUT='{"xgettext": {}}' mdbook build -d po
# 2. Merge the new template into your locale's .po file.
# --backup=none avoids leaving a stray ko.po~ around.
msgmerge --update --backup=none po/ko.po po/messages.pot
# 3. Open po/ko.po in your editor. Look for:
# - new empty `msgstr ""` entries → add translations
# - "#, fuzzy" markers → review and remove flag
docs/po/messages.pot is in .gitignore — it’s regenerated on
every CI run, committing it would create churn. The locale .po
files (docs/po/ko.po, etc.) ARE committed; they’re the
translation memory.
Workflow: previewing a translated build
cd docs
# English (root)
mdbook build -d book
open book/index.html
# Korean (book/ko subdir)
MDBOOK_BOOK__LANGUAGE=ko mdbook build -d book/ko
open book/ko/index.html
mdbook serve works too if you want live reload:
MDBOOK_BOOK__LANGUAGE=ko mdbook serve -d book/ko
Workflow: adding a brand-new locale (e.g. ja)
cd docs
# Initialise an empty translation file.
msginit -i po/messages.pot -l ja -o po/ja.po --no-translator
# Translate msgids in po/ja.po (untranslated ones fall back to English).
# Add a CI build step for the new locale in
# .github/workflows/docs.yml, e.g.:
#
# - name: Build Japanese (book/ja)
# working-directory: docs
# env:
# MDBOOK_BOOK__LANGUAGE: ja
# run: mdbook build -d book/ja
#
# Then expand the locale switcher in docs/theme/head.hbs to
# include the new locale link.
CI guard: stale translations
The Verify ko translations are in sync step in
.github/workflows/docs.yml regenerates the pot template from
the current English source, runs msgmerge --update against
the committed ko.po, and fails the build if the result
differs. This means English edits land with their corresponding
.po updates in the same PR, or they don’t land at all.
To fix a CI failure on this step:
cd docs
MDBOOK_OUTPUT='{"xgettext": {}}' mdbook build -d po
msgmerge --update --backup=none po/ko.po po/messages.pot
git add po/ko.po
git commit --amend # or as a separate fixup commit
Style guidelines
The same casual-tone guidelines from translation
apply here. Don’t try to translate code samples or CLI commands
literally — only the prose around them. Code-block content is
shown verbatim regardless of locale; mdbook-i18n-helpers
intentionally does not interpolate translations inside fenced
code blocks for unmarked code, though it does extract code
comments (# Install Python) as separate msgids so you can
translate those if it helps readability.
Known limitations
- Search index is built per-locale. Korean search results only hit Korean pages, English only hits English. This is the intended mdBook behaviour.
- The locale switcher uses a JS-injected DOM element; users with
JS disabled won’t see it. They can navigate via direct URL
(
/scoop-uv/ko/...) or browser bookmarks. - mdbook-i18n-helpers’
xgettextdoesn’t extract HTML tables’ cell-by-cell content as separate msgids when the table is written in pipe-syntax — it extracts the whole table as a single block. For now this is acceptable; for fine-grained table translation we’d need to switch to per-row HTML tables in the source.
Architecture
scoop is built in Rust using a modular architecture.
Module Structure
src/
├── cli/ # Command-line interface
│ ├── mod.rs # Cli struct, Commands enum, ShellType
│ └── commands/ # Individual command handlers
├── core/ # Domain logic
│ ├── metadata.rs # Virtualenv metadata (JSON)
│ ├── version.rs # Version file resolution
│ ├── virtualenv/ # Virtualenv entity (mod.rs + tests.rs)
│ ├── doctor.rs # Health check system
│ └── migrate/ # Migration from pyenv/conda/virtualenvwrapper
│ ├── mod.rs
│ ├── discovery.rs # Source detection
│ ├── migrator.rs # Migration orchestrator
│ └── ...
├── shell/ # Shell integration
│ ├── mod.rs # Shell module exports & detection
│ ├── common.rs # Shared shell utilities & macros
│ ├── bash.rs # Bash init script
│ ├── zsh.rs # Zsh init script
│ ├── fish.rs # Fish init script
│ └── powershell.rs # PowerShell init script
├── output/ # Terminal UI and JSON output
├── uv/ # uv CLI wrapper
├── error.rs # ScoopError enum
├── paths.rs # Path utilities
├── validate.rs # Input validation
├── i18n.rs # Internationalization
└── config.rs # Configuration management
Module Dependency Graph
graph TB
CLI[cli/] --> Core[core/]
CLI --> Shell[shell/]
CLI --> Output[output/]
CLI --> I18N[i18n]
CLI --> Config[config]
Core --> UV[uv/]
Core --> Paths[paths]
Core --> Error[error]
Core --> Validate[validate]
Core --> Config
Shell --> Paths
Output --> Error
Output --> I18N
UV --> Error
Config --> Paths
Config --> Error
style CLI fill:#e1f5ff
style Core fill:#fff3e0
style Shell fill:#f3e5f5
style Output fill:#e8f5e9
Key Components
CLI Layer (cli/)
- Uses clap for argument parsing
Clistruct defines global optionsCommandsenum defines subcommands- Each command has an
executefunction incommands/
Core Layer (core/)
| Module | Purpose |
|---|---|
doctor | Health check system with Check trait |
metadata | JSON metadata for virtualenvs |
version | Version file discovery and parsing |
virtualenv | Virtualenv entity and operations |
Shell Layer (shell/)
Generates shell scripts for integration:
init_script()- Returns shell initialization code- Wrapper function for
scoopcommand - Auto-activation hooks
- Tab completion definitions
Output Layer (output/)
Handles terminal output formatting:
- Colored output using owo-colors
- JSON output for scripting
- Progress indicators with indicatif
UV Layer (uv/)
Wraps the uv CLI for Python/virtualenv operations:
- Python installation
- Virtualenv creation
- Version listing
Design Patterns
Shell Eval Pattern
The CLI outputs shell code to stdout, which the shell evaluates:
# User runs
scoop activate myenv
# CLI outputs
export VIRTUAL_ENV="/Users/x/.scoop/virtualenvs/myenv"
export PATH="/Users/x/.scoop/virtualenvs/myenv/bin:$PATH"
export SCOOP_ACTIVE="myenv"
# Shell wrapper evaluates this output
eval "$(command scoop activate myenv)"
This pattern is used by pyenv, rbenv, and other version managers.
Error Handling
Uses thiserror for error types:
#![allow(unused)]
fn main() {
// Uses thiserror for Error trait derive
// Display impl is manual (not #[error] attributes) for i18n support
#[derive(Debug, Error)]
pub enum ScoopError {
VirtualenvNotFound { name: String },
VirtualenvExists { name: String },
// ...
}
// Manual Display implementation using rust-i18n
impl std::fmt::Display for ScoopError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::VirtualenvNotFound { name } => {
write!(f, "{}", t!("error.virtualenv_not_found", name = name))
}
// ...
}
}
}
}
Path Management
Centralizes path logic in paths.rs:
scoop_home()- ReturnsSCOOP_HOMEor~/.scoopvirtualenvs_dir()- Returns virtualenvs directoryglobal_version_file()- Returns global version file path (~/.scoop/version)local_version_file(dir)- Returns local version file path in directory
Data Flow
Command Execution Flow
sequenceDiagram
participant User
participant CLI as CLI Parser
participant Cmd as Command Handler
participant Core as Core Logic
participant UV as UV Wrapper
participant Output as Output Formatter
User->>CLI: scoop create myenv 3.12
CLI->>Cmd: parse & dispatch
Cmd->>Core: VirtualenvService::create()
Core->>UV: uv venv create
UV-->>Core: success/error
Core-->>Cmd: Result<()>
Cmd->>Output: format response
Output-->>User: stdout/stderr
Shell Integration Flow
sequenceDiagram
participant User
participant Shell as Shell Wrapper
participant CLI as scoop CLI
participant Core as Core Logic
User->>Shell: scoop use myenv
Shell->>CLI: command scoop use myenv
CLI->>Core: resolve version & path
Core-->>CLI: env vars to export
CLI-->>Shell: echo shell script
Shell->>Shell: eval output
Shell-->>User: (myenv) $
Note over User: Environment activated
Version Resolution Flow
graph LR
Start([User runs command]) --> Env{SCOOP_VERSION<br/>env set?<br/><small>shell hook</small>}
Env -->|Yes| Use[Use env value]
Env -->|No| Local{.scoop-version<br/>in current/parent<br/>dirs?}
Local -->|Yes| Use
Local -->|No| Global{~/.scoop/version<br/>exists?}
Global -->|Yes| Use
Global -->|No| None[No version<br/>system Python]
style Use fill:#c8e6c9
style None fill:#fff9c4
Note:
.python-versionis not supported. Version resolution walks up parent directories to find.scoop-version.
Health Check Flow
flowchart TD
Start([scoop doctor]) --> Init[Initialize Doctor]
Init --> Run[Run all checks]
Run --> UV{UV Check}
UV -->|Pass| Home{Home Check}
UV -->|Fail| Fix1[Suggest: install uv]
Home -->|Pass| Venv{Venv Check}
Home -->|Fail| Fix2[Auto-fix: mkdir]
Venv -->|Pass| Link{Symlink Check}
Venv -->|Warn| Warn1[Warn: corrupted env]
Link -->|Pass| Shell{Shell Check}
Link -->|Fail| Fix3[Auto-fix: remove broken links]
Shell -->|Pass| Ver{Version Check}
Shell -->|Warn| Warn2[Warn: not initialized]
Ver -->|Pass| Done([All checks passed])
Ver -->|Warn| Warn3[Warn: invalid version file]
Fix1 --> Report[Generate Report]
Fix2 --> Report
Fix3 --> Report
Warn1 --> Report
Warn2 --> Report
Warn3 --> Report
Done --> Report
style Done fill:#c8e6c9
style Fix1 fill:#ffcdd2
style Fix2 fill:#ffcdd2
style Fix3 fill:#ffcdd2
style Warn1 fill:#fff9c4
style Warn2 fill:#fff9c4
style Warn3 fill:#fff9c4
Migration Architecture
scoop supports migrating environments from pyenv-virtualenv, virtualenvwrapper, and conda.
graph TD
Start([scoop migrate]) --> Detect[Detect Sources]
Detect --> Pyenv{pyenv-virtualenv}
Detect --> Venv{virtualenvwrapper}
Detect --> Conda{conda}
Pyenv -->|Found| P1[List ~/.pyenv/versions]
Venv -->|Found| V1[List $WORKON_HOME]
Conda -->|Found| C1[conda env list]
P1 --> Parse[Parse metadata]
V1 --> Parse
C1 --> Parse
Parse --> Create[Create in scoop]
Create --> Copy[Copy packages]
Copy --> Meta[Write metadata]
Meta --> Done([Migration complete])
Pyenv -->|Not found| Skip1[Skip]
Venv -->|Not found| Skip2[Skip]
Conda -->|Not found| Skip3[Skip]
Skip1 --> Check{Any source found?}
Skip2 --> Check
Skip3 --> Check
Check -->|Yes| Done
Check -->|No| Error[Error: No sources]
style Done fill:#c8e6c9
style Error fill:#ffcdd2
Dependencies
| Crate | Purpose |
|---|---|
| clap | Argument parsing & completion |
| clap_complete | Shell completion generation |
| serde | JSON serialization |
| serde_json | Metadata persistence |
| thiserror | Error type definitions |
| owo-colors | Terminal colors |
| indicatif | Progress bars & spinners |
| dialoguer | Interactive prompts |
| dirs | Home directory resolution |
| which | Binary lookup (uv, python) |
| regex | Version parsing & validation |
| walkdir | Directory traversal |
| rust-i18n | Internationalization (en, ko, ja, pt-BR) |
| sys-locale | System locale detection |
| chrono | Timestamp generation |
Extension Points
Adding a New Shell
- Create
shell/myshell.rs:
#![allow(unused)]
fn main() {
pub fn init_script() -> &'static str {
// Return shell-specific initialization code as static string
r#"
Shell initialization code here
scoop() { ... }
"#
}
}
Note: Completions are generated by clap via
clap_complete, not per-shell functions.
- Add to
ShellTypeenum incli/mod.rs:
#![allow(unused)]
fn main() {
pub enum ShellType {
// ... existing
MyShell,
}
}
- Add detection to
shell::detect_shell()insrc/shell/mod.rs:
#![allow(unused)]
fn main() {
pub fn detect_shell() -> ShellType {
if env::var("MYSHELL_VERSION").is_ok() {
return ShellType::MyShell;
}
// ... existing checks
}
}
Adding a New Migration Source
- Create
core/migrate/mysource.rs:
#![allow(unused)]
fn main() {
pub struct MySource;
impl MySource {
pub fn detect() -> bool {
// Check if source is available
}
pub fn list_envs() -> Result<Vec<MigrationCandidate>> {
// Return list of environments
}
pub fn migrate_env(name: &str) -> Result<()> {
// Perform migration
}
}
}
- Register in
cli/commands/migrate.rs:
#![allow(unused)]
fn main() {
let sources = vec![
// ... existing
Box::new(MySource),
];
}
Adding a New Doctor Check
See API Reference - Adding a New Health Check for details.
Performance Characteristics
| Operation | Time Complexity | Notes |
|---|---|---|
| List envs | O(n) | n = number of virtualenvs |
| Create env | O(1)* | *Depends on uv performance |
| Delete env | O(1) | Simple directory removal |
| Version resolution | O(d) | d = directory depth (walks parents) |
| Doctor checks | O(n) | n = number of checks (fixed) |
Thread Safety
scoop is a single-threaded CLI application. No concurrent operations are performed.
File locking: Not implemented. Assumes single user on single machine. Concurrent operations (e.g., two terminals creating the same env) may result in race conditions.
Security Considerations
- Path Traversal: All user-provided names are validated via regex before use in filesystem operations.
- Command Injection: uv commands are constructed using typed arguments, not string concatenation.
- Symlink Safety: Doctor checks detect and warn about broken symlinks.
- Metadata Integrity: JSON parsing errors are gracefully handled without panics.
Related Documentation
- API Reference - Detailed API documentation
- Testing - Testing strategies
- Contributing - Development guide
API Reference
This document provides a reference for scoop’s public API, primarily intended for:
- AI/LLM tools analyzing or modifying the codebase
- Contributors extending scoop’s functionality
- Advanced users integrating scoop into custom tooling
Note: This is an internal API reference. For CLI usage, see Commands.
Core Types
VirtualEnv Module (core/virtualenv/)
VirtualenvInfo
Represents basic information about a virtual environment. Marked
#[non_exhaustive] since Unreleased — external Rust consumers can
no longer construct it with struct-literal syntax. Obtain instances
from VirtualenvService::list() (or any future read API) instead.
#![allow(unused)]
fn main() {
#[non_exhaustive]
pub struct VirtualenvInfo {
pub name: String,
pub path: PathBuf,
pub python_version: Option<String>,
pub created_at: Option<DateTime<Utc>>,
pub last_used: Option<DateTime<Utc>>,
}
}
Fields:
name- Environment name (e.g.,"myproject")path- Absolute path to virtualenv directorypython_version- Python version string if metadata exists (e.g.,Some("3.12.1"))created_at- Creation timestamp from metadata, if present (Unreleased)last_used- Most recentscoop activate/run/shelltouch timestamp, if present (Unreleased).Nonefor legacy envs that pre-date the field and fresh envs that have never been activated.
Example (consume-only, struct-literal construction is no longer permitted):
#![allow(unused)]
fn main() {
let service = VirtualenvService::auto()?;
for info in service.list()? {
println!("{} (last used: {:?})", info.name, info.last_used);
}
}
VirtualenvService
Primary service for virtualenv operations.
#![allow(unused)]
fn main() {
pub struct VirtualenvService {
uv: UvClient,
}
impl VirtualenvService {
/// Creates a new service with custom uv wrapper
pub fn new(uv: UvClient) -> Self
/// Creates a service using system's uv installation
pub fn auto() -> Result<Self>
/// Lists all virtualenvs
pub fn list(&self) -> Result<Vec<VirtualenvInfo>>
/// Creates a new virtualenv
pub fn create(&self, name: &str, python_version: &str) -> Result<PathBuf>
/// Creates a new virtualenv with a specific Python executable
pub fn create_with_python_path(&self, name: &str, python_version: &str, python_path: &Path) -> Result<PathBuf>
/// Deletes a virtualenv
pub fn delete(&self, name: &str) -> Result<()>
/// Checks if a virtualenv exists
pub fn exists(&self, name: &str) -> Result<bool>
/// Gets the path to a virtualenv
pub fn get_path(&self, name: &str) -> Result<PathBuf>
/// Reads metadata for a virtualenv (best-effort; collapses
/// missing and corrupt into `None`).
pub fn read_metadata(&self, path: &Path) -> Option<Metadata>
/// Reads metadata distinguishing missing (`Ok(None)`) from
/// corrupt (`Err(_)`). Touch / gc use this so they can refuse
/// to overwrite garbage. (Unreleased)
pub fn read_metadata_result(&self, path: &Path) -> Result<Option<Metadata>>
/// Writes metadata atomically via tempfile + rename. (Unreleased)
pub fn write_metadata_atomic(&self, path: &Path, m: &Metadata) -> Result<()>
/// Best-effort update to `last_used`. Never returns an error;
/// logs `warn!` on failure. (Unreleased)
pub fn touch_metadata_best_effort(&self, env_name: &str)
}
}
Common Usage Pattern:
#![allow(unused)]
fn main() {
// Initialize service
let service = VirtualenvService::auto()?;
// Check if environment exists
if !service.exists("myenv")? {
// Create with Python 3.12
service.create("myenv", "3.12")?;
}
// Get environment path
let path = service.get_path("myenv")?;
println!("Environment at: {}", path.display());
}
Metadata Module (core/metadata.rs)
Metadata
Stores JSON metadata for each virtualenv.
#![allow(unused)]
fn main() {
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Metadata {
pub name: String,
pub python_version: String,
pub created_at: DateTime<Utc>, // Timestamp (ISO 8601 when serialized)
pub created_by: String, // "scoop X.Y.Z" format
pub uv_version: Option<String>, // uv version used
pub python_path: Option<String>, // Custom Python executable path (if --python-path was used)
pub last_used: Option<DateTime<Utc>>, // Last activation timestamp (Unreleased)
}
impl Metadata {
/// Creates new metadata
pub fn new(name: String, python_version: String, uv_version: Option<String>) -> Self
}
}
Storage Location: ~/.scoop/virtualenvs/<name>/.scoop-metadata.json
Example JSON:
{
"name": "myproject",
"python_version": "3.12.1",
"created_at": "2024-01-15T10:30:00Z",
"created_by": "scoop <version>",
"uv_version": "0.1.0"
}
Note:
created_atisDateTime<Utc>in Rust but serializes to ISO 8601 string in JSON.
Doctor Module (core/doctor.rs)
Check Trait
Interface for health checks.
#![allow(unused)]
fn main() {
pub trait Check: Send + Sync {
fn id(&self) -> &'static str;
fn name(&self) -> &'static str;
fn run(&self) -> Vec<CheckResult>; // Returns Vec - a check can produce multiple results
}
}
Implementations:
UvCheck- Verifies uv is installedHomeCheck- ChecksSCOOP_HOMEdirectoryVirtualenvCheck- Validates virtualenvs directorySymlinkCheck- Checks for broken virtualenv Python symlinks (e.g.,<env>/bin/python)ShellCheck- Verifies shell integrationVersionCheck- Validates version files
CheckStatus
Result status for health checks.
#![allow(unused)]
fn main() {
pub enum CheckStatus {
Ok, // ✅ Check passed
Warning(String), // ⚠️ Issue found, but not critical
Error(String), // ❌ Critical issue
}
}
CheckResult
Detailed result from a health check.
#![allow(unused)]
fn main() {
pub struct CheckResult {
pub id: &'static str,
pub name: &'static str,
pub status: CheckStatus,
pub suggestion: Option<String>,
pub details: Option<String>,
}
impl CheckResult {
/// Creates an OK result
pub fn ok(id: &'static str, name: &'static str) -> Self
/// Creates a warning result
pub fn warn(id: &'static str, name: &'static str, message: impl Into<String>) -> Self
/// Creates an error result
pub fn error(id: &'static str, name: &'static str, message: impl Into<String>) -> Self
/// Adds a suggestion for fixing the issue
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self
/// Adds detailed information
pub fn with_details(mut self, details: impl Into<String>) -> Self
// Status checks
pub fn is_ok(&self) -> bool
pub fn is_warning(&self) -> bool
pub fn is_error(&self) -> bool
}
}
Example - Implementing a Custom Check:
#![allow(unused)]
fn main() {
struct MyCustomCheck;
impl Check for MyCustomCheck {
fn id(&self) -> &'static str {
"my_check"
}
fn name(&self) -> &'static str {
"My Custom Check"
}
fn run(&self) -> Vec<CheckResult> {
if some_condition() {
vec![CheckResult::ok(self.id(), self.name())]
} else {
vec![CheckResult::error(self.id(), self.name(), "Error message here")
.with_suggestion("Run: scoop fix-it")
.with_details("Expected X, found Y")]
}
}
}
}
Doctor
Orchestrates all health checks.
#![allow(unused)]
fn main() {
pub struct Doctor {
checks: Vec<Box<dyn Check>>,
}
impl Doctor {
/// Creates a doctor with default checks
pub fn new() -> Self
/// Runs all checks without fixing
pub fn run_all(&self) -> Vec<CheckResult>
/// Runs checks and attempts to fix issues
pub fn run_and_fix(&self, output: &crate::output::Output) -> Vec<CheckResult>
}
impl Default for Doctor {
fn default() -> Self {
Self::new()
}
}
}
Usage:
#![allow(unused)]
fn main() {
let doctor = Doctor::new();
// Run diagnostics
let results = doctor.run_all();
for result in results {
match &result.status {
CheckStatus::Error(msg) => eprintln!("❌ {}: {}", result.name, msg),
CheckStatus::Warning(msg) => println!("⚠️ {}: {}", result.name, msg),
CheckStatus::Ok => println!("✅ {}", result.name),
}
}
// Auto-fix issues (requires Output for progress display)
use scoop::output::Output;
let output = Output::new(0, false, false, false);
let fixed_results = doctor.run_and_fix(&output);
}
Error Handling
ScoopError (error.rs)
Primary error type for all scoop operations.
#![allow(unused)]
fn main() {
#[derive(Error, Debug)]
pub enum ScoopError {
// Virtualenv errors
VirtualenvNotFound { name: String },
VirtualenvExists { name: String },
InvalidEnvName { name: String, reason: String },
// Python errors
PythonNotInstalled { version: String },
PythonInstallFailed { version: String, message: String },
PythonUninstallFailed { version: String, message: String },
InvalidPythonVersion { version: String },
NoPythonVersions { pattern: String },
// uv errors
UvNotFound,
UvCommandFailed { command: String, message: String },
// Path/IO errors
PathError(String), // Tuple variant
HomeNotFound,
Io(#[from] std::io::Error), // Tuple variant with From
Json(#[from] serde_json::Error), // Tuple variant with From
// Config errors
VersionFileNotFound { path: PathBuf },
UnsupportedShell { shell: String },
// CLI errors
InvalidArgument { message: String },
// Migration errors
PyenvNotFound,
PyenvEnvNotFound { name: String },
VenvWrapperEnvNotFound { name: String },
CondaEnvNotFound { name: String },
CorruptedEnvironment { name: String, reason: String },
PackageExtractionFailed { reason: String },
MigrationFailed { reason: String },
MigrationNameConflict { name: String, existing: PathBuf },
// Python path errors
InvalidPythonPath { path: PathBuf, reason: String },
// Cascade errors
CascadeAborted,
}
impl ScoopError {
/// Returns error code string (e.g., "ENV_NOT_FOUND", "UV_COMMAND_FAILED")
pub fn code(&self) -> &'static str
/// Localized message in an explicit locale (bypasses the process-global
/// current locale). `Display` delegates to this with the current locale.
pub fn message_in(&self, locale: &str) -> String
/// Returns user-friendly suggestion in the current locale (if available)
pub fn suggestion(&self) -> Option<String>
/// Locale-explicit sibling of `suggestion()` — useful for deterministic,
/// parallel tests that must not depend on the global locale.
pub fn suggestion_in(&self, locale: &str) -> Option<String>
/// Returns migration-specific exit code
pub fn migration_exit_code(&self) -> MigrationExitCode
}
}
The
*_in(locale)accessors passlocale =to rust-i18n’st!, which bypasses the process-global current locale — this lets tests assert messages deterministically without#[serial].
Error Code String Prefixes:
ENV_*- Environment errors (e.g.,ENV_NOT_FOUND,ENV_ALREADY_EXISTS)PYTHON_*- Python version errors (e.g.,PYTHON_NOT_INSTALLED)UV_*- uv errors (e.g.,UV_NOT_INSTALLED,UV_COMMAND_FAILED)IO_*- Path/IO errors (e.g.,IO_ERROR,PATH_ERROR)CONFIG_*- Config errors (e.g.,CONFIG_VERSION_FILE_NOT_FOUND)SHELL_*- Shell errors (e.g.,SHELL_NOT_SUPPORTED)ARG_*- CLI argument errors (e.g.,ARG_INVALID)SOURCE_*- Migration source errors (e.g.,SOURCE_PYENV_NOT_FOUND)MIGRATE_*- Migration process errors (e.g.,MIGRATE_FAILED)UNINSTALL_*- Uninstall errors (e.g.,UNINSTALL_CASCADE_ABORTED)
Example Error Handling:
#![allow(unused)]
fn main() {
use crate::error::{ScoopError, Result};
fn my_function(name: &str) -> Result<()> {
if !validate_name(name) {
return Err(ScoopError::InvalidEnvName {
name: name.to_string(),
reason: "Must start with a letter".to_string(),
});
}
// ... operation
Ok(())
}
// Usage
match my_function("123invalid") {
Ok(_) => println!("Success"),
Err(e) => {
eprintln!("Error: {}", e);
if let Some(suggestion) = e.suggestion() {
eprintln!("Suggestion: {}", suggestion);
}
eprintln!("Error code: {}", e.code());
std::process::exit(1); // Non-zero exit for error
}
}
}
Shell Integration
Shell Types (cli/mod.rs)
#![allow(unused)]
fn main() {
pub enum ShellType {
Bash,
Zsh,
Fish,
Powershell,
}
// Note: ShellType is a plain enum without methods
// Shell operations are handled by module-level functions:
// - shell::detect_shell() -> ShellType (in shell/mod.rs)
// - shell::bash::init_script() -> &'static str
// - shell::zsh::init_script() -> &'static str
// - shell::fish::init_script() -> &'static str
// - shell::powershell::init_script() -> &'static str
}
Auto-detection Priority:
FISH_VERSION→ FishPSModulePath→ PowerShellZSH_VERSION→ Zsh- Default → Bash
Path Utilities (paths.rs)
#![allow(unused)]
fn main() {
/// Returns scoop home directory (SCOOP_HOME or ~/.scoop)
pub fn scoop_home() -> Result<PathBuf>
/// Returns virtualenvs directory
pub fn virtualenvs_dir() -> Result<PathBuf>
/// Returns global version file path (~/.scoop/version)
pub fn global_version_file() -> Result<PathBuf>
/// Returns local version file path in the given directory
pub fn local_version_file(dir: &std::path::Path) -> PathBuf
}
Version Resolution (core/version.rs)
VersionService
Service for managing version files.
#![allow(unused)]
fn main() {
pub struct VersionService;
impl VersionService {
/// Set the local version for a directory
pub fn set_local(dir: &Path, env_name: &str) -> Result<()>
/// Set the global version
pub fn set_global(env_name: &str) -> Result<()>
/// Get the local version for a directory
pub fn get_local(dir: &Path) -> Option<String>
/// Get the global version
pub fn get_global() -> Option<String>
/// Resolve the version for a directory (local -> parent walk -> global)
pub fn resolve(dir: &Path) -> Option<String>
/// Resolve from current directory
pub fn resolve_current() -> Option<String>
/// Unset local version (removes .scoop-version)
pub fn unset_local(dir: &Path) -> Result<()>
/// Unset global version (removes ~/.scoop/version)
pub fn unset_global() -> Result<()>
}
}
CLI Equivalent (User Workflow):
VersionService::set_global() is exposed by the scoop use <name> --global command.
To set Python 3.11.0 as the global default in practice:
scoop install 3.11.0
scoop create py311 3.11.0
scoop use py311 --global
This writes py311 to ~/.scoop/version. The global value is used when no local
.scoop-version or SCOOP_VERSION override is present.
Resolution Priority Order:
SCOOP_VERSIONenvironment variable (checked at shell hook level, not in VersionService).scoop-versionin current directory.scoop-versionin parent directories (walks up)~/.scoop/version(global default)
Note:
.python-versionis not supported.
Testing Patterns
Property-Based Testing
scoop uses proptest for property-based testing of critical logic:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use proptest::prelude::*;
proptest! {
#[test]
fn env_name_validation_is_consistent(name in "[a-zA-Z][a-zA-Z0-9_-]*") {
assert!(validate_name(&name).is_ok());
}
}
}
}
Integration Testing
Test files follow the pattern:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn setup_test_env() -> TempDir {
// Test setup
}
#[test]
fn test_something() {
let temp = setup_test_env();
// Test logic
}
}
}
Extending scoop
Adding a New Command
- Define command in
cli/mod.rs:
#![allow(unused)]
fn main() {
#[derive(Subcommand)]
pub enum Commands {
// ... existing commands
MyCommand {
#[arg(help = "Argument description")]
arg: String,
},
}
}
- Create handler in
cli/commands/:
#![allow(unused)]
fn main() {
// cli/commands/my_command.rs
use crate::error::Result;
pub fn execute(arg: &str) -> Result<()> {
// Implementation
Ok(())
}
}
- Wire up in
main.rs:
#![allow(unused)]
fn main() {
Commands::MyCommand { arg } => {
commands::my_command::execute(&arg)?
}
}
Adding a New Health Check
#![allow(unused)]
fn main() {
// In core/doctor.rs
struct MyCheck;
impl Check for MyCheck {
fn id(&self) -> &'static str {
"my_check"
}
fn name(&self) -> &'static str {
"My Custom Check"
}
fn run(&self) -> Vec<CheckResult> {
// Check logic
vec![CheckResult::ok(self.id(), self.name())]
}
}
// Register in Doctor::new()
impl Doctor {
pub fn new() -> Self {
Self {
checks: vec![
// ... existing checks
Box::new(MyCheck),
],
}
}
}
}
Related Documentation
- Architecture - System design and patterns
- Commands - CLI reference
- Contributing - Development guide
- Testing - Testing strategies
For AI/LLM Tools
When analyzing or modifying this codebase:
-
Use symbolic tools for precise navigation:
find_symbolto locate specific functions/typesfind_referencing_symbolsto understand usageget_symbols_overviewfor module structure
-
Follow existing patterns:
- Error handling: Always return
Result<T>withScoopError - Shell output: CLI outputs shell code, wrapper evals it
- Testing: Unit tests + integration tests + property tests
- i18n: Use
t!()macro for all user-facing strings
- Error handling: Always return
-
Preserve conventions:
- Error codes are string constants (e.g., “ENV_NOT_FOUND”, “UV_COMMAND_FAILED”)
- Process exit codes follow a layered contract (see Process exit codes below)
- Path handling via
paths.rsutilities (cross-platform — Unixbin/vs WindowsScripts/) - Shell detection via
shell::detect_shell()function
Process exit codes
scoop centralises exit-code policy in src/error/exit.rs via
ScoopError::exit_code(). Commands that already render their own
report (e.g. verify) opt into ErrorRenderPolicy::Quiet so main.rs
does not append a duplicate error: line.
| Code | Meaning |
|---|---|
0 | Success |
1 | Generic failure or semantic finding (verify failed, generic operational error) |
2 | Migration failure / MigrationNameConflict (and reserved for future probe/tool failures) |
3 | Migration source-discovery error (pyenv/conda/venvwrapper missing or corrupted) |
Per-command exit code table:
| Command | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
verify | always (informational) | — | — | — |
verify --strict | clean | any Fail check | — | — |
migrate @env / migrate all | success | — | failure / conflict | source tool not found |
doctor | clean | warning | error | — |
| Other commands | success | failure | — | — |
- Documentation requirements:
- All
pub fnmust have doc comments - Examples in doctests where applicable
- Error conditions documented
- Shell integration changes require cross-shell testing
- All
Last Updated: 2026-06-02 scoop Version: Unreleased (post-0.12.0)
Testing
Comprehensive guide for testing scoop.
Quick Reference
cargo test # Run all tests
cargo test json # Run tests containing "json"
cargo test -- --nocapture # Show println! output
cargo clippy -- -D warnings # Lint check
Test Structure
tests/
└── cli.rs # CLI integration tests
src/
├── error.rs # Unit tests for error types
├── validate.rs # Unit tests for validation
├── paths.rs # Unit tests for path utilities
├── output/
│ └── json.rs # Unit tests for JSON output
├── core/
│ ├── virtualenv/ # virtualenv service (mod.rs + tests.rs)
│ ├── version.rs # Unit tests for version service
│ ├── metadata.rs # Unit tests for metadata
│ └── doctor.rs # Unit tests for doctor
├── shell/
│ ├── bash.rs # Shell script tests
│ └── zsh.rs # Shell script tests
└── uv/
└── client.rs # Unit tests for uv client
Running Tests
All Tests
# Run all tests
cargo test
# Run with all features enabled
cargo test --all-features
# Run in release mode (faster execution)
cargo test --release
Filtered Tests
# By name pattern
cargo test json # Tests containing "json"
cargo test error # Tests containing "error"
cargo test virtualenv # Tests containing "virtualenv"
# By module path
cargo test output::json # Tests in output/json.rs
cargo test error::tests # Tests in error.rs
cargo test core::version # Tests in core/version.rs
cargo test cli::commands # Tests in cli/commands/
# Single test
cargo test test_json_response_success_creates_correct_status
Test Output
# Show stdout/stderr (println!, dbg!, etc.)
cargo test -- --nocapture
# Show test names as they run
cargo test -- --nocapture --test-threads=1
# Only show failed tests
cargo test -- --quiet
Debugging
# Run single-threaded (easier to debug)
cargo test -- --test-threads=1
# Run ignored tests
cargo test -- --ignored
# Run specific test with output
cargo test test_name -- --nocapture --test-threads=1
Test Categories
Unit Tests
The bulk of the suite (703 tests, ~92% of the total 769) lives within source files using #[cfg(test)]:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_something() {
assert_eq!(1 + 1, 2);
}
}
}
Key test modules:
| Module | Tests | Coverage |
|---|---|---|
error::tests | 54 | Error types, codes, suggestions |
output::json::tests | 35 | JSON serialization, edge cases |
validate::tests | 30 | Name/version validation |
core::version::tests | 18 | Version file resolution |
core::virtualenv::tests | 12 | Virtualenv service |
paths::tests | 16 | Path utilities |
shell::*::tests | 14 | Shell scripts (shellcheck) |
Integration Tests (43 tests in tests/cli.rs + 2 in tests/i18n_completeness.rs)
Located in tests/cli.rs:
# Run only integration tests
cargo test --test cli
Categories:
- Error cases - Invalid inputs, missing arguments
- Output format - Help, version, JSON output
- Command behavior - list, create, use, remove
Some tests are marked #[ignore] because they require uv installed:
# Run ignored tests (requires uv)
cargo test -- --ignored
Doc Tests (21 tests)
Examples in documentation comments:
#![allow(unused)]
fn main() {
/// Validates environment name.
///
/// # Examples
///
/// ```
/// use scoop_uv::validate::is_valid_env_name;
/// assert!(is_valid_env_name("myenv"));
/// assert!(!is_valid_env_name("123bad"));
/// ```
pub fn is_valid_env_name(name: &str) -> bool { ... }
}
# Run only doc tests
cargo test --doc
Property Tests
Using proptest for randomized testing:
#![allow(unused)]
fn main() {
use proptest::prelude::*;
proptest! {
#[test]
fn prop_valid_names_accepted(name in "[a-zA-Z][a-zA-Z0-9_-]{0,49}") {
assert!(is_valid_env_name(&name));
}
}
}
Located in src/validate.rs.
Parameterized Tests
Using rstest #[case] tables for input/output matrices (e.g. version parsing,
env-name validation) so each case reports independently:
#![allow(unused)]
fn main() {
use rstest::rstest;
#[rstest]
#[case::simple("myenv", true)]
#[case::digit_start("123", false)]
#[case::reserved("activate", false)]
fn is_valid_env_name_cases(#[case] input: &str, #[case] expected: bool) {
assert_eq!(is_valid_env_name(input), expected);
}
}
Mutation Testing
cargo-mutants verifies the suite actually catches bugs (not just that lines
run). Config in .cargo/mutants.toml; CI runs it on changed lines per PR
(--in-diff) and a full pass weekly.
cargo install cargo-mutants
cargo mutants # full (scoped via mutants.toml)
git diff origin/main.. | cargo mutants --in-diff /dev/stdin # changed lines
Fuzz Testing
cargo-fuzz (libFuzzer) fuzzes the untrusted-input parsers. It lives in an
isolated fuzz/ workspace pinned to nightly, so it never affects the
MSRV-1.85 build; CI runs the targets on a weekly schedule.
cargo install cargo-fuzz
cargo +nightly fuzz run fuzz_env_name -- -max_total_time=60
Writing Tests
Unit Test Template
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
// ========================================
// Test Group Name
// ========================================
#[test]
fn test_function_name_expected_behavior() {
// Arrange
let input = "test input";
// Act
let result = function_under_test(input);
// Assert
assert_eq!(result, expected_value);
}
#[test]
fn test_function_name_edge_case() {
let result = function_under_test("");
assert!(result.is_err());
}
}
}
Integration Test Template
#![allow(unused)]
fn main() {
// tests/cli.rs
use assert_cmd::Command;
use predicates::prelude::*;
#[test]
fn test_command_success() {
Command::cargo_bin("scoop")
.unwrap()
.args(["list"])
.assert()
.success()
.stdout(predicate::str::contains("expected output"));
}
#[test]
fn test_command_failure() {
Command::cargo_bin("scoop")
.unwrap()
.args(["use", "nonexistent"])
.assert()
.failure()
.stderr(predicate::str::contains("not found"));
}
}
JSON Output Testing
#![allow(unused)]
fn main() {
#[test]
fn test_json_serialization() {
let data = MyData { field: "value".into() };
let json = serde_json::to_string(&data).unwrap();
// Check JSON structure
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["field"], "value");
}
#[test]
fn test_optional_field_omitted() {
let data = MyData { optional: None, .. };
let json = serde_json::to_string(&data).unwrap();
// skip_serializing_if = "Option::is_none"
assert!(!json.contains("optional"));
}
}
Test Utilities
Located in src/test_utils.rs:
#![allow(unused)]
fn main() {
use scoop_uv::test_utils::*;
#[test]
fn test_with_temp_environment() {
with_temp_scoop_home(|temp_dir| {
// SCOOP_HOME is set to temp_dir
// Cleanup happens automatically
});
}
#[test]
fn test_with_mock_venv() {
with_temp_scoop_home(|temp_dir| {
create_mock_venv("myenv", Some("3.12"));
// Virtual environment created at temp_dir/virtualenvs/myenv
});
}
}
Coverage
Using cargo-tarpaulin
# Install
cargo install cargo-tarpaulin
# Run with HTML report
cargo tarpaulin --out Html --output-dir coverage
# Run with specific target
cargo tarpaulin --out Html --output-dir coverage --packages scoop-uv
# View report
open coverage/tarpaulin-report.html
Using cargo-llvm-cov
# Install
cargo install cargo-llvm-cov
# Run with HTML report
cargo llvm-cov --html
# View report
open target/llvm-cov/html/index.html
CI/CD Testing
Tests run automatically on:
- Every push to any branch
- Every pull request
GitHub Actions workflow (.github/workflows/ci.yml):
- name: Run tests
run: cargo test --all-features
- name: Run clippy
run: cargo clippy --all-targets -- -D warnings
Troubleshooting
Test Hangs
# Run single-threaded to identify hanging test
cargo test -- --test-threads=1
Flaky Tests
# Run specific test multiple times
for i in {1..10}; do cargo test test_name || break; done
Environment Issues
# Clear test artifacts
cargo clean
# Rebuild and test
cargo test
Shell Tests Fail
ShellCheck must be installed for shell script tests:
# macOS
brew install shellcheck
# Linux
apt install shellcheck
Best Practices
- Test naming:
test_<function>_<scenario>_<expected> - Arrange-Act-Assert: Clear test structure
- One assertion per test: When practical
- Test edge cases: Empty, unicode, special chars, boundaries
- No test interdependencies: Each test should be isolated
- Fast tests: Mock external dependencies
Codespaces / Devcontainer
.devcontainer/devcontainer.json boots a Rust 1.85 dev environment on
mcr.microsoft.com/devcontainers/rust:1-bookworm that matches the local
toolchain pinned by rust-toolchain.toml. Open the repo in VS Code
(“Reopen in Container”) or create a Codespace — both follow the same
lifecycle:
| Hook | Runs | Used for |
|---|---|---|
onCreateCommand | once (in Codespace prebuild) | install nextest / llvm-cov / mutants / uv, warm cargo build |
updateContentCommand | on every prebuild refresh | cargo fetch to keep registry warm |
postCreateCommand | when user creates the Codespace | prek install |
Cargo registry + git caches persist as named Docker volumes scoped per
project so two scoop-uv worktrees don’t share caches. target/ is NOT
volumed — it’s architecture-specific and warmed in the prebuild layer.
Enabling Codespace prebuilds
In repo Settings → Codespaces → “Set up prebuild”, configure for the
main branch on the “configuration change” trigger. This keeps Actions
minutes low (only rebuilds when .devcontainer/** or Dockerfile
changes) while still keeping the cargo registry warm via volume
persistence.
Multi-source Integration (Docker matrix)
The Dockerfile builds three per-source leaf stages — pyenv-test,
conda-test, venvwrapper-test — on top of a shared scoop-test-base.
Each carries only the source-tool it migrates from, so CI can
matrix-build just one variant.
# Local (sequential)
make test-integration-pyenv
make test-integration-conda
make test-integration-venvwrapper
# Local (all three sequentially)
make test-integration-all
# Drop into a single variant for ad-hoc debugging
make docker-shell-conda
CI runs all three in parallel via
.github/workflows/integration-test.yml’s strategy.matrix with
per-source BuildKit cache scoping (cache-from/to=type=gha,scope=<src>).
Benchmarks (Criterion)
Three bench binaries live in benches/:
| Binary | Targets |
|---|---|
parsing | clap parse, TOML manifest, JSON uv python list |
validation | is_valid_env_name across 6 representative inputs |
path_lookup | find_executable_in hit + miss |
Local workflow
# Run all benches once (no baseline diffing)
cargo bench
# Save current results as a named baseline (default: "main")
make bench-save # saves as "main"
make bench-save BENCH_BASELINE=before-X # saves as "before-X"
# Compare current results against a saved baseline
make bench-compare # vs "main"
make bench-compare BENCH_BASELINE=before-X
# Run all benches inside Docker (reproducible)
make bench
Criterion writes HTML reports to target/criterion/report/index.html
for visual diffing.
CI regression gate
.github/workflows/bench.yml runs every PR and push:
- On
main: results are pushed to thegh-pagesbranch as the new baseline. - On PRs: results are compared against that baseline.
- alert at 130% — leaves a PR comment, doesn’t block merge
- fail at 150% — fails the workflow, blocks merge
Thresholds account for GitHub-hosted runner variance (10-30% per-bench noise is normal on shared CPU). Tighten by running on a self-hosted runner with pinned hardware if needed.
Code Quality
Comprehensive guide for maintaining code quality in scoop.
Quick Reference
# Format code
cargo fmt
# Lint check
cargo clippy --all-targets --all-features -- -D warnings
# All checks (pre-commit style)
cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings && cargo test
# Pre-commit hooks
prek run --all-files
Formatting (rustfmt)
Configuration
Located in rustfmt.toml:
edition = "2024"
max_width = 100
tab_spaces = 4
use_field_init_shorthand = true
use_try_shorthand = true
Commands
# Auto-format all files
cargo fmt
# Check formatting (CI mode, no changes)
cargo fmt --check
# Format specific file
rustfmt src/main.rs
# Show diff instead of applying
cargo fmt -- --check --diff
IDE Integration
VS Code (rust-analyzer):
{
"[rust]": {
"editor.formatOnSave": true
}
}
JetBrains (RustRover/CLion):
- Settings → Languages → Rust → Rustfmt → Run on save
Linting (Clippy)
Basic Usage
# Standard lint check
cargo clippy
# Treat warnings as errors (CI mode)
cargo clippy -- -D warnings
# All targets (including tests, examples)
cargo clippy --all-targets
# All features enabled
cargo clippy --all-features
# Full CI check
cargo clippy --all-targets --all-features -- -D warnings
Lint Categories
# Enable specific lint category
cargo clippy -- -W clippy::pedantic
# Deny specific lint
cargo clippy -- -D clippy::unwrap_used
# Allow specific lint
cargo clippy -- -A clippy::too_many_arguments
Common Lints
| Lint | Severity | Description |
|---|---|---|
clippy::unwrap_used | Warn | Use ? or expect() instead |
clippy::panic | Warn | Avoid panic in library code |
clippy::todo | Warn | Remove before release |
clippy::dbg_macro | Warn | Remove debug macros |
clippy::print_stdout | Warn | Use logging instead |
Fixing Lints
# Auto-fix where possible
cargo clippy --fix
# Allow fixes that change behavior
cargo clippy --fix --allow-dirty --allow-staged
Suppressing Lints
#![allow(unused)]
fn main() {
// Single line
#[allow(clippy::too_many_arguments)]
fn complex_function(...) {}
// Entire module
#![allow(clippy::module_inception)]
// With explanation
#[allow(clippy::unwrap_used)] // Safe: validated in parse()
fn get_value() {}
}
Pre-commit Hooks (prek)
Setup
# Install prek
cargo install prek
# or
uv tool install prek
# Install hooks in repository
prek install
Configuration
Located in .pre-commit-config.yaml:
repos:
- repo: local
hooks:
- id: cargo-fmt
name: cargo fmt
entry: cargo fmt --all --
language: system
types: [ rust ]
pass_filenames: false
- id: cargo-clippy
name: cargo clippy
entry: cargo clippy --all-targets --all-features -- -D warnings
language: system
types: [ rust ]
pass_filenames: false
- id: cargo-check
name: cargo check
entry: cargo check --all-targets
language: system
types: [ rust ]
pass_filenames: false
Usage
# Run all hooks on staged files
prek run
# Run all hooks on all files
prek run --all-files
# Run specific hook
prek run cargo-fmt
prek run cargo-clippy
# Run multiple specific hooks
prek run cargo-fmt cargo-clippy
# Skip hooks (emergency only!)
git commit --no-verify
Available Hooks
| Hook | Description | When |
|---|---|---|
cargo-fmt | Code formatting | Pre-commit |
cargo-clippy | Linting | Pre-commit |
cargo-check | Type checking | Pre-commit |
trailing-whitespace | Remove trailing spaces | Pre-commit |
end-of-file-fixer | Ensure newline at EOF | Pre-commit |
check-toml | Validate TOML files | Pre-commit |
check-yaml | Validate YAML files | Pre-commit |
CI Pipeline
GitHub Actions
Located in .github/workflows/ci.yml:
name: CI
on: [ push, pull_request ]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-action@stable
with:
components: rustfmt, clippy
- name: Format check
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Test
run: cargo test --all-features
Local CI Simulation
# Run exactly what CI runs
cargo fmt --check && \
cargo clippy --all-targets --all-features -- -D warnings && \
cargo test --all-features
Code Style Guidelines
Naming Conventions
| Item | Convention | Example |
|---|---|---|
| Modules | snake_case | version_file |
| Functions | snake_case | get_version() |
| Types | PascalCase | VirtualenvService |
| Constants | SCREAMING_SNAKE | MAX_NAME_LENGTH |
| Lifetimes | short lowercase | 'a, 'src |
Documentation
/// Creates a new virtual environment.
///
/// # Arguments
///
/// * `name` - Environment name (must be valid)
/// * `python` - Python version (e.g., "3.12")
///
/// # Returns
///
/// Path to the created environment.
///
/// # Errors
///
/// Returns [`ScoopError::InvalidEnvName`] if name is invalid.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let path = create_env("myenv", "3.12")?;
/// # Ok(())
/// # }
/// ```
pub fn create_env(name: &str, python: &str) -> Result<PathBuf> {
// ...
}
Error Handling
#![allow(unused)]
fn main() {
// Good: Use ? operator
fn process() -> Result<()> {
let file = File::open(path)?;
let data = read_data(&file)?;
Ok(())
}
// Good: Contextual errors
fn process() -> Result<()> {
let file = File::open(path)
.map_err(|e| ScoopError::Io(e))?;
Ok(())
}
// Avoid: unwrap() in library code
fn bad() {
let file = File::open(path).unwrap(); // Bad
}
// OK: expect() with explanation
fn acceptable() {
let home = dirs::home_dir()
.expect("home directory must exist");
}
}
Import Organization
#![allow(unused)]
fn main() {
// 1. Standard library
use std::collections::HashMap;
use std::path::PathBuf;
// 2. External crates
use clap::Parser;
use serde::Serialize;
use thiserror::Error;
// 3. Local modules
use crate::error::Result;
use crate::paths;
}
Security Considerations
Dependency Auditing
# Install cargo-audit
cargo install cargo-audit
# Run audit
cargo audit
# Fix vulnerabilities
cargo audit fix
MSRV (Minimum Supported Rust Version)
- Current MSRV: 1.85
- Defined in
Cargo.toml:[package] rust-version = "1.85"
Unsafe Code
- Avoid
unsafeunless absolutely necessary - Document safety invariants
- Use
#![forbid(unsafe_code)]in library crates
Performance
Profiling
# Build with debug info for release
cargo build --release
# Use flamegraph
cargo install flamegraph
cargo flamegraph --bin scoop -- list
Benchmarks
# Run benchmarks (if defined)
cargo bench
# Using criterion
cargo bench --bench my_benchmark
Continuous Improvement
Regular Checks
# Weekly dependency update check
cargo outdated
# Security audit
cargo audit
# MSRV check
cargo msrv verify
Upgrade Dependencies
# Update Cargo.lock
cargo update
# Upgrade to latest compatible versions
cargo upgrade # requires cargo-edit
Troubleshooting
Clippy False Positives
#![allow(unused)]
fn main() {
// Silence with explanation
#[allow(clippy::needless_return)]
fn explicit_return() -> i32 {
return 42; // Intentional for readability
}
}
Format Conflicts
#![allow(unused)]
fn main() {
// Skip formatting for specific block
#[rustfmt::skip]
const MATRIX: [[i32; 3]; 3] = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
];
}
CI vs Local Differences
# Ensure same toolchain as CI
rustup update stable
rustup default stable
# Check Rust version
rustc --version
Summary Checklist
Before committing:
-
cargo fmt- Code formatted -
cargo clippy -- -D warnings- No lint warnings -
cargo test- All tests pass -
cargo doc- Documentation builds - No
todo!()ordbg!()left in code - Public APIs documented
- Error messages are helpful
LLM Reference
This page provides a concise reference for AI/LLM tools working with scoop.
Tip: The raw text versions are available at
llms.txt(concise) andllms-full.txt(full API reference).
Overview
scoop is a centralized Python virtual environment manager — pyenv-style workflow powered by uv. Written in Rust.
All virtualenvs are stored in ~/.scoop/virtualenvs/. Override with SCOOP_HOME env var.
Commands
| Command | Description |
|---|---|
scoop list | List virtualenvs (aliases: ls) |
scoop list --pythons | List installed Python versions |
scoop list --sort <name|created|last-used> | Sort order; envs missing the timestamp sort last with name tie-break (Unreleased) |
scoop create <name> [version] | Create virtualenv (default: latest Python) |
scoop create <name> <ver> --install-python | Create env; install Python on demand if missing (v0.11.0) |
scoop use <name> | Set + activate environment |
scoop use <name> --global | Set as global default |
scoop use <name> --link | Also create .venv symlink for IDE |
scoop use system | Deactivate, use system Python |
scoop use --unset | Remove version file |
scoop remove <name> | Delete virtualenv (aliases: rm, delete) |
scoop clone <src> <dst> | Duplicate an env in-place (--no-packages for skeleton) (v0.11.0) |
scoop install [version] | Install Python version |
scoop uninstall <version> | Remove Python version |
scoop info <name> | Show virtualenv details (includes Last used: row since Unreleased) |
scoop status | Summarise current state (Active/Configured/System/None) (v0.11.0); includes Last used: since Unreleased |
scoop which <exe> | Resolve an executable inside the active env (v0.11.0) |
scoop run <env> -- <cmd> | Run a command inside an env without activating (v0.11.0) |
scoop sync | Reconcile the active env with .scoop.toml manifest (v0.11.0) |
scoop export <name> | Snapshot an env as JSON (schema v1) (v0.11.0) |
scoop import <file> | Restore an env from a JSON snapshot (v0.11.0) |
scoop doctor | Health check |
scoop doctor --fix | Auto-fix issues |
scoop shell <name> | Set shell-specific env (temporary) |
scoop shell --unset | Clear shell-specific setting |
scoop init <shell> | Output shell init script |
scoop completions <shell> | Generate completion script |
scoop lang [code] | Get/set language (en, ko, ja, pt-BR) |
scoop migrate list | List migratable envs (pyenv, conda, virtualenvwrapper) |
scoop migrate @env <name> | Migrate single environment |
scoop migrate all | Migrate all environments (parallel via rayon since v0.11.0) |
scoop gc | Garbage-collect orphan virtualenvs (--yes to actually remove, --aggressive also for unused Pythons, --older-than <n>d/w/y flags stale envs by last_used; envs with no last_used are never matched) |
scoop prune | Prune the uv cache (uv cache prune wrapper) |
scoop verify [NAME] | Per-env health diagnosis — 6 checks (metadata, python binary, pyvenv.cfg, activate, exec, manifest drift); --strict exits 1 on issues |
scoop man [DIR] | Generate man pages (stdout or one file per subcommand in DIR) |
Most commands support --json for machine-readable output.
Global options: --quiet, --no-color
Key Concepts
Set a Specific Python Version as Global Default
To set Python 3.11.0 as the global default for new shells:
scoop install 3.11.0
scoop create py311 3.11.0
scoop use py311 --global
Important: --global stores an environment name (py311) in ~/.scoop/version,
not the raw Python version string. Local .scoop-version and SCOOP_VERSION
override the global default.
Create a Project Environment with Python 3.9.5
scoop install 3.9.5
scoop create myproject 3.9.5
scoop info myproject
If 3.9.5 is not found, check discovery with uv python list and
scoop list --pythons, then install and retry.
Uninstall Python and Associated Environments
# Optional preview
scoop list --python-version 3.12
# Remove Python 3.12 and all environments that use it
scoop uninstall 3.12 --cascade
# Verify cleanup
scoop list --pythons
scoop doctor
For automation, use scoop uninstall 3.12 --cascade --force.
Without --cascade, dependent environments are not removed and may become broken.
Temporarily Disable or Customize Auto-Activation (Project-Scoped)
# Current shell only (temporary disable)
export SCOOP_NO_AUTO=1
unset SCOOP_NO_AUTO
# Project-local behavior (writes .scoop-version in current dir)
scoop use system
scoop use myproject
# Terminal-only override (no file changes)
scoop shell system
scoop shell --unset
Use these without --global to avoid changing global settings.
Install Dependencies from requirements.txt in Active Environment
# environment already active (prompt shows: (myproject))
pip install -r requirements.txt
Use pip install -r path/to/requirements.txt for non-root files.
Verify with pip list.
List Python Versions and Associated Environments
scoop list --pythons
scoop list
scoop list --python-version 3.12
Use --json for automation and --bare for script-friendly output.
For full mapping in shell scripts, iterate versions from scoop list --pythons --bare
and query each with scoop list --python-version <VERSION> --bare.
Integrate Custom or Pre-Existing Python
# Preferred: explicit interpreter path
scoop create myenv --python-path /opt/python-debug/bin/python3
# Alternative: make interpreter discoverable via PATH
export PATH="/opt/python-debug/bin:$PATH"
scoop create myenv 3.13
Verify with uv python list, scoop info myenv, and scoop doctor -v.
Custom interpreter path is stored in ~/.scoop/virtualenvs/<name>/.scoop-metadata.json
(python_path field).
Version Files
Priority (first match wins):
SCOOP_VERSIONenv var (shell session override, set byscoop shell).scoop-versionin current directory (local, walks parent directories)~/.scoop/version(global default)
Shell Integration
scoop outputs shell code to stdout; the shell wrapper evals it (pyenv pattern).
Auto-activation triggers on directory change when .scoop-version is present.
Supported shells: bash, zsh, fish, PowerShell (Core 7.x+ and Windows PowerShell 5.1+)
Disable auto-activation: export SCOOP_NO_AUTO=1
Environment Name Rules
- Pattern:
^[a-zA-Z][a-zA-Z0-9_-]*$(max 64 chars) - Must start with a letter
- Reserved words: activate, base, completions, create, deactivate, default, delete, global, help, init, install, list, local, remove, resolve, root, system, uninstall, use, version, versions
Migration Sources
Import environments from pyenv-virtualenv, virtualenvwrapper, and conda.
Internationalization
Supported languages: English (en), Korean (ko), Japanese (ja), Portuguese-BR (pt-BR)
Priority: SCOOP_LANG env > ~/.scoop/config.json > system locale > en
Configuration
- Config file:
~/.scoop/config.json - Home directory:
~/.scoop/(override:SCOOP_HOME) - Metadata:
~/.scoop/virtualenvs/<name>/.scoop-metadata.json
Examples
Explore real-world usage examples in the examples/ directory on GitHub.
| Example | Description |
|---|---|
| Basic Workflow | Create, use, and remove environments |
| Migration from pyenv | Import pyenv-virtualenv environments |
| Multi-Project Setup | Manage multiple project environments |
| GitHub Actions CI | Use scoop in CI pipelines |