diff --git a/cmd/caddy2/main.go b/cmd/caddy2/main.go
index 81f670b2c..f311e5c60 100644
--- a/cmd/caddy2/main.go
+++ b/cmd/caddy2/main.go
@@ -6,6 +6,7 @@ import (
 	// this is where modules get plugged in
 	_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
 	_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/caddylog"
+	_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/headers"
 	_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/reverseproxy"
 	_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/staticfiles"
 	_ "bitbucket.org/lightcodelabs/caddy2/modules/caddytls"
diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go
new file mode 100644
index 000000000..1826c7a9c
--- /dev/null
+++ b/modules/caddyhttp/headers/headers.go
@@ -0,0 +1,82 @@
+package headers
+
+import (
+	"net/http"
+	"strings"
+
+	"bitbucket.org/lightcodelabs/caddy2"
+	"bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
+)
+
+func init() {
+	caddy2.RegisterModule(caddy2.Module{
+		Name: "http.middleware.headers",
+		New:  func() (interface{}, error) { return new(Headers), nil },
+	})
+}
+
+// Headers is a middleware which can mutate HTTP headers.
+type Headers struct {
+	Request  HeaderOps
+	Response RespHeaderOps
+}
+
+// HeaderOps defines some operations to
+// perform on HTTP headers.
+type HeaderOps struct {
+	Add    http.Header
+	Set    http.Header
+	Delete []string
+}
+
+// RespHeaderOps is like HeaderOps, but
+// optionally deferred until response time.
+type RespHeaderOps struct {
+	HeaderOps
+	Deferred bool `json:"deferred"`
+}
+
+func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
+	apply(h.Request, r.Header)
+	if h.Response.Deferred {
+		w = &responseWriterWrapper{
+			ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
+			headerOps:             h.Response.HeaderOps,
+		}
+	} else {
+		apply(h.Response.HeaderOps, w.Header())
+	}
+	return next.ServeHTTP(w, r)
+}
+
+func apply(ops HeaderOps, hdr http.Header) {
+	for fieldName, vals := range ops.Add {
+		for _, v := range vals {
+			hdr.Add(fieldName, v)
+		}
+	}
+	for fieldName, vals := range ops.Set {
+		hdr.Set(fieldName, strings.Join(vals, ","))
+	}
+	for _, fieldName := range ops.Delete {
+		hdr.Del(fieldName)
+	}
+}
+
+// responseWriterWrapper defers response header
+// operations until WriteHeader is called.
+type responseWriterWrapper struct {
+	*caddyhttp.ResponseWriterWrapper
+	headerOps HeaderOps
+}
+
+func (rww *responseWriterWrapper) WriteHeader(status int) {
+	apply(rww.headerOps, rww.ResponseWriterWrapper.Header())
+	rww.ResponseWriterWrapper.WriteHeader(status)
+}
+
+// Interface guards
+var (
+	_ caddyhttp.MiddlewareHandler = (*Headers)(nil)
+	_ caddyhttp.HTTPInterfaces    = (*responseWriterWrapper)(nil)
+)
diff --git a/modules/caddyhttp/headers/headers_test.go b/modules/caddyhttp/headers/headers_test.go
new file mode 100644
index 000000000..cb83d47d6
--- /dev/null
+++ b/modules/caddyhttp/headers/headers_test.go
@@ -0,0 +1,7 @@
+package headers
+
+import "testing"
+
+func TestReqHeaders(t *testing.T) {
+	// TODO: write tests
+}
diff --git a/modules/caddyhttp/staticfiles/matcher.go b/modules/caddyhttp/staticfiles/matcher.go
index cccf54b23..9ce3f4c70 100644
--- a/modules/caddyhttp/staticfiles/matcher.go
+++ b/modules/caddyhttp/staticfiles/matcher.go
@@ -16,6 +16,8 @@ func init() {
 	})
 }
 
+// FileMatcher is a matcher that can match requests
+// based on the local file system.
 // TODO: Not sure how to do this well; we'd need the ability to
 // hide files, etc...
 // TODO: Also consider a feature to match directory that
@@ -29,6 +31,7 @@ type FileMatcher struct {
 	Flags []string `json:"flags"`
 }
 
+// Match matches the request r against m.
 func (m FileMatcher) Match(r *http.Request) bool {
 	// TODO: sanitize path
 	fullPath := filepath.Join(m.Root, m.Path)