# SlowShield > SlowShield is a free, open-source, self-hosted proxy for PyPI, npm, Go modules, Maven (Maven, Gradle, sbt), crates.io and container images (Docker Hub, GHCR, Quay, registry.k8s.io and others). It holds new releases back for a few days (7 by default), refuses known malware from the OSV and GitHub advisory feeds, and checks every download against the registry's digest and the fingerprint it recorded the first time. Package managers and container runtimes point at one SlowShield instance instead of the public registries; the instance's Setup page (`/ui/setup`) shows every setting with its address filled in. Each page below is Markdown; the guide's examples use `https://slowshield.example.com`. For agents: if an install fails with `403` and "too new" (Maven: `425 Too Early`), the version is younger than the delay: use the newest version that installs instead, and don't go around SlowShield. A `451` (container images: `403` with reason `blocked`) is known malware: don't install it or a look-alike. The Agents page has the details and an Agent Skill. ## Guide - [Docs](https://slowshield.org/docs/index.md): How to point every package manager and container runtime at SlowShield, what changes for your builds, and how to check that nothing goes around it. - [Python](https://slowshield.org/docs/python/index.md): Use SlowShield as the package index for pip, uv, Poetry, PDM and Pipenv: setup, what changes, the package managers' own release age, and Docker builds. - [JavaScript](https://slowshield.org/docs/javascript/index.md): Use SlowShield as the npm registry for npm, pnpm, Yarn and Bun: setup, what changes, the package managers' own release age, and Docker builds. - [Go](https://slowshield.org/docs/go/index.md): Use SlowShield as your GOPROXY: setup, what changes for go get and go.mod, the checksum database, and Docker builds. - [Java](https://slowshield.org/docs/java/index.md): Use SlowShield as the Maven repository for Maven, Gradle, sbt and Coursier: setup, what changes, 425 Too Early, and Docker builds. - [Rust](https://slowshield.org/docs/rust/index.md): Use SlowShield as Cargo's crates.io source: setup, held versions shown as yanked, Cargo.lock, and Docker builds. - [Container images](https://slowshield.org/docs/containers/index.md): Pull container images through SlowShield with containerd, Kubernetes, Docker, Docker Desktop, Podman and BuildKit: setup, tags that lag behind, refusals, and what can go around it. - [Building images](https://slowshield.org/docs/container-builds/index.md): Build container images with Docker or Podman so that the base images and every dependency of the build come through SlowShield, and check that nothing goes around it. - [Agents](https://slowshield.org/docs/agents/index.md): SlowShield for coding agents: what its answers mean and what to do about them, instructions for AGENTS.md, the SlowShield Agent Skill, and how to put it in front of a sandbox. ## Agent Skill - [SKILL.md](https://slowshield.org/skills/slowshield/SKILL.md): set up and use SlowShield for every ecosystem; references in the same folder - [slowshield-skill.zip](https://slowshield.org/skills/slowshield.zip): the skill folder, for agents that load Agent Skills - Claude Code: `/plugin marketplace add squirro/slowshield`, then `/plugin install slowshield@slowshield` ## Optional - [Source code](https://github.com/squirro/slowshield): README, issues, releases - [Configuration reference](https://github.com/squirro/slowshield/blob/main/docs/configuration.md) - [Container images: ghcr.io/squirro/slowshield](https://github.com/squirro/slowshield/pkgs/container/slowshield) # Docs How to point your package managers and container runtimes at SlowShield, what changes for your builds, and how to check that nothing goes around it. Every page shows the settings for `https://slowshield.example.com`; your instance's Setup page (`/ui/setup`) shows the same settings with its own address filled in. - [PythonPyPI: pip, uv, Poetry, PDM, Pipenv](https://slowshield.org/docs/python/) - [JavaScriptnpm: npm, pnpm, Yarn, Bun](https://slowshield.org/docs/javascript/) - [GoGo modules, as your `GOPROXY`](https://slowshield.org/docs/go/) - [JavaMaven Central, Google Maven, Gradle plugins: Maven, Gradle, sbt, Coursier](https://slowshield.org/docs/java/) - [Rustcrates.io: Cargo](https://slowshield.org/docs/rust/) - [Container imagesDocker Hub, GHCR, Quay, registry.k8s.io and more: containerd, Kubernetes, Docker, Podman](https://slowshield.org/docs/containers/) - [Building imagesBase images and every dependency of a Docker or Podman build through SlowShield](https://slowshield.org/docs/container-builds/) ## What SlowShield does - **New releases wait.** A version becomes installable a number of days after it was published, 7 by default. Until then it is left out of the version lists your package manager reads, so it resolves to the newest version that is old enough. Asking for it directly, from a lockfile or a pinned digest, gets a refusal with the time it becomes available. - **Known malware is refused**, for good, as soon as the OpenSSF/OSV or GitHub advisory feeds report it: HTTP `451` with the advisory (`403` for container images). Administrators can block packages, versions and images themselves too. - **Every download is verified** against the registry's own digest and against the fingerprint SlowShield recorded the first time it served the file. A file that changes is cut off mid-transfer. - **Everything is cached.** Each file comes from the registry once and from SlowShield after that. ## Setting it up 1. **Run SlowShield** on one host your team and CI can reach: Docker Compose, rootless Podman or the Helm chart ([Get started](https://slowshield.org/#get-started)). Use a real host name with HTTPS; the examples here use `slowshield.example.com`. 2. **Point every tool at it.** Four lines in a shell profile cover pip, uv, npm and go. Other tools need a setting each, on the pages above. 3. **Close the way around it.** Block the public registries at the firewall for machines that should only use SlowShield, so a forgotten setting fails loudly instead of quietly going around it. For your shell profile (bash on Linux; the Setup page also has bash on macOS, zsh and fish): ``` 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 ``` ## Two layers Most package managers can now refuse brand-new releases themselves. Turn that on as well, a little shorter than SlowShield: 7 days in SlowShield, 3 in the package manager. On a normal day only SlowShield holds anything back. When something gets past it, a laptop without the setup or a brand-new package SlowShield serves because no version is old enough yet, the package manager still waits. Each tool's section shows its setting and the version it needs. ## What you see when something is held - **Normal installs** just get an older version. Indexes carry `X-SlowShield-Held-Versions`, the number of versions left out. - **A pin that is too new** (lockfile, `==` pin, image digest) gets `403` with `Retry-After` and a message saying when it becomes available (Maven and Gradle: `425 Too Early`, the only status they report clearly). - **A brand-new package** none of whose versions is old enough is served and recorded as *fail-open* on PyPI, npm and Go, and refused on Maven and Cargo, where brand-new packages are the usual attack (typosquats, impersonations). Container images only fail open during an instance's first week. - **Exceptions.** An administrator can release one version early, or hold a package longer, in `config.toml` ([configuration](https://github.com/squirro/slowshield/blob/main/docs/configuration.md)). ## More - [Configuration reference](https://github.com/squirro/slowshield/blob/main/docs/configuration.md) and [config.example.toml](https://github.com/squirro/slowshield/blob/main/config.example.toml) - [Release-age policy](https://github.com/squirro/slowshield/blob/main/docs/policy.md), [integrity checks](https://github.com/squirro/slowshield/blob/main/docs/integrity.md), [threat feeds](https://github.com/squirro/slowshield/blob/main/docs/feeds.md) - [Operations](https://github.com/squirro/slowshield/blob/main/docs/operations.md) and [observability](https://github.com/squirro/slowshield/blob/main/docs/observability.md) --- This page as HTML: https://slowshield.org/docs/. All of the guide in one file: https://slowshield.org/llms-full.txt # 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 - **Files wait, not just versions.** Each wheel and sdist becomes installable 7 days after its own `upload-time` (PEP 700), which PyPI sets and authors can't. A wheel added to an old release waits too. A release none of whose files is old enough is left out of the index, so resolvers pick the newest release that has one. - **A pin that is too new.** Resolvers don't see held files, so `pip install pkg==1.2.3` reports that no matching distribution exists. A lockfile that records file URLs, such as `uv.lock`, downloads the file directly and gets `403` with `Retry-After` and the time it becomes available. - **Malware** from the OSV and GitHub feeds is refused with `451` and the advisory, also when it is older than the delay. - **Brand-new packages** none of whose files is old enough are served and recorded as fail-open (`X-SlowShield-Fail-Open: 1`), unless your administrator set `fail_open = false`. That's what the second layer is for. - **Lockfiles.** `uv.lock` and `Pipfile.lock` record the index URL; lock with SlowShield in place so they name it. Hashes don't change: SlowShield serves PyPI's files byte for byte. ## 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: - **uv**: put `exclude-newer` in `pyproject.toml`, not the environment. uv records it in `uv.lock`, so a different value elsewhere (CI, a Dockerfile) breaks `uv sync --locked`. For one package you need early: `exclude-newer-package`. - **pip**: `PIP_UPLOADED_PRIOR_TO=P3D` needs pip 26.1; pip 26.0 refuses to run with it and older versions ignore it. Installs from `pylock.toml` fail with it, because pip doesn't record upload times there. - **PDM**: in `[tool.pdm.resolution]`; `pdm lock --exclude-newer` isn't kept and re-resolves every pin. ## 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](https://slowshield.org/docs/container-builds/) 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 - Requirements from git repositories or direct URLs (`pkg @ https://…`) don't go through an index, so SlowShield doesn't see them. - `--extra-index-url` pointing at pypi.org goes around SlowShield: pip picks the best version across all indexes. Use private indexes for private packages only, and block pypi.org and files.pythonhosted.org at the firewall. - Plain HTTP (a local test instance) needs `PIP_TRUSTED_HOST` for pip. --- This page as HTML: https://slowshield.org/docs/python/. All of the guide in one file: https://slowshield.org/llms-full.txt # JavaScript SlowShield serves the npm registry at `https://slowshield.example.com/npm/`. npm, pnpm, Yarn and Bun use it like registry.npmjs.org, and so does everything that installs through them, such as `npx`, MCP servers and CI steps. ## Set it up npm and pnpm read `npm_config_registry`, so a line in your shell profile covers 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 ``` Yarn 2 and later ignores `.npmrc` and the npm variables, and Bun's `bunfig.toml` loses to an `npm_config_registry` in the environment. Each tool, with the version its own release age needs: ### npm Release age: npm 11.10 or later (11.0 to 11.9 warn about an unknown setting, 10 ignores it) *command* ``` npm config set registry https://slowshield.example.com/npm/ npm config set min-release-age 3 ``` *.npmrc (project or ~)* ``` registry=https://slowshield.example.com/npm/ min-release-age=3 ``` ### pnpm Release age: pnpm 10.16 or later (older versions ignore it) *command* ``` pnpm config set registry https://slowshield.example.com/npm/ ``` *pnpm-workspace.yaml* ``` minimumReleaseAge: 4320 # 3 days, in minutes ``` ### Yarn Release age: Yarn 4.10 or later (older versions refuse to run with it) *.yarnrc.yml (Yarn Berry)* ``` npmRegistryServer: "https://slowshield.example.com/npm/" npmMinimalAgeGate: "3d" ``` ### Bun Release age: Bun 1.3 or later (older versions ignore it) *bunfig.toml* ``` [install] registry = "https://slowshield.example.com/npm/" minimumReleaseAge = 259200 # 3 days, in seconds ``` ## What changes - **Versions wait.** A version becomes installable 7 days after the time the registry recorded for it. Versions younger than that are left out of the package documents (packuments), and `latest` moves to the newest stable version that is old enough. Other dist-tags that point at a held version are removed. - **A pin that is too new.** `npm install pkg@1.2.3` for a held version reports that no matching version exists. A lockfile that names it downloads the tarball directly and gets `403` with `Retry-After` and the time it becomes available. - **Malware** from the OSV and GitHub feeds is refused with `451` and the advisory. - **Brand-new packages** none of whose versions is old enough are served and recorded as fail-open, unless your administrator set `fail_open = false`. That's what the second layer is for. - **Lockfiles record SlowShield.** npm needs absolute tarball URLs, so SlowShield's point at itself (`https://slowshield.example.com/npm/pkg/-/pkg-1.2.3.tgz`) and `package-lock.json` records them. Integrity hashes don't change: the tarballs are npm's, byte for byte. ## The second layer npm 11.10, pnpm 10.16, Yarn 4.10 and Bun 1.3 can refuse releases younger than a few days themselves (Deno 2.6 too). Set them to 3 days, below SlowShield's 7, and they stay silent on a normal day. Mind the units, checked on 2026-10-06: npm's `min-release-age` counts days, pnpm's `minimumReleaseAge` minutes and Bun's `minimumReleaseAge` seconds. npm 11.0 to 11.9 warn about the unknown setting, npm 10 ignores it, and Yarn before 4.10 refuses to run with it. To let one package through early: `min-release-age-exclude` in npm. ## In Docker and CI A build container doesn't read your shell profile. Pass the registry as a build argument: an `ARG` is visible to `RUN` as an environment variable and isn't kept in the image. ``` FROM node:22-slim ARG npm_config_registry=https://slowshield.example.com/npm/ WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci ``` Tarball URLs come from SlowShield's public URL, so the build must be able to reach SlowShield under that name. [Building images](https://slowshield.org/docs/container-builds/) 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 - Dependencies from git or a tarball URL don't go through the registry, so SlowShield doesn't see them. - Scoped registries (`@company:registry=…`) stay as they are: private packages keep coming from your private registry. - Block registry.npmjs.org at the firewall for machines that should only use SlowShield. --- This page as HTML: https://slowshield.org/docs/javascript/. All of the guide in one file: https://slowshield.org/llms-full.txt # Go SlowShield serves Go modules at `https://slowshield.example.com/go` as a `GOPROXY`, and passes the checksum database (sum.golang.org) through, so the go command needs no other route out. ## Set it up ### Go *command (writes go env)* ``` go env -w GOPROXY=https://slowshield.example.com/go ``` *private modules: fetched directly, not through SlowShield* ``` go env -w GOPRIVATE=git.example.com/* ``` Or the `GOPROXY` line in your shell profile, with the other package managers (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 ``` - **No `,direct` and no `|`.** With `,direct`, the go command fetches from the origin whenever the proxy answers 404 or 410; with `|`, on any error. SlowShield never answers a refusal with 404 or 410, but a module it can't find would still go around it. - **`GOSUMDB` stays at its default.** SlowShield answers `/go/sumdb/sum.golang.org/supported`, so the go command sends its checksum-database requests through SlowShield too. - **Private modules** keep bypassing the proxy through `GOPRIVATE`. ## What changes - **The publish time** is when proxy.golang.org first stored the version (the `Last-Modified` of its `.mod`). The commit time in `.info` is set by the author and can be backdated, so it is never used. - **`go get pkg@latest`** and version queries pick the newest version that is old enough: newer ones are left out of `@v/list` and `@latest`. - **go.mod pins exact versions.** A requirement that is too new fails with `403` instead of picking an older version. The go command prints SlowShield's message: ``` go: example.com/hello@v1.2.0: reading https://slowshield.example.com/go/example.com/hello/@v/v1.2.0.info: 403 Forbidden server response: slowshield: example.com/hello@v1.2.0 is too new. It was published 2026-10-03T08:21:51Z (2.0 days ago); this proxy requires 7 days. It becomes available at 2026-10-10T08:21:51Z. Use an older version, or ask your SlowShield administrator for an exception. ``` - **Malware and tampering** are refused with `451`. Every `.mod` and `.zip` is checked against the checksum database and the fingerprint SlowShield recorded the first time, which also protects builds that set `GOSUMDB=off`. ## The second layer The go command has no release-age setting, so SlowShield is the only layer. ## In Docker and CI ``` FROM golang:1.25 AS build ARG GOPROXY=https://slowshield.example.com/go WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /app . ``` The official images don't set `GOPROXY`, so the `ARG` is what the go command uses. [Building images](https://slowshield.org/docs/container-builds/) covers the base image too, and how to check that nothing in a build goes around SlowShield. ## Limits - Toolchain downloads (`GOTOOLCHAIN` switching to a newer Go) are modules too, so they come through SlowShield like any other. - Block proxy.golang.org and sum.golang.org at the firewall for machines that should only use SlowShield. --- This page as HTML: https://slowshield.org/docs/go/. All of the guide in one file: https://slowshield.org/llms-full.txt # Java SlowShield serves Maven repositories at `https://slowshield.example.com/maven/`, for Maven, Gradle, sbt and Coursier, and so for Java, Kotlin and Scala projects. Builds keep their dependencies and plugins as they are; only where they come from changes. | Path | Serves | For | |---|---|---| | `/maven/all/` | Google's groups from Google Maven, everything else from Maven Central | Maven's mirror, sbt, Coursier | | `/maven/central/` | Maven Central | Gradle `mavenCentral()` | | `/maven/google/` | Google Maven | Gradle `google()` | | `/maven/gradle-plugins/` | the Gradle Plugin Portal | Gradle `gradlePluginPortal()` and plugin resolution | | `/maven//` | a repository your administrator adds (JitPack, Confluent, …) | anything else | ## Set it up Maven and Gradle don't read environment variables for this: each needs a file in your home directory, which covers every project on the machine. ### Maven *~/.m2/settings.xml* ``` slowshield * https://slowshield.example.com/maven/all/ ``` ### Gradle *~/.gradle/init.d/slowshield.init.gradle* ``` def slowshield = [ 'https://repo.maven.apache.org/maven2': 'https://slowshield.example.com/maven/central/', 'https://repo1.maven.org/maven2': 'https://slowshield.example.com/maven/central/', 'https://dl.google.com/dl/android/maven2': 'https://slowshield.example.com/maven/google/', 'https://plugins.gradle.org/m2': 'https://slowshield.example.com/maven/gradle-plugins/', ] def rewrite = { repo -> if (repo instanceof MavenArtifactRepository) { def to = slowshield[repo.url.toString().replaceAll('/$', '')] if (to) { repo.url = new URI(to) } } } beforeSettings { s -> s.pluginManagement.repositories.all(rewrite) } settingsEvaluated { s -> s.pluginManagement.repositories.all(rewrite) s.dependencyResolutionManagement.repositories.all(rewrite) } allprojects { p -> p.buildscript.repositories.all(rewrite) p.repositories.all(rewrite) } ``` ### sbt *~/.sbt/repositories* ``` [repositories] local slowshield: https://slowshield.example.com/maven/all/ ``` *environment* ``` export SBT_OPTS="-Dsbt.override.build.repos=true $SBT_OPTS" ``` ### Coursier *environment* ``` export COURSIER_REPOSITORIES="ivy2Local|https://slowshield.example.com/maven/all/" ``` ## What changes - **The publish time** is each file's `Last-Modified` on the repository (when the repository stored it), or when SlowShield first saw the version listed, whichever is earlier. - **Version ranges and `latest`** pick the newest version that is old enough: newer ones are left out of `maven-metadata.xml`, and `` and `` are recomputed. - **A pin that is too new** fails with `425 Too Early`. Maven and Gradle only show an error's status line, never its body, and a `403` would read like a credentials problem: ``` Could not transfer artifact org.example:lib:jar:1.4.0 from/to slowshield (https://slowshield.example.com/maven/all/): status code: 425, reason phrase: Too Early (425) ``` Maven asks again on every build (it caches a `404`, not a `425`), so the first build after the hold ends works without `-U`. - **Brand-new artifacts** none of whose versions is old enough are held too: Maven doesn't fail open, because brand-new artifacts are the realistic attack (typosquats, dependency confusion). - **Malware and tampering** are refused with `451`. Every file is checked against the repository's checksums and the fingerprint SlowShield recorded the first time. ## The second layer Maven, Gradle, sbt and Coursier have no release-age setting, so SlowShield is the only layer. ## In Docker and CI Write the mirror into the build container's `settings.xml`. With a build argument, the same Dockerfile still builds without SlowShield when the argument is empty: ``` FROM maven:3.9-eclipse-temurin-21 AS build ARG MAVEN_MIRROR=https://slowshield.example.com/maven/all/ RUN if [ -n "$MAVEN_MIRROR" ]; then mkdir -p /root/.m2 && printf '%s' \ "slowshield*$MAVEN_MIRROR" \ > /root/.m2/settings.xml; fi WORKDIR /src COPY pom.xml . COPY src src RUN mvn -B package ``` This one mirror also covers Maven's own plugins: a test build of a small project fetched 107 artifacts through SlowShield, plugins included. For Gradle, copy the init script above to `/root/.gradle/init.d/slowshield.gradle`. [Building images](https://slowshield.org/docs/container-builds/) covers the base image too, and how to check that nothing in a build goes around SlowShield. ## Limits - The Gradle wrapper downloads Gradle itself from services.gradle.org, which SlowShield doesn't serve. Use a Gradle that is already installed, such as the one in the official `gradle` images, where only SlowShield is reachable. - Private repositories stay direct with `*,!their-id`. - Plain HTTP (a local test instance): Maven blocks `http://` repositories; the Setup page's `settings.xml` overrides that block with the mirror id `maven-default-http-blocker`. - Block repo1.maven.org, repo.maven.apache.org, dl.google.com and plugins.gradle.org at the firewall for machines that should only use SlowShield. --- This page as HTML: https://slowshield.org/docs/java/. All of the guide in one file: https://slowshield.org/llms-full.txt # Rust SlowShield serves crates.io at `https://slowshield.example.com/cargo/` as a sparse registry that replaces `crates-io` in Cargo's configuration. Needs Cargo 1.68 or later. ## Set it up Cargo only takes source replacement from a configuration file, not from environment variables: ### Cargo Release age: Cargo's own setting (min-publish-age) isn't stable yet: SlowShield is the only layer. *~/.cargo/config.toml* ``` [source.crates-io] replace-with = "slowshield" [registries.slowshield] index = "sparse+https://slowshield.example.com/cargo/" ``` *CI and Dockerfiles (appends to $CARGO_HOME/config.toml)* ``` mkdir -p "${CARGO_HOME:-$HOME/.cargo}" && printf '%s\n' '[source.crates-io]' 'replace-with = "slowshield"' '[registries.slowshield]' 'index = "sparse+https://slowshield.example.com/cargo/"' >> "${CARGO_HOME:-$HOME/.cargo}/config.toml" ``` - **Cargo.lock doesn't change.** It keeps crates.io as the source, with crates.io's checksums, so a project builds the same with or without SlowShield. - **The official `rust` images** set `CARGO_HOME=/usr/local/cargo`, where `~/.cargo/config.toml` isn't read. The command writes `${CARGO_HOME:-$HOME/.cargo}/config.toml`, which works in both. ## What changes - **The publish time** is each version's `pubtime` in the index, which crates.io sets and authors can't. The first one SlowShield sees is kept, so a rewritten index can't move it earlier. - **Held versions show up as yanked.** Cargo resolves to the newest version that isn't, exactly as when a version is yanked upstream. When only held versions satisfy a requirement, Cargo says the version "is yanked". - **A Cargo.lock that pins a held version** fails with the reason and the command to use instead: ``` failed to get successful HTTP response from `…/cargo/crates/tokio/1.53.2/download`, got 403 body: slowshield: tokio@1.53.2 is too new. It was published 2026-10-03T11:18:32Z (3.0 days ago); this proxy requires 7 days. It becomes available at 2026-10-10T11:18:32Z. Use an older version (cargo update -p tokio@1.53.2 --precise 1.53.1), or ask your SlowShield administrator for an exception. ``` - **Brand-new crates** none of whose versions is old enough are held too: Cargo doesn't fail open, because nearly all malicious crates are brand-new typosquats and impersonations. - **Malware** from OSV (including RustSec's malicious advisories) and GitHub is refused with `451`, and a `.crate` is checked against the index's checksum and the fingerprint SlowShield recorded the first time. ## The second layer Cargo's own setting, `min-publish-age`, isn't stable yet, so SlowShield is the only layer. ## In Docker and CI ``` FROM rust:1 AS build ARG CARGO_INDEX=sparse+https://slowshield.example.com/cargo/ RUN if [ -n "$CARGO_INDEX" ]; then printf '%s\n' '[source.crates-io]' 'replace-with = "slowshield"' \ '[registries.slowshield]' "index = \"$CARGO_INDEX\"" >> "$CARGO_HOME/config.toml"; fi WORKDIR /src COPY . . RUN cargo build --release ``` Or run the Setup page's one-line command in a `RUN` step. [Building images](https://slowshield.org/docs/container-builds/) covers the base image too, and how to check that nothing in a build goes around SlowShield. ## Limits - Commands that need the crates.io web API: `cargo search` fails, `cargo info` needs `--registry slowshield`, and `cargo publish`, `yank` and `owner` need `--registry crates-io`. - Not covered: git dependencies, build scripts that download things, rustup toolchains, and alternative registries. - Block index.crates.io and static.crates.io at the firewall for machines that should only use SlowShield. --- This page as HTML: https://slowshield.org/docs/rust/. All of the guide in one file: https://slowshield.org/llms-full.txt # Container images SlowShield serves container images at `https://slowshield.example.com/v2/`, from Docker Hub, GHCR, Quay, registry.k8s.io, gcr.io, MCR and ECR Public. Tags lag behind: `FROM nginx:latest` keeps working, about a week behind. ## Set it up Each runtime has its own setting. These were checked against containerd 2.2, Docker 29.8, Podman 5.8, BuildKit 0.33, skopeo and crane: with them, containerd, Docker's default image store and Podman only ever ask SlowShield. ### containerd */etc/containerd/certs.d/_default/hosts.toml: every registry, pull only* ``` server = "https://slowshield.example.com" capabilities = ["pull", "resolve"] ``` */etc/containerd/certs.d/ghcr.io/hosts.toml: a registry you also push to* ``` server = "https://ghcr.io" capabilities = ["push"] [host."https://slowshield.example.com"] capabilities = ["pull", "resolve"] ``` ### Docker */etc/docker/certs.d/_default/hosts.toml: the containerd image store (docker info lists io.containerd.snapshotter.v1), for docker pull and docker build* ``` server = "https://slowshield.example.com" capabilities = ["pull", "resolve"] ``` */etc/docker/daemon.json: the classic image store. Docker Hub only, and Docker pulls from Docker Hub itself after a refusal* ``` { "registry-mirrors": ["https://slowshield.example.com"] } ``` ### Podman */etc/containers/registries.conf.d/50-slowshield.conf: Podman, CRI-O, Buildah and skopeo* ``` [[registry]] prefix = "docker.io" location = "slowshield.example.com/docker.io" [[registry]] prefix = "gcr.io" location = "slowshield.example.com/gcr.io" [[registry]] prefix = "ghcr.io" location = "slowshield.example.com/ghcr.io" [[registry]] prefix = "mcr.microsoft.com" location = "slowshield.example.com/mcr.microsoft.com" [[registry]] prefix = "public.ecr.aws" location = "slowshield.example.com/public.ecr.aws" [[registry]] prefix = "quay.io" location = "slowshield.example.com/quay.io" [[registry]] prefix = "registry.k8s.io" location = "slowshield.example.com/registry.k8s.io" ``` ### BuildKit *buildkitd.toml (also docker buildx create --buildkitd-config): BuildKit pulls from the registry itself after a refusal* ``` [registry."docker.io"] mirrors = ["slowshield.example.com"] [registry."gcr.io"] mirrors = ["slowshield.example.com"] [registry."ghcr.io"] mirrors = ["slowshield.example.com"] [registry."mcr.microsoft.com"] mirrors = ["slowshield.example.com"] [registry."public.ecr.aws"] mirrors = ["slowshield.example.com"] [registry."quay.io"] mirrors = ["slowshield.example.com"] [registry."registry.k8s.io"] mirrors = ["slowshield.example.com"] ``` ### Image names *any client: SlowShield's host in front of the image name* ``` crane pull slowshield.example.com/docker.io/library/nginx:1.29 nginx.tar ``` *Dockerfile* ``` FROM slowshield.example.com/docker.io/library/nginx:1.29 ``` ### Docker Desktop (macOS, Windows) Docker Desktop's engine runs in a VM. When Docker Desktop starts, it copies `~/.docker/certs.d` on your machine to `/etc/docker/certs.d` in the VM, so put the file there and restart Docker Desktop: ``` mkdir -p ~/.docker/certs.d/_default cat > ~/.docker/certs.d/_default/hosts.toml <<'EOF' server = "https://slowshield.example.com" capabilities = ["pull", "resolve"] EOF ``` From then on every `docker pull` and every base image of `docker build` comes through SlowShield. If SlowShield is unreachable, pulls fail instead of going to the registry. Pushes need the registry's own file (see containerd above). ### Podman on macOS and Windows Podman runs in a VM too. Put the drop-in there: ``` podman machine ssh 'mkdir -p ~/.config/containers/registries.conf.d && cat > ~/.config/containers/registries.conf.d/50-slowshield.conf' < 50-slowshield.conf ``` (`/etc/containers/registries.conf.d/` for a rootful machine.) Short names like `python:3.13-slim` resolve to `docker.io/library/python` first, and the drop-in then sends them to SlowShield. ### Kubernetes The kubelet pulls through containerd, which reads the `hosts.toml` files only when its CRI plugin's `config_path` names the directory. The default is empty: ``` # /etc/containerd/config.toml (containerd 2.x) [plugins.'io.containerd.cri.v1.images'.registry] config_path = "/etc/containerd/certs.d" ``` In containerd 1.7 the section is `[plugins."io.containerd.grpc.v1.cri".registry]`. Check a node with `containerd config dump | grep config_path`. A pod whose image is held stays in `ImagePullBackOff` and starts by itself once the hold ends. ## What changes - **Tags lag behind.** A tag resolves to the newest digest it has pointed to for 7 days, so `nginx:latest` stays about a week behind. The time a tag got its digest comes from the registry where it keeps one (Docker Hub's API, Quay, Artifact Registry, MCR), and from SlowShield's own first sight, whichever is earlier. GHCR and ECR Public only have the second. - **A pinned digest that is too new** is refused, not swapped for an older one. - **New instances.** During an instance's first 7 days, a tag it has no history for yet is served at its current digest and recorded as fail-open, so a new instance doesn't refuse half of Docker Hub. Your administrator can make it strict from the start. - **Takedowns propagate.** When a registry removes an image, as Docker Hub did with the compromised Trivy images in March 2026, SlowShield stops serving it too: it checks a stored image again at most every 5 minutes while it is being pulled. - **Blocks.** No malware feed covers images; administrators block a repository, tag, digest or layer in `config.toml`. - **Everything is verified.** Manifests must match their digests; layers are checked while they stream. Every refusal is `403` with a message. What the clients print: | Client | Output | |---|---| | Docker (containerd image store) | `Error response from daemon: error from registry: slowshield: docker.io/library/brandnew:latest (sha256:162a60de2ed8…) is too new. It was pushed …; this proxy requires 7 days. It becomes available at …` | | `docker build`, BuildKit | `403 Forbidden`, then `denied: slowshield: … is too new …` | | Podman, skopeo | `reading manifest latest in slowshield.example.com/docker.io/library/brandnew: denied: slowshield: …` | | crane | `DENIED: slowshield: …` | | containerd 2.2 and older (ctr, nerdctl, Kubernetes events) | `… 403 Forbidden`, without the message | ## The second layer No container runtime has a release-age setting, so SlowShield is the only layer. ## What can go around it - **BuildKit** (also `docker buildx` builders with the docker-container or kubernetes driver) and **Docker's classic image store** pull from the registry themselves after SlowShield refuses an image. Only a firewall that blocks the registries makes them binding. - **A `[host]` entry instead of `server`** in containerd's `hosts.toml` falls back to the registry after a refusal. Keep SlowShield as the `server`. - **Apple's `container` CLI** has no mirror setting: name images with SlowShield's host (`slowshield.example.com/docker.io/library/nginx`). Read from its source, not tested yet. - Block registry-1.docker.io, ghcr.io, quay.io, registry.k8s.io and the others, and their CDNs, at the firewall for machines that should only use SlowShield. To build images with SlowShield for the base images *and* every dependency, see [Building images](https://slowshield.org/docs/container-builds/). --- This page as HTML: https://slowshield.org/docs/containers/. All of the guide in one file: https://slowshield.org/llms-full.txt # Building images A container build fetches from two kinds of places: the registries its base images come from, and the package registries its `RUN` steps install from. Both have to go through SlowShield, and they are set up in different places: the base images in the container runtime, the dependencies in the Dockerfile. ## 1. Base images: the runtime Configure the runtime once per machine, as on [Container images](https://slowshield.org/docs/containers/): - **Docker on Linux:** `/etc/docker/certs.d/_default/hosts.toml` (the containerd image store, Docker's default since 29). It covers `docker build` as well as `docker pull`. - **Docker Desktop:** `~/.docker/certs.d/_default/hosts.toml`, then restart Docker Desktop. - **Podman:** the `registries.conf.d` drop-in, inside the machine on macOS and Windows. `podman build` and Buildah use it too. - **BuildKit builders of their own** (`docker buildx create`, BuildKit in CI): `buildkitd.toml` with mirrors. BuildKit goes to the registry itself after a refusal, so pair it with a network that only reaches SlowShield (step 3). Alternatively, name SlowShield in the Dockerfile: `FROM slowshield.example.com/docker.io/library/python:3.13-slim`. That works with any builder but ties the Dockerfile to your SlowShield. ## 2. Dependencies: the Dockerfile A build container doesn't read your shell profile or your `~/.m2`. Give each stage that installs something an `ARG` named like the tool's setting. An `ARG` reaches `RUN` as an environment variable and isn't kept in the image. With SlowShield as the default, every build uses it, and `--build-arg` can still override it: | Language | In the build stage | |---|---| | Python (pip) | `ARG PIP_INDEX_URL=https://slowshield.example.com/pypi/simple/` | | Python (uv) | `ARG UV_DEFAULT_INDEX=https://slowshield.example.com/pypi/simple/` | | JavaScript (npm, pnpm) | `ARG npm_config_registry=https://slowshield.example.com/npm/` | | Go | `ARG GOPROXY=https://slowshield.example.com/go` | | Java (Maven) | a `settings.xml` with the mirror, written in a `RUN` step ([Java](https://slowshield.org/docs/java/#docker)) | | Java (Gradle) | the init script in `/root/.gradle/init.d/` ([Java](https://slowshield.org/docs/java/#gradle)) | | Rust | Cargo's `config.toml`, written in a `RUN` step ([Rust](https://slowshield.org/docs/rust/#docker)) | A complete example, a Go service on a distroless base: ``` FROM golang:1.25 AS build ARG GOPROXY=https://slowshield.example.com/go WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /app . FROM gcr.io/distroless/static-debian12 COPY --from=build /app /app ENTRYPOINT ["/app"] ``` Both `FROM` lines come through SlowShield via the runtime's configuration (step 1), the modules via the `ARG`. An `ARG` applies to the stage it is declared in: repeat it in each stage that installs something. **npm needs SlowShield's public name.** SlowShield's tarball links are absolute, made from its public URL, so the build has to reach SlowShield under that name. With a real host name and HTTPS that's a given. ## 3. Check that nothing goes around it Three checks, from quick to strict: 1. **Look at the dashboard.** After a build, SlowShield's Packages page lists every base image and package it fetched. A dependency that doesn't show up came from somewhere else. 2. **Stop SlowShield and build again** with `--no-cache --pull`. The pull and every install step should fail. If something still downloads, it goes around SlowShield. 3. **Build where SlowShield is the only thing reachable.** A network without internet access that contains SlowShield, and a builder on it. Anything that isn't configured fails, instead of quietly going to the public registry. With Docker: ``` # a network without internet access, joined by the container that serves # SlowShield's HTTPS (Caddy, in the Compose setup) docker network create --internal slowshield-only docker network connect --alias slowshield.example.com slowshield-only # a builder on that network; buildkitd.toml from the Setup page docker buildx create --name slowshield-only --driver docker-container \ --driver-opt network=slowshield-only --buildkitd-config buildkitd.toml # RUN steps don't use the network's DNS: pass SlowShield's address docker buildx build --builder slowshield-only --load \ --add-host slowshield.example.com=$(docker inspect -f \ '{{(index .NetworkSettings.Networks "slowshield-only").IPAddress}}' ) . ``` In CI, the same idea is an egress allowlist that only contains SlowShield. ## What we tested On 2026-10-07, on macOS with Docker Desktop (Docker 29.8.1, containerd image store) and Podman 6.1, against a SlowShield serving the real registries. Six small applications, each with dependencies from its registry: | Application | Base images | Dependencies | |---|---|---| | Python, pip | `python:3.13-slim` | requests and 4 more from PyPI | | Python, uv | `python:3.13-slim`, `ghcr.io/astral-sh/uv` | the same, with uv | | JavaScript, npm | `node:22-slim` | express and 67 more from npm | | Go | `golang:1.25`, `gcr.io/distroless/static-debian12` | github.com/google/uuid, through the module proxy and the checksum database | | Java, Maven | `maven:3.9-eclipse-temurin-21`, `mcr.microsoft.com/openjdk/jdk:21-distroless` | commons-lang3 and Maven's own plugins: 107 artifacts | | Rust, Cargo | `rust:1`, `gcr.io/distroless/cc-debian12` | itoa from crates.io | - **Docker, only SlowShield reachable** (check 3): all six built and ran. Every base image and every dependency came through SlowShield; nothing else could be reached. - **Docker Desktop's own builder** (`docker build`, with the `hosts.toml` in the VM): the five pip, npm, Go, Maven and Cargo applications built. SlowShield's download counts for each package doubled with the second round of builds. With SlowShield stopped, `docker pull` and `docker build --pull` failed instead of going to Docker Hub. - **Podman** (rootless machine, the drop-in): all six built through a second SlowShield, which served the same 8 image repositories and the same packages. With it stopped, `podman pull` and `pip install` failed. What we ran into, so you don't have to: - **BuildKit `RUN` steps don't resolve names on a custom Docker network** (they get public resolvers). Pass `--add-host`, as in check 3. - **Rootless Podman can't attach build steps to a named network** ("cannot use networks as rootless"). For check 3 with Podman, use a rootful machine or a firewall instead. - **A SlowShield container on an internal Podman network with DNS** can't resolve the registries: that network's resolver answers first and only knows the network. Create the network with `--disable-dns`, or give SlowShield `--dns`. - **Plain HTTP** (a local test instance) needs `PIP_TRUSTED_HOST` for pip and Maven's `maven-default-http-blocker` override; everything else accepted it. Use HTTPS for anything real. --- This page as HTML: https://slowshield.org/docs/container-builds/. All of the guide in one file: https://slowshield.org/llms-full.txt # Agents Coding agents install packages on their own: `pip install`, `npm install`, `npx`, `uvx`, `go get`, `docker pull`, often in a fresh sandbox without a lockfile, and nobody reads the output. This page is for the people who run agents, and for the agents. ## If you are an agent SlowShield may be in front of you. Look for its URL in `PIP_INDEX_URL`, `UV_DEFAULT_INDEX`, `npm_config_registry`, `GOPROXY`, `~/.m2/settings.xml`, `$CARGO_HOME/config.toml` or the container runtime's configuration. `GET /readyz` on it answers `ready`. What its answers mean: | You see | It means | Do | |---|---|---| | An older version than you expected, or "no matching version" for an exact new one | Newer versions are held: they are younger than the delay (7 days by default) | Use the newest version that installs. Don't pin the new one. | | `403` with `{"error":"age_too_new", …, "retry_after_secs": …}` (PyPI, npm), "is too new" (Go, Cargo, container images), `425 Too Early` (Maven, Gradle) | A pinned version (lockfile, digest) is held. `Retry-After` and the message say when it becomes available | Use an older version, or tell the user when this one becomes available. | | `451` with the advisory, such as `{"error":"blocked", "advisory_id":"MAL-…", …}` (container images: `403`, "is blocked") | Known malware, or blocked by the administrator | Don't install it, and don't look for a similarly named package. Tell the user. | | The header `X-SlowShield-Fail-Open: 1` | A brand-new package none of whose versions is old enough, served anyway | Check that the name is exactly the one you meant: typosquats and hallucinated names are brand new too. | **Don't go around it.** Never point an install at pypi.org, registry.npmjs.org, proxy.golang.org (or add `,direct` to `GOPROXY`), Maven Central, crates.io or a container registry directly, and don't add them with `--index-url`, `--extra-index-url` or `--registry`. If a version is needed early, ask the user: an administrator can make an exception for it. ## Instructions for your agent For `AGENTS.md`, `CLAUDE.md` or your agent's system prompt, with your instance's address: ``` ## Installing packages Packages and container images come through SlowShield (https://slowshield.example.com), which holds new releases back for 7 days and refuses known malware. - Never point installs at the public registries, or add them as extra indexes, to go around SlowShield. - "Too new" (403, Maven 425): the version is younger than 7 days. Use the newest version that installs; don't pin the new one. If the user needs it now, say so: an administrator can make an exception. - 451, or "blocked": known malware. Don't install it or a look-alike package. Tell the user. - X-SlowShield-Fail-Open: 1 means a brand-new package. Check the name is exactly the one intended. ``` ## The SlowShield skill An [Agent Skill](https://agentskills.io) that teaches an agent to set up every package manager, Dockerfile and container runtime for a SlowShield instance, check that nothing goes around it, and handle its answers. Its references are this guide, kept in step with it. - **Claude Code:** `/plugin marketplace add squirro/slowshield`, then `/plugin install slowshield@slowshield`. From 0.0.9 on, releases carry the skill: add the marketplace at a release tag (`squirro/slowshield#v0.0.9`) to pin it. Or copy the folder to `~/.claude/skills/slowshield/` (all your projects) or `.claude/skills/slowshield/` (one project). - **Other agents that load Agent Skills:** download [slowshield.zip](https://slowshield.org/skills/slowshield.zip) and unpack it where the agent looks for skills. From 0.0.9 on, each release carries it, with a SHA-256 to check it against. - **Read it first:** [SKILL.md](https://slowshield.org/skills/slowshield/SKILL.md), or the source in [plugins/slowshield](https://github.com/squirro/slowshield/tree/main/plugins/slowshield). ## Put SlowShield in front of a sandbox 1. **Set it in the sandbox image.** pip, uv, npm, npx, uvx and go read environment variables, so the agent needs no instructions to use it: ``` 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 ``` Maven, Gradle and Cargo need a file each ([Java](https://slowshield.org/docs/java/), [Rust](https://slowshield.org/docs/rust/)); images the sandbox pulls need the runtime's setting ([Container images](https://slowshield.org/docs/containers/)). 2. **Let only SlowShield out.** With the registries off the sandbox's network allowlist, an agent can't install around SlowShield, even when it passes its own `--index-url`. 3. **No team instance?** An agent working on one machine can run its own, with nothing kept after it stops. Run a release, never `:latest`, and pin its digest where you can: ``` docker run -d --rm --name slowshield -p 127.0.0.1:8080:8080 ghcr.io/squirro/slowshield:0.0.8 export PIP_INDEX_URL=http://localhost:8080/pypi/simple/ UV_DEFAULT_INDEX=http://localhost:8080/pypi/simple/ \ npm_config_registry=http://localhost:8080/npm/ GOPROXY=http://localhost:8080/go ``` Its dashboard at `http://localhost:8080/ui/` shows what was installed and what was held back. ## Reading this site - [/llms.txt](https://slowshield.org/llms.txt): what SlowShield is, and the guide's pages. - [/llms-full.txt](https://slowshield.org/llms-full.txt): the whole guide in one Markdown file. - Every page of the guide as Markdown: add `index.md` to its address, such as [/docs/python/index.md](https://slowshield.org/docs/python/index.md). --- This page as HTML: https://slowshield.org/docs/agents/. All of the guide in one file: https://slowshield.org/llms-full.txt