2.3 Hermeticity & Sandboxing

The suspicious Bazel failure often starts as a contradiction: bazel build //app:server --spawn_strategy=local works, but the same target fails when Bazel runs it in the sandbox. Or it passes on one laptop, fails on CI, and then looks "fixed" after someone runs bazel clean. Those outcomes point to one question: what did the action really depend on, and did Bazel know about it?

Hermeticity is the trust contract underneath every later speed story. Bazel can cache, parallelize, and eventually run actions on other machines only when a planned action means the same thing wherever it runs. Sandboxing is the local test of that contract. Non-determinism is the separate problem where the action saw the right world but still produced unstable bytes. The debugging flags provide the first evidence for deciding which case you are in.

Hidden Inputs Break The Contract

2.3.1 Hermeticity names the core rule: the recorded inputs must be the real inputs. Source files are only part of that story. Tools, configuration, environment, and repository setup can also change what an action does. Hidden inputs are worse than ordinary cache misses because they can create cache hits that are fast and wrong.

2.3.2 Sandboxing turns that idea into execution-time evidence. An action runs in a prepared world containing the inputs Bazel staged for it, not everything your host machine happens to have nearby. If a compiler finds a header only because it existed somewhere outside the declared input set, sandboxing is where that luck often turns into a useful failure.

2.3.3 Non-Determinism Sources catches the next misconception. A sandboxed action can still be unstable. Timestamps, randomness, absolute paths, file ordering, and live network reads can make the same declared action produce different bytes on different runs. Sandboxing asks "did the action see only declared inputs?" Determinism asks "were those inputs enough to produce the same output again?"

2.3.4 First-Response Debugging Flags gives the practical first response. --verbose_failures, --sandbox_debug, and --subcommands are not permanent build policy. They are diagnostic lenses. Use them to see the failed command, inspect the preserved sandbox, and decide whether you are looking at a missing input, a host leak, or a different execution problem.

A Local Success Can Expose A Host Dependency

The developer machine is not supposed to be an implicit action dependency, but it is easy to make it one. $PATH can select a tool, an environment variable can change a command, a test can reach the network, and a generator can embed the current time. If Bazel later reuses the result, each hidden influence makes it unclear whether the cached action describes the same computation.

Sandboxing often turns that incomplete description into a useful failure. If switching to local makes the failure disappear, the comparison is evidence of host coupling, not usually a permanent fix. The genrule-sandbox-paths bad_relative_path target demonstrates the mistake: it hard-codes config/input.txt without declaring the file in srcs, so sandboxed execution reports cat: config/input.txt: No such file or directory while local execution happens to find the host path.

The other trap is expecting sandboxing to solve every reproducibility problem. It cannot. Once an action has the declared inputs, Bazel still needs the tool to behave like a pure function of those inputs. If it writes the current date, embeds an absolute output-base path, or serializes data in random order, the sandbox did its job and the output can still drift. That is why this section keeps hermeticity and determinism adjacent but distinct.

Follow The Broken Contract

Read 2.3.1 Hermeticity first even if your immediate problem is a sandbox error. It gives you the vocabulary for deciding what counts as an input. Then read 2.3.2 Sandboxing to understand why the failure appears specifically during execution and why it prepares you for later execution strategies in 2.5.1 Strategies Abstraction.

If your build succeeds but produces different artifacts, keeps destabilizing cache reuse, or behaves differently after clean builds, move to 2.3.3 Non-Determinism Sources before blaming the cache. If the action is red right now, jump to 2.3.4 First-Response Debugging Flags and use the flags as a short escalation path, then come back to the earlier articles to fix the model rather than preserve the workaround.

Keep one temptation in check while reading: do not turn every local failure into a .bazelrc strategy exception. Strategy and hermeticity configuration do matter later, especially in 2.5.2 Strategies & Mnemonics and 3.2.6 Hermeticity Settings, but this section's first bias is toward making the action honest. Once the action is honest, caching, remote execution, and CI stop depending on the accidental shape of one machine.

Try the hermeticity-settings snippet to compare an undeclared host environment value with an explicitly pinned --action_env. The consuming genrule is intentionally small enough to inspect completely.

key takeaway

Hermeticity makes reuse trustworthy: the declared action must contain its real inputs, and those inputs must produce stable outputs. Sandboxing exposes missing declarations; determinism checks the behavior that remains once the declared world is complete.