3.2.8 tools/bazel Wrapper
recommendedBazelisk — the version manager introduced in 0.1.1 Bazelisk & .bazelversion — does more than download the right Bazel binary. Before launching it, Bazelisk checks for a wrapper script in the project's tools/ directory. If one exists, Bazelisk runs the wrapper instead of Bazel, passing the path to the real binary through the BAZEL_REAL environment variable1. This gives maintainers a project-level hook to prepare the environment, route downloads, or enforce policies before any build begins.
How Bazelisk Finds the Wrapper
Bazelisk searches for wrapper scripts in a specific order, stopping at the first match1:
tools/bazel.<OSNAME>-<ARCH>— OS- and architecture-specific (e.g.tools/bazel.linux-amd64)tools/bazel.<ARCH>— architecture-specific only (for single-OS projects)tools/bazel— generic, portable scripttools/bazel.ps1— PowerShell script (Windows)tools/bazel.bat— batch file (Windows)
On Windows, the .exe extension is required for binary wrappers. The OS/arch-specific variants let you ship different wrappers per platform without conditionals inside the script itself.
BAZEL_REAL and the Wrapper Contract
When Bazelisk invokes the wrapper, it unconditionally sets several environment variables1:
BAZEL_REAL— absolute path to the downloaded Bazel binary. The wrapper uses this to call the real Bazel after its own setup logic.BAZELISK— path to the Bazelisk binary itself. Useful for scripts that need to know whether they are running under Bazelisk.PATHprepend — Bazelisk adds the directory containing the downloaded Bazel binary to the front ofPATH, so nestedbazelinvocations (e.g. fromgenruleorbazel runtargets) resolve to the same version.
A minimal wrapper that simply delegates after some setup:
#!/usr/bin/env bash
# tools/bazel — project-level wrapper
# Custom setup logic here
export PATH="/opt/hermetic-tools/bin:$PATH"
exec "$BAZEL_REAL" "$@"
The wrapper receives all command-line arguments that the user typed, so exec "$BAZEL_REAL" "$@" preserves them exactly.
The maintainer-workspace tools/bazel shows the same shape in a runnable repo: it asserts BAZEL_REAL is set (and exits with a clear error if not, catching direct-Bazel users early), exports a project-specific BAZEL_MAINTAINER_SAMPLE_MODE=ci when CI=true, then execs the real binary.
What Wrappers Are For
The wrapper runs before any .bazelrc processing or Bazel server startup. This makes it the right place for concerns that sit outside Bazel's own flag and configuration machinery from 3.2.1 .bazelrc Hierarchy.
Stabilizing the environment. A wrapper can narrow PATH, unset variables that leak into repository rules, or set hermetic defaults. Using tools/bazel to stabilize the environment passed to the Bazel client has limits worth understanding: rule sets that rely on less common environment variables can break, and engineers using bazel run may find their environment unexpectedly stripped2. The wrapper controls what Bazel sees, but that includes what bazel run targets inherit.
Routing downloads through internal mirrors. A wrapper can detect the client's location (CI region, office, VPN) and write or select a URL rewriter configuration that prioritizes the nearest mirror. Combining a tools/bazel hook with Bazel's built-in URL rewriter to reorganize mirror priority by geographic proximity measurably reduces download times for a distributed workforce2.
Detecting CI vs. local context. The wrapper can inspect environment variables like CI, GITHUB_ACTIONS, or BUILDKITE and adjust behavior accordingly — for example, injecting --config=ci or writing temporary configuration files that .bazelrc then imports.
Enforcing Bazelisk Usage
If a developer downloads a raw Bazel binary and puts it on PATH directly — rather than using Bazelisk or a package installer — then neither tools/bazel nor .bazelversion are checked at all1. System package installers ship a shell wrapper (bazel.sh) that does call tools/bazel with BAZEL_REAL, but without BAZELISK_SKIP_WRAPPER set1. In either case, the developer bypasses Bazelisk's version pinning.
Bazelisk provides a detection mechanism: it sets BAZELISK_SKIP_WRAPPER=true when calling the wrapper, to avoid calling itself in a loop1. The wrapper can check for this variable and fail with a helpful message when it is absent:
#!/usr/bin/env bash
# tools/bazel — enforce Bazelisk usage
if [[ -z "${BAZELISK_SKIP_WRAPPER:-}" ]]; then
echo "ERROR: Please use Bazelisk instead of a raw Bazel binary." >&2
echo "Install: brew install bazelisk (macOS) or see https://github.com/bazelbuild/bazelisk" >&2
exit 1
fi
exec "$BAZEL_REAL" "$@"
This turns a confusing version-mismatch failure into a clear, actionable error at the earliest possible point.
Disabling the Wrapper
Setting BAZELISK_SKIP_WRAPPER to any non-empty value before launching Bazelisk bypasses the wrapper entirely1. This is useful for debugging wrapper issues or running a clean Bazelisk-only invocation:
BAZELISK_SKIP_WRAPPER=1 bazel build //...
The variable can also be set persistently in .bazeliskrc for specific environments.
tools/bazel is a project-level hook that runs before Bazel starts. Use it for concerns that sit below .bazelrc — environment stabilization, mirror routing, CI detection, and enforcing that every developer runs Bazelisk. Keep the wrapper thin: its job is setup, not build logic. Build-time configuration belongs in .bazelrc from 3.2.1 .bazelrc Hierarchy and flag management from 3.2.4 Command Line Flags. For CI-level resilience patterns built on top of wrappers, see 6.6.5 Classifying Failures Before Retrying and 6.6.6 Build-Service Incident Recovery.
Check your understanding · 3 questions
1.A developer installs a raw Bazel binary directly on their PATH instead of using Bazelisk. What happens to the tools/bazel wrapper?
Select one answer
2.Which are legitimate uses of the tools/bazel wrapper? Select all that apply.
Select all that apply
3.True or false about the tools/bazel wrapper:
Choose True or False for each sentence
Footnotes
-
Bazelisk — A user-friendly launcher for Bazel — tools/bazel wrapper search order, BAZEL_REAL, BAZELISK_SKIP_WRAPPER, environment variables set by Bazelisk, and PATH prepend behavior ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Determinism in a Non-Hermetic World — Canva's practical experience using tools/bazel for environment narrowing and URL rewriter hook for mirror priority by client location ↩1 ↩2