Buf plugin quickstart#
Buf's lint and breaking change checks come with pre-defined rules and categories that cover the vast majority of customer needs. However, organizations sometimes need to enforce different or additional rules. For these cases, you can create Buf plugins that work with the Buf checkers so you can integrate your own rules and categories into your workflows.
This quickstart demonstrates how to define your own rules in a Buf plugin and how to install and use it locally.
Prerequisites#
- Install the Buf CLI or update your existing version to >=1.40
-
Clone the
buf-examples
repo and go to this quickstart's directory:
This quickstart shows how to write a Buf plugin in Go, taking advantage of the bufplugin-go
library, but you can write them in any language as long as the plugin conforms to the Bufplugin framework.
Inspect the workspace#
The sample module contains a buf.yaml
and a pet.proto
with definitions related to a pet store:
For the sake of this quickstart, pet.proto
includes an undesirable naming style:
service PetStoreService {
rpc GetPetMethod(GetPetRequest) returns (GetPetResponse) {}
}
Notice that GetPetMethod
is an RPC method but shouldn't end with the word Method
, so you'll write a lint plugin that checks for this style error.
Write a simple plugin#
Run the following to scaffold the plugin:
Copy and paste the following content into cmd/rpc-suffix/main.go
:
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"buf.build/go/bufplugin/check"
"buf.build/go/bufplugin/check/checkutil"
"google.golang.org/protobuf/reflect/protoreflect"
)
var (
rpcSuffixRuleSpec = &check.RuleSpec{
ID: "RPC_SUFFIX",
Default: true,
Purpose: "Checks that RPC names don't end with an illegal suffix.",
Type: check.RuleTypeLint,
Handler: checkutil.NewMethodRuleHandler(checkRPCSuffix, checkutil.WithoutImports()),
}
)
func main() {
check.Main(&check.Spec{
Rules: []*check.RuleSpec{
rpcSuffixRuleSpec,
},
})
}
func checkRPCSuffix(
_ context.Context,
responseWriter check.ResponseWriter,
_ check.Request,
methodDescriptor protoreflect.MethodDescriptor,
) error {
responseWriter.AddAnnotation(
check.WithMessage("hello world"),
)
return nil
}
The plugin's main.go
file imports the bufplugin-go
SDK and has three components:
- An
rpcSuffixRuleSpec
definition, which defines the lint rule with fields like its ID and purpose. - A
main
function that callscheck.Main
, which creates a fully functional plugin function using the lint rule you just defined. - A
checkRPCSuffix
handler function that will contain our linting logic but for now just returns "hello world" regardless of the Protobuf file's content.
To see the plugin in action, first install its binary:
Then add the plugin and its rule ID to the buf.yaml
config file:
version: v2
modules:
- path: proto
name: buf.build/tutorials/lint
lint:
use:
- MINIMAL
+ - RPC_SUFFIX
+plugins:
+ - plugin: rpc-suffix
You can now verify that the new rule is being checked when you lint:
With the dummy plugin working, you now need to add the rule logic:
package main
import (
"context"
+ "strings"
+
"buf.build/go/bufplugin/check"
"buf.build/go/bufplugin/check/checkutil"
"google.golang.org/protobuf/reflect/protoreflect"
)
+const forbiddenRPCSuffix = "Method"
+
var (
rpcSuffixRuleSpec = &check.RuleSpec{
ID: "RPC_SUFFIX",
Default: true,
Purpose: "Checks that RPC names don't end with an illegal suffix.",
Type: check.RuleTypeLint,
Handler: checkutil.NewMethodRuleHandler(checkRPCSuffix, checkutil.WithoutImports()),
}
)
func main() {
check.Main(&check.Spec{
Rules: []*check.RuleSpec{
rpcSuffixRuleSpec,
},
})
}
func checkRPCSuffix(
_ context.Context,
responseWriter check.ResponseWriter,
_ check.Request,
methodDescriptor protoreflect.MethodDescriptor,
) error {
- responseWriter.AddAnnotation(
- check.WithMessage("hello world"),
- )
+ methodName := string(methodDescriptor.Name())
+ if strings.HasSuffix(methodName, forbiddenRPCSuffix) {
+ responseWriter.AddAnnotation(
+ check.WithDescriptor(methodDescriptor),
+ check.WithMessagef("method name should not end with %q", forbiddenRPCSuffix),
+ )
+ }
return nil
}
Now the Buf linter only prints error when a method ends with the keyword Method
.
In addition, the plugin passes the check.WithDescriptor(methodDescriptor)
option for printing, which provides location information about where the error
happened.
To verify this, re-install and run the plugin:
$ go install ./cmd/rpc-suffix
$ buf lint
proto/pet/v1/pet.proto:44:3:method name should not end with "Method" (rpc-suffix)
Now that you've implemented the rule logic, let's look at how to make a rule configurable.
Add options to a rule#
Instead of hard-coding the check against the Method
suffix, suppose you want the user to be able to set which suffixes to check for.
You can enable this by making the plugin configurable from the buf.yaml
config file.
From the user's perspective, it looks like this:
version: v2
modules:
- path: proto
name: buf.build/tutorials/lint
lint:
use:
- STANDARD
- RPC_SUFFIX
plugins:
- plugin: rpc-suffix
+ options:
+ forbidden_rpc_suffixes:
+ - Method
+ - RPC
Plugin options are key-value pairs, so this configuration passes the forbidden_rpc_suffixes
key and its values ["Method", "RPC"]
to the plugin.
To enable the plugin to interpret the option, you remove the hard-coded value and add the key in its place, then check to see if the user has configured it:
package main
import (
"context"
"strings"
"buf.build/go/bufplugin/check"
"buf.build/go/bufplugin/check/checkutil"
+ "buf.build/go/bufplugin/option"
"google.golang.org/protobuf/reflect/protoreflect"
)
-const forbiddenRPCSuffix = "Method"
+const (
+ defaultForbiddenRPCSuffix = "Method"
+ forbiddenRPCSuffixesOptionKey = "forbidden_rpc_suffixes"
+)
var (
rpcSuffixRuleSpec = &check.RuleSpec{
ID: "RPC_SUFFIX",
Default: true,
Purpose: "Checks that RPC names don't end with an illegal suffix.",
Type: check.RuleTypeLint,
Handler: checkutil.NewMethodRuleHandler(checkRPCSuffix, checkutil.WithoutImports()),
}
)
func main() {
check.Main(&check.Spec{
Rules: []*check.RuleSpec{
rpcSuffixRuleSpec,
},
})
}
func checkRPCSuffix(
_ context.Context,
responseWriter check.ResponseWriter,
- _ check.Request,
+ request check.Request,
methodDescriptor protoreflect.MethodDescriptor,
) error {
methodName := string(methodDescriptor.Name())
- if strings.HasSuffix(methodName, forbiddenRPCSuffix) {
- responseWriter.AddAnnotation(
- check.WithDescriptor(methodDescriptor),
- check.WithMessagef("method name should not end with %q", forbiddenRPCSuffix),
- )
+ forbiddenRPCSuffixes, err := option.GetStringSliceValue(request.Options(), forbiddenRPCSuffixesOptionKey)
+ if err != nil {
+ return err
+ }
+ if len(forbiddenRPCSuffixes) == 0 {
+ forbiddenRPCSuffixes = append(forbiddenRPCSuffixes, defaultForbiddenRPCSuffix)
+ }
+ for _, forbiddenRPCSuffix := range forbiddenRPCSuffixes {
+ if strings.HasSuffix(methodName, forbiddenRPCSuffix) {
+ responseWriter.AddAnnotation(
+ check.WithDescriptor(methodDescriptor),
+ check.WithMessagef("method name should not end with %q", forbiddenRPCSuffix),
+ )
+ }
}
return nil
}
Now the plugin uses option.GetStringSliceValue(request.Options(), forbiddenRPCSuffixesOptionKey)
to check whether the option is specified by the user, and uses its list of values if so.
To verify that it uses the option values, change pet.proto
so that it violates the additional rule:
// Code omitted for brevity
service PetStoreService {
- rpc GetPetMethod(GetPetRequest) returns (GetPetResponse) {}
+ rpc GetPetRPC(GetPetRequest) returns (GetPetResponse) {}
}
Then re-install and run the plugin:
$ go install ./cmd/rpc-suffix
$ buf lint
proto/pet/v1/pet.proto:44:3:method name should not end with "RPC" (rpc-suffix)
Add a breaking change rule to the same plugin#
Buf plugins can support breaking change rules in addition to lint rules. Adding a breaking change rule to your plugin is almost identical to adding a lint rule, but there are three key differences:
- A breaking change checking function requires an additional descriptor prefixed with
against
because it compares the input against another set of files. - Checking functions for breaking change rules should be passed to
checkutil.NewFieldPairRuleHandler
handler helpers instead ofcheckutil.NewMethodRuleHandler
. - The checking function's
Type
should be set to type shouldcheck.RuleTypeBreaking
.
For example, this rule checks that a field option stays set to True
in both versions of a file:
fieldOptionSafeForMLStaysTrueRuleSpec = &check.RuleSpec{
...
Type: check.RuleTypeBreaking,
Handler: checkutil.NewFieldPairRuleHandler(checkFieldOptionSafeForMLStaysTrue, checkutil.WithoutImports()),
}
func checkFieldOptionSafeForMLStaysTrue(
_ context.Context,
responseWriter check.ResponseWriter,
_ check.Request,
fieldDescriptor protoreflect.FieldDescriptor,
againstFieldDescriptor protoreflect.FieldDescriptor,
) error {
Default
property#
Each rule can set a Default
boolean property that controls whether the rule is called by default if no rules are specified in buf.yaml
.
For example, adding it to the preceding example ensures that the field option is always checked when the plugin is present:
fieldOptionSafeForMLStaysTrueRuleSpec = &check.RuleSpec{
Default: True,
Type: check.RuleTypeBreaking,
Handler: checkutil.NewFieldPairRuleHandler(checkFieldOptionSafeForMLStaysTrue, checkutil.WithoutImports()),
}
Now that you've built your first Buf plugin, learn how to compile it to WebAssembly so you can upload it to the BSR.