Repository: uber/prototool
Pull request #295 (2019-01-12 20:19:02)

Add additional linters for V2 Style Guide and file header handling
Re: #265.

This adds three normal linters:

- Checks that request/response types are defined after the service is defined, and that a request type is defined before a response type for a given RPC.
- Checks that there are no inline comments, only comments above types.
- Checks that the method option `google.api.http` is not used. This indicates usage of grpc-gateway, which we want to outlaw by default. This can be suppressed with `// @suppresswarnings google-api-http`.

This also adds file header handling, typically for license declarations. A file header specified in a `prototool.yaml`:

```yaml
lint:
  file_header:
    path: path/to/header.txt
    is_commented: true
```

The `path` option specifies the path to the file that contains the header data. The `is_commented` option specifies whether the header data is already commented, and if not, `// ` will be added before all non-empty lines, `//` added before all empty lines.

This PR:

- Updates `prototool config init` to reflect this new option.
- Updates `prototool create` to take file header into account if present.
- Updates `prototool format --fix` to add the file header if present.
- Updates `prototool lint` to lint for the file header if present.

This PR also adds a check to make sure lint IDs referenced in the `prototool.yaml` are actually known lint IDs.


Patch:
diff --git a/Makefile b/Makefile
index 7b12af7..ac9c38b 100644
--- a/Makefile
+++ b/Makefile
@@ -59,7 +59,7 @@ example: install
 	@go install github.com/golang/protobuf/protoc-gen-go
 	@mkdir -p $(TMP_ETC)
 	rm -rf example/gen
-	prototool all example/proto/uber
+	prototool all --fix example/proto/uber
 	go build ./example/gen/go/uber/foo/v1
 	go build ./example/gen/go/uber/bar/v1
 	go build -o $(TMP_ETC)/excited ./example/cmd/excited/main.go
@@ -71,7 +71,7 @@ internalgen: install
 	rm -rf internal/cmd/testdata/grpc/gen
 	prototool generate internal/cmd/testdata/grpc
 	rm -rf internal/reflect/gen
-	prototool all internal/reflect/proto
+	prototool all --fix internal/reflect/proto
 	rm -f etc/config/example/prototool.yaml
 	prototool config init etc/config/example --uncomment
 
diff --git a/README.md b/README.md
index a802de6..568f97a 100644
--- a/README.md
+++ b/README.md
@@ -200,10 +200,11 @@ commonly used, but still have validity within the lint group construct. These ru
 to the element comment. The following lint rules can be suppressed:
 
 
-| Lint Rule                   | Containing Lint Groups   | Suppressing Annotation   |
-|-----------------------------|--------------------------|--------------------------|
-| `MESSAGE_FIELDS_NOT_FLOATS` | `uber2`                  | `floats`                 |
-| `PACKAGE_NO_KEYWORDS`       | `uber2`                  | `keywords`               |
+| Lint Rule                        | Containing Lint Groups   | Suppressing Annotation   |
+|----------------------------------|--------------------------|--------------------------|
+| `MESSAGE_FIELDS_NOT_FLOATS`      | `uber2`                  | `floats`                 |
+| `PACKAGE_NO_KEYWORDS`            | `uber2`                  | `keywords`               |
+| `RPC_OPTIONS_NO_GOOGLE_API_HTTP` | `uber2`                  | `google-api-http`        |
 
 As an example:
 
@@ -213,12 +214,37 @@ As an example:
 // @suppresswarnings keywords
 package foo.public.bar;
 
-// Hello is a field where we understand the concerns with using doubles but require them.
+// Hello is a field where we understand the concerns with using floats but require them.
 //
 // @suppresswarnings floats
-double hello = 1;
+float hello = 1;
+
+service FooAPI {
+  // We understand the concerns with using the http option so we allow it here.
+  // @suppresswarnings google-api-http
+  rpc Bar(BarRequest) returns (BarResponse) {
+    option (google.api.http).get = "/bar";
+  }
+}
+```
+
+Linting also understands the concept of file headers, typically license headers. To specify a license header, add the following to your
+`prototool.yaml`:
+
+```yaml
+lint:
+  file_header:
+    path: path/to/header.txt
+    is_commented: true
 ```
 
+The `path` option specifies the path to the file that contains the header data.
+The `is_commented` option specifies whether the header data is already commented, and if not, `// ` will be added before all non-empty lines,
+and `//` will be added before all empty lines. `is_commented` is optional and generally will not be set if the file is not commented, for
+example if `path` points to a text LICENSE file.
+
+If `lint.file_header.path` is set, `prototool create`, `prototool format --fix`, and `prototool lint` will all take the file header into account.
+
 See [internal/cmd/testdata/lint](internal/cmd/testdata/lint) for additional examples of configurations, and run `prototool lint internal/cmd/testdata/lint/DIR` from a checkout of this repository to see example failures.
 
 Files must be valid Protobuf that can be compiled with `protoc`, so prior to linting, `prototool lint` will compile your using `protoc`.
diff --git a/etc/config/example/prototool.yaml b/etc/config/example/prototool.yaml
index 2270675..7e381cc 100644
--- a/etc/config/example/prototool.yaml
+++ b/etc/config/example/prototool.yaml
@@ -71,6 +71,20 @@ lint:
     remove:
       - ENUM_NAMES_CAMEL_CASE
 
+  # The path to the file header for all Protobuf files.
+  # If this is set and the FILE_HEADER linter is turned on, files will
+  # be checked to begin with the contents of this file, and format --fix
+  # will place this header before the syntax declaration. Note that
+  # format --fix will delete anything before the syntax declaration
+  # if this is set.
+  #
+  # If is_commented is set, this file is assumed to already have comments
+  # and will be added directly. If is_commented is not set, "// " will be
+  # added before every line.
+  file_header:
+    path: path/to/protobuf_file_header.txt
+    is_commented: true
+
 # Code generation directives.
 generate:
   # Options that will apply to all plugins of type go and gogo.
diff --git a/example/gen/go/uber/bar/v1/bar.pb.go b/example/gen/go/uber/bar/v1/bar.pb.go
index c2ff3b4..b029f9e 100644
--- a/example/gen/go/uber/bar/v1/bar.pb.go
+++ b/example/gen/go/uber/bar/v1/bar.pb.go
@@ -29,7 +29,7 @@ func (m *Hello) Reset()         { *m = Hello{} }
 func (m *Hello) String() string { return proto.CompactTextString(m) }
 func (*Hello) ProtoMessage()    {}
 func (*Hello) Descriptor() ([]byte, []int) {
-	return fileDescriptor_bar_f78368c124c4d9fc, []int{0}
+	return fileDescriptor_bar_ab45b574cbbc9a08, []int{0}
 }
 func (m *Hello) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_Hello.Unmarshal(m, b)
@@ -60,9 +60,9 @@ func init() {
 	proto.RegisterType((*Hello)(nil), "uber.bar.v1.Hello")
 }
 
-func init() { proto.RegisterFile("uber/bar/v1/bar.proto", fileDescriptor_bar_f78368c124c4d9fc) }
+func init() { proto.RegisterFile("uber/bar/v1/bar.proto", fileDescriptor_bar_ab45b574cbbc9a08) }
 
-var fileDescriptor_bar_f78368c124c4d9fc = []byte{
+var fileDescriptor_bar_ab45b574cbbc9a08 = []byte{
 	// 138 bytes of a gzipped FileDescriptorProto
 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x2d, 0x4d, 0x4a, 0x2d,
 	0xd2, 0x4f, 0x4a, 0x2c, 0xd2, 0x2f, 0x33, 0x04, 0x51, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42,
diff --git a/example/gen/go/uber/foo/v1/excited_api.pb.go b/example/gen/go/uber/foo/v1/excited_api.pb.go
index 78a1b00..d7c4db3 100644
--- a/example/gen/go/uber/foo/v1/excited_api.pb.go
+++ b/example/gen/go/uber/foo/v1/excited_api.pb.go
@@ -34,7 +34,7 @@ func (m *ExclamationRequest) Reset()         { *m = ExclamationRequest{} }
 func (m *ExclamationRequest) String() string { return proto.CompactTextString(m) }
 func (*ExclamationRequest) ProtoMessage()    {}
 func (*ExclamationRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_excited_api_8e3a03afe7e468e9, []int{0}
+	return fileDescriptor_excited_api_33d65e5c5e15bc95, []int{0}
 }
 func (m *ExclamationRequest) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_ExclamationRequest.Unmarshal(m, b)
@@ -72,7 +72,7 @@ func (m *ExclamationResponse) Reset()         { *m = ExclamationResponse{} }
 func (m *ExclamationResponse) String() string { return proto.CompactTextString(m) }
 func (*ExclamationResponse) ProtoMessage()    {}
 func (*ExclamationResponse) Descriptor() ([]byte, []int) {
-	return fileDescriptor_excited_api_8e3a03afe7e468e9, []int{1}
+	return fileDescriptor_excited_api_33d65e5c5e15bc95, []int{1}
 }
 func (m *ExclamationResponse) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_ExclamationResponse.Unmarshal(m, b)
@@ -380,10 +380,10 @@ var _ExcitedAPI_serviceDesc = grpc.ServiceDesc{
 }
 
 func init() {
-	proto.RegisterFile("uber/foo/v1/excited_api.proto", fileDescriptor_excited_api_8e3a03afe7e468e9)
+	proto.RegisterFile("uber/foo/v1/excited_api.proto", fileDescriptor_excited_api_33d65e5c5e15bc95)
 }
 
-var fileDescriptor_excited_api_8e3a03afe7e468e9 = []byte{
+var fileDescriptor_excited_api_33d65e5c5e15bc95 = []byte{
 	// 262 bytes of a gzipped FileDescriptorProto
 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x2d, 0x4d, 0x4a, 0x2d,
 	0xd2, 0x4f, 0xcb, 0xcf, 0xd7, 0x2f, 0x33, 0xd4, 0x4f, 0xad, 0x48, 0xce, 0x2c, 0x49, 0x4d, 0x89,
diff --git a/example/gen/go/uber/foo/v1/foo.pb.go b/example/gen/go/uber/foo/v1/foo.pb.go
index 680e6dc..a6f8d61 100644
--- a/example/gen/go/uber/foo/v1/foo.pb.go
+++ b/example/gen/go/uber/foo/v1/foo.pb.go
@@ -48,7 +48,7 @@ func (x Hello) String() string {
 	return proto.EnumName(Hello_name, int32(x))
 }
 func (Hello) EnumDescriptor() ([]byte, []int) {
-	return fileDescriptor_foo_e3b8a0f6d36d9093, []int{0}
+	return fileDescriptor_foo_18696cee75944a2c, []int{0}
 }
 
 // Bar is a bar.
@@ -69,7 +69,7 @@ func (x Bar) String() string {
 	return proto.EnumName(Bar_name, int32(x))
 }
 func (Bar) EnumDescriptor() ([]byte, []int) {
-	return fileDescriptor_foo_e3b8a0f6d36d9093, []int{1}
+	return fileDescriptor_foo_18696cee75944a2c, []int{1}
 }
 
 // Baz is a baz.
@@ -90,7 +90,7 @@ func (x Foo_Bar_Baz) String() string {
 	return proto.EnumName(Foo_Bar_Baz_name, int32(x))
 }
 func (Foo_Bar_Baz) EnumDescriptor() ([]byte, []int) {
-	return fileDescriptor_foo_e3b8a0f6d36d9093, []int{0, 0, 0}
+	return fileDescriptor_foo_18696cee75944a2c, []int{0, 0, 0}
 }
 
 // Bat is a bat.
@@ -111,7 +111,7 @@ func (x Foo_Bar_Bat) String() string {
 	return proto.EnumName(Foo_Bar_Bat_name, int32(x))
 }
 func (Foo_Bar_Bat) EnumDescriptor() ([]byte, []int) {
-	return fileDescriptor_foo_e3b8a0f6d36d9093, []int{0, 0, 1}
+	return fileDescriptor_foo_18696cee75944a2c, []int{0, 0, 1}
 }
 
 // Foo is a foo.
@@ -126,7 +126,7 @@ func (m *Foo) Reset()         { *m = Foo{} }
 func (m *Foo) String() string { return proto.CompactTextString(m) }
 func (*Foo) ProtoMessage()    {}
 func (*Foo) Descriptor() ([]byte, []int) {
-	return fileDescriptor_foo_e3b8a0f6d36d9093, []int{0}
+	return fileDescriptor_foo_18696cee75944a2c, []int{0}
 }
 func (m *Foo) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_Foo.Unmarshal(m, b)
@@ -166,7 +166,7 @@ func (m *Foo_Bar) Reset()         { *m = Foo_Bar{} }
 func (m *Foo_Bar) String() string { return proto.CompactTextString(m) }
 func (*Foo_Bar) ProtoMessage()    {}
 func (*Foo_Bar) Descriptor() ([]byte, []int) {
-	return fileDescriptor_foo_e3b8a0f6d36d9093, []int{0, 0}
+	return fileDescriptor_foo_18696cee75944a2c, []int{0, 0}
 }
 func (m *Foo_Bar) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_Foo_Bar.Unmarshal(m, b)
@@ -214,7 +214,7 @@ func (m *Barr) Reset()         { *m = Barr{} }
 func (m *Barr) String() string { return proto.CompactTextString(m) }
 func (*Barr) ProtoMessage()    {}
 func (*Barr) Descriptor() ([]byte, []int) {
-	return fileDescriptor_foo_e3b8a0f6d36d9093, []int{1}
+	return fileDescriptor_foo_18696cee75944a2c, []int{1}
 }
 func (m *Barr) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_Barr.Unmarshal(m, b)
@@ -278,7 +278,7 @@ func (m *Another) Reset()         { *m = Another{} }
 func (m *Another) String() string { return proto.CompactTextString(m) }
 func (*Another) ProtoMessage()    {}
 func (*Another) Descriptor() ([]byte, []int) {
-	return fileDescriptor_foo_e3b8a0f6d36d9093, []int{2}
+	return fileDescriptor_foo_18696cee75944a2c, []int{2}
 }
 func (m *Another) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_Another.Unmarshal(m, b)
@@ -468,7 +468,7 @@ func (m *HasWKT) Reset()         { *m = HasWKT{} }
 func (m *HasWKT) String() string { return proto.CompactTextString(m) }
 func (*HasWKT) ProtoMessage()    {}
 func (*HasWKT) Descriptor() ([]byte, []int) {
-	return fileDescriptor_foo_e3b8a0f6d36d9093, []int{3}
+	return fileDescriptor_foo_18696cee75944a2c, []int{3}
 }
 func (m *HasWKT) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_HasWKT.Unmarshal(m, b)
@@ -506,7 +506,7 @@ func (m *Bazz) Reset()         { *m = Bazz{} }
 func (m *Bazz) String() string { return proto.CompactTextString(m) }
 func (*Bazz) ProtoMessage()    {}
 func (*Bazz) Descriptor() ([]byte, []int) {
-	return fileDescriptor_foo_e3b8a0f6d36d9093, []int{4}
+	return fileDescriptor_foo_18696cee75944a2c, []int{4}
 }
 func (m *Bazz) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_Bazz.Unmarshal(m, b)
@@ -540,9 +540,9 @@ func init() {
 	proto.RegisterEnum("uber.foo.v1.Foo_Bar_Bat", Foo_Bar_Bat_name, Foo_Bar_Bat_value)
 }
 
-func init() { proto.RegisterFile("uber/foo/v1/foo.proto", fileDescriptor_foo_e3b8a0f6d36d9093) }
+func init() { proto.RegisterFile("uber/foo/v1/foo.proto", fileDescriptor_foo_18696cee75944a2c) }
 
-var fileDescriptor_foo_e3b8a0f6d36d9093 = []byte{
+var fileDescriptor_foo_18696cee75944a2c = []byte{
 	// 625 bytes of a gzipped FileDescriptorProto
 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x53, 0x51, 0x6f, 0x12, 0x41,
 	0x10, 0xee, 0x71, 0x40, 0xcb, 0x9c, 0x6d, 0x71, 0x53, 0x9b, 0x13, 0x13, 0x6d, 0x78, 0x30, 0xd8,
diff --git a/example/gen/go/uber/foo/v1/hello_api.pb.go b/example/gen/go/uber/foo/v1/hello_api.pb.go
index 17694af..fe737ae 100644
--- a/example/gen/go/uber/foo/v1/hello_api.pb.go
+++ b/example/gen/go/uber/foo/v1/hello_api.pb.go
@@ -33,7 +33,7 @@ func (m *FooRequest) Reset()         { *m = FooRequest{} }
 func (m *FooRequest) String() string { return proto.CompactTextString(m) }
 func (*FooRequest) ProtoMessage()    {}
 func (*FooRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_hello_api_450e0bab4f010f19, []int{0}
+	return fileDescriptor_hello_api_d217dc40b042ecd5, []int{0}
 }
 func (m *FooRequest) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_FooRequest.Unmarshal(m, b)
@@ -63,7 +63,7 @@ func (m *FooResponse) Reset()         { *m = FooResponse{} }
 func (m *FooResponse) String() string { return proto.CompactTextString(m) }
 func (*FooResponse) ProtoMessage()    {}
 func (*FooResponse) Descriptor() ([]byte, []int) {
-	return fileDescriptor_hello_api_450e0bab4f010f19, []int{1}
+	return fileDescriptor_hello_api_d217dc40b042ecd5, []int{1}
 }
 func (m *FooResponse) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_FooResponse.Unmarshal(m, b)
@@ -94,7 +94,7 @@ func (m *BarRequest) Reset()         { *m = BarRequest{} }
 func (m *BarRequest) String() string { return proto.CompactTextString(m) }
 func (*BarRequest) ProtoMessage()    {}
 func (*BarRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_hello_api_450e0bab4f010f19, []int{2}
+	return fileDescriptor_hello_api_d217dc40b042ecd5, []int{2}
 }
 func (m *BarRequest) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_BarRequest.Unmarshal(m, b)
@@ -131,7 +131,7 @@ func (m *BarResponse) Reset()         { *m = BarResponse{} }
 func (m *BarResponse) String() string { return proto.CompactTextString(m) }
 func (*BarResponse) ProtoMessage()    {}
 func (*BarResponse) Descriptor() ([]byte, []int) {
-	return fileDescriptor_hello_api_450e0bab4f010f19, []int{3}
+	return fileDescriptor_hello_api_d217dc40b042ecd5, []int{3}
 }
 func (m *BarResponse) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_BarResponse.Unmarshal(m, b)
@@ -268,10 +268,10 @@ var _HelloAPI_serviceDesc = grpc.ServiceDesc{
 }
 
 func init() {
-	proto.RegisterFile("uber/foo/v1/hello_api.proto", fileDescriptor_hello_api_450e0bab4f010f19)
+	proto.RegisterFile("uber/foo/v1/hello_api.proto", fileDescriptor_hello_api_d217dc40b042ecd5)
 }
 
-var fileDescriptor_hello_api_450e0bab4f010f19 = []byte{
+var fileDescriptor_hello_api_d217dc40b042ecd5 = []byte{
 	// 227 bytes of a gzipped FileDescriptorProto
 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x2e, 0x4d, 0x4a, 0x2d,
 	0xd2, 0x4f, 0xcb, 0xcf, 0xd7, 0x2f, 0x33, 0xd4, 0xcf, 0x48, 0xcd, 0xc9, 0xc9, 0x8f, 0x4f, 0x2c,
diff --git a/example/proto/prototool.yaml b/example/proto/prototool.yaml
index dea330c..2267410 100644
--- a/example/proto/prototool.yaml
+++ b/example/proto/prototool.yaml
@@ -6,6 +6,8 @@ lint:
     remove:
       - REQUEST_RESPONSE_NAMES_MATCH_RPC
       - REQUEST_RESPONSE_TYPES_UNIQUE
+  file_header:
+    path: ../../LICENSE
 generate:
   go_options:
     import_path: github.com/uber/prototool/example/proto
diff --git a/example/proto/uber/bar/v1/bar.proto b/example/proto/uber/bar/v1/bar.proto
index 945b104..256f544 100644
--- a/example/proto/uber/bar/v1/bar.proto
+++ b/example/proto/uber/bar/v1/bar.proto
@@ -1,3 +1,23 @@
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
 syntax = "proto3";
 
 package uber.bar.v1;
diff --git a/example/proto/uber/foo/v1/excited_api.proto b/example/proto/uber/foo/v1/excited_api.proto
index 5a6f635..cb6a673 100644
--- a/example/proto/uber/foo/v1/excited_api.proto
+++ b/example/proto/uber/foo/v1/excited_api.proto
@@ -1,3 +1,23 @@
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
 syntax = "proto3";
 
 package uber.foo.v1;
diff --git a/example/proto/uber/foo/v1/foo.proto b/example/proto/uber/foo/v1/foo.proto
index 8880af7..142d56d 100644
--- a/example/proto/uber/foo/v1/foo.proto
+++ b/example/proto/uber/foo/v1/foo.proto
@@ -1,3 +1,23 @@
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
 syntax = "proto3";
 
 package uber.foo.v1;
diff --git a/example/proto/uber/foo/v1/hello_api.proto b/example/proto/uber/foo/v1/hello_api.proto
index ff37e4b..70c4e7c 100644
--- a/example/proto/uber/foo/v1/hello_api.proto
+++ b/example/proto/uber/foo/v1/hello_api.proto
@@ -1,3 +1,23 @@
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
 syntax = "proto3";
 
 package uber.foo.v1;
diff --git a/internal/cfginit/cfginit.go b/internal/cfginit/cfginit.go
index ddf85bb..08765fc 100644
--- a/internal/cfginit/cfginit.go
+++ b/internal/cfginit/cfginit.go
@@ -101,6 +101,20 @@ protoc:
 {{.V}}    remove:
 {{.V}}      - ENUM_NAMES_CAMEL_CASE
 
+  # The path to the file header for all Protobuf files.
+  # If this is set and the FILE_HEADER linter is turned on, files will
+  # be checked to begin with the contents of this file, and format --fix
+  # will place this header before the syntax declaration. Note that
+  # format --fix will delete anything before the syntax declaration
+  # if this is set.
+  #
+  # If is_commented is set, this file is assumed to already have comments
+  # and will be added directly. If is_commented is not set, "// " will be
+  # added before every line.
+{{.V}}  file_header:
+{{.V}}    path: path/to/protobuf_file_header.txt
+{{.V}}    is_commented: true
+
 # Code generation directives.
 {{.V}}generate:
   # Options that will apply to all plugins of type go and gogo.
diff --git a/internal/create/handler.go b/internal/create/handler.go
index 5846a11..dfd89e1 100644
--- a/internal/create/handler.go
+++ b/internal/create/handler.go
@@ -36,7 +36,7 @@ import (
 )
 
 var (
-	tmplV1 = template.Must(template.New("tmplV1").Parse(`syntax = "proto3";
+	tmplV1 = template.Must(template.New("tmplV1").Parse(`{{.Header}}syntax = "proto3";
 
 package {{.Pkg}};
 
@@ -45,7 +45,7 @@ option java_multiple_files = true;
 option java_outer_classname = "{{.JavaOuterClassname}}";
 option java_package = "{{.JavaPkg}}";`))
 
-	tmplV2 = template.Must(template.New("tmplV2").Parse(`syntax = "proto3";
+	tmplV2 = template.Must(template.New("tmplV2").Parse(`{{.Header}}syntax = "proto3";
 
 package {{.Pkg}};
 
@@ -59,6 +59,7 @@ option php_namespace = "{{.PHPNamespace}}";`))
 )
 
 type tmplData struct {
+	Header             string
 	Pkg                string
 	CSharpNamespace    string
 	GoPkg              string
@@ -135,6 +136,13 @@ func (h *handler) create(filePath string) error {
 	if isV2 {
 		defaultPackage = DefaultPackageV2
 	}
+	fileHeader, err := h.getFileHeader(filePath)
+	if err != nil {
+		return err
+	}
+	if fileHeader != "" {
+		fileHeader = fileHeader + "\n\n"
+	}
 	pkg, err := h.getPkg(filePath, defaultPackage)
 	if err != nil {
 		return err
@@ -143,6 +151,7 @@ func (h *handler) create(filePath string) error {
 	if isV2 {
 		data, err = getDataV2(
 			&tmplData{
+				Header:             fileHeader,
 				Pkg:                pkg,
 				CSharpNamespace:    protostrs.CSharpNamespace(pkg),
 				GoPkg:              protostrs.GoPackageV2(pkg),
@@ -158,6 +167,7 @@ func (h *handler) create(filePath string) error {
 	} else {
 		data, err = getDataV1(
 			&tmplData{
+				Header:             fileHeader,
 				Pkg:                pkg,
 				GoPkg:              protostrs.GoPackage(pkg),
 				JavaOuterClassname: protostrs.JavaOuterClassname(filePath),
@@ -188,6 +198,23 @@ func (h *handler) isV2(filePath string) (bool, error) {
 	return strings.ToLower(config.Lint.Group) == "uber2", nil
 }
 
+func (h *handler) getFileHeader(filePath string) (string, error) {
+	absFilePath, err := filepath.Abs(filePath)
+	if err != nil {
+		return "", err
+	}
+	absDirPath := filepath.Dir(absFilePath)
+	config, err := h.configProvider.GetForDir(absDirPath)
+	if err != nil {
+		return "", err
+	}
+	// no config file found
+	if config.DirPath == "" {
+		return "", nil
+	}
+	return config.Lint.FileHeader, nil
+}
+
 func (h *handler) getPkg(filePath string, defaultPackage string) (string, error) {
 	if h.pkg != "" {
 		return h.pkg, nil
diff --git a/internal/exec/runner.go b/internal/exec/runner.go
index 2bbe0fe..dbd3257 100644
--- a/internal/exec/runner.go
+++ b/internal/exec/runner.go
@@ -388,14 +388,14 @@ func (r *runner) Format(args []string, overwrite, diffMode, lintMode, fixFlag bo
 	if _, err := r.compile(false, false, false, meta); err != nil {
 		return err
 	}
-	return r.format(overwrite, diffMode, lintMode, getFormatFixValue(fixFlag, meta), meta)
+	return r.format(overwrite, diffMode, lintMode, getFormatFixValue(fixFlag, meta), getFormatFileHeaderValue(fixFlag, meta), meta)
 }
 
-func (r *runner) format(overwrite, diffMode, lintMode bool, fix int, meta *meta) error {
+func (r *runner) format(overwrite, diffMode, lintMode bool, fix int, fileHeader string, meta *meta) error {
 	success := true
 	for _, protoFiles := range meta.ProtoSet.DirPathToFiles {
 		for _, protoFile := range protoFiles {
-			fileSuccess, err := r.formatFile(overwrite, diffMode, lintMode, fix, meta, protoFile)
+			fileSuccess, err := r.formatFile(overwrite, diffMode, lintMode, fix, fileHeader, meta, protoFile)
 			if err != nil {
 				return err
 			}
@@ -413,7 +413,7 @@ func (r *runner) format(overwrite, diffMode, lintMode bool, fix int, meta *meta)
 // return true if there was no unexpected diff and we should exit with 0
 // return false if we should exit with non-zero
 // if false and nil error, we will return an ExitError outside of this function
-func (r *runner) formatFile(overwrite bool, diffMode bool, lintMode bool, fix int, meta *meta, protoFile *file.ProtoFile) (bool, error) {
+func (r *runner) formatFile(overwrite bool, diffMode bool, lintMode bool, fix int, fileHeader string, meta *meta, protoFile *file.ProtoFile) (bool, error) {
 	absSingleFilename, err := file.AbsClean(meta.SingleFilename)
 	if err != nil {
 		return false, err
@@ -426,7 +426,7 @@ func (r *runner) formatFile(overwrite bool, diffMode bool, lintMode bool, fix in
 	if err != nil {
 		return false, err
 	}
-	data, failures, err := r.newTransformer(fix).Transform(protoFile.Path, input)
+	data, failures, err := r.newTransformer(fix, fileHeader).Transform(protoFile.Path, input)
 	if err != nil {
 		return false, err
 	}
@@ -479,7 +479,7 @@ func (r *runner) All(args []string, disableFormat, disableLint, fixFlag bool) er
 		return err
 	}
 	if !disableFormat {
-		if err := r.format(true, false, false, getFormatFixValue(fixFlag, meta), meta); err != nil {
+		if err := r.format(true, false, false, getFormatFixValue(fixFlag, meta), getFormatFileHeaderValue(fixFlag, meta), meta); err != nil {
 			return err
 		}
 	}
@@ -804,11 +804,14 @@ func (r *runner) newLintRunner() lint.Runner {
 	)
 }
 
-func (r *runner) newTransformer(fix int) format.Transformer {
+func (r *runner) newTransformer(fix int, fileHeader string) format.Transformer {
 	transformerOptions := []format.TransformerOption{format.TransformerWithLogger(r.logger)}
 	if fix != format.FixNone {
 		transformerOptions = append(transformerOptions, format.TransformerWithFix(fix))
 	}
+	if fileHeader != "" {
+		transformerOptions = append(transformerOptions, format.TransformerWithFileHeader(fileHeader))
+	}
 	return format.NewTransformer(transformerOptions...)
 }
 
@@ -1018,6 +1021,13 @@ func getFormatFixValue(fixFlag bool, meta *meta) int {
 	return format.FixV1
 }
 
+func getFormatFileHeaderValue(fixFlag bool, meta *meta) string {
+	if !fixFlag {
+		return ""
+	}
+	return meta.ProtoSet.Config.Lint.FileHeader
+}
+
 func extractSortPackageNames(m map[string]*extract.Package) []string {
 	s := make([]string, 0, len(m))
 	for key := range m {
diff --git a/internal/format/first_pass_visitor.go b/internal/format/first_pass_visitor.go
index a62ffb9..48a134e 100644
--- a/internal/format/first_pass_visitor.go
+++ b/internal/format/first_pass_visitor.go
@@ -42,6 +42,7 @@ type firstPassVisitor struct {
 
 	filename                 string
 	fix                      int
+	fileHeader               string
 	goPackageOption          *proto.Option
 	javaMultipleFilesOption  *proto.Option
 	javaOuterClassnameOption *proto.Option
@@ -51,11 +52,15 @@ type firstPassVisitor struct {
 	phpNamespaceOption       *proto.Option
 }
 
-func newFirstPassVisitor(filename string, fix int) *firstPassVisitor {
-	return &firstPassVisitor{baseVisitor: newBaseVisitor(), filename: filename, fix: fix}
+func newFirstPassVisitor(filename string, fix int, fileHeader string) *firstPassVisitor {
+	return &firstPassVisitor{baseVisitor: newBaseVisitor(), filename: filename, fix: fix, fileHeader: fileHeader}
 }
 
 func (v *firstPassVisitor) Do() []*text.Failure {
+	if v.fix != FixNone && v.fileHeader != "" {
+		v.P(v.fileHeader)
+		v.P()
+	}
 	if v.Syntax != nil {
 		v.PComment(v.Syntax.Comment)
 		if v.Syntax.Comment != nil {
@@ -237,8 +242,10 @@ func (v *firstPassVisitor) VisitComment(element *proto.Comment) {
 	// We only print file-level comments before syntax, package, file-level options,
 	// or package if they are at the top of the file
 	if !v.haveHitNonComment {
-		v.PComment(element)
-		v.P()
+		if v.fix == FixNone || v.fileHeader == "" {
+			v.PComment(element)
+			v.P()
+		}
 	}
 }
 
diff --git a/internal/format/format.go b/internal/format/format.go
index 3fc6127..973ef86 100644
--- a/internal/format/format.go
+++ b/internal/format/format.go
@@ -64,6 +64,16 @@ func TransformerWithFix(fix int) TransformerOption {
 	}
 }
 
+// TransformerWithFileHeader returns a TransformerOption that will update the file
+// header to match the given file header.
+//
+// This is only valid if fix is set to a value other than FixNone.
+func TransformerWithFileHeader(fileHeader string) TransformerOption {
+	return func(transformer *transformer) {
+		transformer.fileHeader = fileHeader
+	}
+}
+
 // NewTransformer returns a new Transformer.
 func NewTransformer(options ...TransformerOption) Transformer {
 	return newTransformer(options...)
diff --git a/internal/format/transformer.go b/internal/format/transformer.go
index 69ed6fa..ed0d366 100644
--- a/internal/format/transformer.go
+++ b/internal/format/transformer.go
@@ -31,8 +31,9 @@ import (
 )
 
 type transformer struct {
-	logger *zap.Logger
-	fix    int
+	logger     *zap.Logger
+	fix        int
+	fileHeader string
 }
 
 func newTransformer(options ...TransformerOption) *transformer {
@@ -56,7 +57,7 @@ func (t *transformer) Transform(filename string, data []byte) ([]byte, []*text.F
 	}
 	descriptor.Filename = filename
 
-	firstPassVisitor := newFirstPassVisitor(filename, t.fix)
+	firstPassVisitor := newFirstPassVisitor(filename, t.fix, t.fileHeader)
 	for _, element := range descriptor.Elements {
 		element.Accept(firstPassVisitor)
 	}
diff --git a/internal/lint/base_linter.go b/internal/lint/base_linter.go
index 3108032..2b6ad58 100644
--- a/internal/lint/base_linter.go
+++ b/internal/lint/base_linter.go
@@ -23,20 +23,19 @@ package lint
 import (
 	"strings"
 
-	"github.com/emicklei/proto"
 	"github.com/uber/prototool/internal/text"
 )
 
 type baseLinter struct {
 	id       string
 	purpose  string
-	addCheck func(func(*text.Failure), string, []*proto.Proto) error
+	addCheck func(func(*text.Failure), string, []*FileDescriptor) error
 }
 
 func newBaseLinter(
 	id string,
 	purpose string,
-	addCheck func(func(*text.Failure), string, []*proto.Proto) error,
+	addCheck func(func(*text.Failure), string, []*FileDescriptor) error,
 ) *baseLinter {
 	return &baseLinter{
 		id:       strings.ToUpper(id),
@@ -53,7 +52,7 @@ func (c *baseLinter) Purpose() string {
 	return c.purpose
 }
 
-func (c *baseLinter) Check(dirPath string, descriptors []*proto.Proto) ([]*text.Failure, error) {
+func (c *baseLinter) Check(dirPath string, descriptors []*FileDescriptor) ([]*text.Failure, error) {
 	var failures []*text.Failure
 	err := c.addCheck(
 		func(failure *text.Failure) {
diff --git a/internal/lint/base_visitor.go b/internal/lint/base_visitor.go
index f36bce0..84d3059 100644
--- a/internal/lint/base_visitor.go
+++ b/internal/lint/base_visitor.go
@@ -31,7 +31,7 @@ var _ proto.Visitor = baseVisitor{}
 
 type baseVisitor struct{}
 
-func (baseVisitor) OnStart(*proto.Proto) error { return nil }
+func (baseVisitor) OnStart(*FileDescriptor) error { return nil }
 func (baseVisitor) Finally() error             { return nil }
 
 func (baseVisitor) VisitMessage(m *proto.Message)         {}
@@ -72,12 +72,12 @@ type extendedVisitor interface {
 	proto.Visitor
 
 	// OnStart is called when visiting is started.
-	OnStart(*proto.Proto) error
+	OnStart(*FileDescriptor) error
 	// Finally is called when visiting is done.
 	Finally() error
 }
 
-func runVisitor(visitor extendedVisitor, descriptors []*proto.Proto) error {
+func runVisitor(visitor extendedVisitor, descriptors []*FileDescriptor) error {
 	for _, descriptor := range descriptors {
 		if err := visitor.OnStart(descriptor); err != nil {
 			return err
diff --git a/internal/lint/check_comments_no_c_style.go b/internal/lint/check_comments_no_c_style.go
index 8bd4236..ac2e9f5 100644
--- a/internal/lint/check_comments_no_c_style.go
+++ b/internal/lint/check_comments_no_c_style.go
@@ -33,7 +33,7 @@ var commentsNoCStyleLinter = NewLinter(
 	checkCommentsNoCStyle,
 )
 
-func checkCommentsNoCStyle(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkCommentsNoCStyle(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&commentsNoCStyleVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_comments_no_inline.go b/internal/lint/check_comments_no_inline.go
new file mode 100644
index 0000000..8d75476
--- /dev/null
+++ b/internal/lint/check_comments_no_inline.go
@@ -0,0 +1,135 @@
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package lint
+
+import (
+	"github.com/emicklei/proto"
+	"github.com/uber/prototool/internal/text"
+)
+
+var commentsNoInlineLinter = NewLinter(
+	"COMMENTS_NO_INLINE",
+	"Verifies that there are no inline comments.",
+	checkCommentsNoInline,
+)
+
+func checkCommentsNoInline(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
+	return runVisitor(&commentsNoInlineVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
+}
+
+type commentsNoInlineVisitor struct {
+	baseAddVisitor
+}
+
+func (v commentsNoInlineVisitor) VisitMessage(element *proto.Message) {
+	for _, child := range element.Elements {
+		child.Accept(v)
+	}
+}
+
+func (v commentsNoInlineVisitor) VisitService(element *proto.Service) {
+	for _, child := range element.Elements {
+		child.Accept(v)
+	}
+}
+
+func (v commentsNoInlineVisitor) VisitSyntax(element *proto.Syntax) {
+	v.checkInlineComment("syntax", element.InlineComment)
+}
+
+func (v commentsNoInlineVisitor) VisitPackage(element *proto.Package) {
+	v.checkInlineComment("packages", element.InlineComment)
+}
+
+func (v commentsNoInlineVisitor) VisitOption(element *proto.Option) {
+	v.checkInlineComment("options", element.InlineComment)
+}
+
+func (v commentsNoInlineVisitor) VisitImport(element *proto.Import) {
+	v.checkInlineComment("imports", element.InlineComment)
+}
+
+func (v commentsNoInlineVisitor) VisitNormalField(element *proto.NormalField) {
+	v.checkInlineComment("fields", element.InlineComment)
+	for _, child := range element.Options {
+		child.Accept(v)
+	}
+}
+
+func (v commentsNoInlineVisitor) VisitEnumField(element *proto.EnumField) {
+	v.checkInlineComment("enum values", element.InlineComment)
+	if element.ValueOption != nil {
+		element.ValueOption.Accept(v)
+	}
+}
+
+func (v commentsNoInlineVisitor) VisitEnum(element *proto.Enum) {
+	for _, child := range element.Elements {
+		child.Accept(v)
+	}
+}
+
+func (v commentsNoInlineVisitor) VisitOneof(element *proto.Oneof) {
+	for _, child := range element.Elements {
+		child.Accept(v)
+	}
+}
+
+func (v commentsNoInlineVisitor) VisitOneofField(element *proto.OneOfField) {
+	v.checkInlineComment("oneofs", element.InlineComment)
+	for _, child := range element.Options {
+		child.Accept(v)
+	}
+}
+
+func (v commentsNoInlineVisitor) VisitReserved(element *proto.Reserved) {
+	v.checkInlineComment("reserved", element.InlineComment)
+}
+
+func (v commentsNoInlineVisitor) VisitRPC(element *proto.RPC) {
+	v.checkInlineComment("RPCs", element.InlineComment)
+	for _, child := range element.Elements {
+		child.Accept(v)
+	}
+}
+
+func (v commentsNoInlineVisitor) VisitMapField(element *proto.MapField) {
+	v.checkInlineComment("fields", element.InlineComment)
+	for _, child := range element.Options {
+		child.Accept(v)
+	}
+}
+
+func (v commentsNoInlineVisitor) VisitGroup(element *proto.Group) {
+	for _, child := range element.Elements {
+		child.Accept(v)
+	}
+}
+
+func (v commentsNoInlineVisitor) VisitExtensions(element *proto.Extensions) {
+	v.checkInlineComment("extensions", element.InlineComment)
+}
+
+func (v commentsNoInlineVisitor) checkInlineComment(t string, inlineComment *proto.Comment) {
+	if inlineComment != nil {
+		v.AddFailuref(inlineComment.Position, "Inline comments are not allowed on %s, only comment above the type.", t)
+	}
+}
diff --git a/internal/lint/check_enum_field_names_upper_snake_case.go b/internal/lint/check_enum_field_names_upper_snake_case.go
index de77c4c..b6e2cb6 100644
--- a/internal/lint/check_enum_field_names_upper_snake_case.go
+++ b/internal/lint/check_enum_field_names_upper_snake_case.go
@@ -32,7 +32,7 @@ var enumFieldNamesUpperSnakeCaseLinter = NewLinter(
 	checkEnumFieldNamesUpperSnakeCase,
 )
 
-func checkEnumFieldNamesUpperSnakeCase(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkEnumFieldNamesUpperSnakeCase(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(enumFieldNamesUpperSnakeCaseVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_enum_field_names_uppercase.go b/internal/lint/check_enum_field_names_uppercase.go
index 770a7cd..2baee77 100644
--- a/internal/lint/check_enum_field_names_uppercase.go
+++ b/internal/lint/check_enum_field_names_uppercase.go
@@ -32,7 +32,7 @@ var enumFieldNamesUppercaseLinter = NewLinter(
 	checkEnumFieldNamesUppercase,
 )
 
-func checkEnumFieldNamesUppercase(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkEnumFieldNamesUppercase(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(enumFieldNamesUppercaseVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_enum_field_prefixes.go b/internal/lint/check_enum_field_prefixes.go
index b64e980..e44042e 100644
--- a/internal/lint/check_enum_field_prefixes.go
+++ b/internal/lint/check_enum_field_prefixes.go
@@ -34,7 +34,7 @@ var enumFieldPrefixesLinter = NewLinter(
 	checkEnumFieldPrefixes,
 )
 
-func checkEnumFieldPrefixes(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkEnumFieldPrefixes(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&enumFieldPrefixesVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_enum_field_prefixes_except_message.go b/internal/lint/check_enum_field_prefixes_except_message.go
index f2d1fd0..8de58f9 100644
--- a/internal/lint/check_enum_field_prefixes_except_message.go
+++ b/internal/lint/check_enum_field_prefixes_except_message.go
@@ -34,7 +34,7 @@ var enumFieldPrefixesExceptMessageLinter = NewLinter(
 	checkEnumFieldPrefixesExceptMessage,
 )
 
-func checkEnumFieldPrefixesExceptMessage(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkEnumFieldPrefixesExceptMessage(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&enumFieldPrefixesExceptMessageVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_enum_names_camel_case.go b/internal/lint/check_enum_names_camel_case.go
index 353b583..3c00080 100644
--- a/internal/lint/check_enum_names_camel_case.go
+++ b/internal/lint/check_enum_names_camel_case.go
@@ -32,7 +32,7 @@ var enumNamesCamelCaseLinter = NewLinter(
 	checkEnumNamesCamelCase,
 )
 
-func checkEnumNamesCamelCase(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkEnumNamesCamelCase(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(enumNamesCamelCaseVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_enum_names_capitalized.go b/internal/lint/check_enum_names_capitalized.go
index 6fdd6a6..190024e 100644
--- a/internal/lint/check_enum_names_capitalized.go
+++ b/internal/lint/check_enum_names_capitalized.go
@@ -32,7 +32,7 @@ var enumNamesCapitalizedLinter = NewLinter(
 	checkEnumNamesCapitalized,
 )
 
-func checkEnumNamesCapitalized(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkEnumNamesCapitalized(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(enumNamesCapitalizedVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_enum_zero_values_invalid.go b/internal/lint/check_enum_zero_values_invalid.go
index dc6a260..ad4f7b1 100644
--- a/internal/lint/check_enum_zero_values_invalid.go
+++ b/internal/lint/check_enum_zero_values_invalid.go
@@ -34,7 +34,7 @@ var enumZeroValuesInvalidLinter = NewLinter(
 	checkEnumZeroValuesInvalid,
 )
 
-func checkEnumZeroValuesInvalid(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkEnumZeroValuesInvalid(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&enumZeroValuesInvalidVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_enum_zero_values_invalid_except_message.go b/internal/lint/check_enum_zero_values_invalid_except_message.go
index 5d7a417..8110d2a 100644
--- a/internal/lint/check_enum_zero_values_invalid_except_message.go
+++ b/internal/lint/check_enum_zero_values_invalid_except_message.go
@@ -32,7 +32,7 @@ var enumZeroValuesInvalidExceptMessageLinter = NewLinter(
 	checkEnumZeroValuesInvalidExceptMessage,
 )
 
-func checkEnumZeroValuesInvalidExceptMessage(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkEnumZeroValuesInvalidExceptMessage(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&enumZeroValuesInvalidExceptMessageVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_enums_have_comments.go b/internal/lint/check_enums_have_comments.go
index 3fe28d8..acb82a5 100644
--- a/internal/lint/check_enums_have_comments.go
+++ b/internal/lint/check_enums_have_comments.go
@@ -34,7 +34,7 @@ var enumsHaveCommentsLinter = NewLinter(
 	checkEnumsHaveComments,
 )
 
-func checkEnumsHaveComments(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkEnumsHaveComments(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(enumsHaveCommentsVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_enums_no_allow_alias.go b/internal/lint/check_enums_no_allow_alias.go
index e8a5c1b..33448fd 100644
--- a/internal/lint/check_enums_no_allow_alias.go
+++ b/internal/lint/check_enums_no_allow_alias.go
@@ -31,7 +31,7 @@ var enumsNoAllowAliasLinter = NewLinter(
 	checkEnumsNoAllowAlias,
 )
 
-func checkEnumsNoAllowAlias(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkEnumsNoAllowAlias(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(enumsNoAllowAliasVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_fields_not_reserved.go b/internal/lint/check_fields_not_reserved.go
index 4740f9a..48d7a62 100644
--- a/internal/lint/check_fields_not_reserved.go
+++ b/internal/lint/check_fields_not_reserved.go
@@ -33,7 +33,7 @@ var fieldsNotReservedLinter = NewLinter(
 	checkFieldsNotReserved,
 )
 
-func checkFieldsNotReserved(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkFieldsNotReserved(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(fieldsNotReservedVisitor{baseAddVisitor: newBaseAddVisitor(add), outerNameStack: make([]string, 0)}, descriptors)
 }
 
diff --git a/internal/lint/check_file_header.go b/internal/lint/check_file_header.go
new file mode 100644
index 0000000..052f118
--- /dev/null
+++ b/internal/lint/check_file_header.go
@@ -0,0 +1,51 @@
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package lint
+
+import (
+	"strings"
+	"text/scanner"
+
+	"github.com/uber/prototool/internal/text"
+)
+
+var fileHeaderLinter = NewLinter(
+	"FILE_HEADER",
+	"Verifies that the file header matches the expected file header if the file_header option is set in the configuration file.",
+	checkFileHeader,
+)
+
+func checkFileHeader(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
+	return runVisitor(fileHeaderVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
+}
+
+type fileHeaderVisitor struct {
+	baseAddVisitor
+}
+
+func (v fileHeaderVisitor) OnStart(descriptor *FileDescriptor) error {
+	if fileHeader := descriptor.ProtoSet.Config.Lint.FileHeader; fileHeader != "" {
+		if !strings.HasPrefix(descriptor.FileData, fileHeader) {
+			v.AddFailuref(scanner.Position{Filename: descriptor.Filename}, "File is expected to begin with the file header specified from the configuration file.")
+		}
+	}
+	return nil
+}
diff --git a/internal/lint/check_file_options_equal.go b/internal/lint/check_file_options_equal.go
index c5805d9..c7efe6e 100644
--- a/internal/lint/check_file_options_equal.go
+++ b/internal/lint/check_file_options_equal.go
@@ -31,7 +31,7 @@ import (
 var fileOptionsEqualCSharpNamespaceCapitalizedLinter = NewLinter(
 	"FILE_OPTIONS_EQUAL_CSHARP_NAMESPACE_CAPITALIZED",
 	`Verifies that the file option "csharp_namespace" is the capitalized version of the package.`,
-	newCheckFileOptionsEqual("csharp_namespace", func(_ *proto.Proto, pkg *proto.Package) string {
+	newCheckFileOptionsEqual("csharp_namespace", func(_ *FileDescriptor, pkg *proto.Package) string {
 		return protostrs.CSharpNamespace(pkg.Name)
 	}),
 )
@@ -39,7 +39,7 @@ var fileOptionsEqualCSharpNamespaceCapitalizedLinter = NewLinter(
 var fileOptionsEqualGoPackagePbSuffixLinter = NewLinter(
 	"FILE_OPTIONS_EQUAL_GO_PACKAGE_PB_SUFFIX",
 	`Verifies that the file option "go_package" is equal to $(basename PACKAGE)pb.`,
-	newCheckFileOptionsEqual("go_package", func(_ *proto.Proto, pkg *proto.Package) string {
+	newCheckFileOptionsEqual("go_package", func(_ *FileDescriptor, pkg *proto.Package) string {
 		return protostrs.GoPackage(pkg.Name)
 	}),
 )
@@ -47,7 +47,7 @@ var fileOptionsEqualGoPackagePbSuffixLinter = NewLinter(
 var fileOptionsEqualGoPackageV2SuffixLinter = NewLinter(
 	"FILE_OPTIONS_EQUAL_GO_PACKAGE_V2_SUFFIX",
 	`Verifies that the file option "go_package" is equal to the last two values of the package separated by "."s, or just the package name if there are no "."s.`,
-	newCheckFileOptionsEqual("go_package", func(_ *proto.Proto, pkg *proto.Package) string {
+	newCheckFileOptionsEqual("go_package", func(_ *FileDescriptor, pkg *proto.Package) string {
 		return protostrs.GoPackageV2(pkg.Name)
 	}),
 )
@@ -55,7 +55,7 @@ var fileOptionsEqualGoPackageV2SuffixLinter = NewLinter(
 var fileOptionsEqualJavaMultipleFilesTrueLinter = NewLinter(
 	"FILE_OPTIONS_EQUAL_JAVA_MULTIPLE_FILES_TRUE",
 	`Verifies that the file option "java_multiple_files" is equal to true.`,
-	newCheckFileOptionsEqual("java_multiple_files", func(*proto.Proto, *proto.Package) string {
+	newCheckFileOptionsEqual("java_multiple_files", func(*FileDescriptor, *proto.Package) string {
 		return "true"
 	}),
 )
@@ -63,7 +63,7 @@ var fileOptionsEqualJavaMultipleFilesTrueLinter = NewLinter(
 var fileOptionsEqualJavaOuterClassnameProtoSuffixLinter = NewLinter(
 	"FILE_OPTIONS_EQUAL_JAVA_OUTER_CLASSNAME_PROTO_SUFFIX",
 	`Verifies that the file option "java_outer_classname" is equal to $(upperCamelCase $(basename FILE))Proto.`,
-	newCheckFileOptionsEqual("java_outer_classname", func(descriptor *proto.Proto, _ *proto.Package) string {
+	newCheckFileOptionsEqual("java_outer_classname", func(descriptor *FileDescriptor, _ *proto.Package) string {
 		return protostrs.JavaOuterClassname(descriptor.Filename)
 	}),
 )
@@ -71,7 +71,7 @@ var fileOptionsEqualJavaOuterClassnameProtoSuffixLinter = NewLinter(
 var fileOptionsEqualJavaPackageComPrefixLinter = NewLinter(
 	"FILE_OPTIONS_EQUAL_JAVA_PACKAGE_COM_PREFIX",
 	`Verifies that the file option "java_package" is equal to com.PACKAGE.`,
-	newCheckFileOptionsEqual("java_package", func(_ *proto.Proto, pkg *proto.Package) string {
+	newCheckFileOptionsEqual("java_package", func(_ *FileDescriptor, pkg *proto.Package) string {
 		return protostrs.JavaPackage(pkg.Name)
 	}),
 )
@@ -79,7 +79,7 @@ var fileOptionsEqualJavaPackageComPrefixLinter = NewLinter(
 var fileOptionsEqualOBJCClassPrefixAbbrLinter = NewLinter(
 	"FILE_OPTIONS_EQUAL_OBJC_CLASS_PREFIX_ABBR",
 	`Verifies that the file option "objc_class_prefix" is the abbreviated version of the package.`,
-	newCheckFileOptionsEqual("objc_class_prefix", func(_ *proto.Proto, pkg *proto.Package) string {
+	newCheckFileOptionsEqual("objc_class_prefix", func(_ *FileDescriptor, pkg *proto.Package) string {
 		return protostrs.OBJCClassPrefix(pkg.Name)
 	}),
 )
@@ -87,13 +87,13 @@ var fileOptionsEqualOBJCClassPrefixAbbrLinter = NewLinter(
 var fileOptionsEqualPHPNamespaceCapitalizedLinter = NewLinter(
 	"FILE_OPTIONS_EQUAL_PHP_NAMESPACE_CAPITALIZED",
 	`Verifies that the file option "php_namespace" is the capitalized version of the package.`,
-	newCheckFileOptionsEqual("php_namespace", func(_ *proto.Proto, pkg *proto.Package) string {
+	newCheckFileOptionsEqual("php_namespace", func(_ *FileDescriptor, pkg *proto.Package) string {
 		return protostrs.PHPNamespace(pkg.Name)
 	}),
 )
 
-func newCheckFileOptionsEqual(fileOption string, expectedValueFunc func(*proto.Proto, *proto.Package) string) func(func(*text.Failure), string, []*proto.Proto) error {
-	return func(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func newCheckFileOptionsEqual(fileOption string, expectedValueFunc func(*FileDescriptor, *proto.Package) string) func(func(*text.Failure), string, []*FileDescriptor) error {
+	return func(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 		return runVisitor(&fileOptionsEqualVisitor{
 			baseAddVisitor:    newBaseAddVisitor(add),
 			fileOption:        fileOption,
@@ -106,14 +106,14 @@ type fileOptionsEqualVisitor struct {
 	baseAddVisitor
 
 	fileOption        string
-	expectedValueFunc func(*proto.Proto, *proto.Package) string
+	expectedValueFunc func(*FileDescriptor, *proto.Package) string
 
-	descriptor *proto.Proto
+	descriptor *FileDescriptor
 	pkg        *proto.Package
 	option     *proto.Option
 }
 
-func (v *fileOptionsEqualVisitor) OnStart(descriptor *proto.Proto) error {
+func (v *fileOptionsEqualVisitor) OnStart(descriptor *FileDescriptor) error {
 	v.descriptor = descriptor
 	v.pkg = nil
 	v.option = nil
diff --git a/internal/lint/check_file_options_required.go b/internal/lint/check_file_options_required.go
index 5e755d9..d3443d8 100644
--- a/internal/lint/check_file_options_required.go
+++ b/internal/lint/check_file_options_required.go
@@ -69,8 +69,8 @@ var fileOptionsRequirePHPNamespaceLinter = NewLinter(
 	newCheckFileOptionsRequire("php_namespace"),
 )
 
-func newCheckFileOptionsRequire(fileOption string) func(func(*text.Failure), string, []*proto.Proto) error {
-	return func(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func newCheckFileOptionsRequire(fileOption string) func(func(*text.Failure), string, []*FileDescriptor) error {
+	return func(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 		return runVisitor(&fileOptionsRequireVisitor{
 			baseAddVisitor: newBaseAddVisitor(add),
 			fileOption:     fileOption,
@@ -87,7 +87,7 @@ type fileOptionsRequireVisitor struct {
 	seen     bool
 }
 
-func (v *fileOptionsRequireVisitor) OnStart(descriptor *proto.Proto) error {
+func (v *fileOptionsRequireVisitor) OnStart(descriptor *FileDescriptor) error {
 	v.filename = descriptor.Filename
 	v.seen = false
 	return nil
diff --git a/internal/lint/check_file_options_same_in_dir.go b/internal/lint/check_file_options_same_in_dir.go
index 66d796e..538f760 100644
--- a/internal/lint/check_file_options_same_in_dir.go
+++ b/internal/lint/check_file_options_same_in_dir.go
@@ -65,8 +65,8 @@ var fileOptionsPHPNamespaceSameInDirLinter = NewLinter(
 	newCheckFileOptionsSameInDir("php_namespace"),
 )
 
-func newCheckFileOptionsSameInDir(fileOption string) func(func(*text.Failure), string, []*proto.Proto) error {
-	return func(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func newCheckFileOptionsSameInDir(fileOption string) func(func(*text.Failure), string, []*FileDescriptor) error {
+	return func(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 		visitor := &fileOptionsSameInDirVisitor{
 			baseAddVisitor:   newBaseAddVisitor(add),
 			fileOption:       fileOption,
@@ -105,7 +105,7 @@ type fileOptionsSameInDirVisitor struct {
 	option *proto.Option
 }
 
-func (v *fileOptionsSameInDirVisitor) OnStart(*proto.Proto) error {
+func (v *fileOptionsSameInDirVisitor) OnStart(*FileDescriptor) error {
 	v.option = nil
 	return nil
 }
diff --git a/internal/lint/check_file_options_unset.go b/internal/lint/check_file_options_unset.go
index 84d04e0..83fda64 100644
--- a/internal/lint/check_file_options_unset.go
+++ b/internal/lint/check_file_options_unset.go
@@ -39,8 +39,8 @@ var fileOptionsUnsetJavaOuterClassnameLinter = NewLinter(
 	newCheckFileOptionsUnset("java_outer_classname"),
 )
 
-func newCheckFileOptionsUnset(fileOption string) func(func(*text.Failure), string, []*proto.Proto) error {
-	return func(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func newCheckFileOptionsUnset(fileOption string) func(func(*text.Failure), string, []*FileDescriptor) error {
+	return func(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 		return runVisitor(&fileOptionsUnsetVisitor{
 			baseAddVisitor: newBaseAddVisitor(add),
 			fileOption:     fileOption,
@@ -57,7 +57,7 @@ type fileOptionsUnsetVisitor struct {
 	position scanner.Position
 }
 
-func (v *fileOptionsUnsetVisitor) OnStart(descriptor *proto.Proto) error {
+func (v *fileOptionsUnsetVisitor) OnStart(descriptor *FileDescriptor) error {
 	v.seen = false
 	v.position = scanner.Position{}
 	return nil
diff --git a/internal/lint/check_go_package_not_long_form.go b/internal/lint/check_go_package_not_long_form.go
index 1e5c5da..c2c4edc 100644
--- a/internal/lint/check_go_package_not_long_form.go
+++ b/internal/lint/check_go_package_not_long_form.go
@@ -33,7 +33,7 @@ var fileOptionsGoPackageNotLongFormLinter = NewLinter(
 	checkFileOptionsGoPackageNotLongForm,
 )
 
-func checkFileOptionsGoPackageNotLongForm(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkFileOptionsGoPackageNotLongForm(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&fileOptionsGoPackageNotLongFormVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
@@ -43,7 +43,7 @@ type fileOptionsGoPackageNotLongFormVisitor struct {
 	option *proto.Option
 }
 
-func (v *fileOptionsGoPackageNotLongFormVisitor) OnStart(descriptor *proto.Proto) error {
+func (v *fileOptionsGoPackageNotLongFormVisitor) OnStart(descriptor *FileDescriptor) error {
 	v.option = nil
 	return nil
 }
diff --git a/internal/lint/check_gogo_not_imported.go b/internal/lint/check_gogo_not_imported.go
index 0e1d9f8..742f7e4 100644
--- a/internal/lint/check_gogo_not_imported.go
+++ b/internal/lint/check_gogo_not_imported.go
@@ -35,7 +35,7 @@ var (
 	)
 )
 
-func checkGogoNotImported(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkGogoNotImported(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&gogoNotImportedVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_imports_not_public.go b/internal/lint/check_imports_not_public.go
index fc123c6..986999b 100644
--- a/internal/lint/check_imports_not_public.go
+++ b/internal/lint/check_imports_not_public.go
@@ -33,7 +33,7 @@ var (
 	)
 )
 
-func checkImportsNotPublic(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkImportsNotPublic(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&importsNotPublicVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_imports_not_weak.go b/internal/lint/check_imports_not_weak.go
index 4783496..fb21731 100644
--- a/internal/lint/check_imports_not_weak.go
+++ b/internal/lint/check_imports_not_weak.go
@@ -33,7 +33,7 @@ var (
 	)
 )
 
-func checkImportsNotWeak(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkImportsNotWeak(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&importsNotWeakVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_message_field_names_lower_snake_case.go b/internal/lint/check_message_field_names_lower_snake_case.go
index 817e917..9a62402 100644
--- a/internal/lint/check_message_field_names_lower_snake_case.go
+++ b/internal/lint/check_message_field_names_lower_snake_case.go
@@ -32,7 +32,7 @@ var messageFieldNamesLowerSnakeCaseLinter = NewLinter(
 	checkMessageFieldNamesLowerSnakeCase,
 )
 
-func checkMessageFieldNamesLowerSnakeCase(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkMessageFieldNamesLowerSnakeCase(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(messageFieldNamesLowerSnakeCaseVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_message_field_names_lowercase.go b/internal/lint/check_message_field_names_lowercase.go
index 3aaaacf..734f9bf 100644
--- a/internal/lint/check_message_field_names_lowercase.go
+++ b/internal/lint/check_message_field_names_lowercase.go
@@ -32,7 +32,7 @@ var messageFieldNamesLowercaseLinter = NewLinter(
 	checkMessageFieldNamesLowercase,
 )
 
-func checkMessageFieldNamesLowercase(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkMessageFieldNamesLowercase(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(messageFieldNamesLowercaseVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_message_fields_no_json_name.go b/internal/lint/check_message_fields_no_json_name.go
index 5704ebc..7b5ad12 100644
--- a/internal/lint/check_message_fields_no_json_name.go
+++ b/internal/lint/check_message_fields_no_json_name.go
@@ -31,7 +31,7 @@ var messageFieldsNoJSONNameLinter = NewLinter(
 	checkMessageFieldsNoJSONName,
 )
 
-func checkMessageFieldsNoJSONName(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkMessageFieldsNoJSONName(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(messageFieldsNoJSONNameVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_message_fields_not_floats.go b/internal/lint/check_message_fields_not_floats.go
index 0927aaf..36d77d0 100644
--- a/internal/lint/check_message_fields_not_floats.go
+++ b/internal/lint/check_message_fields_not_floats.go
@@ -35,7 +35,7 @@ var messageFieldsNotFloatsLinter = NewLinter(
 	checkMessageFieldsNotFloats,
 )
 
-func checkMessageFieldsNotFloats(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkMessageFieldsNotFloats(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(messageFieldsNotFloatsVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_message_names_camel_case.go b/internal/lint/check_message_names_camel_case.go
index 66943b6..b2b5697 100644
--- a/internal/lint/check_message_names_camel_case.go
+++ b/internal/lint/check_message_names_camel_case.go
@@ -32,7 +32,7 @@ var messageNamesCamelCaseLinter = NewLinter(
 	checkMessageNamesCamelCase,
 )
 
-func checkMessageNamesCamelCase(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkMessageNamesCamelCase(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(messageNamesCamelCaseVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_message_names_capitalized.go b/internal/lint/check_message_names_capitalized.go
index 823397f..b1e1d14 100644
--- a/internal/lint/check_message_names_capitalized.go
+++ b/internal/lint/check_message_names_capitalized.go
@@ -32,7 +32,7 @@ var messageNamesCapitalizedLinter = NewLinter(
 	checkMessageNamesCapitalized,
 )
 
-func checkMessageNamesCapitalized(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkMessageNamesCapitalized(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(messageNamesCapitalizedVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_messages_have_comments.go b/internal/lint/check_messages_have_comments.go
index c039179..3b348f2 100644
--- a/internal/lint/check_messages_have_comments.go
+++ b/internal/lint/check_messages_have_comments.go
@@ -34,7 +34,7 @@ var messagesHaveCommentsLinter = NewLinter(
 	checkMessagesHaveComments,
 )
 
-func checkMessagesHaveComments(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkMessagesHaveComments(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(messagesHaveCommentsVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_messages_have_comments_except_request_response_types.go b/internal/lint/check_messages_have_comments_except_request_response_types.go
index 19a94eb..d679c05 100644
--- a/internal/lint/check_messages_have_comments_except_request_response_types.go
+++ b/internal/lint/check_messages_have_comments_except_request_response_types.go
@@ -34,7 +34,7 @@ var messagesHaveCommentsExceptRequestResponseTypesLinter = NewLinter(
 	checkMessagesHaveCommentsExceptRequestResponseTypes,
 )
 
-func checkMessagesHaveCommentsExceptRequestResponseTypes(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkMessagesHaveCommentsExceptRequestResponseTypes(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&messagesHaveCommentsExceptRequestResponseTypesVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
@@ -45,7 +45,7 @@ type messagesHaveCommentsExceptRequestResponseTypesVisitor struct {
 	nestedMessageNames   []string
 }
 
-func (v *messagesHaveCommentsExceptRequestResponseTypesVisitor) OnStart(*proto.Proto) error {
+func (v *messagesHaveCommentsExceptRequestResponseTypesVisitor) OnStart(*FileDescriptor) error {
 	v.messageNameToMessage = nil
 	v.requestResponseTypes = nil
 	v.nestedMessageNames = nil
diff --git a/internal/lint/check_oneof_names_lower_snake_case.go b/internal/lint/check_oneof_names_lower_snake_case.go
index d1060d4..bb9d97c 100644
--- a/internal/lint/check_oneof_names_lower_snake_case.go
+++ b/internal/lint/check_oneof_names_lower_snake_case.go
@@ -32,7 +32,7 @@ var oneofNamesLowerSnakeCaseLinter = NewLinter(
 	checkOneofNamesLowerSnakeCase,
 )
 
-func checkOneofNamesLowerSnakeCase(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkOneofNamesLowerSnakeCase(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(oneofNamesLowerSnakeCaseVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_package_is_declared.go b/internal/lint/check_package_is_declared.go
index 389bd67..509b6ae 100644
--- a/internal/lint/check_package_is_declared.go
+++ b/internal/lint/check_package_is_declared.go
@@ -33,7 +33,7 @@ var packageIsDeclaredLinter = NewLinter(
 	checkPackageIsDeclared,
 )
 
-func checkPackageIsDeclared(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkPackageIsDeclared(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&packageIsDeclaredVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
@@ -44,7 +44,7 @@ type packageIsDeclaredVisitor struct {
 	pkg      *proto.Package
 }
 
-func (v *packageIsDeclaredVisitor) OnStart(descriptor *proto.Proto) error {
+func (v *packageIsDeclaredVisitor) OnStart(descriptor *FileDescriptor) error {
 	v.filename = descriptor.Filename
 	v.pkg = nil
 	return nil
diff --git a/internal/lint/check_package_lower_snake_case.go b/internal/lint/check_package_lower_snake_case.go
index 88e34a4..6825064 100644
--- a/internal/lint/check_package_lower_snake_case.go
+++ b/internal/lint/check_package_lower_snake_case.go
@@ -34,7 +34,7 @@ var packageLowerSnakeCaseLinter = NewLinter(
 	checkPackageLowerSnakeCase,
 )
 
-func checkPackageLowerSnakeCase(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkPackageLowerSnakeCase(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&packageLowerSnakeCaseVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
@@ -45,7 +45,7 @@ type packageLowerSnakeCaseVisitor struct {
 	pkg      *proto.Package
 }
 
-func (v *packageLowerSnakeCaseVisitor) OnStart(descriptor *proto.Proto) error {
+func (v *packageLowerSnakeCaseVisitor) OnStart(descriptor *FileDescriptor) error {
 	v.filename = descriptor.Filename
 	v.pkg = nil
 	return nil
diff --git a/internal/lint/check_package_major_beta_versioned.go b/internal/lint/check_package_major_beta_versioned.go
index 125aa94..8bc78d3 100644
--- a/internal/lint/check_package_major_beta_versioned.go
+++ b/internal/lint/check_package_major_beta_versioned.go
@@ -34,7 +34,7 @@ var packageMajorBetaVersionedLinter = NewLinter(
 	checkPackageMajorBetaVersioned,
 )
 
-func checkPackageMajorBetaVersioned(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkPackageMajorBetaVersioned(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&packageMajorBetaVersionedVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
@@ -45,7 +45,7 @@ type packageMajorBetaVersionedVisitor struct {
 	pkg      *proto.Package
 }
 
-func (v *packageMajorBetaVersionedVisitor) OnStart(descriptor *proto.Proto) error {
+func (v *packageMajorBetaVersionedVisitor) OnStart(descriptor *FileDescriptor) error {
 	v.filename = descriptor.Filename
 	v.pkg = nil
 	return nil
diff --git a/internal/lint/check_package_no_keywords.go b/internal/lint/check_package_no_keywords.go
index 4568691..6e4a7e5 100644
--- a/internal/lint/check_package_no_keywords.go
+++ b/internal/lint/check_package_no_keywords.go
@@ -55,7 +55,7 @@ func init() {
 
 }
 
-func checkPackageNoKeywords(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkPackageNoKeywords(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(packageNoKeywordsVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_packages_same_in_dir.go b/internal/lint/check_packages_same_in_dir.go
index adf0555..f646204 100644
--- a/internal/lint/check_packages_same_in_dir.go
+++ b/internal/lint/check_packages_same_in_dir.go
@@ -35,7 +35,7 @@ var packagesSameInDirLinter = NewLinter(
 	checkPackagesSameInDir,
 )
 
-func checkPackagesSameInDir(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkPackagesSameInDir(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	visitor := &packagesSameInDirVisitor{baseAddVisitor: newBaseAddVisitor(add), pkgs: make(map[string]struct{})}
 	if err := runVisitor(visitor, descriptors); err != nil {
 		return err
@@ -61,7 +61,7 @@ type packagesSameInDirVisitor struct {
 	pkg *proto.Package
 }
 
-func (v *packagesSameInDirVisitor) OnStart(*proto.Proto) error {
+func (v *packagesSameInDirVisitor) OnStart(*FileDescriptor) error {
 	v.pkg = nil
 	return nil
 }
diff --git a/internal/lint/check_request_response_names_match_rpc.go b/internal/lint/check_request_response_names_match_rpc.go
index d97697c..cc07205 100644
--- a/internal/lint/check_request_response_names_match_rpc.go
+++ b/internal/lint/check_request_response_names_match_rpc.go
@@ -31,7 +31,7 @@ var requestResponseNamesMatchRPCLinter = NewLinter(
 	checkRequestResponseNamesMatchRPC,
 )
 
-func checkRequestResponseNamesMatchRPC(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkRequestResponseNamesMatchRPC(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(requestResponseNamesMatchRPCVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_request_response_types_after_service.go b/internal/lint/check_request_response_types_after_service.go
new file mode 100644
index 0000000..48a5914
--- /dev/null
+++ b/internal/lint/check_request_response_types_after_service.go
@@ -0,0 +1,110 @@
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package lint
+
+import (
+	"text/scanner"
+
+	"github.com/emicklei/proto"
+	"github.com/uber/prototool/internal/text"
+)
+
+var requestResponseTypesAfterServiceLinter = NewLinter(
+	"REQUEST_RESPONSE_TYPES_AFTER_SERVICE",
+	"Verifies that request and response types are defined after any services and the response type is defined after the request type.",
+	checkRequestResponseTypesAfterService,
+)
+
+func checkRequestResponseTypesAfterService(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
+	return runVisitor(&requestResponseTypesAfterServiceVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
+}
+
+type requestResponseTypesAfterServiceVisitor struct {
+	baseAddVisitor
+	messageNameToMessage map[string]*proto.Message
+	rpcs                 []*requestResponseTypesAfterServiceVisitorRPCMapKey
+}
+
+func (v *requestResponseTypesAfterServiceVisitor) OnStart(*FileDescriptor) error {
+	v.messageNameToMessage = nil
+	v.rpcs = nil
+	return nil
+}
+
+func (v *requestResponseTypesAfterServiceVisitor) VisitMessage(message *proto.Message) {
+	if v.messageNameToMessage == nil {
+		v.messageNameToMessage = make(map[string]*proto.Message)
+	}
+	// only does top-level messages
+	// nested messages are verified in checkRequestResponseTypesInSameFile
+	v.messageNameToMessage[message.Name] = message
+}
+
+func (v *requestResponseTypesAfterServiceVisitor) VisitService(service *proto.Service) {
+	for _, child := range service.Elements {
+		child.Accept(v)
+	}
+}
+
+func (v *requestResponseTypesAfterServiceVisitor) VisitRPC(rpc *proto.RPC) {
+	v.rpcs = append(v.rpcs, &requestResponseTypesAfterServiceVisitorRPCMapKey{
+		Position:     rpc.Position,
+		Name:         rpc.Name,
+		RequestType:  rpc.RequestType,
+		ResponseType: rpc.ReturnsType,
+	})
+}
+
+func (v *requestResponseTypesAfterServiceVisitor) Finally() error {
+	if len(v.rpcs) == 0 {
+		return nil
+	}
+	if v.messageNameToMessage == nil {
+		v.messageNameToMessage = make(map[string]*proto.Message)
+	}
+	for _, rpc := range v.rpcs {
+		requestMessage, requestOK := v.messageNameToMessage[rpc.RequestType]
+		if requestOK {
+			if rpc.Position.Line > requestMessage.Position.Line {
+				v.AddFailuref(requestMessage.Position, `Message %q is a request for RPC %q and should be defined in the file after the RPC.`, requestMessage.Name, rpc.Name)
+			}
+		}
+		responseMessage, responseOK := v.messageNameToMessage[rpc.ResponseType]
+		if responseOK {
+			if rpc.Position.Line > responseMessage.Position.Line {
+				v.AddFailuref(responseMessage.Position, `Message %q is a response for RPC %q and should be defined in the file after the RPC.`, responseMessage.Name, rpc.Name)
+			}
+		}
+		if requestOK && responseOK {
+			if requestMessage.Position.Line > responseMessage.Position.Line {
+				v.AddFailuref(responseMessage.Position, `Message %q is a response for RPC %q and should be defined in the file after the request %q.`, responseMessage.Name, rpc.Name, requestMessage.Name)
+			}
+		}
+	}
+	return nil
+}
+
+type requestResponseTypesAfterServiceVisitorRPCMapKey struct {
+	Position     scanner.Position
+	Name         string
+	RequestType  string
+	ResponseType string
+}
diff --git a/internal/lint/check_request_response_types_in_same_file.go b/internal/lint/check_request_response_types_in_same_file.go
index 723b6a1..626c811 100644
--- a/internal/lint/check_request_response_types_in_same_file.go
+++ b/internal/lint/check_request_response_types_in_same_file.go
@@ -34,7 +34,7 @@ var requestResponseTypesInSameFileLinter = NewLinter(
 	checkRequestResponseTypesInSameFile,
 )
 
-func checkRequestResponseTypesInSameFile(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkRequestResponseTypesInSameFile(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&requestResponseTypesInSameFileVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
@@ -45,7 +45,7 @@ type requestResponseTypesInSameFileVisitor struct {
 	nestedMessageNames []string
 }
 
-func (v *requestResponseTypesInSameFileVisitor) OnStart(*proto.Proto) error {
+func (v *requestResponseTypesInSameFileVisitor) OnStart(*FileDescriptor) error {
 	v.messageTypes = nil
 	v.rpcs = nil
 	v.nestedMessageNames = nil
diff --git a/internal/lint/check_request_response_types_only_in_file.go b/internal/lint/check_request_response_types_only_in_file.go
index 2406e72..aa6046f 100644
--- a/internal/lint/check_request_response_types_only_in_file.go
+++ b/internal/lint/check_request_response_types_only_in_file.go
@@ -33,7 +33,7 @@ var requestResponseTypesOnlyInFileLinter = NewLinter(
 	checkRequestResponseTypesOnlyInFile,
 )
 
-func checkRequestResponseTypesOnlyInFile(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkRequestResponseTypesOnlyInFile(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&requestResponseTypesOnlyInFileVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
@@ -44,7 +44,7 @@ type requestResponseTypesOnlyInFileVisitor struct {
 	rpcs                 []*requestResponseTypesOnlyInFileVisitorRPCMapKey
 }
 
-func (v *requestResponseTypesOnlyInFileVisitor) OnStart(*proto.Proto) error {
+func (v *requestResponseTypesOnlyInFileVisitor) OnStart(*FileDescriptor) error {
 	v.enumNameToEnum = nil
 	v.messageNameToMessage = nil
 	v.rpcs = nil
diff --git a/internal/lint/check_request_response_types_unique.go b/internal/lint/check_request_response_types_unique.go
index bb96e57..7ce51f3 100644
--- a/internal/lint/check_request_response_types_unique.go
+++ b/internal/lint/check_request_response_types_unique.go
@@ -31,7 +31,7 @@ var requestResponseTypesUniqueLinter = NewLinter(
 	checkRequestResponseTypesUnique,
 )
 
-func checkRequestResponseTypesUnique(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkRequestResponseTypesUnique(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&requestResponseTypesUniqueVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
@@ -40,7 +40,7 @@ type requestResponseTypesUniqueVisitor struct {
 	seenTypes map[string]struct{}
 }
 
-func (v *requestResponseTypesUniqueVisitor) OnStart(*proto.Proto) error {
+func (v *requestResponseTypesUniqueVisitor) OnStart(*FileDescriptor) error {
 	v.seenTypes = nil
 	return nil
 }
diff --git a/internal/lint/check_rpc_names_camel_case.go b/internal/lint/check_rpc_names_camel_case.go
index c3a3f84..2372bdd 100644
--- a/internal/lint/check_rpc_names_camel_case.go
+++ b/internal/lint/check_rpc_names_camel_case.go
@@ -32,7 +32,7 @@ var rpcNamesCamelCaseLinter = NewLinter(
 	checkRPCNamesCamelCase,
 )
 
-func checkRPCNamesCamelCase(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkRPCNamesCamelCase(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(rpcNamesCamelCaseVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_rpc_names_capitalized.go b/internal/lint/check_rpc_names_capitalized.go
index 3da5d99..2a2f8ba 100644
--- a/internal/lint/check_rpc_names_capitalized.go
+++ b/internal/lint/check_rpc_names_capitalized.go
@@ -32,7 +32,7 @@ var rpcNamesCapitalizedLinter = NewLinter(
 	checkRPCNamesCapitalized,
 )
 
-func checkRPCNamesCapitalized(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkRPCNamesCapitalized(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(rpcNamesCapitalizedVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_rpc_options_no_google_api_http.go b/internal/lint/check_rpc_options_no_google_api_http.go
new file mode 100644
index 0000000..e017c60
--- /dev/null
+++ b/internal/lint/check_rpc_options_no_google_api_http.go
@@ -0,0 +1,74 @@
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package lint
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/emicklei/proto"
+	"github.com/uber/prototool/internal/text"
+)
+
+const rpcOptionsNoGoogleAPIHTTPSuppressableAnnotation = "google-api-http"
+
+var rpcOptionsNoGoogleAPIHTTPLinter = NewLinter(
+	"RPC_OPTIONS_NO_GOOGLE_API_HTTP",
+	fmt.Sprintf(`Suppressable with "@suppresswarnings %s". Verifies that the RPC option "google.api.http" is not used.`, rpcOptionsNoGoogleAPIHTTPSuppressableAnnotation),
+	checkRPCOptionsNoGoogleAPIHTTP,
+)
+
+func checkRPCOptionsNoGoogleAPIHTTP(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
+	return runVisitor(&rpcOptionsNoGoogleAPIHTTPVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
+}
+
+type rpcOptionsNoGoogleAPIHTTPVisitor struct {
+	baseAddVisitor
+	rpc *proto.RPC
+}
+
+func (v *rpcOptionsNoGoogleAPIHTTPVisitor) OnStart(*FileDescriptor) error {
+	v.rpc = nil
+	return nil
+}
+
+func (v *rpcOptionsNoGoogleAPIHTTPVisitor) VisitService(service *proto.Service) {
+	for _, child := range service.Elements {
+		child.Accept(v)
+	}
+}
+
+func (v *rpcOptionsNoGoogleAPIHTTPVisitor) VisitRPC(rpc *proto.RPC) {
+	v.rpc = rpc
+	for _, child := range rpc.Elements {
+		child.Accept(v)
+	}
+	v.rpc = nil
+}
+
+func (v *rpcOptionsNoGoogleAPIHTTPVisitor) VisitOption(option *proto.Option) {
+	if strings.HasPrefix(option.Name, "(google.api.http)") || strings.HasPrefix(option.Name, "(.google.api.http)") {
+		if v.rpc != nil && isSuppressed(v.rpc.Comment, rpcOptionsNoGoogleAPIHTTPSuppressableAnnotation) {
+			return
+		}
+		v.AddFailuref(option.Position, `Option "google.api.http" is not allowed. This option signifies HTTP/REST usage, use the RPC framework instead. This can be suppressed by adding "@suppresswarnings %s" to the RPC comment.`, rpcOptionsNoGoogleAPIHTTPSuppressableAnnotation)
+	}
+}
diff --git a/internal/lint/check_rpcs_have_comments.go b/internal/lint/check_rpcs_have_comments.go
index 16be02f..e2520b5 100644
--- a/internal/lint/check_rpcs_have_comments.go
+++ b/internal/lint/check_rpcs_have_comments.go
@@ -34,7 +34,7 @@ var rpcsHaveCommentsLinter = NewLinter(
 	checkRPCsHaveComments,
 )
 
-func checkRPCsHaveComments(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkRPCsHaveComments(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(rpcsHaveCommentsVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_service_names_api_suffix.go b/internal/lint/check_service_names_api_suffix.go
index d17072e..0834e0f 100644
--- a/internal/lint/check_service_names_api_suffix.go
+++ b/internal/lint/check_service_names_api_suffix.go
@@ -33,7 +33,7 @@ var serviceNamesAPISuffixLinter = NewLinter(
 	checkServiceNamesAPISuffix,
 )
 
-func checkServiceNamesAPISuffix(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkServiceNamesAPISuffix(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(serviceNamesAPISuffixVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_service_names_camel_case.go b/internal/lint/check_service_names_camel_case.go
index 327f86f..d4fbb23 100644
--- a/internal/lint/check_service_names_camel_case.go
+++ b/internal/lint/check_service_names_camel_case.go
@@ -32,7 +32,7 @@ var serviceNamesCamelCaseLinter = NewLinter(
 	checkServiceNamesCamelCase,
 )
 
-func checkServiceNamesCamelCase(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkServiceNamesCamelCase(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(serviceNamesCamelCaseVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_service_names_capitalized.go b/internal/lint/check_service_names_capitalized.go
index b9c504a..fe6f0ed 100644
--- a/internal/lint/check_service_names_capitalized.go
+++ b/internal/lint/check_service_names_capitalized.go
@@ -32,7 +32,7 @@ var serviceNamesCapitalizedLinter = NewLinter(
 	checkServiceNamesCapitalized,
 )
 
-func checkServiceNamesCapitalized(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkServiceNamesCapitalized(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(serviceNamesCapitalizedVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_service_names_match_file_name.go b/internal/lint/check_service_names_match_file_name.go
index 5733ebc..3ba1a44 100644
--- a/internal/lint/check_service_names_match_file_name.go
+++ b/internal/lint/check_service_names_match_file_name.go
@@ -34,7 +34,7 @@ var serviceNamesMatchFileNameLinter = NewLinter(
 	checkServiceNamesMatchFileName,
 )
 
-func checkServiceNamesMatchFileName(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkServiceNamesMatchFileName(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&serviceNamesMatchFileNameVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
@@ -44,7 +44,7 @@ type serviceNamesMatchFileNameVisitor struct {
 	services []*proto.Service
 }
 
-func (v *serviceNamesMatchFileNameVisitor) OnStart(descriptor *proto.Proto) error {
+func (v *serviceNamesMatchFileNameVisitor) OnStart(descriptor *FileDescriptor) error {
 	v.filename = descriptor.Filename
 	v.services = nil
 	return nil
diff --git a/internal/lint/check_services_have_comments.go b/internal/lint/check_services_have_comments.go
index 0ac690e..51c3a83 100644
--- a/internal/lint/check_services_have_comments.go
+++ b/internal/lint/check_services_have_comments.go
@@ -34,7 +34,7 @@ var servicesHaveCommentsLinter = NewLinter(
 	checkServicesHaveComments,
 )
 
-func checkServicesHaveComments(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkServicesHaveComments(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(servicesHaveCommentsVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_syntax_proto3.go b/internal/lint/check_syntax_proto3.go
index fa46ab2..b8898b8 100644
--- a/internal/lint/check_syntax_proto3.go
+++ b/internal/lint/check_syntax_proto3.go
@@ -33,7 +33,7 @@ var syntaxProto3Linter = NewLinter(
 	checkSyntaxProto3,
 )
 
-func checkSyntaxProto3(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkSyntaxProto3(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&syntaxProto3Visitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
@@ -44,7 +44,7 @@ type syntaxProto3Visitor struct {
 	syntax   *proto.Syntax
 }
 
-func (v *syntaxProto3Visitor) OnStart(descriptor *proto.Proto) error {
+func (v *syntaxProto3Visitor) OnStart(descriptor *FileDescriptor) error {
 	v.filename = descriptor.Filename
 	v.syntax = nil
 	return nil
diff --git a/internal/lint/check_wkt_directly_imported.go b/internal/lint/check_wkt_directly_imported.go
index 6f84350..a08a5dc 100644
--- a/internal/lint/check_wkt_directly_imported.go
+++ b/internal/lint/check_wkt_directly_imported.go
@@ -39,7 +39,7 @@ var (
 	)
 )
 
-func checkWKTDirectlyImported(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkWKTDirectlyImported(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(&wktDirectlyImportedVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_wkt_duration_suffix.go b/internal/lint/check_wkt_duration_suffix.go
index 4911f82..c911b9b 100644
--- a/internal/lint/check_wkt_duration_suffix.go
+++ b/internal/lint/check_wkt_duration_suffix.go
@@ -33,7 +33,7 @@ var wktDurationSuffixLinter = NewLinter(
 	checkWKTDurationSuffix,
 )
 
-func checkWKTDurationSuffix(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkWKTDurationSuffix(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(wktDurationSuffixVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/check_wkt_timestamp_suffix.go b/internal/lint/check_wkt_timestamp_suffix.go
index c78a510..e8b8e55 100644
--- a/internal/lint/check_wkt_timestamp_suffix.go
+++ b/internal/lint/check_wkt_timestamp_suffix.go
@@ -33,7 +33,7 @@ var wktTimestampSuffixLinter = NewLinter(
 	checkWKTTimestampSuffix,
 )
 
-func checkWKTTimestampSuffix(add func(*text.Failure), dirPath string, descriptors []*proto.Proto) error {
+func checkWKTTimestampSuffix(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
 	return runVisitor(wktTimestampSuffixVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
 }
 
diff --git a/internal/lint/lint.go b/internal/lint/lint.go
index 3dbcd9c..284efaa 100644
--- a/internal/lint/lint.go
+++ b/internal/lint/lint.go
@@ -22,6 +22,7 @@ package lint
 
 import (
 	"fmt"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
@@ -37,6 +38,7 @@ var (
 	// AllLinters is the slice of all known Linters.
 	AllLinters = []Linter{
 		commentsNoCStyleLinter,
+		commentsNoInlineLinter,
 		enumFieldNamesUppercaseLinter,
 		enumFieldNamesUpperSnakeCaseLinter,
 		enumFieldPrefixesLinter,
@@ -48,6 +50,7 @@ var (
 		enumsHaveCommentsLinter,
 		enumsNoAllowAliasLinter,
 		fieldsNotReservedLinter,
+		fileHeaderLinter,
 		fileOptionsCSharpNamespaceSameInDirLinter,
 		fileOptionsEqualCSharpNamespaceCapitalizedLinter,
 		fileOptionsEqualGoPackageV2SuffixLinter,
@@ -92,6 +95,8 @@ var (
 		rpcsHaveCommentsLinter,
 		rpcNamesCamelCaseLinter,
 		rpcNamesCapitalizedLinter,
+		rpcOptionsNoGoogleAPIHTTPLinter,
+		requestResponseTypesAfterServiceLinter,
 		requestResponseTypesInSameFileLinter,
 		requestResponseTypesOnlyInFileLinter,
 		requestResponseTypesUniqueLinter,
@@ -115,6 +120,7 @@ var (
 		enumFieldNamesUpperSnakeCaseLinter,
 		enumNamesCamelCaseLinter,
 		enumNamesCapitalizedLinter,
+		fileHeaderLinter,
 		messageFieldNamesLowerSnakeCaseLinter,
 		messageNamesCamelCaseLinter,
 		messageNamesCapitalizedLinter,
@@ -133,6 +139,7 @@ var (
 		enumNamesCapitalizedLinter,
 		enumZeroValuesInvalidLinter,
 		enumsNoAllowAliasLinter,
+		fileHeaderLinter,
 		fileOptionsEqualGoPackagePbSuffixLinter,
 		fileOptionsEqualJavaMultipleFilesTrueLinter,
 		fileOptionsEqualJavaOuterClassnameProtoSuffixLinter,
@@ -165,6 +172,7 @@ var (
 	// Uber2Linters is the slice of linters for the uber2 lint group.
 	Uber2Linters = []Linter{
 		commentsNoCStyleLinter,
+		commentsNoInlineLinter,
 		enumFieldNamesUpperSnakeCaseLinter,
 		enumFieldPrefixesExceptMessageLinter,
 		enumNamesCamelCaseLinter,
@@ -172,6 +180,7 @@ var (
 		enumZeroValuesInvalidExceptMessageLinter,
 		enumsNoAllowAliasLinter,
 		fieldsNotReservedLinter,
+		fileHeaderLinter,
 		fileOptionsCSharpNamespaceSameInDirLinter,
 		fileOptionsEqualCSharpNamespaceCapitalizedLinter,
 		fileOptionsEqualGoPackageV2SuffixLinter,
@@ -209,7 +218,9 @@ var (
 		packagesSameInDirLinter,
 		rpcNamesCamelCaseLinter,
 		rpcNamesCapitalizedLinter,
+		rpcOptionsNoGoogleAPIHTTPLinter,
 		requestResponseNamesMatchRPCLinter,
+		requestResponseTypesAfterServiceLinter,
 		requestResponseTypesInSameFileLinter,
 		requestResponseTypesOnlyInFileLinter,
 		requestResponseTypesUniqueLinter,
@@ -229,15 +240,16 @@ var (
 		"uber":   UberLinters,
 		"uber2":  Uber2Linters,
 	}
+
+	allLintIDs = make(map[string]struct{})
 )
 
 func init() {
-	ids := make(map[string]struct{})
 	for _, linter := range AllLinters {
-		if _, ok := ids[linter.ID()]; ok {
+		if _, ok := allLintIDs[linter.ID()]; ok {
 			panic(fmt.Sprintf("duplicate linter id %s", linter.ID()))
 		}
-		ids[linter.ID()] = struct{}{}
+		allLintIDs[linter.ID()] = struct{}{}
 	}
 }
 
@@ -263,6 +275,14 @@ func NewRunner(options ...RunnerOption) Runner {
 	return newRunner(options...)
 }
 
+// FileDescriptor is a wrapper for proto.Proto.
+type FileDescriptor struct {
+	*proto.Proto
+
+	ProtoSet *file.ProtoSet
+	FileData string
+}
+
 // The below should not be needed in the CLI
 // TODO make private
 
@@ -277,7 +297,7 @@ type Linter interface {
 	// slice and does not return an error. An error is returned if something
 	// unexpected happens. Callers should verify the files are compilable
 	// before running this.
-	Check(dirPath string, descriptors []*proto.Proto) ([]*text.Failure, error)
+	Check(dirPath string, descriptors []*FileDescriptor) ([]*text.Failure, error)
 }
 
 // NewLinter is a convenience function that returns a new Linter for the
@@ -286,7 +306,7 @@ type Linter interface {
 // The ID will be upper-cased.
 //
 // Failures returned from check do not need to set the ID, this will be overwritten.
-func NewLinter(id string, purpose string, addCheck func(func(*text.Failure), string, []*proto.Proto) error) Linter {
+func NewLinter(id string, purpose string, addCheck func(func(*text.Failure), string, []*FileDescriptor) error) Linter {
 	return newBaseLinter(id, purpose, addCheck)
 }
 
@@ -322,6 +342,9 @@ func GetLinters(config settings.LintConfig) ([]Linter, error) {
 	if len(config.IncludeIDs) > 0 {
 		for _, l := range AllLinters {
 			for _, id := range config.IncludeIDs {
+				if err := checkLintID(id); err != nil {
+					return nil, err
+				}
 				if l.ID() == id {
 					linterMap[id] = l
 				}
@@ -329,8 +352,16 @@ func GetLinters(config settings.LintConfig) ([]Linter, error) {
 		}
 	}
 	for _, excludeID := range config.ExcludeIDs {
+		if err := checkLintID(excludeID); err != nil {
+			return nil, err
+		}
 		delete(linterMap, excludeID)
 	}
+	for ignoreID := range config.IgnoreIDToFilePaths {
+		if err := checkLintID(ignoreID); err != nil {
+			return nil, err
+		}
+	}
 
 	result := make([]Linter, 0, len(linterMap))
 	for _, l := range linterMap {
@@ -341,10 +372,10 @@ func GetLinters(config settings.LintConfig) ([]Linter, error) {
 
 // GetDirPathToDescriptors is a convenience function that gets the
 // descriptors for the given ProtoSet.
-func GetDirPathToDescriptors(protoSet *file.ProtoSet) (map[string][]*proto.Proto, error) {
-	dirPathToDescriptors := make(map[string][]*proto.Proto, len(protoSet.DirPathToFiles))
+func GetDirPathToDescriptors(protoSet *file.ProtoSet) (map[string][]*FileDescriptor, error) {
+	dirPathToDescriptors := make(map[string][]*FileDescriptor, len(protoSet.DirPathToFiles))
 	for dirPath, protoFiles := range protoSet.DirPathToFiles {
-		descriptors := make([]*proto.Proto, len(protoFiles))
+		descriptors := make([]*FileDescriptor, len(protoFiles))
 		for i, protoFile := range protoFiles {
 			file, err := os.Open(protoFile.Path)
 			if err != nil {
@@ -353,11 +384,25 @@ func GetDirPathToDescriptors(protoSet *file.ProtoSet) (map[string][]*proto.Proto
 			parser := proto.NewParser(file)
 			parser.Filename(protoFile.DisplayPath)
 			descriptor, err := parser.Parse()
-			_ = file.Close()
 			if err != nil {
+				_ = file.Close()
 				return nil, err
 			}
-			descriptors[i] = descriptor
+			if _, err := file.Seek(0, 0); err != nil {
+				_ = file.Close()
+				return nil, err
+			}
+			fileData, err := ioutil.ReadAll(file)
+			if err != nil {
+				_ = file.Close()
+				return nil, err
+			}
+			_ = file.Close()
+			descriptors[i] = &FileDescriptor{
+				Proto:    descriptor,
+				ProtoSet: protoSet,
+				FileData: string(fileData),
+			}
 		}
 		dirPathToDescriptors[dirPath] = descriptors
 	}
@@ -365,7 +410,7 @@ func GetDirPathToDescriptors(protoSet *file.ProtoSet) (map[string][]*proto.Proto
 }
 
 // CheckMultiple is a convenience function that checks multiple linters and multiple descriptors.
-func CheckMultiple(linters []Linter, dirPathToDescriptors map[string][]*proto.Proto, ignoreIDToFilePaths map[string][]string) ([]*text.Failure, error) {
+func CheckMultiple(linters []Linter, dirPathToDescriptors map[string][]*FileDescriptor, ignoreIDToFilePaths map[string][]string) ([]*text.Failure, error) {
 	var allFailures []*text.Failure
 	for dirPath, descriptors := range dirPathToDescriptors {
 		for _, linter := range linters {
@@ -380,7 +425,7 @@ func CheckMultiple(linters []Linter, dirPathToDescriptors map[string][]*proto.Pr
 	return allFailures, nil
 }
 
-func checkOne(linter Linter, dirPath string, descriptors []*proto.Proto, ignoreIDToFilePaths map[string][]string) ([]*text.Failure, error) {
+func checkOne(linter Linter, dirPath string, descriptors []*FileDescriptor, ignoreIDToFilePaths map[string][]string) ([]*text.Failure, error) {
 	filteredDescriptors, err := filterIgnores(linter, descriptors, ignoreIDToFilePaths)
 	if err != nil {
 		return nil, err
@@ -388,8 +433,8 @@ func checkOne(linter Linter, dirPath string, descriptors []*proto.Proto, ignoreI
 	return linter.Check(dirPath, filteredDescriptors)
 }
 
-func filterIgnores(linter Linter, descriptors []*proto.Proto, ignoreIDToFilePaths map[string][]string) ([]*proto.Proto, error) {
-	var filteredDescriptors []*proto.Proto
+func filterIgnores(linter Linter, descriptors []*FileDescriptor, ignoreIDToFilePaths map[string][]string) ([]*FileDescriptor, error) {
+	var filteredDescriptors []*FileDescriptor
 	for _, descriptor := range descriptors {
 		ignore, err := shouldIgnore(linter, descriptor, ignoreIDToFilePaths)
 		if err != nil {
@@ -402,7 +447,7 @@ func filterIgnores(linter Linter, descriptors []*proto.Proto, ignoreIDToFilePath
 	return filteredDescriptors, nil
 }
 
-func shouldIgnore(linter Linter, descriptor *proto.Proto, ignoreIDToFilePaths map[string][]string) (bool, error) {
+func shouldIgnore(linter Linter, descriptor *FileDescriptor, ignoreIDToFilePaths map[string][]string) (bool, error) {
 	filePath := descriptor.Filename
 	var err error
 	if !filepath.IsAbs(filePath) {
@@ -423,6 +468,13 @@ func shouldIgnore(linter Linter, descriptor *proto.Proto, ignoreIDToFilePaths ma
 	return false, nil
 }
 
+func checkLintID(lintID string) error {
+	if _, ok := allLintIDs[lintID]; !ok {
+		return fmt.Errorf("unknown lint id in configuration file: %s", lintID)
+	}
+	return nil
+}
+
 func isSuppressed(comment *proto.Comment, annotation string) bool {
 	if comment == nil {
 		return false
diff --git a/internal/settings/config_provider.go b/internal/settings/config_provider.go
index a67e274..dddccc5 100644
--- a/internal/settings/config_provider.go
+++ b/internal/settings/config_provider.go
@@ -32,7 +32,7 @@ import (
 
 	"github.com/uber/prototool/internal/strs"
 	"go.uber.org/zap"
-	"gopkg.in/yaml.v2"
+	yaml "gopkg.in/yaml.v2"
 )
 
 type configProvider struct {
@@ -286,6 +286,28 @@ func externalConfigToConfig(e ExternalConfig, dirPath string) (Config, error) {
 		createDirPathToBasePackage = nil
 	}
 
+	var fileHeader string
+	if e.Lint.FileHeader.Path != "" {
+		if filepath.IsAbs(e.Lint.FileHeader.Path) {
+			return Config{}, fmt.Errorf("path for file header must be relative: %s", e.Lint.FileHeader.Path)
+		}
+		fileHeaderData, err := ioutil.ReadFile(filepath.Join(dirPath, e.Lint.FileHeader.Path))
+		if err != nil {
+			return Config{}, err
+		}
+		fileHeaderLines := getFileHeaderLines(fileHeaderData)
+		if !e.Lint.FileHeader.IsCommented {
+			for i, fileHeaderLine := range fileHeaderLines {
+				if fileHeaderLine == "" {
+					fileHeaderLines[i] = "//"
+				} else {
+					fileHeaderLines[i] = "// " + fileHeaderLine
+				}
+			}
+		}
+		fileHeader = strings.Join(fileHeaderLines, "\n")
+	}
+
 	config := Config{
 		DirPath:         dirPath,
 		ExcludePrefixes: excludePrefixes,
@@ -304,6 +326,7 @@ func externalConfigToConfig(e ExternalConfig, dirPath string) (Config, error) {
 			Group:               strings.ToLower(e.Lint.Group),
 			NoDefault:           e.Lint.Rules.NoDefault,
 			IgnoreIDToFilePaths: ignoreIDToFilePaths,
+			FileHeader:          fileHeader,
 		},
 		Gen: GenConfig{
 			GoPluginOptions: GenGoPluginOptions{
@@ -379,3 +402,11 @@ func jsonUnmarshalStrict(data []byte, v interface{}) error {
 	decoder.DisallowUnknownFields()
 	return decoder.Decode(v)
 }
+
+func getFileHeaderLines(data []byte) []string {
+	var lines []string
+	for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") {
+		lines = append(lines, strings.TrimSpace(line))
+	}
+	return lines
+}
diff --git a/internal/settings/settings.go b/internal/settings/settings.go
index badf38e..20c3cf0 100644
--- a/internal/settings/settings.go
+++ b/internal/settings/settings.go
@@ -191,6 +191,13 @@ type LintConfig struct {
 	// IDs expected to be all upper-case.
 	// File paths expected to be absolute paths.
 	IgnoreIDToFilePaths map[string][]string
+	// FileHeader is contents of the file that contains the header for all
+	// Protobuf files, typically a license header. If this is set and the
+	// FILE_HEADER linter is turned on, files will be checked to begin
+	// with the contents of this file, and format --fix will place this
+	// header before the syntax declaration. Note that format --fix will delete
+	// anything before the syntax declaration if this is set.
+	FileHeader string
 }
 
 // GenConfig is the gen config.
@@ -281,6 +288,10 @@ type ExternalConfig struct {
 			Add       []string `json:"add" yaml:"add"`
 			Remove    []string `json:"remove" yaml:"remove"`
 		}
+		FileHeader struct {
+			Path        string `json:"path,omitempty" yaml:"path,omitempty"`
+			IsCommented bool   `json:"is_commented,omitempty" yaml:"is_commented,omitempty"`
+		} `json:"file_header,omitempty" yaml:"file_header,omitempty"`
 	} `json:"lint,omitempty" yaml:"lint,omitempty"`
 	Gen struct {
 		GoOptions struct {


Test patch:
diff --git a/internal/cmd/cmd_test.go b/internal/cmd/cmd_test.go
index 2ab5864..9a4df39 100644
--- a/internal/cmd/cmd_test.go
+++ b/internal/cmd/cmd_test.go
@@ -407,6 +407,65 @@ func TestLint(t *testing.T) {
 		"testdata/lint/apisuffix/foo.proto",
 	)
 
+	assertDoLintFile(
+		t,
+		false,
+		`13:1:REQUEST_RESPONSE_TYPES_AFTER_SERVICE`,
+		"testdata/lint/afterservice/foo/v1/hello_api.proto",
+	)
+
+	assertDoLintFile(
+		t,
+		false,
+		`13:1:REQUEST_RESPONSE_TYPES_AFTER_SERVICE
+		15:1:REQUEST_RESPONSE_TYPES_AFTER_SERVICE`,
+		"testdata/lint/afterservice/foo/v1/hello2api.proto",
+	)
+
+	assertDoLintFile(
+		t,
+		false,
+		`17:1:REQUEST_RESPONSE_TYPES_AFTER_SERVICE`,
+		"testdata/lint/afterservice/foo/v1/hello3api.proto",
+	)
+
+	assertDoLintFile(
+		t,
+		false,
+		`1:20:COMMENTS_NO_INLINE
+		3:17:COMMENTS_NO_INLINE
+		6:30:COMMENTS_NO_INLINE
+		15:23:COMMENTS_NO_INLINE
+		21:21:COMMENTS_NO_INLINE
+		30:25:COMMENTS_NO_INLINE
+		36:20:COMMENTS_NO_INLINE
+		37:37:COMMENTS_NO_INLINE
+		38:37:COMMENTS_NO_INLINE
+		41:23:COMMENTS_NO_INLINE
+		47:18:COMMENTS_NO_INLINE
+		48:35:COMMENTS_NO_INLINE
+		49:35:COMMENTS_NO_INLINE`,
+		"testdata/lint/inlinecomments/foo/v1/foo.proto",
+	)
+
+	assertDoLintFile(
+		t,
+		false,
+		`13:28:COMMENTS_NO_INLINE
+		16:58:COMMENTS_NO_INLINE`,
+		"testdata/lint/inlinecomments/foo/v1/hello_api.proto",
+	)
+
+	assertDoLintFile(
+		t,
+		false,
+		`17:5:RPC_OPTIONS_NO_GOOGLE_API_HTTP
+		22:5:RPC_OPTIONS_NO_GOOGLE_API_HTTP
+		25:5:RPC_OPTIONS_NO_GOOGLE_API_HTTP
+		30:5:RPC_OPTIONS_NO_GOOGLE_API_HTTP`,
+		"testdata/lint/nogoogleapihttp/foo/v1/hello_api.proto",
+	)
+
 	assertDoLintFile(
 		t,
 		false,
@@ -663,7 +722,11 @@ option java_package = "com.uber.prototool.generated";`,
 		true,
 		"testdata/create/two/baz.proto",
 		"",
-		`syntax = "proto3";
+		`// this
+// is a
+// header
+
+syntax = "proto3";
 
 package foo;
 
@@ -769,7 +832,11 @@ option php_namespace = "B\\C\\Bar";`,
 		true,
 		"testdata/create/version2two/baz.proto",
 		"",
-		`syntax = "proto3";
+		`// this
+// is a
+// header
+
+syntax = "proto3";
 
 package foo;
 
diff --git a/internal/cmd/testdata/create/two/protobuf_file_header.txt b/internal/cmd/testdata/create/two/protobuf_file_header.txt
new file mode 100644
index 0000000..45380c9
--- /dev/null
+++ b/internal/cmd/testdata/create/two/protobuf_file_header.txt
@@ -0,0 +1,3 @@
+this
+  is a
+    header
diff --git a/internal/cmd/testdata/create/two/prototool.yaml b/internal/cmd/testdata/create/two/prototool.yaml
index 65d7fda..ccb0061 100644
--- a/internal/cmd/testdata/create/two/prototool.yaml
+++ b/internal/cmd/testdata/create/two/prototool.yaml
@@ -2,3 +2,6 @@ create:
   packages:
     - directory: .
       name: foo
+lint:
+  file_header:
+    path: protobuf_file_header.txt
diff --git a/internal/cmd/testdata/create/version2two/protobuf_file_header.txt b/internal/cmd/testdata/create/version2two/protobuf_file_header.txt
new file mode 100644
index 0000000..a545b79
--- /dev/null
+++ b/internal/cmd/testdata/create/version2two/protobuf_file_header.txt
@@ -0,0 +1,3 @@
+// this
+  // is a
+    // header
diff --git a/internal/cmd/testdata/create/version2two/prototool.yaml b/internal/cmd/testdata/create/version2two/prototool.yaml
index 02aa4fb..15693fb 100644
--- a/internal/cmd/testdata/create/version2two/prototool.yaml
+++ b/internal/cmd/testdata/create/version2two/prototool.yaml
@@ -1,5 +1,8 @@
 lint:
   group: uber2
+  file_header:
+    path: protobuf_file_header.txt
+    is_commented: true
 create:
   packages:
     - directory: .
diff --git a/internal/cmd/testdata/lint/afterservice/foo/v1/hello2api.proto b/internal/cmd/testdata/lint/afterservice/foo/v1/hello2api.proto
new file mode 100644
index 0000000..62325ba
--- /dev/null
+++ b/internal/cmd/testdata/lint/afterservice/foo/v1/hello2api.proto
@@ -0,0 +1,19 @@
+syntax = "proto3";
+
+package foo.v1;
+
+option csharp_namespace = "Foo.V1";
+option go_package = "foov1";
+option java_multiple_files = true;
+option java_outer_classname = "Hello2apiProto";
+option java_package = "com.foo.v1";
+option objc_class_prefix = "FXX";
+option php_namespace = "Foo\\V1";
+
+message SendFoo2Request {}
+
+message SendFoo2Response {}
+
+service Hello2API {
+  rpc SendFoo2(SendFoo2Request) returns (SendFoo2Response);
+}
diff --git a/internal/cmd/testdata/lint/afterservice/foo/v1/hello3api.proto b/internal/cmd/testdata/lint/afterservice/foo/v1/hello3api.proto
new file mode 100644
index 0000000..e0ebe24
--- /dev/null
+++ b/internal/cmd/testdata/lint/afterservice/foo/v1/hello3api.proto
@@ -0,0 +1,19 @@
+syntax = "proto3";
+
+package foo.v1;
+
+option csharp_namespace = "Foo.V1";
+option go_package = "foov1";
+option java_multiple_files = true;
+option java_outer_classname = "Hello3apiProto";
+option java_package = "com.foo.v1";
+option objc_class_prefix = "FXX";
+option php_namespace = "Foo\\V1";
+
+service Hello3API {
+  rpc SendFoo3(SendFoo3Request) returns (SendFoo3Response);
+}
+
+message SendFoo3Response {}
+
+message SendFoo3Request {}
diff --git a/internal/cmd/testdata/lint/afterservice/foo/v1/hello_api.proto b/internal/cmd/testdata/lint/afterservice/foo/v1/hello_api.proto
new file mode 100644
index 0000000..a7ff857
--- /dev/null
+++ b/internal/cmd/testdata/lint/afterservice/foo/v1/hello_api.proto
@@ -0,0 +1,20 @@
+syntax = "proto3";
+
+package foo.v1;
+
+option csharp_namespace = "Foo.V1";
+option go_package = "foov1";
+option java_multiple_files = true;
+option java_outer_classname = "HelloApiProto";
+option java_package = "com.foo.v1";
+option objc_class_prefix = "FXX";
+option php_namespace = "Foo\\V1";
+
+message SendFooRequest {}
+
+service HelloAPI {
+  rpc SendFoo(SendFooRequest) returns (SendFooResponse);
+}
+
+message SendFooResponse {}
+
diff --git a/internal/cmd/testdata/lint/afterservice/prototool.yaml b/internal/cmd/testdata/lint/afterservice/prototool.yaml
new file mode 100644
index 0000000..092dbf5
--- /dev/null
+++ b/internal/cmd/testdata/lint/afterservice/prototool.yaml
@@ -0,0 +1,2 @@
+lint:
+  group: uber2
diff --git a/internal/cmd/testdata/lint/inlinecomments/foo/v1/foo.proto b/internal/cmd/testdata/lint/inlinecomments/foo/v1/foo.proto
new file mode 100644
index 0000000..5c93f38
--- /dev/null
+++ b/internal/cmd/testdata/lint/inlinecomments/foo/v1/foo.proto
@@ -0,0 +1,50 @@
+syntax = "proto3"; // inline comment
+
+package foo.v1; // inline comment
+
+option csharp_namespace = "Foo.V1";
+option go_package = "foov1"; // inline comment
+option java_multiple_files = true;
+option java_outer_classname = "FooProto";
+option java_package = "com.foo.v1";
+option objc_class_prefix = "FXX";
+option php_namespace = "Foo\\V1";
+
+enum FooType {
+  FOO_TYPE_INVALID = 0;
+  FOO_TYPE_UNSET = 1; // inline comment
+  FOO_TYPE_BALLOON = 2;
+  FOO_TYPE_TREE = 3;
+}
+
+message Foo {
+  FooType type = 1; // inline comment
+  sint32 latitude_micros = 3;
+  sint32 longitude_micros = 4;
+  string bar_id = 5;
+}
+
+message Bar {
+  message Nested {
+    enum Type {
+      TYPE_INVALID = 0; // inline comment
+      TYPE_UNSET = 1;
+      TYPE_REMOTE_CONTROL = 2;
+      TYPE_FAN = 3;
+    }
+    Type type = 1;
+    string id = 2; // inline comment
+    repeated string planet_ids = 3; // inline comment
+    map<int64, string> map_ids = 4; // inline comment
+  }
+  enum Type {
+    TYPE_INVALID = 0; // inline comment
+    TYPE_UNSET = 1;
+    TYPE_REMOTE_CONTROL = 2;
+    TYPE_FAN = 3;
+  }
+  Type type = 1;
+  string id = 2; // inline comment
+  repeated string planet_ids = 3; // inline comment
+  map<int64, string> map_ids = 4; // inline comment
+}
diff --git a/internal/cmd/testdata/lint/inlinecomments/foo/v1/hello_api.proto b/internal/cmd/testdata/lint/inlinecomments/foo/v1/hello_api.proto
new file mode 100644
index 0000000..096cd77
--- /dev/null
+++ b/internal/cmd/testdata/lint/inlinecomments/foo/v1/hello_api.proto
@@ -0,0 +1,23 @@
+syntax = "proto3";
+
+package foo.v1;
+
+option csharp_namespace = "Foo.V1";
+option go_package = "foov1";
+option java_multiple_files = true;
+option java_outer_classname = "HelloApiProto";
+option java_package = "com.foo.v1";
+option objc_class_prefix = "FXX";
+option php_namespace = "Foo\\V1";
+
+import "foo/v1/foo.proto"; // inline comment
+
+service HelloAPI {
+  rpc SendFoo(SendFooRequest) returns (SendFooResponse); // inline comment
+}
+
+message SendFooRequest {
+  Foo foo = 1;
+}
+
+message SendFooResponse {}
diff --git a/internal/cmd/testdata/lint/inlinecomments/prototool.yaml b/internal/cmd/testdata/lint/inlinecomments/prototool.yaml
new file mode 100644
index 0000000..092dbf5
--- /dev/null
+++ b/internal/cmd/testdata/lint/inlinecomments/prototool.yaml
@@ -0,0 +1,2 @@
+lint:
+  group: uber2
diff --git a/internal/cmd/testdata/lint/nogoogleapihttp/foo/v1/hello_api.proto b/internal/cmd/testdata/lint/nogoogleapihttp/foo/v1/hello_api.proto
new file mode 100644
index 0000000..fcfe354
--- /dev/null
+++ b/internal/cmd/testdata/lint/nogoogleapihttp/foo/v1/hello_api.proto
@@ -0,0 +1,84 @@
+syntax = "proto3";
+
+package foo.v1;
+
+option csharp_namespace = "Foo.V1";
+option go_package = "foov1";
+option java_multiple_files = true;
+option java_outer_classname = "HelloApiProto";
+option java_package = "com.foo.v1";
+option objc_class_prefix = "FXX";
+option php_namespace = "Foo\\V1";
+
+import "google/api/api.proto";
+
+service HelloAPI {
+  rpc SendFoo(SendFooRequest) returns (SendFooResponse) {
+    option (google.api.http) = {
+      get: "test"
+    };
+  }
+  rpc SendBar(SendBarRequest) returns (SendBarResponse) {
+    option (google.api.http).get = "test";
+  }
+  rpc SendBaz(SendBazRequest) returns (SendBazResponse) {
+    option (.google.api.http) = {
+      get: "test"
+    };
+  }
+  rpc SendBat(SendBatRequest) returns (SendBatResponse) {
+    option (.google.api.http).get = "test";
+  }
+  // @suppresswarnings google-api-http
+  rpc SendTwoFoo(SendTwoFooRequest) returns (SendTwoFooResponse) {
+    option (google.api.http) = {
+      get: "test"
+    };
+  }
+  // @suppresswarnings google-api-http
+  rpc SendTwoBar(SendTwoBarRequest) returns (SendTwoBarResponse) {
+    option (google.api.http).get = "test";
+  }
+  // @suppresswarnings google-api-http
+  rpc SendTwoBaz(SendTwoBazRequest) returns (SendTwoBazResponse) {
+    option (.google.api.http) = {
+      get: "test"
+    };
+  }
+  // @suppresswarnings google-api-http
+  rpc SendTwoBat(SendTwoBatRequest) returns (SendTwoBatResponse) {
+    option (.google.api.http).get = "test";
+  }
+}
+
+message SendFooRequest {}
+
+message SendFooResponse {}
+
+message SendBarRequest {}
+
+message SendBarResponse {}
+
+message SendBazRequest {}
+
+message SendBazResponse {}
+
+message SendBatRequest {}
+
+message SendBatResponse {}
+
+message SendTwoFooRequest {}
+
+message SendTwoFooResponse {}
+
+message SendTwoBarRequest {}
+
+message SendTwoBarResponse {}
+
+message SendTwoBazRequest {}
+
+message SendTwoBazResponse {}
+
+message SendTwoBatRequest {}
+
+message SendTwoBatResponse {}
diff --git a/internal/cmd/testdata/lint/nogoogleapihttp/google/api/api.proto b/internal/cmd/testdata/lint/nogoogleapihttp/google/api/api.proto
new file mode 100644
index 0000000..b174ed9
--- /dev/null
+++ b/internal/cmd/testdata/lint/nogoogleapihttp/google/api/api.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+package google.api;
+
+option csharp_namespace = "Google.Api";
+option go_package = "apipb";
+option java_multiple_files = true;
+option java_outer_classname = "ApiProto";
+option java_package = "com.google.api";
+option objc_class_prefix = "GAX";
+option php_namespace = "Google\\Api";
+
+import "google/protobuf/descriptor.proto";
+
+extend google.protobuf.MethodOptions {
+  Http http = 50001;
+}
+
+message Http {
+  string get = 1;
+}
diff --git a/internal/cmd/testdata/lint/nogoogleapihttp/prototool.yaml b/internal/cmd/testdata/lint/nogoogleapihttp/prototool.yaml
new file mode 100644
index 0000000..f93e4d0
--- /dev/null
+++ b/internal/cmd/testdata/lint/nogoogleapihttp/prototool.yaml
@@ -0,0 +1,6 @@
+lint:
+  group: uber2
+  ignores:
+    - id: PACKAGE_MAJOR_BETA_VERSIONED
+      files:
+        - google/api/api.proto
diff --git a/internal/cmd/testdata/lint/samedirjavapkg/prototool.yaml b/internal/cmd/testdata/lint/samedirjavapkg/prototool.yaml
index bb69f08..9dace5f 100644
--- a/internal/cmd/testdata/lint/samedirjavapkg/prototool.yaml
+++ b/internal/cmd/testdata/lint/samedirjavapkg/prototool.yaml
@@ -1,5 +1,5 @@
 lint:
   rules:
     remove:
-      - FILE_OPTIONS_EQUAL_JAVA_PACKAGE_COM_PB
+      - FILE_OPTIONS_EQUAL_JAVA_PACKAGE_COM_PREFIX
       - FILE_OPTIONS_REQUIRE_JAVA_PACKAGE