Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 WayThe scoop Way
.venv scattered across projects~/.scoop/virtualenvs/ centralized
Manual source .venv/bin/activateAuto-activate on directory entry
pyenv-virtualenv is slowuv-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 friendlyscoop use --link creates .venv symlink for IDE discovery
  • Health checksscoop doctor diagnoses your setup

Getting Started

Ready to scoop? Head to the Installation guide to get started.

Installation

Prerequisites

DependencyVersionInstall Command
uvLatestcurl -LsSf https://astral.sh/uv/install.sh | sh
Rust1.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:

  1. Creates .scoop-version file in the current directory
  2. 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

TaskCommand
List environmentsscoop list
List Python versionsscoop list --pythons
Show environment infoscoop info myproject
Remove environmentscoop remove myproject
Check installationscoop 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

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):

PrioritySourceSet by
1SCOOP_VERSION env varscoop shell
2.scoop-version filescoop use (walks parent directories)
3~/.scoop/version filescoop 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

VariableDescriptionDefault
SCOOP_HOMEBase directory~/.scoop
SCOOP_VERSIONOverride version (highest priority)(unset)
SCOOP_NO_AUTODisable auto-activation(unset)
SCOOP_ACTIVECurrently active environment(set by scoop)
SCOOP_RESOLVE_MAX_DEPTHLimit 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

ShellStatus
ZshFull support (auto-activation, completion)
BashFull support (auto-activation, completion)
FishFull support (auto-activation, completion)
PowerShellFull 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:

  1. uv-managed installations in ~/.local/share/uv/python/ (installed via scoop install or uv python install)
  2. System Python on PATH — executables named python, python3, or python3.x
  3. 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

# 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:

  1. Direct path (recommended): scoop create <env> --python-path /path/to/python
  2. PATH-based discovery: add your Python to PATH, then run scoop create <env> <version>

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:

  1. Discovers environments from pyenv (~/.pyenv/versions/), conda (conda info --envs), or virtualenvwrapper ($WORKON_HOME)
  2. Creates a new scoop environment with the same Python version
  3. Reinstalls packages using uv for improved performance
  4. Preserves originals by default (use --delete-source to 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

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

ScenarioWhat to do
Standard Python versionscoop 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 executablescoop create myenv --python-path /path/to/python
Custom Python in non-standard pathAdd to PATH, then scoop create myenv <version>
PyPy or alternative interpreterscoop create myenv --python-path /opt/pypy/bin/pypy3
Existing pyenv/conda environmentsscoop migrate all
Shared Python installationsSet UV_PYTHON_INSTALL_DIR
Force system-only PythonSet UV_PYTHON_PREFERENCE=only-system
Uninstall Python + cleanup envsscoop uninstall 3.12 --cascade (or manual workflow)
Find envs using a Python versionscoop list --python-version 3.12
Fix broken environmentsscoop 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:

  • --global stores an environment name in ~/.scoop/version, not a raw version like 3.11.0.
  • This global default is applied in new shells and directories without a local .scoop-version.
  • Priority is: SCOOP_VERSION env 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-version changes from scoop 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 --json for machine-readable output.
  • Use --bare for 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_path field.

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:

ShellConfig FileLine to Remove
Bash~/.bashrceval "$(scoop init bash)"
Zsh~/.zshrceval "$(scoop init zsh)"
Fish~/.config/fish/config.fisheval (scoop init fish)
PowerShell$PROFILEInvoke-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:

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

CommandAliasesDescription
scoop listlsList virtualenvs or Python versions
scoop create-Create virtualenv
scoop use-Set + activate environment
scoop removerm, deleteRemove 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:

OptionDescription
-q, --quietSuppress all output
--no-colorDisable colored output
-h, --helpShow help message
-V, --versionShow version

Environment Variables

VariableDescriptionDefault
SCOOP_HOMEBase directory for scoop~/.scoop
SCOOP_NO_AUTODisable auto-activation(unset)
SCOOP_LANGDisplay language (en, ko, ja, pt-BR)System locale
NO_COLORDisable colored output(unset)

Directory Layout

LocationPurpose
~/.scoop/virtualenvs/Virtual environments storage
~/.scoop/versionGlobal default environment
.scoop-versionLocal environment preference
.venvSymlink to active environment (with --link)

list

List all virtual environments or installed Python versions.

Aliases: ls

Usage

scoop list [options]

Options

OptionDescription
--pythonsShow 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
--bareOutput names only (for scripting)
--jsonOutput as JSON

Sort

--sort reorders the output without changing what’s shown:

ModeOrderTie-break
nameAlphabetical (default, back-compat)
createdNewest created_at firstName (asc)
last-usedMost recently activated firstName (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-version cannot be combined with --pythons (which lists Python installations, not environments).

Empty Results

  • If no Python versions are installed, scoop list --pythons shows no entries.
  • If no environments exist, scoop list shows 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

ArgumentRequiredDefaultDescription
nameYes-Name for the new virtualenv
python-versionNo3 (latest)Python version (e.g., 3.12, 3.11.8)

Options

OptionDescription
--force, -fOverwrite existing virtualenv
--python-path <PATH>Use a specific Python executable instead of version discovery
--install-pythonInstall 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:

  1. uv-managed Python installations
  2. System Python on PATH (Homebrew, apt, pyenv, etc.)
  3. 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:

  1. Validate the path (exists, is a file, is executable)
  2. Auto-detect the Python version from the binary
  3. 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

ArgumentRequiredDescription
nameNoName of the virtualenv, or system for system Python

Options

OptionDescription
--unsetRemove version file (local or global)
--global, -gSet as global default
--linkCreate .venv symlink for IDE compatibility
--no-linkDo not create .venv symlink (default)

Behavior

  • Creates .scoop-version file 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

ArgumentRequiredDescription
nameYesName of the virtualenv to remove

Options

OptionDescription
--force, -fSkip 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

ArgumentRequiredDefaultDescription
versionNolatestPython version (e.g., 3.12, 3.11.8)

Options

OptionDescription
--latestInstall latest stable Python (default)
--stableInstall 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 patch
  • 3.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:

  1. uv-managed — installed via scoop install or uv python install
  2. System PATH — Homebrew, apt, pyenv, or any Python on your PATH
  3. 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

ArgumentRequiredDescription
versionYesPython version to remove

Options

OptionDescription
--cascadeAlso remove all virtual environments using this Python version
--force, -fSkip 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 --cascade to 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

OptionDescription
-v, --verboseShow more details (can repeat: -vv)
--jsonOutput diagnostics as JSON
--fixAuto-fix issues where possible

Checks Performed

CheckWhat it verifies
uv installationuv is installed and accessible
Shell integrationShell hook is properly configured
Environment integrityPython 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 python binary 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 doctor periodically 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

ArgumentRequiredDescription
nameYesName of the virtualenv

Options

OptionDescription
--all-packagesShow the full installed-package list (default: top 5)
--no-sizeSkip the directory-size walk
--jsonOutput 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:

StateTrigger
active$SCOOP_ACTIVE is set (shell-activated)
configuredA .scoop-version file or ~/.scoop/version selects an env
systemThe configured env is the literal name system
noneNothing 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

ArgumentRequiredDescription
exeYesExecutable name to locate (e.g. python, pip, pytest)

Options

OptionDescription
--env <name>Look in this environment instead of the active one
--jsonOutput as JSON

Resolution Order

  1. --env <name> if provided
  2. $SCOOP_ACTIVE (set by scoop activate / scoop shell)
  3. .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

CodeMeaning
0Path printed to stdout
1No 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

ArgumentRequiredDescription
envYesName of the virtualenv
commandYesProgram (and arguments) to execute

Environment Wiring

The spawned child sees the same vars that scoop activate would set:

VariableValue
VIRTUAL_ENVAbsolute path of the env
SCOOP_ACTIVE<env>
PATHEnv’s bin/ prepended to inherited PATH
PYTHONHOMERemoved

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

OptionDescription
--with <GROUP>Install an extra package group on top of default (repeatable)
--dry-runPrint the resolved plan without creating env or installing packages
--jsonOutput 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].name follows the same validation as scoop create <NAME> (letter-leading, [a-zA-Z][a-zA-Z0-9_-]*, not a reserved subcommand name).
  • [environment].python is forwarded to uv venv --python as-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

StateWhat scoop sync does
Env missingAuto-installs the requested Python (if uv doesn’t have it), creates the env, then installs packages
Env exists, Python matchesJust installs packages (idempotent — pip resolves and skips already-satisfied entries)
Env exists, Python mismatchWarns 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

CodeMeaning
0Sync succeeded (or dry-run produced a plan)
1Manifest 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-path flag on scoop 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

ArgumentRequiredDescription
nameYesName of the environment to export

Options

OptionDescription
-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.python is the resolved version recorded in the env’s metadata (e.g. 3.12.7), not the original specifier you typed.
  • environment.created_at is RFC 3339 and may be absent for hand-authored or pre-metadata exports.
  • packages is 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_used timestamp. last_used is 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 no last_used and the field populates the first time the new env is activated locally.

Exit Codes

CodeMeaning
0Export written successfully (or printed to stdout)
1Env not found, or writing the destination file failed

See Also

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

ArgumentRequiredDescription
pathYesPath to the export JSON, or - to read from stdin

Options

OptionDescription
--name <NAME>Override the env name from the file (validated like scoop create)
-f, --forceOverwrite an existing environment with the same name
--jsonOutput as JSON

Behaviour

  1. Reads and validates the export schema. Mismatched scoop_export_version produces a clear EXPORT_UNSUPPORTED_VERSION error pointing at upgrade guidance instead of trying to limp on.
  2. Applies --name override (if any) and validates the resulting name.
  3. If the target env already exists: errors out unless --force is set, in which case the existing env is removed first.
  4. Auto-installs the requested Python if it isn’t already available via uv (matches the ergonomics of scoop sync).
  5. 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

CodeMeaning
0Imported successfully
1Invalid file, unsupported schema version, invalid name, or env existed without --force

See Also

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

ArgumentRequiredDescription
srcYesName of the source environment
dstYesName for the new environment

Options

OptionDescription
--no-packagesSkip package copy — create an empty env at the same Python version
-f, --forceOverwrite the destination if it already exists
--jsonOutput as JSON

Behaviour

  1. Validates <DST> (rejects reserved names like list, clone, …).
  2. Refuses a self-clone (src == dst).
  3. Resolves <SRC>’s recorded Python version from its metadata; surfaces a clear CorruptedEnvironment error when metadata is missing so you know recreate-from-scratch is the right next step.
  4. Creates <DST> at the same Python version.
  5. 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

CodeMeaning
0Cloned successfully
1Invalid dst name, self-clone, src missing, dst exists without --force, or src corrupted

See Also

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

SubcommandDescription
listList environments available for migration
@env <name>Migrate a single environment by name
allMigrate all discovered environments

Supported Sources

SourceDetection
pyenv-virtualenv~/.pyenv/versions/ (non-system virtualenvs)
virtualenvwrapper$WORKON_HOME or ~/.virtualenvs/
condaconda info --envs

Options

OptionSubcommandDescription
--source <pyenv|virtualenvwrapper|conda>all subcommandsRestrict to a single source tool
--jsonall subcommandsMachine-readable output (see JSON Output)
--dry-run@env, allPreview without making changes
--force@env, allOverwrite existing scoop env with the same name; bypass EOL Python guard
--yes@env, allSkip the interactive confirmation prompt
--strict@env, allFail on the first package install error inside an env (default: keep going)
--delete-source@env, allRemove the source env after successful migration
--rename <new-name>@envMigrate under a different name
--auto-rename@envOn 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

CodeReturned 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)
2At least one per-env failure or at least one preflight name conflict without --force. Returned via MigrationBatchFailed
3No source tool (pyenv / virtualenvwrapper / conda) is detected on the system. Returned via MigrationSourcesNotFound

migrate @env <name>

CodeReturned when
0The env migrated successfully (or the user chose Skip at the interactive conflict prompt)
2MigrationNameConflict (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)
3The 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 all always exited 0 regardless of per-env outcome. CI gates need 0.14+ to distinguish success from a batch where some envs failed.
  • When migrate all returns MigrationBatchFailed, the human summary and JSON envelope are already on stdout/stderr; main.rs suppresses the global error: 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.

Status of source envWith --force
ReadyMigrated normally
Name conflict with an existing scoop envThe existing scoop env is overwritten in place
EOL Python version (e.g. 2.7)Migrated anyway (the EOL guard is intentionally bypassed)
Corrupted source envNot 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>-pyenv even for virtualenvwrapper / conda sources; 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-rename proceeds with the EOL Python silently. Prefer --force if 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 stable ScoopError::code() constant (e.g. "MIGRATE_EXTRACTION_FAILED", "MIGRATE_NAME_CONFLICT", "UV_COMMAND_FAILED"). Scripts branch on this instead of parsing error (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.

CommandError variantTrigger
migrate allMigrationSourcesNotFoundNo source tool detected at all (pyenv / virtualenvwrapper / conda)
migrate @env <name>PyenvEnvNotFound / VenvWrapperEnvNotFound / CondaEnvNotFoundThe named env isn’t present in the requested (or any) source
migrate @env <name>CorruptedEnvironmentThe 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

  1. Discovery: Scans configured source paths for virtual environments
  2. Extraction: Identifies Python version and installed packages
  3. Recreation: Creates new scoop environment with same Python version
  4. Package Install: Reinstalls packages using uv pip install
  5. Cleanup: Originals are preserved by default; --delete-source removes them after successful migration

Notes

  • Original environments are preserved by default; use --delete-source to remove sources after migration
  • Package versions are preserved where possible
  • Migration creates fresh environments using uv for 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/python on Unix / Scripts/python.exe on 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 rejectedm 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, manual date set, or hibernated VM that woke up with a stale time) can shift which envs gc --older-than considers stale. This is a best-effort heuristic, not a security boundary — pair it with --yes only 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_used is 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

OptionDescription
-y, --yesActually remove the candidates (default: preview only)
--aggressiveAlso remove uv-managed Python versions that no environment uses
--older-than <DURATION>Also flag envs idle past the cutoff (30d / 2w / 1y)
--jsonOutput 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 cache
  • doctor — diagnose without removing
  • remove — 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 gc for orphan virtualenvs

Options

OptionDescription
--jsonOutput the result as JSON

Examples

# Standard cleanup
scoop prune

# Capture freed-bytes for a script
scoop prune --json | jq -r '.data.output'

See also

  • gc — garbage-collect orphan virtual environments
  • doctor — health check (does not delete anything)

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:

CheckStatusWhat it means
metadataFail.scoop-metadata.json is missing or unreadable
python_binaryFailbin/python (Scripts/python.exe on Windows) is missing
pyvenv_cfgFailpyvenv.cfg venv marker is missing
activate_scriptFailbin/activate (Scripts/Activate.ps1 on Windows) is missing
python_executesFailpython --version fails to run (Skip if the binary is already missing)
manifest_matchWarnenv’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

CommandScopeAction
doctorSystem (uv install, shell wrapper, paths)Diagnose + optional --fix
verifyPer-env (file presence, exec, manifest)Diagnose only
gcAll 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 diagnostics
  • gc — remove broken envs detected here
  • info — 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:

SectionFields
Pythonpython_version from each env’s metadata
Packagesname==version set via uv pip list --format=json on each env
Metadatapython_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

OptionDescription
--jsonOutput as JSON (see JSON Output)
--strictExit 1 if any differences are detected (default: always exit 0)
--packages-onlySkip the metadata section; package enumeration still runs
--metadata-onlySkip 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:

CodeReturned when
0Default — 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>:

abchanged
"x""x"false
"x""y"true
"x"nulltrue
nullnullfalse

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-onlydata.metadata is null.
  • --metadata-onlydata.packages is null; no uv subprocess 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 env
  • list — enumerate all envs
  • api.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

ArgumentDescription
<code>Language code to set (e.g., en, ko)

Options

OptionDescription
--listList all supported languages
--resetReset to system default language
--jsonOutput as JSON

Supported Languages

CodeLanguage
enEnglish (default)
ko한국어 (Korean)
ja日本語 (Japanese)
pt-BRPortuguês (Brazilian Portuguese)

Language Detection Priority

  1. SCOOP_LANG environment variable
  2. ~/.scoop/config.json setting
  3. System locale (via sys-locale)
  4. 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

ArgumentRequiredDescription
shellYesShell 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), the eval is automatic:

scoop shell myenv    # Works directly

Arguments

ArgumentRequiredDescription
nameNoEnvironment name or system

Options

OptionDescription
--unsetClear shell-specific environment
--shell <SHELL>Target shell type (auto-detected if not specified)

Behavior

  • Sets SCOOP_VERSION environment variable
  • If name is an environment: also outputs activation script
  • If name is system: also outputs deactivation script
  • --unset: outputs unset 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

ArgumentRequiredDescription
shellYesShell 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 init includes 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

ArgumentDescription
[DIR]Write scoop.1 + scoop-<sub>.1 files into this directory. Omit to print the top-level page to stdout.

Options

OptionDescription
--jsonOutput 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 from clap::Command)

Contributing

Guide for contributing to scoop development.

Prerequisites

  • Rust 1.85+ (Edition 2024)
  • uv - Python package manager (install)
  • prek - Pre-commit hooks (install)

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:

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-version files
  • 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 uv CLI commands
  • Python version management

Shell Integration

Shell scripts are embedded in Rust code:

  • src/shell/bash.rs - Bash init script
  • src/shell/zsh.rs - Zsh init script

Key components:

  1. Wrapper function - Intercepts use/activate/deactivate
  2. Hook function - Auto-activation on directory change
  3. Completion function - Tab completion

Adding a New Command

  1. Define command in src/cli/mod.rs:
#![allow(unused)]
fn main() {
#[derive(Subcommand)]
pub enum Commands {
    // ...
    MyCommand {
        #[arg(short, long)]
        option: bool,
    },
}
}
  1. Create handler in src/cli/commands/my_command.rs:
#![allow(unused)]
fn main() {
pub fn execute(output: &Output, option: bool) -> Result<()> {
    // Implementation
    Ok(())
}
}
  1. Export in src/cli/commands/mod.rs

  2. Wire up in src/main.rs

  3. 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:

  1. Create PR with changes
  2. Merge to main
  3. release-plz creates release PR
  4. Merge release PR to publish to crates.io

Internal Documentation

See .docs/ for internal technical references:

  • TECHNICAL_REFERENCE.md - Implementation details
  • SHELL_GOTCHAS.md - Shell integration pitfalls
  • IMPLEMENTATION_PLAN.md - Development roadmap
  • brand/brand.md - Brand guidelines

Code Style

  • Follow Rust conventions
  • Run cargo fmt before committing
  • Keep functions small and focused
  • Document public APIs with /// comments
  • Use thiserror for 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:


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

  1. Concise: CLI messages should be short and clear
  2. Natural: Use natural phrasing, not word-for-word translation
  3. Casual: Friendly, approachable tone — like talking to a colleague
  4. 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

TypeEnglish ExampleGuidance
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:

  1. Check your community — What do Python developers in your language use?
  2. Consistency — Pick one term and stick with it throughout
  3. 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:

TermReason
scoopBrand name
uvTool name
pyenvTool name
condaTool name
virtualenvTechnical term
virtualenvwrapperTool name
PythonLanguage name
shellTechnical term (bash, zsh, fish)
JSONFormat name
PATHEnvironment variable
pipTool 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:

EnglishWhat to look for
environmentYour language’s term for “environment”
createCommon verb for “make/create”
remove/deleteCommon verb for “delete/remove”
installStandard software installation term
uninstallStandard software removal term
activateTerm for “enable/turn on”
deactivateTerm for “disable/turn off”
migrateIT term for migration (often kept as loanword)
versionYour language’s term for “version”
pathYour language’s term for file path
errorYour language’s term for “error”
successYour 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:

TermMeaningGuidance
scoopThe toolAlways keep as “scoop”
flavorvirtualenvTranslate if the metaphor works in your language
freezer~/.scoop/ directoryTranslate 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 build succeeds
  • cargo test passes
  • SCOOP_LANG={code} scoop lang shows 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.yml consumed 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’ xgettext doesn’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
  • Cli struct defines global options
  • Commands enum defines subcommands
  • Each command has an execute function in commands/

Core Layer (core/)

ModulePurpose
doctorHealth check system with Check trait
metadataJSON metadata for virtualenvs
versionVersion file discovery and parsing
virtualenvVirtualenv entity and operations

Shell Layer (shell/)

Generates shell scripts for integration:

  • init_script() - Returns shell initialization code
  • Wrapper function for scoop command
  • Auto-activation hooks
  • Tab completion definitions

Output Layer (output/)

Handles terminal output formatting:

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() - Returns SCOOP_HOME or ~/.scoop
  • virtualenvs_dir() - Returns virtualenvs directory
  • global_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-version is 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

CratePurpose
clapArgument parsing & completion
clap_completeShell completion generation
serdeJSON serialization
serde_jsonMetadata persistence
thiserrorError type definitions
owo-colorsTerminal colors
indicatifProgress bars & spinners
dialoguerInteractive prompts
dirsHome directory resolution
whichBinary lookup (uv, python)
regexVersion parsing & validation
walkdirDirectory traversal
rust-i18nInternationalization (en, ko, ja, pt-BR)
sys-localeSystem locale detection
chronoTimestamp generation

Extension Points

Adding a New Shell

  1. 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.

  1. Add to ShellType enum in cli/mod.rs:
#![allow(unused)]
fn main() {
pub enum ShellType {
    // ... existing
    MyShell,
}
}
  1. Add detection to shell::detect_shell() in src/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

  1. 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
    }
}
}
  1. 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

OperationTime ComplexityNotes
List envsO(n)n = number of virtualenvs
Create envO(1)**Depends on uv performance
Delete envO(1)Simple directory removal
Version resolutionO(d)d = directory depth (walks parents)
Doctor checksO(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

  1. Path Traversal: All user-provided names are validated via regex before use in filesystem operations.
  2. Command Injection: uv commands are constructed using typed arguments, not string concatenation.
  3. Symlink Safety: Doctor checks detect and warn about broken symlinks.
  4. Metadata Integrity: JSON parsing errors are gracefully handled without panics.

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 directory
  • python_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 recent scoop activate/run/shell touch timestamp, if present (Unreleased). None for 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_at is DateTime<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 installed
  • HomeCheck - Checks SCOOP_HOME directory
  • VirtualenvCheck - Validates virtualenvs directory
  • SymlinkCheck - Checks for broken virtualenv Python symlinks (e.g., <env>/bin/python)
  • ShellCheck - Verifies shell integration
  • VersionCheck - 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 pass locale = to rust-i18n’s t!, 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:

  1. FISH_VERSION → Fish
  2. PSModulePath → PowerShell
  3. ZSH_VERSION → Zsh
  4. 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:

  1. SCOOP_VERSION environment variable (checked at shell hook level, not in VersionService)
  2. .scoop-version in current directory
  3. .scoop-version in parent directories (walks up)
  4. ~/.scoop/version (global default)

Note: .python-version is 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

  1. Define command in cli/mod.rs:
#![allow(unused)]
fn main() {
#[derive(Subcommand)]
pub enum Commands {
    // ... existing commands
    MyCommand {
        #[arg(help = "Argument description")]
        arg: String,
    },
}
}
  1. 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(())
}
}
  1. 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),
            ],
        }
    }
}
}


For AI/LLM Tools

When analyzing or modifying this codebase:

  1. Use symbolic tools for precise navigation:

    • find_symbol to locate specific functions/types
    • find_referencing_symbols to understand usage
    • get_symbols_overview for module structure
  2. Follow existing patterns:

    • Error handling: Always return Result<T> with ScoopError
    • 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
  3. 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.rs utilities (cross-platform — Unix bin/ vs Windows Scripts/)
    • 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.

CodeMeaning
0Success
1Generic failure or semantic finding (verify failed, generic operational error)
2Migration failure / MigrationNameConflict (and reserved for future probe/tool failures)
3Migration source-discovery error (pyenv/conda/venvwrapper missing or corrupted)

Per-command exit code table:

Command0123
verifyalways (informational)
verify --strictcleanany Fail check
migrate @env / migrate allsuccessfailure / conflictsource tool not found
doctorcleanwarningerror
Other commandssuccessfailure
  1. Documentation requirements:
    • All pub fn must have doc comments
    • Examples in doctests where applicable
    • Error conditions documented
    • Shell integration changes require cross-shell testing

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:

ModuleTestsCoverage
error::tests54Error types, codes, suggestions
output::json::tests35JSON serialization, edge cases
validate::tests30Name/version validation
core::version::tests18Version file resolution
core::virtualenv::tests12Virtualenv service
paths::tests16Path utilities
shell::*::tests14Shell 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

  1. Test naming: test_<function>_<scenario>_<expected>
  2. Arrange-Act-Assert: Clear test structure
  3. One assertion per test: When practical
  4. Test edge cases: Empty, unicode, special chars, boundaries
  5. No test interdependencies: Each test should be isolated
  6. 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:

HookRunsUsed for
onCreateCommandonce (in Codespace prebuild)install nextest / llvm-cov / mutants / uv, warm cargo build
updateContentCommandon every prebuild refreshcargo fetch to keep registry warm
postCreateCommandwhen user creates the Codespaceprek 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/:

BinaryTargets
parsingclap parse, TOML manifest, JSON uv python list
validationis_valid_env_name across 6 representative inputs
path_lookupfind_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 the gh-pages branch 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

LintSeverityDescription
clippy::unwrap_usedWarnUse ? or expect() instead
clippy::panicWarnAvoid panic in library code
clippy::todoWarnRemove before release
clippy::dbg_macroWarnRemove debug macros
clippy::print_stdoutWarnUse 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

HookDescriptionWhen
cargo-fmtCode formattingPre-commit
cargo-clippyLintingPre-commit
cargo-checkType checkingPre-commit
trailing-whitespaceRemove trailing spacesPre-commit
end-of-file-fixerEnsure newline at EOFPre-commit
check-tomlValidate TOML filesPre-commit
check-yamlValidate YAML filesPre-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

ItemConventionExample
Modulessnake_caseversion_file
Functionssnake_caseget_version()
TypesPascalCaseVirtualenvService
ConstantsSCREAMING_SNAKEMAX_NAME_LENGTH
Lifetimesshort 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 unsafe unless 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!() or dbg!() 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) and llms-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

CommandDescription
scoop listList virtualenvs (aliases: ls)
scoop list --pythonsList 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-pythonCreate env; install Python on demand if missing (v0.11.0)
scoop use <name>Set + activate environment
scoop use <name> --globalSet as global default
scoop use <name> --linkAlso create .venv symlink for IDE
scoop use systemDeactivate, use system Python
scoop use --unsetRemove 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 statusSummarise 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 syncReconcile 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 doctorHealth check
scoop doctor --fixAuto-fix issues
scoop shell <name>Set shell-specific env (temporary)
scoop shell --unsetClear 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 listList migratable envs (pyenv, conda, virtualenvwrapper)
scoop migrate @env <name>Migrate single environment
scoop migrate allMigrate all environments (parallel via rayon since v0.11.0)
scoop gcGarbage-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 prunePrune 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):

  1. SCOOP_VERSION env var (shell session override, set by scoop shell)
  2. .scoop-version in current directory (local, walks parent directories)
  3. ~/.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.

ExampleDescription
Basic WorkflowCreate, use, and remove environments
Migration from pyenvImport pyenv-virtualenv environments
Multi-Project SetupManage multiple project environments
GitHub Actions CIUse scoop in CI pipelines