1.1.2 bazel run & Runfiles
bazel run is the CLI verb you use when the process itself is the outcome: Bazel builds one runnable target, then launches it1,2. That sounds like "do a bazel build, then execute whatever appeared in bazel-bin", but the runtime environment is different enough that it needs its own mental model.
One target, then execution
1.1.1 Commands established that bazel run resolves to a single runnable target. In practice, that usually means naming one explicit label rather than a broad sweep such as //.... Bazel still analyzes and builds that target first, but the last step is different: instead of stopping at an artifact, it starts the executable and connects the program's I/O to your terminal1,2.
bazel run //app:server -- --port=8080
The -- separator matters for the same reason it does in other CLIs: flags after it belong to the launched program, not to Bazel1.
Runfiles Are The Runtime View
0.3.3 Attributes & Semantic Roles introduced data as files a program needs while it is running rather than while it is compiling. bazel run is where that distinction becomes visible. Files declared in data become part of the target's runfiles set, so the program can reach them at runtime3,4,5. Files left out of data are outside that contract.
In the runnable example below, the executable gets a declared text file through
data and receives its logical path via $(rlocationpath ...):
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
sh_binary(
name = "inspect",
srcs = ["inspect.sh"],
args = ["$(rlocationpath //app:message.txt)"],
data = ["message.txt"],
deps = ["@bazel_tools//tools/bash/runfiles"],
)
If the program needs a file after Bazel has finished building, declare it in data. From the caller's side you see the last step of a longer contract: a rule publishes runtime files, any intermediate target that republishes that runtime contract preserves or merges them, and the launched program resolves their logical names. 4.2.5 DefaultInfo & Runfiles traces that producer-to-consumer-to-runtime handoff for rule authors. As an operator, you only need to declare the data and use the language's lookup library3.
On Unix and macOS, a non-test binary launched with bazel run starts inside the main-repository segment of its runfiles tree1,6. In this example, that segment appears as _main, but the name is an implementation detail of this Bzlmod-based setup, not the portable contract. Windows may use a manifest instead of a symlink tree, and Bzlmod can rewrite repository names in ways code should not hard-code3,4,6,5. The portable pattern is to pass $(rlocationpath ...) into the binary and resolve it with the runfiles library for your language. The path should use the apparent repository name, not a guessed canonical directory segment3,4,6,5. The app:uses_runfiles target carries the same shape across an external repository so the runtime lookup keeps working even when the on-disk canonical name differs from the apparent name in the label. For a concrete C++ version of this runfiles-library pattern, see L2.1.7 Generated Sources, Data & Runfiles.
The Program Does Not Run In Your Shell Directory
For a normal non-test binary, Bazel starts the program in the binary's runfiles tree rather than preserving the directory where you typed bazel run1. It exports BUILD_WORKING_DIRECTORY for the shell directory that invoked Bazel and BUILD_WORKSPACE_DIRECTORY for the workspace root1,2.
The runfiles-working-directory snippet shows the difference directly:
cwd=/Users/lukasz/Library/Caches/bazel/_bazel_lukasz/2e8cb856873863d7583b9495156c0a72/execroot/_main/bazel-out/darwin_arm64-fastbuild/bin/app/inspect.runfiles/_main
BUILD_WORKING_DIRECTORY=/Users/lukasz/code/bazel/bazel-materials/knowledge/examples/snippets/runfiles-working-directory
BUILD_WORKSPACE_DIRECTORY=/Users/lukasz/code/bazel/bazel-materials/knowledge/examples/snippets/runfiles-working-directory
message_path=/Users/lukasz/code/bazel/bazel-materials/knowledge/examples/snippets/runfiles-working-directory/app/message.txt
message=hello from declared data
naive_relative=missing
The process runs in the target's runfiles tree, and the declared file is resolved through the runfiles library rather than by guessing a relative path or hard-coding something under bazel-bin or the source tree. In this concrete snippet, the main repository shows up under _main. In other setups that prefix can differ, so the stable contract is the runfiles tree layout rather than the literal path segment. If you invoke Bazel from a subdirectory, BUILD_WORKING_DIRECTORY follows that shell directory while BUILD_WORKSPACE_DIRECTORY stays pinned to the repo root.
If the file is not declared in data, the build can still succeed and the program can fail only after launch:
missing runfile: _main/app/message.txt
declare it in data and pass it with $(rlocationpath ...)
That difference matters for developer tools. A formatter, Terraform wrapper, or deploy helper that assumes . means "the user's project root" can break under plain bazel run even though the target itself built successfully7,8. In those cases, either teach the program to use BUILD_WORKING_DIRECTORY / BUILD_WORKSPACE_DIRECTORY intentionally, or use a wrapper that restores the caller's expected cwd semantics2,8.
Decide: A developer tool opens config.yaml with a plain relative path. It works when someone runs the binary by hand from bazel-bin, but fails under bazel run. Should the wrapper chdir back to the checkout, or should config.yaml become runfiles data?
Reveal
First decide who owns the file. If config.yaml is part of the executable's runtime contract, declare it in data and resolve it through runfiles. Then it travels with the target, no matter where Bazel starts the process.
If config.yaml is intentionally a file in the caller's checkout, do not smuggle that through runfiles. Use BUILD_WORKING_DIRECTORY or BUILD_WORKSPACE_DIRECTORY, or pass an explicit path from the wrapper. The underlying problem is not that Bazel picked the wrong working directory. The program made . part of its API without saying which path world . belongs to.
This section describes the common non-test case. bazel run can also execute a test target, but the working directory and environment differ from the non-test case described above1.
bazel run Is Bazel's Side-Effect Verb
bazel run is not limited to starting an application. Image pushes, deploy
steps, documentation publishing, formatter wrappers, and golden-file accept
targets all fit because the side effect is the point2,7.
That is why rules and macros often expose targets such as :staging.apply,
:docs.publish, or :foo.accept instead of inventing new top-level Bazel
commands7. The same pattern appears in 1.2.9 Golden File Testing (Test-Accept Pattern), while
3.5.3 Developer Tool Management develops it into a tool-management approach.
bazel run launches one executable target, so you still need to understand
that target's runtime environment. If you author rules that manufacture the
executable wrapper itself, 4.2.6 Executable & Test Rules covers portable
launcher options.
Use bazel build when you only need an artifact. Use bazel run when the process itself matters. Once you switch to run, think in three pieces: one runnable target, runtime files declared in data, and a runtime cwd that is not your shell directory.
Check your understanding · 2 questions
1.Which practices make a file available portably to a program launched with bazel run?
Select all that apply
2.True or false: bazel run process arguments and working directories.
Choose True or False for each sentence
Footnotes
-
Commands and Options —
bazel runbuilds and runs a single target, passes program args after--, uses the runfiles tree as cwd for non-test binaries, exposesBUILD_WORKING_DIRECTORYandBUILD_WORKSPACE_DIRECTORY, and treats test targets as a special case ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Bazel Training 101 (Part 11): The 'build' and 'run' commands — operator-facing explanation of
bazel runas build-plus-execute, direct program output, and theBUILD_*environment variables ↩1 ↩2 ↩3 ↩4 ↩5 -
Writing Bazel rules: data and runfiles —
datamakes files available at runtime, runfiles libraries are the portable access mechanism, and Windows differs from Unix symlink-tree behavior ↩1 ↩2 ↩3 ↩4 -
Runfiles and where to find them — runfiles tree vs manifest, execution-context differences,
$(rlocationpath ...), and the reason to rely on runfiles libraries ↩1 ↩2 ↩3 -
Runfiles — official guidance to avoid hard-coded runfiles paths, use language-specific runfiles libraries, and pass stable
$(rlocationpath ...)values based on apparent repository names ↩1 ↩2 ↩3 -
Migrating to Bazel Modules (a.k.a. Bzlmod) - Repo Names and Runfiles —
rlocationpath/rlocationpaths,_mainpaths for the main repo, and why Bzlmod makes runfiles-aware lookup more important ↩1 ↩2 ↩3 -
Using Macros to Create Custom Verbs —
bazel runas the right entry point for deploy, publish, and.accepttargets that intentionally have side effects ↩1 ↩2 ↩3 -
Running local tools installed by Bazel — practical write-up of the working-directory footgun and why teams wrap some Bazel-managed tools to restore user-expected cwd behavior ↩1 ↩2