4.9 Repository Rules

The first time you write a repository rule, it can look like an oddly powerful macro:

def _sdk_repo_impl(rctx):
    rctx.download_and_extract(url = rctx.attr.urls, sha256 = rctx.attr.sha256)
    rctx.file("BUILD.bazel", rctx.attr.build_file_content)

That little function does not produce a target. It makes a whole repository appear, early enough that Bazel can load packages and labels from it. That timing is the source of both the usefulness and the danger. Repository rules are where Bazel reaches outside the already-known build graph: archive servers, local SDKs, credential systems, host tools, generated BUILD files, and sometimes whole package-manager worlds.

Use this section as a boundary review. The question is not "can Starlark fetch or generate this?" The question is "what outside facts can change the repository Bazel will later load?"

First Ask Whether You Need A Custom Rule

4.9.1 Repository Rule Fundamentals gives the base model: a repo rule materializes a repository-shaped directory tree on demand during loading, so labels such as @repo//pkg:target can resolve when a package needs them. This fetching work is part of loading, but it is not a normal target action. 4.9.2 Archive Downloads & Integrity shows the safest ordinary shape for that boundary: stable archive bytes, URL mirrors, sha256 or integrity, deterministic extraction, and generated BUILD files.

If all you need is "download these bytes and expose them to Bazel," the right answer is usually a built-in rule such as http_archive, not new Starlark. 4.9.3 When to Write Custom Repository Rules is the decision point: write a custom repository rule only when the materialization logic itself is custom, such as lockfile translation, a non-HTTP protocol, SDK discovery, generated toolchain repositories, or controlled binary bootstrap. Once that decision is justified, 4.9.4 Repository Rule API introduces repository_rule(), implementation functions, attrs, generated files, downloads, templates, paths, and the sharp edge of repository_ctx.execute().

Then Account For Every Outside Fact

4.9.5 Reproducibility Concerns is the heart of the section. A repository rule runs before ordinary action sandboxing, so every meaningful network result, host file, environment value, helper program, and generated repo name must become part of the rule's declared input story.

4.9.6 getenv() vs os.environ Pitfall is the small example that reveals the larger rule: repository_ctx.getenv() records an environment dependency, while repository_ctx.os.environ can read the same value without giving Bazel a refetch edge. 4.9.7 watch() / watch_tree() File Dependencies is the file-system version. If the rule reads a local config file, probes an SDK directory, or branches on whether a path exists, it must use watch(), read(..., watch = ...), path.readdir(watch = ...), or watch_tree() at the right scope.

4.9.8 Hermetic Host Tooling zooms out from individual APIs to the design pattern: when host discovery is unavoidable, model the helper executable, environment, watched files, generated BUILD labels, and local-repository boundary honestly. Repository rules are not hermetic because they are written in Starlark. They become dependable when the external facts they use are visible enough for Bazel to invalidate and reproduce the generated repository.

Keep Fetching On Bazel's Infrastructure Path

Real organizations fetch private archives, mirror public dependencies, restrict egress, run air-gapped CI, and try to avoid repeated dependency setup on fresh workers. Repository rules are only useful in those environments when they stay on Bazel's structured downloader path.

4.9.9 Credential Helper Protocol adds the secret boundary: authentication should come from domain-scoped helpers that return HTTP headers to Bazel's downloader, not from tokens embedded in BUILD files, repository-rule attrs, or command-line header flags. The hash still matters. Credentials open the URL, but sha256 or integrity verifies the bytes.

4.9.10 Remote Downloader moves from per-request auth to fleet infrastructure, in the experimental infrastructure layer of the repository-rule story. A remote downloader can centralize URL fetching behind a Remote Asset service and remote CAS, while repository rules continue to say the same boring thing: download this URL with this expected hash. The rule API does not need to know whether the bytes came from a local repository cache, a mirror, a credential-protected endpoint, or a remote asset service.

4.9.11 Non-incremental External Evaluation is the warning label for the whole section. If a repository rule hides Nix, CMake, or another large evaluator behind repository_ctx.execute(), Bazel can track only the repo rule's declared inputs and final repository output. The external tool's own dependency graph stays opaque, so every refetch can become a full external evaluation. Any package or target that needs the repository cannot finish loading and analysis until that setup completes.

Why This Feels Different From Actions

Repository rules surprise rule authors because they run before ordinary action execution, outside the sandboxing habits learned earlier. An environment read can miss a refetch edge unless it goes through getenv(). A local file probe can look deterministic while Bazel has no watch on the path. A helper program can smuggle host state into generated BUILD files. A private download can work on one laptop and fail in CI unless authentication stays on Bazel's downloader path.

Those are all boundary mistakes. A repository rule is allowed to touch the outside world, but it must turn each outside fact into something Bazel can track, verify, or route through infrastructure. Later module extensions in 4.10 Authoring Module Extensions build on this boundary rather than replacing it.

Read the section by the job in front of you. If you are new to repository rules, start with 4.9.1 Repository Rule Fundamentals, 4.9.2 Archive Downloads & Integrity, and 4.9.3 When to Write Custom Repository Rules to decide whether you need a custom rule at all. If you are implementing one, treat 4.9.4 Repository Rule API, 4.9.5 Reproducibility Concerns, 4.9.6 getenv() vs os.environ Pitfall, and 4.9.7 watch() / watch_tree() File Dependencies as a single correctness unit: attrs, downloads, environment, files, and generated names have to be considered together. If you are reviewing or operating a ruleset, jump to the boundary article that matches the risk: host discovery, private downloads, remote downloader infrastructure, or opaque non-incremental setup.

think

Trace: A generated repository depends on an archive, a local configuration file, an environment variable, a helper program, and its user-facing repository name. Where should each outside fact appear so Bazel can refetch correctly and consumers see stable labels?

Reveal

Represent the archive with a downloader call and checksum, the local file with a watched read or explicit watch, the environment value with repository_ctx.getenv(), and the helper with a declared label attribute rather than ambient PATH. Use repository_ctx.original_name and apparent-name-aware labels for generated references instead of parsing canonical names. Credentials and fleet download policy should remain in Bazel's credential-helper and downloader infrastructure, not in generated BUILD content.

The mini-ruleset keeps its materializer in a private repository_rule and centralizes the public repository naming convention.

key takeaway

Repository rules are Bazel's loading-phase boundary with the outside world. Use them to fetch, verify, and shape repositories, not to hide arbitrary setup scripts. Good repository rules keep external facts explicit: stable bytes, checksums, attrs, watched files, tracked environment, apparent names, and downloader-aware authentication or mirroring. That is what lets Bazel know when to refetch, and what lets module extensions build on repo rules without inheriting hidden host state.