Last reviewed: 2026-08-24
Direct answer
A patch from a coding agent should not be considered portable merely because it builds on the machine where the agent created it. Verify it from an exact revision in a fresh workspace, with declared inputs, a controlled toolchain, an explicit environment, empty or isolated caches, and the narrowest practical access to the host and network. Then repeat the build independently and compare the specified outputs.
This process catches files, packages, executables, environment variables, and cached products that the patch uses without declaring. It also separates two different claims: a sandboxed build limits what an action can see or change, while a reproducible build produces the same specified artifacts from the same source, environment, and instructions. The Reproducible Builds definition sets the stronger target as bit-for-bit identical specified artifacts.
Do not treat a container as automatic proof of hermeticity. A job container provides a controlled execution surface, but its environment variables, mounted volumes, ports, image reference, shell, and network exposure still form part of the contract. Likewise, Bazel sandboxing limits build actions to known inputs in their working directory, yet Bazel explicitly notes that its filesystem sandbox does not hide the entire host environment. The practical gate is therefore evidence-based: declare the boundary, run inside it, detect any fallback or undeclared input, rebuild independently, and retain the result.
Who this is for
This workflow is for maintainers reviewing agent-generated pull requests, CI engineers defining required checks, and platform teams operating shared build infrastructure. It is especially useful when a patch changes dependency manifests, build scripts, generators, compiler settings, packaging, native extensions, or CI configuration.
The same method also helps teams whose local builds regularly disagree with remote execution. Before starting, record the intended change in a change-scope note so a reviewer can distinguish a legitimate new build input from an accidental dependency.
Key takeaways
- Build the exact reviewed revision, not an unrecorded working directory.
- Declare the builder image, toolchain, dependency inputs, commands, environment settings, cache policy, network policy, and expected artifacts before running the gate.
- Use a fresh workspace and isolated caches. A warm developer cache can conceal a missing dependency or generated file.
- Require the configured sandbox strategy. A silent downgrade to a weaker strategy invalidates a strict verification claim.
- Run tests and package steps, but do not confuse passing tests with artifact reproducibility.
- Rebuild in a second clean environment and compare the specified artifacts. Record both successful and failed comparisons.
- Keep logs structured and sanitized. Store repo-relative paths and bounded failure classifications rather than complete environments or unrestricted command output.
Sources checked
- Bazel: Sandboxing explains how known-input working directories expose undeclared dependencies, why those dependencies can corrupt incremental or remote-cache results, and how OS-specific sandbox strategies differ from the generic process wrapper.
- GitHub Docs: Running jobs in a container
documents the job-level container boundary, including image selection, environment variables, volumes, ports, resource options, and the default
shshell used for run steps inside a container. - Reproducible Builds: Definitions defines reproducibility in terms of the same source code, build environment, build instructions, and bit-for-bit identical specified artifacts.
- Nix Reference Manual: Glossary describes derivations as sandboxed processes that read specified input store objects and write designated outputs. It also cautions that purity is an assumption rather than a universal guarantee when external inputs such as the network or system time remain available.
Contract details to verify
Write the verification contract before executing the patch. A reviewer should be able to answer each of these questions without consulting the agent’s original machine:
- Source identity: Which revision is under review, and must the worktree be clean? Record the revision and a clean/dirty state. Do not build from uncommitted files unless those files are intentionally packaged as a reviewed input.
- Builder identity: Which operating system, container image, compiler, runtime, and build tool versions are allowed? Use an immutable builder reference where the platform supports one.
- Declared inputs: Which lockfiles, source archives, toolchains, generators, configuration files, and prebuilt assets may the build consume?
- Environment: Which locale, time zone, feature flags, and other build-relevant variables are set? Start from an allowlist rather than inheriting the runner’s complete environment.
- Access policy: Is network access disabled during verification? Which directories are readable or writable? Are host mounts prohibited? If an external service is genuinely required, name and constrain that exception.
- Cache policy: Are dependency and compiler caches empty, read-only, isolated by revision, or disabled? Record the policy instead of inferring it from build speed.
- Commands: Which bootstrap, build, test, and package commands are authoritative, and which shell interprets them? GitHub’s container documentation matters here because container jobs default to
shunless the workflow overrides it. - Outputs: Which files are primary artifacts, and how are they compared? Logs are evidence, but the Reproducible Builds definition distinguishes them from the primary artifacts being reproduced.
- Strictness: What happens if the preferred sandbox cannot start? For a strict gate, fail closed and report the unavailable strategy rather than continuing under a weaker boundary.
A compact contract can be kept beside the CI workflow. The names below are illustrative and should map to repository-owned commands:
revision: abc1234
workspace_state: clean
builder_image: approved-image-by-digest
shell: sh
sandbox_strategy: required
network_mode: disabled
cache_mode: empty
locale: C.UTF-8
timezone: UTC
commands:
- ./ci/bootstrap-declared-inputs
- ./ci/build
- ./ci/test
- ./ci/package
specified_artifacts:
- dist/application.tar
comparison: byte-for-byte
Happy path operator workflow
- Confirm that the reviewed revision and the declared change scope match. Reject unrelated generated files or build configuration changes until they are explained.
- Create a new, ephemeral workspace from that revision. Verify that it begins clean and has no bind mount to a developer home directory.
- Start the approved builder with the required sandbox strategy. Make the allowed inputs available, initialize an empty cache location, set the environment allowlist, and disable network access for the verification phase where the build permits it.
- Run the repository-owned commands in order. A representative entry sequence is:
./ci/bootstrap-declared-inputs
./ci/build
./ci/test
./ci/package
./ci/record-artifacts
- Record the exit code, sandbox strategy actually used, cache and network modes, expected output list, and artifact digests. A passing run must produce every specified artifact without a sandbox downgrade.
- Destroy the workspace. Repeat the same contract in a second clean workspace, preferably on a separate runner with the same declared builder identity.
- Compare the specified artifacts byte for byte. If they match and both runs satisfy the contract, attach the bounded evidence summary to the pull request. Keep the commands understandable by following the guidance for reviewable terminal evidence .
Error path operator workflow
Suppose the clean build fails because a compiler cannot find a file that existed on the agent’s machine. Treat that as an undeclared-input finding, not as a reason to mount the missing local directory.
- Stop the gate and mark the result as blocked.
- Preserve the failing command identifier, nonzero exit code, repo-relative location, selected sandbox strategy, and a short sanitized error class.
- Determine whether the missing item should be source-controlled, generated by a declared step, supplied through the build graph, or removed from the patch’s assumptions.
- Update the relevant build rule, manifest, lockfile, or generator declaration. Do not repair the run by copying an unexplained machine-local file into the builder.
- Start both clean runs again from the beginning. A retry in the contaminated workspace does not close the finding.
- If both builds complete but their artifacts differ, keep the merge blocked. Compare build-relevant timestamps, locale, time zone, input ordering, paths, and randomness controls before narrowing the difference to the producing action.
Use structured logs that reveal the verification state without reproducing sensitive values or entire process environments:
{
"run_id": "run-1842",
"revision": "abc1234",
"workspace_state": "clean",
"builder_image_ref": "approved-build-environment",
"sandbox_strategy": "linux-sandbox",
"network_mode": "disabled",
"cache_mode": "empty",
"command_id": "build-and-test",
"exit_code": 1,
"failure_class": "undeclared-input",
"repo_relative_path": "src/module/file.c",
"artifact_digest": "not-produced",
"duration_ms": 8421,
"sensitive_values": "[REDACTED]"
}
Keep absolute home paths, unrestricted environment dumps, registry authentication material, and raw dependency-service responses out of the review log. Store a bounded diagnostic separately when deeper investigation requires access controls.
Failure modes
The container passes, but the boundary is porous. GitHub job containers can receive environment variables, volumes, exposed ports, and resource options. A broad host mount or inherited environment can reintroduce the state the clean build was meant to exclude. Review the container configuration as part of the patch.
The requested sandbox silently falls back. Bazel documents that OS-specific sandboxes may be unavailable in nested scenarios and that it can fall back to processwrapper-sandbox. That wrapper still helps detect undeclared inputs, but it is less hermetic than the OS-specific strategy. Record the strategy actually selected and fail if it is weaker than the contract.
A hidden input poisons incremental or remote-cache results. Bazel explains that a build tool cannot invalidate an action for a changed file it never knew was an input. One machine may reuse an incorrect cached result, and a shared bad entry can affect other developers. Fix the declaration rather than clearing the cache and declaring success.
A warm cache masks an incomplete bootstrap. The patch passes only because a package, generated source, or compiler product already exists. Empty-cache verification should be a deliberate run, not an occasional cleanup experiment.
Two runs share the same contamination. Rebuilding twice in one workspace against one populated cache provides weak evidence. Destroy the first workspace and isolate the second run’s writable state.
Tests pass while packages differ. Behavioral tests and reproducible artifacts answer different questions. If the release contract names an archive, executable, package, or filesystem image, compare that output rather than relying only on the test exit code.
Network access adds undeclared variance. A build that resolves current remote content during verification has an input that may change independently of the revision. Separate acquisition from the restricted build where practical, and make every approved fetched input explicit.
Diagnostics leak more than reviewers need. Complete environment dumps and raw tool output can expose host paths or sensitive operational data. Emit the small set of sanitized fields needed to reproduce and classify the failure.
FAQ
Is a Docker or GitHub Actions container a hermetic build?
Not by itself. A container gives the job a defined execution surface, but the image reference, mounts, environment, shell, services, and network policy still determine what the build can observe. It is one control in the contract, not the final proof.
Does a sandboxed pass prove the artifact is reproducible?
No. Sandboxing helps constrain access and reveal undeclared inputs. Reproducibility requires an independent rebuild of the same specified artifacts from the same declared source, environment, and instructions, followed by a bit-for-bit comparison.
Should every agent patch run through the strictest gate?
Use risk-based routing. Patches that alter dependencies, build rules, packaging, generated code, compilers, native components, or CI deserve the strict gate. A documentation-only change may use a narrower clean build, but it should still follow the repository’s required validation.
What if the build genuinely needs network access?
Document the exception and reduce it. Prefer a separate input-acquisition phase that records the approved material, followed by a restricted verification phase. If live access cannot be removed, do not describe the result as fully hermetic; record the remaining external input and its effect on the claim.
What is the difference between a missing dependency and a nondeterministic output?
A missing dependency usually stops a clean build because an input was never declared or produced. A nondeterministic output appears after two nominally successful builds produce different specified artifacts. The first calls for repairing the input graph; the second calls for locating uncontrolled build variance.
Reader next step
Choose one open coding-agent pull request that changes a manifest, build rule, generator, or package step. Write the nine-part verification contract, create two fresh workspaces, require the intended sandbox, run the same repository-owned commands with isolated caches, and compare the specified artifacts. Deliberately remove one declared input in a temporary test branch to confirm that the gate takes the error path and records a useful sanitized finding.
Add the clean-build result, actual sandbox strategy, artifact comparison, and any remaining exception to the pull-request evidence. If the gate only passes after mounting local state, enabling unrestricted network access, or reusing a warm cache, the next step is to repair the build declaration, not to approve the patch.