Python

SlowShield serves PyPI at https://slowshield.example.com/pypi/simple/, a standard package index (PEP 503 and PEP 691). Every tool that takes an index URL works with it: pip, uv, Poetry, PDM, Pipenv, and what installs through them, such as uvx, pipx and pre-commit hooks.

Set it up

pip and uv read environment variables, so a few lines in your shell profile cover them in every new terminal (the Setup page has bash on macOS, zsh and fish too):

cat >> ~/.bashrc <<'EOF'
export PIP_INDEX_URL=https://slowshield.example.com/pypi/simple/
export UV_DEFAULT_INDEX=https://slowshield.example.com/pypi/simple/
export npm_config_registry=https://slowshield.example.com/npm/
export GOPROXY=https://slowshield.example.com/go
export PIP_UPLOADED_PRIOR_TO=P3D
export npm_config_min_release_age=3
EOF
source ~/.bashrc

Remove the PIP_UPLOADED_PRIOR_TO and npm lines if you don't want the second layer or don't use npm. Each tool, with the version its own release age needs:

pip

Release age: pip 26.1 or later (pip 26.0 fails with it, 25 and older ignore it). Installs from pylock.toml fail with it, because pip doesn't record upload times there

command

pip config set global.index-url https://slowshield.example.com/pypi/simple/

release age

pip config set global.uploaded-prior-to P3D

uv

Release age: uv 0.9.17 or later (older versions fail with it). In pyproject.toml, not the environment: uv records it in uv.lock, so a different value elsewhere breaks uv sync --locked

pyproject.toml

[[tool.uv.index]]
name = "slowshield"
url = "https://slowshield.example.com/pypi/simple/"
default = true

[tool.uv]
exclude-newer = "P3D"

environment

export UV_DEFAULT_INDEX=https://slowshield.example.com/pypi/simple/

Poetry

Release age: Poetry 2.4 or later

command

poetry source add --priority=primary slowshield https://slowshield.example.com/pypi/simple/

release age

poetry config solver.min-release-age 3

PDM

Release age: PDM 2.27 or later. In pyproject.toml, not as pdm lock --exclude-newer, which isn't kept and re-resolves every pin

pyproject.toml

[[tool.pdm.source]]
name = "pypi"
url = "https://slowshield.example.com/pypi/simple/"

[tool.pdm.resolution]
exclude-newer = "3d"

Pipenv

Pipfile

[[source]]
url = "https://slowshield.example.com/pypi/simple/"
verify_ssl = true
name = "slowshield"

What changes

The second layer

pip 26.1, uv 0.9.17, Poetry 2.4 and PDM 2.27 can refuse releases younger than a few days themselves. Set them to 3 days, below SlowShield's 7, and they stay silent on a normal day. Notes from testing them on 2026-10-06:

In Docker and CI

A build container doesn't read your shell profile. Pass the index as a build argument: an ARG is visible to RUN as an environment variable and isn't kept in the image.

FROM python:3.13-slim
ARG PIP_INDEX_URL=https://slowshield.example.com/pypi/simple/
# uv: ARG UV_DEFAULT_INDEX=https://slowshield.example.com/pypi/simple/
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

With a default in the Dockerfile, every build uses SlowShield and --build-arg PIP_INDEX_URL=… can still override it. Building images covers the base image too, and how to check that nothing in a build goes around SlowShield. In CI, set the variables for the job:

# GitHub Actions (workflow or job)
env:
  PIP_INDEX_URL: https://slowshield.example.com/pypi/simple/
  UV_DEFAULT_INDEX: https://slowshield.example.com/pypi/simple/
  npm_config_registry: https://slowshield.example.com/npm/
  GOPROXY: https://slowshield.example.com/go
  PIP_UPLOADED_PRIOR_TO: P3D
  npm_config_min_release_age: 3

Limits

For agents: this page as Markdown · llms.txt · the SlowShield skill