> ## Documentation Index
> Fetch the complete documentation index at: https://site.aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> Overview of the Aspect CLI, an open-source task runner extending Bazel with built-in workflows for format, lint, gazelle, delivery, and AXL tasks.

The Aspect CLI (`aspect`) is a free, open-source task runner that extends Bazel with first-class developer workflows. Apache 2.0, source on [github.com/aspect-build/aspect-cli](https://github.com/aspect-build/aspect-cli).

## Why use the Aspect CLI?

`bazel` is a build system, not a developer-workflow tool. Every Bazel monorepo eventually grows the same pile of bespoke shell scripts and CI YAML for the things teams actually do every day, format pre-submits, lint enforcement, BUILD-file generation, release-tag delivery, CI-platform reporting. Each is written in a different language, breaks differently, and behaves differently in CI versus on a laptop. There is no shared abstraction, no caching across teams, and no single place to look when something breaks.

The Aspect CLI (`aspect`) replaces that pile with a single task runner you program in Starlark. Built-in tasks like `aspect build`, `aspect test`, `aspect format`, `aspect lint`, `aspect gazelle`, and `aspect delivery` ship with the patterns every Bazel monorepo eventually re-invents, hold-the-line lint that only fails on violations *you* introduced, change-detection-driven delivery, structured artifact upload, native integration with GitHub Status Checks, Buildkite Annotations, and the equivalent on GitLab and CircleCI. When the built-ins aren't enough you write custom tasks in [Starlark](https://starlark-lang.org), the same language you already use for `.bzl` files, and they run *identically* on every laptop and every CI provider.

### Why it's worth a 10-minute try

* **Zero adoption cost.** `aspect build`, `aspect test`, and `aspect run` work in any Bazel workspace with no config. Your existing `bazel` workflow (`bazel query`, `bazel info`, plain `bazel build`) keeps working alongside, you're not switching build systems, you're extending the one you already have with a programmable task-runner layer. Don't want to teach your team a new command name? Drop in the [`tools/bazel` wrapper](/docs/cli/install#keep-your-team-typing-bazel-with-the-tools%2Fbazel-wrapper) and `bazel` routes through `aspect` transparently (requires [Bazelisk](https://github.com/bazelbuild/bazelisk), the standard setup).
* **One language for everything.** `.aspect/config.axl` configures the CLI in the same Starlark dialect you write `.bzl` files in. No new YAML schema, no new template language.
* **Same command in every environment.** `aspect lint` does the same thing on a laptop as in a CI pipeline. Eliminates an entire class of "works on my machine but not in CI" bugs.
* **No vendor lock-in.** Your Bazel build never depends on it to keep working. [Aspect Workflows](/docs/aspect-workflows) is a separate, optional product.

```shell theme={null}
curl -fsSL https://install.aspect.build | bash
```

In a hurry? The [Quickstart](/quickstart) walks you from install to a custom task in 10 minutes. See [How to install the Aspect CLI](/docs/cli/install) for other installation methods.

## Built-in tasks

`aspect` ships with a set of built-in tasks that work out of the box on any CI provider and on any developer machine:

| Task                                              | What it does                                                         |
| ------------------------------------------------- | -------------------------------------------------------------------- |
| [`aspect build`](/docs/cli/tasks/build_test)      | Build Bazel targets with retry on transient errors and BES streaming |
| [`aspect test`](/docs/cli/tasks/build_test)       | Run Bazel tests with coverage and test-log upload                    |
| [`aspect run`](/docs/cli/tasks/run)               | Build and run a binary target                                        |
| [`aspect format`](/docs/cli/tasks/format)         | Format only the files changed in the PR                              |
| [`aspect buildifier`](/docs/cli/tasks/buildifier) | Format Starlark files (opt-in via `format.alias()`)                  |
| [`aspect lint`](/docs/cli/tasks/lint)             | Run linters with hold-the-line strategy                              |
| [`aspect gazelle`](/docs/cli/tasks/gazelle)       | Generate and sync BUILD files                                        |
| [`aspect delivery`](/docs/cli/tasks/delivery)     | Deliver only targets whose outputs actually changed                  |

Run `aspect help` to list the tasks available in your repo (built-ins plus any custom ones you've added).

## What you'll see

`bazel`'s output is a wall of `INFO:` lines. `aspect` wraps each task in a phased UI, names what each phase is doing, and prints a friendly summary at the end with a per-phase timing breakdown.

The simplest example, `aspect test` on a cached run:

```text theme={null}
$ aspect test //...
→ 🎬 Running `test` task

→ 🔧 Setup · Running setup

→ 🧪 Test · Spawning bazel test
INFO: Analyzed 147 targets (0 packages loaded, 0 targets configured).
INFO: Found 122 targets and 25 test targets...
INFO: Elapsed time: 0.866s, Critical Path: 0.07s
INFO: 1 process: 31 action cache hit, 1 internal.
INFO: Build completed successfully, 1 total action

Executed 0 out of 25 tests: 25 tests pass.
INFO: Build Event Protocol files produced successfully.

→ ✅ Passed `test` task in 1.2s · Tests passed (cached)
    🔧 Setup   2ms  Prepare the task environment
    🧪 Test   1.2s  Run bazel tests
```

`aspect format` shows more phases, it builds the formatter, detects which files changed in the diff against `origin/main`, formats just those, and reports whether anything was modified:

```text theme={null}
$ aspect format
→ 🎬 Running `format` task

→ 🔧 Setup · Running setup

→ 🔨 Build · Building formatter (//tools/format)
INFO: Analyzed target //tools/format:format (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //tools/format:format up-to-date:
  bazel-bin/tools/format/format.bash
INFO: Elapsed time: 0.321s, Critical Path: 0.04s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Build Event Protocol files produced successfully.

→ 🔍 Detect · Detecting changed files
Detecting changed files via local git diff 94f7d5a..663cf4f (git merge-base HEAD origin/main)
Changed files:
  README.md

→ ✨ Format · Formatting 1 file in scope

→ 📋 Diff · Computing format diff
No files in scope were modified.

→ ✅ Passed `format` task in 1.0s · All files are properly formatted
    🔧 Setup     2ms  Prepare the task environment
    🔨 Build   598ms  Build formatter
    🔍 Detect  147ms  Detect changed files
    ✨ Format  260ms  Format files
    📋 Diff     15ms  Detect formatter-induced changes
```

When `aspect buildifier` (or `aspect format`) actually reformats files, it tells you the exact command to re-run locally to apply the same fix, *and* the raw `bazel run` invocation it shells out to under the hood, so you're never locked out of reproducing what just happened:

````text theme={null}
$ aspect buildifier
→ 🎬 Running `buildifier` task

→ 🔧 Setup · Running setup

→ 🔨 Build · Building formatter (@buildifier_prebuilt//buildifier)
INFO: Analyzed target @@buildifier_prebuilt+//buildifier:buildifier (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target @@buildifier_prebuilt+//buildifier:buildifier up-to-date:
  bazel-bin/external/buildifier_prebuilt+/buildifier/buildifier
INFO: Elapsed time: 0.340s, Critical Path: 0.04s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Build Event Protocol files produced successfully.

→ 🔍 Detect · Detecting changed files
Detecting changed files via local git diff 94f7d5a..45815fe (git merge-base HEAD origin/main)
Changed files:
  BUILD.bazel
  README.md
INFO: Skipped 1 file(s) not matching --include-pattern.

→ ✨ Format · Formatting 1 file in scope

→ 📋 Diff · Computing format diff
1 file in scope was modified:
  - BUILD.bazel
Formatted 1 file(s). Review and commit the changes.

🛠️ Fix:
```
aspect buildifier --severity=info -- BUILD.bazel
```

```
# without Aspect CLI
bazel run --run_in_cwd @buildifier_prebuilt//buildifier -- BUILD.bazel
```


→ ✅ Passed `buildifier` task in 786ms · Reformatted 1 file
    🔧 Setup     2ms  Prepare the task environment
    🔨 Build   597ms  Build formatter
    🔍 Detect  150ms  Detect changed files
    ✨ Format    3ms  Format files
    📋 Diff     29ms  Detect formatter-induced changes
````

`aspect lint` is the most interesting one because of **hold-the-line**: the linter can surface findings in files the PR didn't touch, but the task still passes, only *new* violations introduced by the PR's diff fail the build. In the run below ShellCheck found three issues in `examples/lint/hello.sh`, but since that file isn't in the changed-file set (only `README.md` is), the task ends with **No findings**:

```text theme={null}
$ aspect lint --aspect=//tools/lint:linters.bzl%shellcheck -- //...
→ 🎬 Running `lint` task

→ 🔧 Setup · Running setup

→ 🔍 Detect · Detecting changed files
Detecting changed files via local git diff 94f7d5a..663cf4f (git merge-base HEAD origin/main)
Changed files:
  README.md [+104 / -30] (changed)

→ 🔬 Lint · Running lint aspects
INFO: Analyzed 147 targets (0 packages loaded, 0 targets configured).
INFO: Found 147 targets...
INFO: Elapsed time: 0.377s, Critical Path: 0.07s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Build Event Protocol files produced successfully.

In examples/lint/hello.sh line 10:
if [ $name = "world" ]; then
     ^---^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
if [ "$name" = "world" ]; then


In examples/lint/hello.sh line 17:
cd /tmp
^-----^ SC2164 (warning): Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

Did you mean:
cd /tmp || exit


In examples/lint/hello.sh line 19:
echo $name
     ^---^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
echo "$name"

For more information:
  https://www.shellcheck.net/wiki/SC2164 -- Use 'cd ... || exit' or 'cd ... |...
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...

→ 🔎 Filter · Filtering diagnostics

🧹 Linters (1): ShellCheck

→ ✅ Passed `lint` task in 756ms · No findings
    🔧 Setup     2ms  Prepare the task environment
    🔍 Detect   93ms  Detect changed files
    🔬 Lint    657ms  Run lint aspects
    🔎 Filter    0ms  Filter diagnostics
```

The per-phase timing breakdown is the most useful piece on a slow CI step, it tells you whether the time is going into the actual build, the lint discovery, the diff computation, or the artifact upload. On a PR, the same content lands in [Buildkite annotations, GitHub Status Checks, and a PR task summary comment](/docs/cli/tasks-ci#live-examples).

## How tasks are configured

Each task is implemented in [AXL](#aspect-extension-language), a Starlark dialect. Built-in tasks are zero-config, `aspect build`, `aspect test`, and `aspect run` work the moment you install the CLI. When you need to tune behaviour or register a new task, all knobs live in `.aspect/config.axl` at your repo root.

A minimal example, set `--config=ci` on every Bazel invocation when running in CI, and upload failing test logs:

```python title=".aspect/config.axl" theme={null}
load("@aspect//traits.axl", "BazelTrait")
load("@aspect//feature/artifacts.axl", "ArtifactUpload")

def config(ctx: ConfigContext):
    if bool(ctx.std.env.var("CI")):
        ctx.traits[BazelTrait].extra_flags.extend(["--config=ci"])

    ctx.features[ArtifactUpload].args.upload_test_logs = "failed"
```

The same config evaluates locally and in CI, so developers and pipelines stay in sync.

A more realistic example that exercises several built-ins, for a full live config running in CI, see [`aspect-build/bazel-examples`](https://github.com/aspect-build/bazel-examples/blob/main/.aspect/config.axl). The example below sets `--config=ci` on CI, registers `aspect buildifier` via `format.alias()`, declares the lint aspects so CI invocations don't need to repeat `--aspect`, points `aspect delivery` at OCI-push rules, and enables artifact uploads for failed tests, the Bazel profile, and the BEP:

```python title=".aspect/config.axl" theme={null}
"""Aspect CLI configuration."""

load("@aspect//feature/artifacts.axl", "ArtifactUpload")
load("@aspect//format.axl", "format")
load("@aspect//traits.axl", "BazelTrait")

buildifier = format.alias(
    defaults = {
        "formatter_target": "@buildifier_prebuilt//buildifier",
        "run_in_cwd": True,
        "include_patterns": [
            "**/BUILD",
            "**/BUILD.bazel",
            "**/MODULE.bazel",
            "**/*.MODULE.bazel",
            "**/WORKSPACE",
            "**/WORKSPACE.bazel",
            "**/*.axl",
            "**/*.bzl",
        ],
    },
    summary = "Format Starlark files using buildifier.",
)

def config(ctx: ConfigContext):
    # Set --config=ci on all bazel commands on all CI environments.
    if bool(ctx.std.env.var("CI")):
        ctx.traits[BazelTrait].extra_flags.extend([
            "--config=ci",
        ])

    # Register the buildifier alias as a CLI command.
    ctx.tasks.add(buildifier)

    # Lint aspects, required by the built-in `aspect lint` task. Same set
    # locally as on CI; CI invocations don't need to repeat the --aspect flags.
    ctx.tasks["lint"].args.aspects = [
        "//tools/lint:linters.bzl%buf",
        "//tools/lint:linters.bzl%checkstyle",
        "//tools/lint:linters.bzl%clippy",
        "//tools/lint:linters.bzl%eslint",
        "//tools/lint:linters.bzl%keep_sorted",
        "//tools/lint:linters.bzl%pmd",
        "//tools/lint:linters.bzl%ruff",
        "//tools/lint:linters.bzl%shellcheck",
    ]

    # Delivery. release_bazel_flags applies only to the final delivery build,
    # so stamping flags (--stamp, --workspace_status_command) and any --config
    # that enables them belong here, keeping their volatile values out of the
    # change-detection digest. (It defaults to ["--stamp"].) bazel_flags, by
    # contrast, applies to every phase including change detection, so reserve
    # it for flags confirmed not to change outputs (e.g. --jobs, --remote_cache).
    ctx.tasks["delivery"].args.query = "kind(\"oci_push rule\", //...)"
    ctx.tasks["delivery"].args.release_bazel_flags = [
        "--config=release",
    ]

    # Enable artifact uploads for testlogs, profile, and BEP.
    # upload_test_logs="failed", the logs from passing tests are noise;
    # failing/flaky tests' logs are the ones anyone would actually open.
    ctx.features[ArtifactUpload].args.upload_test_logs = "failed"
    ctx.features[ArtifactUpload].args.upload_profile = True
    ctx.features[ArtifactUpload].args.upload_bep = True
```

## Custom tasks

When the built-ins don't cover what you need, write your own. Drop a `.axl` file into `.aspect/`, define a task, and it's automatically discoverable via `aspect <name>`:

```python title=".aspect/codegen.axl" theme={null}
def _impl(ctx: TaskContext) -> int:
    return ctx.bazel.build(*ctx.args.targets).wait().code

codegen = task(
    summary = "Run the code generator.",
    implementation = _impl,
    args = {
        "targets": args.positional(default = ["//gen/..."]),
    },
)
```

```shell theme={null}
aspect codegen //gen/services/...
```

See [How to run and define tasks](/docs/cli/guides/basic) for a full walkthrough including arguments, processes, filesystem access, and cleanup.

## Aspect Extension Language

AXL, the **Aspect Extension Language**, is a [Starlark](https://starlark-lang.org/) dialect for configuring the CLI, writing custom tasks, and extending Bazel with new BUILD-file generators (see [Gazelle extensions](#gazelle-extensions) below) and Build Event Protocol subscribers. Starlark was chosen because Bazel users already know it from `.bzl` files, the mental model (functions, load statements, deterministic evaluation) transfers directly. AXL draws on [Buck's BXL](https://buck2.build/docs/bxl/) and [Tilt](https://docs.tilt.dev/api.html), which use Starlark for the same purpose.

The [AXL reference](/docs/axl/types) documents every type and built-in. For day-to-day work, the [Guides](/docs/cli/guides/basic) section covers what most extensions need.

Watch Alex's BazelCon 2025 talk for an overview of the extension language and the design decisions behind it:

<iframe className="w-full aspect-video rounded-xl" src="https://www.youtube.com/embed/j7-IMZ2q5W4?si=R9wZVzDi-hgcArsy" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen />

## Gazelle extensions

The same Starlark engine powers a separate extension surface: BUILD-file generation. You can teach [Gazelle](https://github.com/bazel-contrib/bazel-gazelle) about a new language or project layout in Starlark instead of Go, see [How to extend Gazelle BUILD file generation](/docs/cli/guides/gazelle) and the [Aspect 150 course](/learning/aspect-150).

## Running in CI

The same `aspect <task>` command you run locally works identically in CI. [Running tasks in CI](/docs/cli/tasks-ci) has ready-to-paste YAML for GitHub Actions, Buildkite, GitLab CI, and CircleCI.

[Aspect Workflows](/docs/aspect-workflows) self-hosted runners take this further: tasks detect the runner environment automatically and pick up remote cache, remote execution, and pre-warmed NVMe output bases. No extra configuration, the same commands that run locally just go faster.

## Live examples

The [`aspect-build/bazel-examples`](https://github.com/aspect-build/bazel-examples) repo runs `aspect <task>` pipelines on every commit across all four supported CI providers. Follow the link to inspect a current build, plus per-task Buildkite annotations, GitHub Status Checks, and a sample PR task summary comment: [Live examples](/docs/cli/tasks-ci#live-examples).
