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.

bazel run //app:inspect
The shell directory, runtime working directory, and runfiles path are different things
CALLER
Where did Bazel start?
Shell directory
BUILD_WORKING_DIRECTORY
RUNTIME
Where does the binary start?
Runtime working directory
Platform- and launcher-dependent
RUNFILES
How does it find runtime files?
Declare them in data
Logical path, then runfiles API
$(rlocationpath //app:data) logical runfiles path runfiles library file on this system
ENV VARS
BUILD_WORKING_DIRECTORY where you invoked Bazel
BUILD_WORKSPACE_DIRECTORY workspace root
Do not build runtime paths from the current directory. Declare the file, obtain its logical $(rlocationpath ...), and resolve that path with the language's runfiles library.

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 ...)

Reproduce this error

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.

think

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.

key takeaway

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

Arguments after -- are passed to the launched program.
A normal non-test binary is guaranteed to start in the shell directory that invoked Bazel.
BUILD_WORKING_DIRECTORY records the caller's original directory.
0 of 2 answered

Footnotes

  1. Commands and Optionsbazel run builds and runs a single target, passes program args after --, uses the runfiles tree as cwd for non-test binaries, exposes BUILD_WORKING_DIRECTORY and BUILD_WORKSPACE_DIRECTORY, and treats test targets as a special case 1 2 3 4 5 6 7

  2. Bazel Training 101 (Part 11): The 'build' and 'run' commands — operator-facing explanation of bazel run as build-plus-execute, direct program output, and the BUILD_* environment variables 1 2 3 4 5

  3. Writing Bazel rules: data and runfilesdata makes files available at runtime, runfiles libraries are the portable access mechanism, and Windows differs from Unix symlink-tree behavior 1 2 3 4

  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

  5. 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

  6. Migrating to Bazel Modules (a.k.a. Bzlmod) - Repo Names and Runfilesrlocationpath / rlocationpaths, _main paths for the main repo, and why Bzlmod makes runfiles-aware lookup more important 1 2 3

  7. Using Macros to Create Custom Verbsbazel run as the right entry point for deploy, publish, and .accept targets that intentionally have side effects 1 2 3

  8. 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