Skip to main content
Version: 5.1.x

Aspect CLI Plugins

Plugins allow you to customize Bazel's behavior to fit your developer workflows. You can use an existing plugin from the catalog, or write your own.


A plugin can subscribe to the Build Event Protocol (BEP), to react in real-time during the build. Plugins can contribute custom commands like lint so developers can live in a single tool.

A plugin is a program with a gRPC server that implements our plugin protocol. You can write a plugin in any language. We provide a quickstart for writing plugins in Go, and plan to provide examples for Python and JavaScript plugins soon.

Plugins are hosted and versioned independently from the aspect CLI.

The plugin system is based on the excellent system developed by HashiCorp for the terraform CLI. You can read more about this archecture here:


Use the repo to create a starter repo.

This tutorial walks you through how to use that template to write a simple plugin.

Plugin configuration

In a .aspect/cli/plugins.yaml file at the repository root, list the plugins you'd like to install.

This is a YAML file. As an example, this file will install the plugin defined by the aspect-cli-plugin-template starter repo referenced earlier:

- name: hello-world
version: v0.3.1

The from line points to the plugin binary and can take one of these forms:

  1. A string with no slash characters, which is interpreted as a program on your system PATH.

  2. A filesystem path, either relative to the workspace root or absolute.

  3. A string of the form

    In this case, a version property is required as well. This form follows the convention in where a GitHub release at a tag contains the plugin binaries as assets.

    To get a binary for the right platform, we append one of these platform suffixes before fetching: -darwin_amd64, -darwin_arm64, -linux_amd64, -linux_arm64, -windows_amd64.exe

    In the yaml example above, on an x86_64 architecture Linux machine, we would download from

  4. An http/https URL from which the plugin can be downloaded.

    As in the previous case, a platform suffix is appended to the URL before fetching.

Avoid many plugins

As you add new features to your Aspect CLI plugin set, you'll naturally run into a question: "how many plugins should I write".

Having many plugins has downsides:

  • Each plugin runs a gRPC server, so it consumes some memory on the users machine.
  • Managing each plugin as a separate repository adds governance overhead.

The "composite" design pattern is a good solution. Each independent unit of functionality still implements the plugin API. We like to call these "features". You then write a "composite" plugin that contains a list of these features, and delegates every API call by calling that function on each feature in turn.

Here is a code sample for how the composite pattern might look in Go:

package main

import (

goplugin ""

func main() {
// config.NewConfigFor accepts a plugin implementation and returns the go-plugin
// configuration required to serve the plugin to the CLI core.

// CompositePlugin implements an aspect CLI plugin. It is just an composite pattern for multiple "child" features.
type CompositePlugin struct {
features map[string]common.Feature

// NewPlugin creates a new CompositePlugin with the default dependencies.
func NewPlugin() *CompositePlugin {
features := map[string]common.Feature{
"my_feature_1": xx.NewDefaultFeature(),
"my_feature_2": yy.NewDefaultFeature(),
return NewPlugin(features)

// BEPEventCallback satisfies the Plugin interface.
func (p *CompositePlugin) BEPEventCallback(event *buildeventstream.BuildEvent) error {
for _, c := range p.features {
if err := c.BEPEventCallback(event); err != nil {
return err
return nil

// CustomCommands satisfies the Plugin interface.
func (p *CompositePlugin) CustomCommands() ([]*plugin.Command, error) {
result := []*plugin.Command{}
for _, c := range p.features {
if childCommands, err := c.CustomCommands(); err != nil {
return nil, err
} else {
result = append(result, childCommands...)
return result, nil

// PostBuildHook satisfies the Plugin interface. You could similarly implement PostTestHook and PostRunHook.
func (p *CompositePlugin) PostBuildHook(
isInteractiveMode bool,
promptRunner ioutils.PromptRunner,
) error {
for _, c := range p.features {
if err := c.PostBuildHook(isInteractiveMode, promptRunner); err != nil {
return err

return nil


In the future, we plan to allow semantic versioning ranges to constrain the versions which can be used. When aspect runs, would then prompt you to re-lock the dependencies to exact versions if they have changed, and can verify the integrity of the plugin contents against what was first installed.


The locking semantics follow the Trust on first use approach.

Another future enhancement is for From to accept a string starting with //, which is interpreted as a Bazel Label in the current workspace.

When the from line is a label, it will be a *_binary rule which builds a plugin binary. When the CLI loads this plugin, it first builds it from source. This is useful as a local development round-trip while authoring a plugin. However, it is not a great way to deploy a plugin to users, as it causes them to perform an extra build every time they run aspect, whether they intend to use the plugin or not.