feat: plugin system (#64)

* feat: add the fix-visibility example

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>

* feat: plugin system

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>

* fix: tests

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>

* fix: remove BUILD.bazel

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>

* refactor: move pkgs

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>

* refactor: move setup outside versioned sdk

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
diff --git a/.bazelignore b/.bazelignore
new file mode 100644
index 0000000..d838da9
--- /dev/null
+++ b/.bazelignore
@@ -0,0 +1 @@
+examples/
diff --git a/cmd/aspect/build/BUILD.bazel b/cmd/aspect/build/BUILD.bazel
index 2acd656..1bbec84 100644
--- a/cmd/aspect/build/BUILD.bazel
+++ b/cmd/aspect/build/BUILD.bazel
@@ -6,12 +6,13 @@
     importpath = "aspect.build/cli/cmd/aspect/build",
     visibility = ["//cmd/aspect/root:__pkg__"],
     deps = [
+        "//cmd/aspect/root/flags",
         "//pkg/aspect/build",
         "//pkg/aspect/build/bep",
         "//pkg/bazel",
         "//pkg/hooks",
         "//pkg/ioutils",
-        "//pkg/plugins/fix_visibility",
+        "//pkg/plugin/system",
         "@com_github_spf13_cobra//:cobra",
     ],
 )
diff --git a/cmd/aspect/build/build.go b/cmd/aspect/build/build.go
index e7a88ca..04b1150 100644
--- a/cmd/aspect/build/build.go
+++ b/cmd/aspect/build/build.go
@@ -9,12 +9,13 @@
 import (
 	"github.com/spf13/cobra"
 
+	rootFlags "aspect.build/cli/cmd/aspect/root/flags"
 	"aspect.build/cli/pkg/aspect/build"
 	"aspect.build/cli/pkg/aspect/build/bep"
 	"aspect.build/cli/pkg/bazel"
 	"aspect.build/cli/pkg/hooks"
 	"aspect.build/cli/pkg/ioutils"
-	"aspect.build/cli/pkg/plugins/fix_visibility"
+	"aspect.build/cli/pkg/plugin/system"
 )
 
 // NewDefaultBuildCmd creates a new build cobra command with the default
@@ -35,21 +36,30 @@
 	besBackend bep.BESBackend,
 	hooks *hooks.Hooks,
 ) *cobra.Command {
-	// TODO(f0rmiga): this should also be part of the plugin design, as
-	// registering BEP event subscribers should not be hardcoded here.
-	var fixVisibilityPlugin build.Plugin = fix_visibility.NewDefaultPlugin()
-	besBackend.RegisterSubscriber(fixVisibilityPlugin.BEPEventCallback)
-	hooks.RegisterPostBuild(fixVisibilityPlugin.PostBuildHook)
-
-	b := build.New(streams, bzl, besBackend, hooks)
-
 	cmd := &cobra.Command{
 		Use:   "build",
 		Short: "Builds the specified targets, using the options.",
 		Long: "Invokes bazel build on the specified targets. " +
 			"See 'bazel help target-syntax' for details and examples on how to specify targets to build.",
 		RunE: func(cmd *cobra.Command, args []string) (exitErr error) {
-			return b.Run(cmd.Context(), cmd, args)
+			pluginSystem := system.NewPluginSystem()
+			if err := pluginSystem.Configure(streams); err != nil {
+				return err
+			}
+			defer pluginSystem.TearDown()
+
+			for node := pluginSystem.PluginList().Head; node != nil; node = node.Next {
+				besBackend.RegisterSubscriber(node.Plugin.BEPEventCallback)
+				hooks.RegisterPostBuild(node.Plugin.PostBuildHook)
+			}
+
+			isInteractiveMode, err := cmd.Root().PersistentFlags().GetBool(rootFlags.InteractiveFlagName)
+			if err != nil {
+				return err
+			}
+
+			b := build.New(streams, bzl, besBackend, hooks)
+			return b.Run(cmd.Context(), cmd, args, isInteractiveMode)
 		},
 	}
 
diff --git a/cmd/aspect/root/BUILD.bazel b/cmd/aspect/root/BUILD.bazel
index cea46cb..9f2f380 100644
--- a/cmd/aspect/root/BUILD.bazel
+++ b/cmd/aspect/root/BUILD.bazel
@@ -13,6 +13,7 @@
         "//cmd/aspect/clean",
         "//cmd/aspect/docs",
         "//cmd/aspect/info",
+        "//cmd/aspect/root/flags",
         "//cmd/aspect/test",
         "//cmd/aspect/version",
         "//docs/help/topics",
diff --git a/cmd/aspect/root/flags/BUILD.bazel b/cmd/aspect/root/flags/BUILD.bazel
new file mode 100644
index 0000000..f88d95c
--- /dev/null
+++ b/cmd/aspect/root/flags/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "flags",
+    srcs = ["config.go"],
+    importpath = "aspect.build/cli/cmd/aspect/root/flags",
+    visibility = ["//visibility:public"],
+)
diff --git a/cmd/aspect/root/flags/config.go b/cmd/aspect/root/flags/config.go
new file mode 100644
index 0000000..7bde932
--- /dev/null
+++ b/cmd/aspect/root/flags/config.go
@@ -0,0 +1,14 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package flags
+
+const (
+	// ConfigFlagName is the --config flag for the root command.
+	ConfigFlagName = "config"
+	// InteractiveFlagName is the --interactive flag for the root command.
+	InteractiveFlagName = "interactive"
+)
diff --git a/cmd/aspect/root/root.go b/cmd/aspect/root/root.go
index 5f92d59..ac8d5eb 100644
--- a/cmd/aspect/root/root.go
+++ b/cmd/aspect/root/root.go
@@ -19,6 +19,7 @@
 	"aspect.build/cli/cmd/aspect/clean"
 	"aspect.build/cli/cmd/aspect/docs"
 	"aspect.build/cli/cmd/aspect/info"
+	"aspect.build/cli/cmd/aspect/root/flags"
 	"aspect.build/cli/cmd/aspect/test"
 	"aspect.build/cli/cmd/aspect/version"
 	"aspect.build/cli/docs/help/topics"
@@ -49,8 +50,8 @@
 	// ### Flags
 	var cfgFile string
 	var interactive bool
-	cmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.aspect.yaml)")
-	cmd.PersistentFlags().BoolVar(&interactive, "interactive", defaultInteractive, "Interactive mode (e.g. prompts for user input)")
+	cmd.PersistentFlags().StringVar(&cfgFile, flags.ConfigFlagName, "", "config file (default is $HOME/.aspect.yaml)")
+	cmd.PersistentFlags().BoolVar(&interactive, flags.InteractiveFlagName, defaultInteractive, "Interactive mode (e.g. prompts for user input)")
 
 	// ### Viper
 	if cfgFile != "" {
diff --git a/examples/fix-visibility/.aspectplugins b/examples/fix-visibility/.aspectplugins
new file mode 100644
index 0000000..7e6fdb0
--- /dev/null
+++ b/examples/fix-visibility/.aspectplugins
@@ -0,0 +1,4 @@
+- name: fix-visibility
+  from: ../../bazel-bin/plugins/fix_visibility/fix_visibility_/fix_visibility
+  # The possible log levels are: TRACE, DEBUG, INFO, WARN, ERROR, OFF.
+  log_level: OFF
diff --git a/examples/fix-visibility/.gitignore b/examples/fix-visibility/.gitignore
new file mode 100644
index 0000000..ec8b135
--- /dev/null
+++ b/examples/fix-visibility/.gitignore
@@ -0,0 +1,2 @@
+.aspect/
+bazel-*
diff --git a/examples/fix-visibility/README.md b/examples/fix-visibility/README.md
new file mode 100644
index 0000000..ea1a49b
--- /dev/null
+++ b/examples/fix-visibility/README.md
@@ -0,0 +1,4 @@
+# fix-visibility
+
+This example exercises the `fix-visibility` plugin. Run the `aspect` CLI in this
+workspace and it should fix the visibility issue.
diff --git a/examples/fix-visibility/WORKSPACE b/examples/fix-visibility/WORKSPACE
new file mode 100644
index 0000000..8b9fdfd
--- /dev/null
+++ b/examples/fix-visibility/WORKSPACE
@@ -0,0 +1 @@
+workspace(name = "fix_visibility")
diff --git a/examples/fix-visibility/bar/BUILD.bazel b/examples/fix-visibility/bar/BUILD.bazel
new file mode 100644
index 0000000..6494351
--- /dev/null
+++ b/examples/fix-visibility/bar/BUILD.bazel
@@ -0,0 +1,7 @@
+genrule(
+    name = "bar",
+    srcs = [],
+    outs = ["bar.txt"],
+    cmd = "echo 'bar' > '$@'",
+    visibility = ["//visibility:private"],
+)
diff --git a/examples/fix-visibility/foo/BUILD.bazel b/examples/fix-visibility/foo/BUILD.bazel
new file mode 100644
index 0000000..4c7d2a1
--- /dev/null
+++ b/examples/fix-visibility/foo/BUILD.bazel
@@ -0,0 +1,8 @@
+genrule(
+    name = "foo",
+    srcs = ["//bar"],
+    outs = ["foo.sh"],
+    cmd = "echo '#!/bin/sh\ncat $(execpath //bar)' > '$@'",
+    executable = True,
+    visibility = ["//visibility:public"],
+)
diff --git a/go.bzl b/go.bzl
index a806200..4fec50c 100644
--- a/go.bzl
+++ b/go.bzl
@@ -341,6 +341,12 @@
         version = "v0.5.1",
     )
     go_repository(
+        name = "com_github_hashicorp_go_hclog",
+        importpath = "github.com/hashicorp/go-hclog",
+        sum = "h1:bkKf0BeBXcSYa7f5Fyi9gMuQ8gNsxeiNpZjR6VxNZeo=",
+        version = "v1.0.0",
+    )
+    go_repository(
         name = "com_github_hashicorp_go_immutable_radix",
         importpath = "github.com/hashicorp/go-immutable-radix",
         sum = "h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=",
@@ -365,6 +371,12 @@
         version = "v0.0.1",
     )
     go_repository(
+        name = "com_github_hashicorp_go_plugin",
+        importpath = "github.com/hashicorp/go-plugin",
+        sum = "h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM=",
+        version = "v1.4.3",
+    )
+    go_repository(
         name = "com_github_hashicorp_go_rootcerts",
         importpath = "github.com/hashicorp/go-rootcerts",
         sum = "h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=",
@@ -431,6 +443,12 @@
         version = "v0.8.2",
     )
     go_repository(
+        name = "com_github_hashicorp_yamux",
+        importpath = "github.com/hashicorp/yamux",
+        sum = "h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=",
+        version = "v0.0.0-20180604194846-3520598351bb",
+    )
+    go_repository(
         name = "com_github_hpcloud_tail",
         importpath = "github.com/hpcloud/tail",
         sum = "h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=",
@@ -449,7 +467,12 @@
         sum = "h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=",
         version = "v1.0.0",
     )
-
+    go_repository(
+        name = "com_github_jhump_protoreflect",
+        importpath = "github.com/jhump/protoreflect",
+        sum = "h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=",
+        version = "v1.6.0",
+    )
     go_repository(
         name = "com_github_json_iterator_go",
         importpath = "github.com/json-iterator/go",
@@ -607,7 +630,12 @@
         sum = "h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=",
         version = "v1.4.8",
     )
-
+    go_repository(
+        name = "com_github_oklog_run",
+        importpath = "github.com/oklog/run",
+        sum = "h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=",
+        version = "v1.0.0",
+    )
     go_repository(
         name = "com_github_onsi_ginkgo",
         importpath = "github.com/onsi/ginkgo",
@@ -939,6 +967,7 @@
     )
     go_repository(
         name = "org_golang_google_grpc",
+        build_file_proto_mode = "disable",
         importpath = "google.golang.org/grpc",
         sum = "h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=",
         version = "v1.38.0",
diff --git a/go.mod b/go.mod
index 799cc8c..35f81e4 100644
--- a/go.mod
+++ b/go.mod
@@ -6,10 +6,11 @@
 	github.com/bazelbuild/bazel-gazelle v0.23.0
 	github.com/bazelbuild/bazelisk v1.10.1
 	github.com/bazelbuild/buildtools v0.0.0-20210920153738-d6daef01a1a2
-	github.com/bazelbuild/rules_go v0.28.0
 	github.com/fatih/color v1.12.0
 	github.com/golang/mock v1.5.0
 	github.com/golang/protobuf v1.5.2
+	github.com/hashicorp/go-hclog v1.0.0
+	github.com/hashicorp/go-plugin v1.4.3
 	github.com/manifoldco/promptui v0.8.0
 	github.com/mattn/go-isatty v0.0.13
 	github.com/mitchellh/go-homedir v1.1.0
@@ -23,4 +24,5 @@
 	google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c
 	google.golang.org/grpc v1.38.0
 	google.golang.org/protobuf v1.26.0
+	gopkg.in/yaml.v2 v2.4.0
 )
diff --git a/go.sum b/go.sum
index 49ee2c7..613653a 100644
--- a/go.sum
+++ b/go.sum
@@ -51,7 +51,6 @@
 github.com/bazelbuild/buildtools v0.0.0-20210920153738-d6daef01a1a2 h1:hWvEw/36XcpZzKZB2LBYhKSGt72ETiIhudjxd637+4w=
 github.com/bazelbuild/buildtools v0.0.0-20210920153738-d6daef01a1a2/go.mod h1:689QdV3hBP7Vo9dJMmzhoYIyo/9iMhEmHkJcnaPRCbo=
 github.com/bazelbuild/rules_go v0.0.0-20190719190356-6dae44dc5cab/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M=
-github.com/bazelbuild/rules_go v0.28.0 h1:fNtx0dJpG5ENGdMj3/GICoi/7z+ixB3IIW5rERTzOgM=
 github.com/bazelbuild/rules_go v0.28.0/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
@@ -165,9 +164,14 @@
 github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-hclog v1.0.0 h1:bkKf0BeBXcSYa7f5Fyi9gMuQ8gNsxeiNpZjR6VxNZeo=
+github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM=
+github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
@@ -184,11 +188,15 @@
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
+github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@@ -211,10 +219,13 @@
 github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
 github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
 github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
 github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
@@ -223,6 +234,8 @@
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
@@ -236,6 +249,8 @@
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
@@ -351,6 +366,7 @@
 golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -421,6 +437,7 @@
 golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -431,6 +448,7 @@
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -566,6 +584,7 @@
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -608,6 +627,7 @@
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
diff --git a/pkg/aspect/build/BUILD.bazel b/pkg/aspect/build/BUILD.bazel
index f0f3e75..e8aa7e3 100644
--- a/pkg/aspect/build/BUILD.bazel
+++ b/pkg/aspect/build/BUILD.bazel
@@ -11,7 +11,6 @@
         "//pkg/bazel",
         "//pkg/hooks",
         "//pkg/ioutils",
-        "//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto",
         "@com_github_spf13_cobra//:cobra",
     ],
 )
diff --git a/pkg/aspect/build/build.go b/pkg/aspect/build/build.go
index fa45e9b..faf024c 100644
--- a/pkg/aspect/build/build.go
+++ b/pkg/aspect/build/build.go
@@ -14,7 +14,6 @@
 
 	"github.com/spf13/cobra"
 
-	buildeventstream "aspect.build/cli/bazel/buildeventstream/proto"
 	"aspect.build/cli/pkg/aspect/build/bep"
 	"aspect.build/cli/pkg/aspecterrors"
 	"aspect.build/cli/pkg/bazel"
@@ -47,11 +46,16 @@
 
 // Run runs the aspect build command, calling `bazel build` with a local Build
 // Event Protocol backend used by Aspect plugins to subscribe to build events.
-func (b *Build) Run(ctx context.Context, cmd *cobra.Command, args []string) (exitErr error) {
+func (b *Build) Run(
+	ctx context.Context,
+	cmd *cobra.Command,
+	args []string,
+	isInteractiveMode bool,
+) (exitErr error) {
 	// TODO(f0rmiga): this is a hook for the build command and should be discussed
 	// as part of the plugin design.
 	defer func() {
-		errs := b.hooks.ExecutePostBuild().Errors()
+		errs := b.hooks.ExecutePostBuild(isInteractiveMode).Errors()
 		if len(errs) > 0 {
 			for _, err := range errs {
 				fmt.Fprintf(b.Streams.Stderr, "Error: failed to run build command: %v\n", err)
@@ -95,12 +99,3 @@
 
 	return nil
 }
-
-// Plugin defines only the methods for the build command.
-type Plugin interface {
-	// BEPEventsSubscriber is used to verify whether an Aspect plugin registers
-	// itself to receive the Build Event Protocol events.
-	BEPEventCallback(event *buildeventstream.BuildEvent) error
-	// TODO(f0rmiga): test the build hooks after implementing the plugin system.
-	PostBuildHook() error
-}
diff --git a/pkg/aspect/build/build_test.go b/pkg/aspect/build/build_test.go
index 1cd88c4..23e4a5a 100644
--- a/pkg/aspect/build/build_test.go
+++ b/pkg/aspect/build/build_test.go
@@ -62,7 +62,7 @@
 		hooks := hooks.New()
 		b := build.New(streams, spawner, besBackend, hooks)
 		ctx := context.Background()
-		err := b.Run(ctx, nil, []string{"//..."})
+		err := b.Run(ctx, nil, []string{"//..."}, false)
 
 		g.Expect(err).To(MatchError(fmt.Errorf("failed to run build command: %w", setupErr)))
 	})
@@ -106,7 +106,7 @@
 		hooks := hooks.New()
 		b := build.New(streams, spawner, besBackend, hooks)
 		ctx := context.Background()
-		err := b.Run(ctx, nil, []string{"//..."})
+		err := b.Run(ctx, nil, []string{"//..."}, false)
 
 		g.Expect(err).To(MatchError(fmt.Errorf("failed to run build command: %w", serveWaitErr)))
 	})
@@ -154,7 +154,7 @@
 		hooks := hooks.New()
 		b := build.New(streams, spawner, besBackend, hooks)
 		ctx := context.Background()
-		err := b.Run(ctx, nil, []string{"//..."})
+		err := b.Run(ctx, nil, []string{"//..."}, false)
 
 		g.Expect(err).To(MatchError(expectErr))
 	})
@@ -203,7 +203,7 @@
 		hooks := hooks.New()
 		b := build.New(streams, spawner, besBackend, hooks)
 		ctx := context.Background()
-		err := b.Run(ctx, nil, []string{"//..."})
+		err := b.Run(ctx, nil, []string{"//..."}, false)
 
 		g.Expect(err).To(MatchError(&aspecterrors.ExitError{ExitCode: 1}))
 		g.Expect(stderr.String()).To(Equal("Error: failed to run build command: error 1\nError: failed to run build command: error 2\n"))
@@ -249,7 +249,7 @@
 		hooks := hooks.New()
 		b := build.New(streams, spawner, besBackend, hooks)
 		ctx := context.Background()
-		err := b.Run(ctx, nil, []string{"//..."})
+		err := b.Run(ctx, nil, []string{"//..."}, false)
 
 		g.Expect(err).To(BeNil())
 	})
diff --git a/pkg/hooks/BUILD.bazel b/pkg/hooks/BUILD.bazel
index 182b928..b4f2bb3 100644
--- a/pkg/hooks/BUILD.bazel
+++ b/pkg/hooks/BUILD.bazel
@@ -5,5 +5,8 @@
     srcs = ["hooks.go"],
     importpath = "aspect.build/cli/pkg/hooks",
     visibility = ["//visibility:public"],
-    deps = ["//pkg/aspecterrors"],
+    deps = [
+        "//pkg/aspecterrors",
+        "//pkg/ioutils",
+    ],
 )
diff --git a/pkg/hooks/hooks.go b/pkg/hooks/hooks.go
index c1ff33a..81eda8c 100644
--- a/pkg/hooks/hooks.go
+++ b/pkg/hooks/hooks.go
@@ -8,27 +8,37 @@
 
 import (
 	"aspect.build/cli/pkg/aspecterrors"
+	"aspect.build/cli/pkg/ioutils"
 )
 
+// Hooks represent the possible hook points from the plugin system. It accepts
+// registrations and can execute them as requested at the appropriate times.
 type Hooks struct {
 	postBuild *hookList
 }
 
+// New instantiates a new Hooks.
 func New() *Hooks {
 	return &Hooks{
 		postBuild: &hookList{},
 	}
 }
 
+// RegisterPostBuild registers a post-build hook function.
 func (hooks *Hooks) RegisterPostBuild(fn PostBuildFn) {
 	hooks.postBuild.insert(fn)
 }
 
-func (hooks *Hooks) ExecutePostBuild() *aspecterrors.ErrorList {
+// ExecutePostBuild executes the post-build hook functions in sequence they were
+// registered.
+func (hooks *Hooks) ExecutePostBuild(isInteractiveMode bool) *aspecterrors.ErrorList {
 	errors := &aspecterrors.ErrorList{}
 	node := hooks.postBuild.head
 	for node != nil {
-		if err := node.fn.(PostBuildFn)(); err != nil {
+		// promptRunner is nil here because it has to satisfy the PostBuild
+		// signature to comply with the go-plugin library. The real promptRunner is
+		// instantiated when the gRPC call is made.
+		if err := node.fn.(PostBuildFn)(isInteractiveMode, nil); err != nil {
 			errors.Insert(err)
 		}
 		node = node.next
@@ -36,7 +46,8 @@
 	return errors
 }
 
-type PostBuildFn func() error
+// PostBuildFn matches the plugin PostBuildHook method signature.
+type PostBuildFn func(isInteractiveMode bool, promptRunner ioutils.PromptRunner) error
 
 type hookList struct {
 	head *hookNode
diff --git a/pkg/ioutils/BUILD.bazel b/pkg/ioutils/BUILD.bazel
index 3cdce77..0222c9a 100644
--- a/pkg/ioutils/BUILD.bazel
+++ b/pkg/ioutils/BUILD.bazel
@@ -2,7 +2,11 @@
 
 go_library(
     name = "ioutils",
-    srcs = ["streams.go"],
+    srcs = [
+        "prompt.go",
+        "streams.go",
+    ],
     importpath = "aspect.build/cli/pkg/ioutils",
     visibility = ["//visibility:public"],
+    deps = ["@com_github_manifoldco_promptui//:promptui"],
 )
diff --git a/pkg/ioutils/prompt.go b/pkg/ioutils/prompt.go
new file mode 100644
index 0000000..4aa3375
--- /dev/null
+++ b/pkg/ioutils/prompt.go
@@ -0,0 +1,15 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package ioutils
+
+import "github.com/manifoldco/promptui"
+
+// PromptRunner is the interface that wraps the promptui.Prompt and makes a call
+// to it from the aspect CLI Core.
+type PromptRunner interface {
+	Run(prompt promptui.Prompt) (string, error)
+}
diff --git a/pkg/plugin/sdk/v1alpha1/config/BUILD.bazel b/pkg/plugin/sdk/v1alpha1/config/BUILD.bazel
new file mode 100644
index 0000000..5374025
--- /dev/null
+++ b/pkg/plugin/sdk/v1alpha1/config/BUILD.bazel
@@ -0,0 +1,12 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "config",
+    srcs = ["config.go"],
+    importpath = "aspect.build/cli/pkg/plugin/sdk/v1alpha1/config",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/plugin/sdk/v1alpha1/plugin",
+        "@com_github_hashicorp_go_plugin//:go-plugin",
+    ],
+)
diff --git a/pkg/plugin/sdk/v1alpha1/config/config.go b/pkg/plugin/sdk/v1alpha1/config/config.go
new file mode 100644
index 0000000..4a16614
--- /dev/null
+++ b/pkg/plugin/sdk/v1alpha1/config/config.go
@@ -0,0 +1,41 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package config
+
+import (
+	goplugin "github.com/hashicorp/go-plugin"
+
+	"aspect.build/cli/pkg/plugin/sdk/v1alpha1/plugin"
+)
+
+// DefaultPluginName is the name each aspect plugin must provide.
+const DefaultPluginName = "aspectplugin"
+
+// Handshake is the shared handshake config for the v1alpha1 protocol.
+var Handshake = goplugin.HandshakeConfig{
+	ProtocolVersion:  1,
+	MagicCookieKey:   "PLUGIN",
+	MagicCookieValue: "ASPECT",
+}
+
+// PluginMap represents the plugin interfaces allowed to be implemented by a
+// plugin executable.
+var PluginMap = map[string]goplugin.Plugin{
+	DefaultPluginName: &plugin.GRPCPlugin{},
+}
+
+// NewConfigFor returns the default configuration for the passed Plugin
+// implementation.
+func NewConfigFor(p plugin.Plugin) *goplugin.ServeConfig {
+	return &goplugin.ServeConfig{
+		HandshakeConfig: Handshake,
+		Plugins: map[string]goplugin.Plugin{
+			DefaultPluginName: &plugin.GRPCPlugin{Impl: p},
+		},
+		GRPCServer: goplugin.DefaultGRPCServer,
+	}
+}
diff --git a/pkg/plugin/sdk/v1alpha1/plugin/BUILD.bazel b/pkg/plugin/sdk/v1alpha1/plugin/BUILD.bazel
new file mode 100644
index 0000000..47bd569
--- /dev/null
+++ b/pkg/plugin/sdk/v1alpha1/plugin/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "plugin",
+    srcs = [
+        "grpc.go",
+        "interface.go",
+    ],
+    importpath = "aspect.build/cli/pkg/plugin/sdk/v1alpha1/plugin",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/ioutils",
+        "//pkg/plugin/sdk/v1alpha1/proto",
+        "//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto",
+        "@com_github_hashicorp_go_plugin//:go-plugin",
+        "@com_github_manifoldco_promptui//:promptui",
+        "@org_golang_google_grpc//:go_default_library",
+    ],
+)
diff --git a/pkg/plugin/sdk/v1alpha1/plugin/grpc.go b/pkg/plugin/sdk/v1alpha1/plugin/grpc.go
new file mode 100644
index 0000000..97fb60d
--- /dev/null
+++ b/pkg/plugin/sdk/v1alpha1/plugin/grpc.go
@@ -0,0 +1,175 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+// grpc.go hides all the complexity of doing the gRPC calls between the aspect
+// Core and a Plugin implementation by providing simple abstractions from the
+// point of view of Plugin maintainers.
+package plugin
+
+import (
+	"context"
+	"fmt"
+
+	goplugin "github.com/hashicorp/go-plugin"
+	"github.com/manifoldco/promptui"
+	"google.golang.org/grpc"
+
+	buildeventstream "aspect.build/cli/bazel/buildeventstream/proto"
+	"aspect.build/cli/pkg/ioutils"
+	"aspect.build/cli/pkg/plugin/sdk/v1alpha1/proto"
+)
+
+// GRPCPlugin represents a Plugin that communicates over gRPC.
+type GRPCPlugin struct {
+	goplugin.Plugin
+	Impl Plugin
+}
+
+// GRPCServer registers an instance of the GRPCServer in the Plugin binary.
+func (p *GRPCPlugin) GRPCServer(broker *goplugin.GRPCBroker, s *grpc.Server) error {
+	proto.RegisterPluginServer(s, &GRPCServer{Impl: p.Impl, broker: broker})
+	return nil
+}
+
+// GRPCClient returns a client to perform the RPC calls to the Plugin
+// instance from the Core.
+func (p *GRPCPlugin) GRPCClient(ctx context.Context, broker *goplugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
+	return &GRPCClient{client: proto.NewPluginClient(c), broker: broker}, nil
+}
+
+// GRPCServer implements the gRPC server that runs on the Plugin instances.
+type GRPCServer struct {
+	Impl   Plugin
+	broker *goplugin.GRPCBroker
+}
+
+// BEPEventCallback translates the gRPC call to the Plugin BEPEventCallback
+// implementation.
+func (m *GRPCServer) BEPEventCallback(
+	ctx context.Context,
+	req *proto.BEPEventCallbackReq,
+) (*proto.BEPEventCallbackRes, error) {
+	return &proto.BEPEventCallbackRes{}, m.Impl.BEPEventCallback(req.Event)
+}
+
+// PostBuildHook translates the gRPC call to the Plugin PostBuildHook
+// implementation. It starts a prompt runner that is passed to the Plugin
+// instance to be able to perform prompt actions to the CLI user.
+func (m *GRPCServer) PostBuildHook(
+	ctx context.Context,
+	req *proto.PostBuildHookReq,
+) (*proto.PostBuildHookRes, error) {
+	conn, err := m.broker.Dial(req.BrokerId)
+	if err != nil {
+		return nil, err
+	}
+	defer conn.Close()
+
+	client := proto.NewPrompterClient(conn)
+	prompter := &PrompterGRPCClient{client: client}
+	return &proto.PostBuildHookRes{},
+		m.Impl.PostBuildHook(req.IsInteractiveMode, prompter)
+}
+
+// GRPCClient implements the gRPC client that is used by the Core to communicate
+// with the Plugin instances.
+type GRPCClient struct {
+	client proto.PluginClient
+	broker *goplugin.GRPCBroker
+}
+
+// BEPEventCallback is called from the Core to execute the Plugin
+// BEPEventCallback.
+func (m *GRPCClient) BEPEventCallback(event *buildeventstream.BuildEvent) error {
+	_, err := m.client.BEPEventCallback(context.Background(), &proto.BEPEventCallbackReq{Event: event})
+	return err
+}
+
+// PostBuildHook is called from the Core to execute the Plugin PostBuildHook. It
+// starts the prompt runner server and ignores the prompt runner argument since
+// the signature of this method has to match the Plugin interface.
+func (m *GRPCClient) PostBuildHook(isInteractiveMode bool, _ ioutils.PromptRunner) error {
+	prompterServer := &PrompterGRPCServer{}
+	var s *grpc.Server
+	serverFunc := func(opts []grpc.ServerOption) *grpc.Server {
+		s = grpc.NewServer(opts...)
+		proto.RegisterPrompterServer(s, prompterServer)
+		return s
+	}
+	brokerID := m.broker.NextId()
+	go m.broker.AcceptAndServe(brokerID, serverFunc)
+	req := &proto.PostBuildHookReq{
+		BrokerId:          brokerID,
+		IsInteractiveMode: isInteractiveMode,
+	}
+	_, err := m.client.PostBuildHook(context.Background(), req)
+	s.Stop()
+	return err
+}
+
+// PrompterGRPCServer implements the gRPC server that runs on the Core and is
+// passed to the Plugin to allow prompt actions to the CLI user.
+type PrompterGRPCServer struct{}
+
+// Run translates the gRPC call to perform a prompt Run on the Core.
+func (p *PrompterGRPCServer) Run(
+	ctx context.Context,
+	req *proto.PromptRunReq,
+) (*proto.PromptRunRes, error) {
+	prompt := &promptui.Prompt{
+		Label:       req.GetLabel(),
+		Default:     req.GetDefault(),
+		AllowEdit:   req.GetAllowEdit(),
+		Mask:        []rune(req.GetMask())[0],
+		HideEntered: req.GetHideEntered(),
+		IsConfirm:   req.GetIsConfirm(),
+		IsVimMode:   req.GetIsVimMode(),
+	}
+
+	result, err := prompt.Run()
+	res := &proto.PromptRunRes{Result: result}
+	if err != nil {
+		res.Error = &proto.PromptRunRes_Error{
+			Happened: true,
+			Message:  err.Error(),
+		}
+	}
+
+	return res, nil
+}
+
+// PrompterGRPCClient implements the gRPC client that is used by the Plugin
+// instance to communicate with the Core to request prompt actions from the
+// user.
+type PrompterGRPCClient struct {
+	client proto.PrompterClient
+}
+
+// Run is called from the Plugin to request the Core to run the given
+// promptui.Prompt.
+func (p *PrompterGRPCClient) Run(prompt promptui.Prompt) (string, error) {
+	label, isString := prompt.Label.(string)
+	if !isString {
+		return "", fmt.Errorf("label '%+v' must be a string", prompt.Label)
+	}
+	req := &proto.PromptRunReq{
+		Label:       label,
+		Default:     prompt.Default,
+		AllowEdit:   prompt.AllowEdit,
+		Mask:        string(prompt.Mask),
+		HideEntered: prompt.HideEntered,
+		IsConfirm:   prompt.IsConfirm,
+		IsVimMode:   prompt.IsVimMode,
+	}
+	res, err := p.client.Run(context.Background(), req)
+	if err != nil {
+		return "", err
+	}
+	if res.Error != nil && res.Error.Happened {
+		return "", fmt.Errorf(res.Error.Message)
+	}
+	return res.Result, nil
+}
diff --git a/pkg/plugin/sdk/v1alpha1/plugin/interface.go b/pkg/plugin/sdk/v1alpha1/plugin/interface.go
new file mode 100644
index 0000000..aac80ce
--- /dev/null
+++ b/pkg/plugin/sdk/v1alpha1/plugin/interface.go
@@ -0,0 +1,21 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package plugin
+
+import (
+	buildeventstream "aspect.build/cli/bazel/buildeventstream/proto"
+	"aspect.build/cli/pkg/ioutils"
+)
+
+// Plugin determines how an aspect Plugin should be implemented.
+type Plugin interface {
+	BEPEventCallback(event *buildeventstream.BuildEvent) error
+	PostBuildHook(
+		isInteractiveMode bool,
+		promptRunner ioutils.PromptRunner,
+	) error
+}
diff --git a/pkg/plugin/sdk/v1alpha1/proto/BUILD.bazel b/pkg/plugin/sdk/v1alpha1/proto/BUILD.bazel
new file mode 100644
index 0000000..fba85b5
--- /dev/null
+++ b/pkg/plugin/sdk/v1alpha1/proto/BUILD.bazel
@@ -0,0 +1,26 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+load("@rules_proto//proto:defs.bzl", "proto_library")
+
+proto_library(
+    name = "proto_proto",
+    srcs = ["plugin.proto"],
+    visibility = ["//visibility:public"],
+    deps = ["//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto:proto_proto"],
+)
+
+go_proto_library(
+    name = "proto_go_proto",
+    compilers = ["@io_bazel_rules_go//proto:go_grpc"],
+    importpath = "aspect.build/cli/pkg/plugin/sdk/v1alpha1/proto",
+    proto = ":proto_proto",
+    visibility = ["//visibility:public"],
+    deps = ["//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto"],
+)
+
+go_library(
+    name = "proto",
+    embed = [":proto_go_proto"],
+    importpath = "aspect.build/cli/pkg/plugin/sdk/v1alpha1/proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/pkg/plugin/sdk/v1alpha1/proto/dummy.go b/pkg/plugin/sdk/v1alpha1/proto/dummy.go
new file mode 100644
index 0000000..7ecf3b1
--- /dev/null
+++ b/pkg/plugin/sdk/v1alpha1/proto/dummy.go
@@ -0,0 +1,6 @@
+//go:build dummy
+// +build dummy
+
+// This file exists to make the go tooling happy. This package is generated by
+// bazel.
+package proto
diff --git a/pkg/plugin/sdk/v1alpha1/proto/plugin.proto b/pkg/plugin/sdk/v1alpha1/proto/plugin.proto
new file mode 100644
index 0000000..0adcde8
--- /dev/null
+++ b/pkg/plugin/sdk/v1alpha1/proto/plugin.proto
@@ -0,0 +1,68 @@
+syntax = "proto3";
+
+package proto;
+
+import "third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto";
+
+option go_package = "aspect.build/cli/pkg/plugin/sdk/v1alpha1/proto";
+
+// Plugin is the service used by the Core to communicate with a Plugin instance.
+service Plugin {
+  rpc BEPEventCallback(BEPEventCallbackReq) returns (BEPEventCallbackRes);
+  rpc PostBuildHook(PostBuildHookReq) returns (PostBuildHookRes);
+}
+
+message BEPEventCallbackReq {
+  build_event_stream.BuildEvent event = 1;
+}
+
+message BEPEventCallbackRes {}
+
+message PostBuildHookReq {
+  uint32 broker_id = 1;
+  bool is_interactive_mode = 2;
+}
+
+message PostBuildHookRes {}
+
+// Prompter is the service used by the Plugin instances to request prompt
+// actions to the Core from the CLI users.
+service Prompter {
+  rpc Run(PromptRunReq) returns (PromptRunRes);
+}
+
+// PromptRunReq maps the relevant values from
+// (github.com/manifoldco/promptui).Prompt.
+message PromptRunReq {
+  // Label is the value displayed on the command line prompt.
+  string label = 1;
+  // Default is the initial value for the prompt. This value will be displayed
+  // next to the prompt's label and the user will be able to view or change it
+  // depending on the options.
+  string default = 2;
+  // AllowEdit lets the user edit the default value. If false, any key press
+	// other than <Enter> automatically clears the default value.
+  bool allow_edit = 3;
+  // Mask is an optional rune that sets which character to display instead of
+  // the entered characters. This allows hiding private information like
+  // passwords.
+  string mask = 5;
+  // HideEntered sets whether to hide the text after the user has pressed enter.
+  bool hide_entered = 6;
+  // IsConfirm makes the prompt ask for a yes or no ([Y/N]) question rather than
+  // request an input. When set, most properties related to input will be
+  // ignored.
+  bool is_confirm = 8;
+  // IsVimMode enables vi-like movements (hjkl) and editing.
+  bool is_vim_mode = 9;
+}
+
+// PromptRunRes maps the returned values from promptui.Run.
+message PromptRunRes {
+  string result = 1;
+  message Error {
+    bool happened = 1;
+    string message = 2;
+  }
+  Error error = 2;
+}
diff --git a/pkg/plugin/system/BUILD.bazel b/pkg/plugin/system/BUILD.bazel
new file mode 100644
index 0000000..c60e02e
--- /dev/null
+++ b/pkg/plugin/system/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "system",
+    srcs = [
+        "aspectplugins.go",
+        "setup.go",
+    ],
+    importpath = "aspect.build/cli/pkg/plugin/system",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/ioutils",
+        "//pkg/plugin/sdk/v1alpha1/config",
+        "//pkg/plugin/sdk/v1alpha1/plugin",
+        "@com_github_hashicorp_go_hclog//:go-hclog",
+        "@com_github_hashicorp_go_plugin//:go-plugin",
+        "@in_gopkg_yaml_v2//:yaml_v2",
+    ],
+)
diff --git a/pkg/plugin/system/aspectplugins.go b/pkg/plugin/system/aspectplugins.go
new file mode 100644
index 0000000..f605be9
--- /dev/null
+++ b/pkg/plugin/system/aspectplugins.go
@@ -0,0 +1,115 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package system
+
+import (
+	"fmt"
+	"io/fs"
+	"io/ioutil"
+	"os"
+	"path"
+	"path/filepath"
+
+	yaml "gopkg.in/yaml.v2"
+)
+
+const (
+	workspaceFilename     = "WORKSPACE"
+	aspectpluginsFilename = ".aspectplugins"
+)
+
+// AspectPlugin represents a plugin entry in the plugins file.
+type AspectPlugin struct {
+	Name       string                 `yaml:"name"`
+	From       string                 `yaml:"from"`
+	LogLevel   string                 `yaml:"log_level"`
+	Properties map[string]interface{} `yaml:"properties"`
+}
+
+// Finder is the interface that wraps the simple Find method that performs the
+// finding of the plugins file in the user system.
+type Finder interface {
+	Find() (string, error)
+}
+
+type finder struct {
+	osGetwd func() (string, error)
+	osStat  func(string) (fs.FileInfo, error)
+}
+
+// NewFinder instantiates a default internal implementation of the Finder
+// interface.
+func NewFinder() Finder {
+	return &finder{
+		osGetwd: os.Getwd,
+		osStat:  os.Stat,
+	}
+}
+
+// Find finds the .aspectplugins file under a Bazel workspace. If the returned
+// path is empty and no error was produced, the file doesn't exist.
+func (f *finder) Find() (string, error) {
+	cwd, err := f.osGetwd()
+	if err != nil {
+		return "", fmt.Errorf("failed to find .aspectplugins: %w", err)
+	}
+	for {
+		workspacePath := path.Join(cwd, workspaceFilename)
+		if _, err := f.osStat(workspacePath); err != nil {
+			if !os.IsNotExist(err) {
+				return "", fmt.Errorf("failed to find .aspectplugins: %w", err)
+			}
+			cwd = filepath.Dir(cwd)
+			continue
+		}
+		aspectpluginsPath := path.Join(cwd, aspectpluginsFilename)
+		if _, err := f.osStat(aspectpluginsPath); err != nil {
+			if !os.IsNotExist(err) {
+				return "", fmt.Errorf("failed to find .aspectplugins: %w", err)
+			}
+			break
+		}
+		return aspectpluginsPath, nil
+	}
+	return "", nil
+}
+
+// Parser is the interface that wraps the Parse method that performs the parsing
+// of a plugins file.
+type Parser interface {
+	Parse(aspectpluginsPath string) ([]AspectPlugin, error)
+}
+
+type parser struct {
+	ioutilReadFile      func(filename string) ([]byte, error)
+	yamlUnmarshalStrict func(in []byte, out interface{}) (err error)
+}
+
+// NewParser instantiates a default internal implementation of the Parser
+// interface.
+func NewParser() Parser {
+	return &parser{
+		ioutilReadFile:      ioutil.ReadFile,
+		yamlUnmarshalStrict: yaml.UnmarshalStrict,
+	}
+}
+
+// Parse parses a plugins file.
+func (p *parser) Parse(aspectpluginsPath string) ([]AspectPlugin, error) {
+	if aspectpluginsPath == "" {
+		return []AspectPlugin{}, nil
+	}
+	aspectpluginsData, err := p.ioutilReadFile(aspectpluginsPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse .aspectplugins: %w", err)
+	}
+	var aspectplugins []AspectPlugin
+	if err := p.yamlUnmarshalStrict(aspectpluginsData, &aspectplugins); err != nil {
+		return nil, fmt.Errorf("failed to parse .aspectplugins: %w", err)
+	}
+	return aspectplugins, nil
+}
diff --git a/pkg/plugin/system/setup.go b/pkg/plugin/system/setup.go
new file mode 100644
index 0000000..46271ca
--- /dev/null
+++ b/pkg/plugin/system/setup.go
@@ -0,0 +1,153 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package system
+
+import (
+	"fmt"
+	"os/exec"
+
+	hclog "github.com/hashicorp/go-hclog"
+	goplugin "github.com/hashicorp/go-plugin"
+
+	"aspect.build/cli/pkg/ioutils"
+	"aspect.build/cli/pkg/plugin/sdk/v1alpha1/config"
+	"aspect.build/cli/pkg/plugin/sdk/v1alpha1/plugin"
+)
+
+// PluginSystem is the interface that defines all the methods for the aspect CLI
+// plugin system intended to be used by the Core.
+type PluginSystem interface {
+	Configure(streams ioutils.Streams) error
+	PluginList() *PluginList
+	TearDown()
+}
+
+type pluginSystem struct {
+	finder        Finder
+	parser        Parser
+	clientFactory ClientFactory
+	clients       []ClientProvider
+	plugins       *PluginList
+}
+
+// NewPluginSystem instantiates a default internal implementation of the
+// PluginSystem interface.
+func NewPluginSystem() PluginSystem {
+	return &pluginSystem{
+		finder:        NewFinder(),
+		parser:        NewParser(),
+		clientFactory: &clientFactory{},
+		plugins:       &PluginList{},
+	}
+}
+
+// Configure configures the plugin system.
+func (ps *pluginSystem) Configure(streams ioutils.Streams) error {
+	aspectpluginsPath, err := ps.finder.Find()
+	if err != nil {
+		return fmt.Errorf("failed to configure plugin system: %w", err)
+	}
+	aspectplugins, err := ps.parser.Parse(aspectpluginsPath)
+	if err != nil {
+		return fmt.Errorf("failed to configure plugin system: %w", err)
+	}
+
+	ps.clients = make([]ClientProvider, 0, len(aspectplugins))
+	for _, aspectplugin := range aspectplugins {
+		logLevel := hclog.LevelFromString(aspectplugin.LogLevel)
+		if logLevel == hclog.NoLevel {
+			logLevel = hclog.Error
+		}
+		pluginLogger := hclog.New(&hclog.LoggerOptions{
+			Name:  aspectplugin.Name,
+			Level: logLevel,
+		})
+		// TODO(f0rmiga): make this loop concurrent so that all plugins are
+		// configured faster.
+		clientConfig := &goplugin.ClientConfig{
+			HandshakeConfig:  config.Handshake,
+			Plugins:          config.PluginMap,
+			Cmd:              exec.Command(aspectplugin.From),
+			AllowedProtocols: []goplugin.Protocol{goplugin.ProtocolGRPC},
+			SyncStdout:       streams.Stdout,
+			SyncStderr:       streams.Stderr,
+			Logger:           pluginLogger,
+		}
+		client := ps.clientFactory.New(clientConfig)
+		ps.clients = append(ps.clients, client)
+
+		rpcClient, err := client.Client()
+		if err != nil {
+			return fmt.Errorf("failed to configure plugin system: %w", err)
+		}
+
+		rawplugin, err := rpcClient.Dispense(config.DefaultPluginName)
+		if err != nil {
+			return fmt.Errorf("failed to configure plugin system: %w", err)
+		}
+
+		aspectplugin := rawplugin.(plugin.Plugin)
+		ps.plugins.insert(aspectplugin)
+	}
+
+	return nil
+}
+
+// TearDown tears down the plugin system, making all the necessary actions to
+// clean up the system.
+func (ps *pluginSystem) TearDown() {
+	for _, client := range ps.clients {
+		client.Kill()
+	}
+}
+
+// PluginList returns the list of configures plugins.
+func (ps *pluginSystem) PluginList() *PluginList {
+	return ps.plugins
+}
+
+// ClientFactory hides the call to goplugin.NewClient.
+type ClientFactory interface {
+	New(*goplugin.ClientConfig) ClientProvider
+}
+
+type clientFactory struct{}
+
+// New calls the goplugin.NewClient with the given config.
+func (*clientFactory) New(config *goplugin.ClientConfig) ClientProvider {
+	return goplugin.NewClient(config)
+}
+
+// ClientProvider is an interface for goplugin.Client returned by
+// goplugin.NewClient.
+type ClientProvider interface {
+	Client() (goplugin.ClientProtocol, error)
+	Kill()
+}
+
+// PluginList implements a simple linked list for the parsed plugins from the
+// plugins file.
+type PluginList struct {
+	Head *PluginNode
+	tail *PluginNode
+}
+
+func (l *PluginList) insert(p plugin.Plugin) {
+	node := &PluginNode{Plugin: p}
+	if l.Head == nil {
+		l.Head = node
+	} else {
+		l.tail.Next = node
+	}
+	l.tail = node
+}
+
+// PluginNode is a node in the PluginList linked list.
+type PluginNode struct {
+	Next   *PluginNode
+	Plugin plugin.Plugin
+}
diff --git a/pkg/plugins/fix_visibility/BUILD.bazel b/pkg/plugins/fix_visibility/BUILD.bazel
deleted file mode 100644
index 460857b..0000000
--- a/pkg/plugins/fix_visibility/BUILD.bazel
+++ /dev/null
@@ -1,15 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
-
-go_library(
-    name = "fix_visibility",
-    srcs = ["plugin.go"],
-    importpath = "aspect.build/cli/pkg/plugins/fix_visibility",
-    visibility = ["//cmd/aspect/build:__pkg__"],
-    deps = [
-        "//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto",
-        "@bazel_gazelle//label:go_default_library",
-        "@com_github_bazelbuild_buildtools//edit:go_default_library",
-        "@com_github_manifoldco_promptui//:promptui",
-        "@com_github_mattn_go_isatty//:go-isatty",
-    ],
-)
diff --git a/pkg/plugins/fix_visibility/plugin.go b/pkg/plugins/fix_visibility/plugin.go
deleted file mode 100644
index 8085c57..0000000
--- a/pkg/plugins/fix_visibility/plugin.go
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
-Copyright © 2021 Aspect Build Systems Inc
-
-Not licensed for re-use.
-*/
-
-package fix_visibility
-
-import (
-	"bytes"
-	"fmt"
-	"io"
-	"os"
-	"regexp"
-	"strings"
-
-	"github.com/bazelbuild/bazel-gazelle/label"
-	"github.com/bazelbuild/buildtools/edit"
-	"github.com/manifoldco/promptui"
-	isatty "github.com/mattn/go-isatty"
-
-	buildeventstream "aspect.build/cli/bazel/buildeventstream/proto"
-)
-
-type FixVisibilityPlugin struct {
-	stdout            io.Writer
-	buildozer         Runner
-	isInteractiveMode bool
-	applyFixPrompt    promptui.Prompt
-	targetsToFix      *fixOrderedSet
-}
-
-func NewDefaultPlugin() *FixVisibilityPlugin {
-	isInteractiveMode := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
-	applyFixPrompt := promptui.Prompt{
-		Label:     "Would you like to apply the visibility fixes",
-		IsConfirm: true,
-	}
-	return NewPlugin(os.Stdout, &buildozer{}, isInteractiveMode, applyFixPrompt)
-}
-
-func NewPlugin(
-	stdout io.Writer,
-	buildozer Runner,
-	isInteractiveMode bool,
-	applyFixPrompt promptui.Prompt,
-) *FixVisibilityPlugin {
-	return &FixVisibilityPlugin{
-		stdout:            stdout,
-		buildozer:         buildozer,
-		isInteractiveMode: isInteractiveMode,
-		targetsToFix:      &fixOrderedSet{nodes: make(map[fixNode]struct{})},
-		applyFixPrompt:    applyFixPrompt,
-	}
-}
-
-var visibilityIssueRegex = regexp.MustCompile(`.*target '(.*)' is not visible from target '(.*)'.*`)
-
-const visibilityIssueSubstring = "is not visible from target"
-
-func (plugin *FixVisibilityPlugin) BEPEventCallback(event *buildeventstream.BuildEvent) error {
-	aborted := event.GetAborted()
-	if aborted != nil &&
-		aborted.Reason == buildeventstream.Aborted_ANALYSIS_FAILURE &&
-		strings.Contains(aborted.Description, visibilityIssueSubstring) {
-		matches := visibilityIssueRegex.FindStringSubmatch(aborted.Description)
-		if len(matches) == 3 {
-			plugin.targetsToFix.insert(matches[1], matches[2])
-		}
-	}
-	return nil
-}
-
-const removePrivateVisibilityBuildozerCommand = "remove visibility //visibility:private"
-
-func (plugin *FixVisibilityPlugin) PostBuildHook() error {
-	if plugin.targetsToFix.size == 0 {
-		return nil
-	}
-
-	for node := plugin.targetsToFix.head; node != nil; node = node.next {
-		fromLabel, err := label.Parse(node.from)
-		if err != nil {
-			return fmt.Errorf("failed to fix visibility: %w", err)
-		}
-		fromLabel.Name = "__pkg__"
-
-		hasPrivateVisibility, err := plugin.hasPrivateVisibility(node.toFix)
-		if err != nil {
-			return fmt.Errorf("failed to fix visibility: %w", err)
-		}
-
-		var applyFix bool
-		if plugin.isInteractiveMode {
-			_, err := plugin.applyFixPrompt.Run()
-			applyFix = err == nil
-		}
-
-		addVisibilityBuildozerCommand := fmt.Sprintf("add visibility %s", fromLabel)
-		if applyFix {
-			if _, err := plugin.buildozer.Run(addVisibilityBuildozerCommand, node.toFix); err != nil {
-				return fmt.Errorf("failed to fix visibility: %w", err)
-			}
-			if hasPrivateVisibility {
-				if _, err := plugin.buildozer.Run(removePrivateVisibilityBuildozerCommand, node.toFix); err != nil {
-					return fmt.Errorf("failed to fix visibility: %w", err)
-				}
-			}
-		} else {
-			fmt.Fprintf(plugin.stdout, "To fix the visibility errors, run:\n")
-			fmt.Fprintf(plugin.stdout, "buildozer '%s' %s\n", addVisibilityBuildozerCommand, node.toFix)
-			if hasPrivateVisibility {
-				fmt.Fprintf(plugin.stdout, "buildozer '%s' %s\n", removePrivateVisibilityBuildozerCommand, node.toFix)
-			}
-		}
-	}
-
-	return nil
-}
-
-func (plugin *FixVisibilityPlugin) hasPrivateVisibility(toFix string) (bool, error) {
-	visibility, err := plugin.buildozer.Run("print visibility", toFix)
-	if err != nil {
-		return false, fmt.Errorf("failed to check if target has private visibility: %w", err)
-	}
-	return bytes.Contains(visibility, []byte("//visibility:private")), nil
-}
-
-type fixOrderedSet struct {
-	head  *fixNode
-	tail  *fixNode
-	nodes map[fixNode]struct{}
-	size  int
-}
-
-func (s *fixOrderedSet) insert(toFix, from string) {
-	node := fixNode{
-		toFix: toFix,
-		from:  from,
-	}
-	if _, exists := s.nodes[node]; !exists {
-		s.nodes[node] = struct{}{}
-		if s.head == nil {
-			s.head = &node
-		} else {
-			s.tail.next = &node
-		}
-		s.tail = &node
-		s.size++
-	}
-}
-
-type fixNode struct {
-	next  *fixNode
-	toFix string
-	from  string
-}
-
-type Runner interface {
-	Run(args ...string) ([]byte, error)
-}
-
-type buildozer struct{}
-
-func (b *buildozer) Run(args ...string) ([]byte, error) {
-	var stdout bytes.Buffer
-	var stderr strings.Builder
-	edit.ShortenLabelsFlag = true
-	edit.DeleteWithComments = true
-	opts := &edit.Options{
-		OutWriter: &stdout,
-		ErrWriter: &stderr,
-		NumIO:     200,
-	}
-	if ret := edit.Buildozer(opts, args); ret != 0 {
-		return stdout.Bytes(), fmt.Errorf("failed to run buildozer: exit code %d: %s", ret, stderr.String())
-	}
-	return stdout.Bytes(), nil
-}
diff --git a/plugins/fix_visibility/BUILD.bazel b/plugins/fix_visibility/BUILD.bazel
new file mode 100644
index 0000000..69b8313
--- /dev/null
+++ b/plugins/fix_visibility/BUILD.bazel
@@ -0,0 +1,27 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "fix_visibility_lib",
+    srcs = ["plugin.go"],
+    importpath = "aspect.build/cli/plugins/fix_visibility",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//pkg/ioutils",
+        "//pkg/plugin/sdk/v1alpha1/config",
+        "//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto",
+        "@bazel_gazelle//label:go_default_library",
+        "@com_github_bazelbuild_buildtools//edit:go_default_library",
+        "@com_github_hashicorp_go_plugin//:go-plugin",
+        "@com_github_manifoldco_promptui//:promptui",
+    ],
+)
+
+go_binary(
+    name = "fix_visibility",
+    embed = [":fix_visibility_lib"],
+    gc_linkopts = [
+        "-s",
+        "-w",
+    ],
+    visibility = ["//visibility:public"],
+)
diff --git a/plugins/fix_visibility/plugin.go b/plugins/fix_visibility/plugin.go
new file mode 100644
index 0000000..c7cf21d
--- /dev/null
+++ b/plugins/fix_visibility/plugin.go
@@ -0,0 +1,186 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"regexp"
+	"strings"
+
+	"github.com/bazelbuild/bazel-gazelle/label"
+	"github.com/bazelbuild/buildtools/edit"
+	goplugin "github.com/hashicorp/go-plugin"
+	"github.com/manifoldco/promptui"
+
+	buildeventstream "aspect.build/cli/bazel/buildeventstream/proto"
+	"aspect.build/cli/pkg/ioutils"
+	"aspect.build/cli/pkg/plugin/sdk/v1alpha1/config"
+)
+
+func main() {
+	goplugin.Serve(config.NewConfigFor(NewDefaultPlugin()))
+}
+
+// FixVisibilityPlugin implements an aspect CLI plugin.
+type FixVisibilityPlugin struct {
+	buildozer    runner
+	targetsToFix *fixOrderedSet
+}
+
+// NewDefaultPlugin creates a new FixVisibilityPlugin with the default
+// dependencies.
+func NewDefaultPlugin() *FixVisibilityPlugin {
+	return NewPlugin(&buildozer{})
+}
+
+// NewPlugin creates a new FixVisibilityPlugin.
+func NewPlugin(buildozer runner) *FixVisibilityPlugin {
+	return &FixVisibilityPlugin{
+		buildozer:    buildozer,
+		targetsToFix: &fixOrderedSet{nodes: make(map[fixNode]struct{})},
+	}
+}
+
+const visibilityIssueSubstring = "is not visible from target"
+
+var visibilityIssueRegex = regexp.MustCompile(fmt.Sprintf(`.*target '(.*)' %s '(.*)'.*`, visibilityIssueSubstring))
+
+// BEPEventCallback satisfies the Plugin interface. It process all the analysis
+// failures that represent a visibility issue, collecting them for later
+// processing in the post-build hook execution.
+func (plugin *FixVisibilityPlugin) BEPEventCallback(event *buildeventstream.BuildEvent) error {
+	aborted := event.GetAborted()
+	if aborted != nil &&
+		aborted.Reason == buildeventstream.Aborted_ANALYSIS_FAILURE &&
+		strings.Contains(aborted.Description, visibilityIssueSubstring) {
+		matches := visibilityIssueRegex.FindStringSubmatch(aborted.Description)
+		if len(matches) == 3 {
+			plugin.targetsToFix.insert(matches[1], matches[2])
+		}
+	}
+	return nil
+}
+
+const removePrivateVisibilityBuildozerCommand = "remove visibility //visibility:private"
+
+// PostBuildHook satisfies the Plugin interface. It prompts the user for
+// automatic fixes when in interactive mode. If the user rejects the automatic
+// fixes, or if running in non-interactive mode, the commands to perform the fixes
+// are printed to the terminal.
+func (plugin *FixVisibilityPlugin) PostBuildHook(
+	isInteractiveMode bool,
+	promptRunner ioutils.PromptRunner,
+) error {
+	if plugin.targetsToFix.size == 0 {
+		return nil
+	}
+
+	for node := plugin.targetsToFix.head; node != nil; node = node.next {
+		fromLabel, err := label.Parse(node.from)
+		if err != nil {
+			return fmt.Errorf("failed to fix visibility: %w", err)
+		}
+		fromLabel.Name = "__pkg__"
+
+		hasPrivateVisibility, err := plugin.hasPrivateVisibility(node.toFix)
+		if err != nil {
+			return fmt.Errorf("failed to fix visibility: %w", err)
+		}
+
+		var applyFix bool
+		if isInteractiveMode {
+			applyFixPrompt := promptui.Prompt{
+				Label:     "Would you like to apply the visibility fixes",
+				IsConfirm: true,
+			}
+			_, err := promptRunner.Run(applyFixPrompt)
+			applyFix = err == nil
+		}
+
+		addVisibilityBuildozerCommand := fmt.Sprintf("add visibility %s", fromLabel)
+		if applyFix {
+			if _, err := plugin.buildozer.run(addVisibilityBuildozerCommand, node.toFix); err != nil {
+				return fmt.Errorf("failed to fix visibility: %w", err)
+			}
+			if hasPrivateVisibility {
+				if _, err := plugin.buildozer.run(removePrivateVisibilityBuildozerCommand, node.toFix); err != nil {
+					return fmt.Errorf("failed to fix visibility: %w", err)
+				}
+			}
+		} else {
+			fmt.Fprintf(os.Stdout, "To fix the visibility errors, run:\n")
+			fmt.Fprintf(os.Stdout, "buildozer '%s' %s\n", addVisibilityBuildozerCommand, node.toFix)
+			if hasPrivateVisibility {
+				fmt.Fprintf(os.Stdout, "buildozer '%s' %s\n", removePrivateVisibilityBuildozerCommand, node.toFix)
+			}
+		}
+	}
+
+	return nil
+}
+
+func (plugin *FixVisibilityPlugin) hasPrivateVisibility(toFix string) (bool, error) {
+	visibility, err := plugin.buildozer.run("print visibility", toFix)
+	if err != nil {
+		return false, fmt.Errorf("failed to check if target has private visibility: %w", err)
+	}
+	return bytes.Contains(visibility, []byte("//visibility:private")), nil
+}
+
+type fixOrderedSet struct {
+	head  *fixNode
+	tail  *fixNode
+	nodes map[fixNode]struct{}
+	size  int
+}
+
+func (s *fixOrderedSet) insert(toFix, from string) {
+	node := fixNode{
+		toFix: toFix,
+		from:  from,
+	}
+	if _, exists := s.nodes[node]; !exists {
+		s.nodes[node] = struct{}{}
+		if s.head == nil {
+			s.head = &node
+		} else {
+			s.tail.next = &node
+		}
+		s.tail = &node
+		s.size++
+	}
+}
+
+type fixNode struct {
+	next  *fixNode
+	toFix string
+	from  string
+}
+
+type runner interface {
+	run(args ...string) ([]byte, error)
+}
+
+type buildozer struct{}
+
+func (b *buildozer) run(args ...string) ([]byte, error) {
+	var stdout bytes.Buffer
+	var stderr strings.Builder
+	edit.ShortenLabelsFlag = true
+	edit.DeleteWithComments = true
+	opts := &edit.Options{
+		OutWriter: &stdout,
+		ErrWriter: &stderr,
+		NumIO:     200,
+	}
+	if ret := edit.Buildozer(opts, args); ret != 0 {
+		return stdout.Bytes(), fmt.Errorf("failed to run buildozer: exit code %d: %s", ret, stderr.String())
+	}
+	return stdout.Bytes(), nil
+}