0.2.4 Visibility

Visibility is Bazel's dependency-policy mechanism for the build graph. It determines which targets can reference a given target's label in attributes like deps1. It does not prevent someone from reading source files or provide runtime access control. When target A depends on target B, Bazel checks whether B's visibility grants access to A's package. If not, the build fails during the analysis phase, before any compilation happens1.

This is how you enforce architectural boundaries mechanically. A library can expose its public API to consumers across the repository while keeping internal helpers invisible. Bazel enforces those boundaries at build time rather than by convention.

Visibility Specifications

Every rule target has a visibility attribute that accepts a list of package specifications2. The first four forms use special placeholder syntax and do not correspond to real targets in the build graph:

  • "//visibility:public" — accessible from all packages.
  • "//visibility:private" — accessible only within the declaring package.
  • "//foo/bar:__pkg__" — accessible to targets in //foo/bar, but not its subpackages.
  • "//foo/bar:__subpackages__" — accessible to //foo/bar and all packages nested under it, at any depth.

The fifth form is an ordinary label pointing at a real package_group target:

  • "//some_pkg:my_package_group" — accessible to all packages in the named package_group (see 0.2.5 package_group).

Note the asymmetry between __pkg__ and __subpackages__. Granting visibility to //tests:__pkg__ allows targets in //tests/BUILD.bazel to depend on you, but targets in //tests/integration/BUILD.bazel are blocked2. The __subpackages__ form covers the entire subtree.

Multiple specifications combine into a union. A target visible to //src/app:__subpackages__ and //tests:__pkg__ can be used anywhere under //src/app/... and in //tests, but not in //tests/integration:

cc_library(
    name = "internal_utils",
    srcs = ["utils.cc"],
    visibility = [
        "//src/app:__subpackages__",
        "//tests:__pkg__",
    ],
)

When granting access to another team's project, prefer __subpackages__ over __pkg__3. Projects evolve and add subdirectories, so __pkg__ forces a visibility update every time that happens.

Defaults

When a target omits the visibility attribute, Bazel applies the package's default_visibility if one is set via package(). Otherwise, the target is private and visible only within its own package4. The package() function is covered in 0.2.6 package() Function.

Generated file targets, which are outputs produced by rules, inherit the visibility of the rule that creates them4. A java_binary visible to //friend:__pkg__ makes its implicit outputs (like _deploy.jar) visible to //friend as well.

What a Visibility Violation Looks Like

Visibility errors surface during analysis, before compilation, linking, or any real work1. Bazel reports which target is inaccessible and from where:

Won't build
java_binary(
    name = "server",
    srcs = ["Main.java"],
    main_class = "app.Main",
    deps = ["//lib:internal"],
)
ERROR: .../app/BUILD.bazel:3:12: in java_binary rule //app:server:
    Visibility error:
    target '//lib:internal' is not visible from
    target '//app:server'
    Recommendation: modify the visibility declaration if you think
    the dependency is legitimate. For more info see
    https://bazel.build/concepts/visibility

Reproduce this error

The fix is either to widen the target's visibility or to restructure the dependency. Visibility errors stop developers from reaching into implementation details that were never intended as stable API. Visibility becomes a key tool for enforcing dependency and API policy as the codebase scales (H.5.2 Dependency Boundaries). It is not a confidentiality or runtime-authorization mechanism. H.9.2 Trust Boundaries shows where build-graph policy ends and security controls must begin.

For source-file visibility rather than target visibility, compare the private and package-scoped declarations in data/BUILD.bazel.

key takeaway

Visibility controls who can depend on your target. The five forms (public, private, __pkg__, __subpackages__, and package_group references) scope access from "everyone" down to "just this package." Targets default to private when no visibility or default_visibility is set. Violations are caught during analysis, before any compilation.

extra

Disabling Visibility Checks

The flag --check_visibility=false turns off visibility enforcement entirely2. This can unblock prototyping when you're iterating on package structure, but it should never appear in committed .bazelrc files or CI configurations because it defeats the architectural guardrails that visibility provides.

Check your understanding · 2 questions

1.Match each visibility specification to the access it grants:

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
//visibility:public
//visibility:private
//foo/bar:__pkg__
//foo/bar:__subpackages__

2.True or false: visibility defaults and enforcement.

Choose True or False for each sentence

If a target omits visibility and no default_visibility is set by package(), it defaults to private.
Visibility violations are caught during compilation, after all source files are processed.
Generated file targets inherit the visibility of the rule that produces them.
When granting access to another team's project, __pkg__ is safer than __subpackages__ because it is more explicit.
0 of 2 answered

Footnotes

  1. Visibility — Target visibility concept and analysis-phase enforcement 1 2 3

  2. Visibility — Visibility specifications and --check_visibility flag 1 2 3

  3. Visibility — Best practice: prefer __subpackages__ over __pkg__ to avoid churn

  4. Visibility — Rule target default visibility and generated file target inheritance 1 2