Repository: perkeep/perkeep

Issue:
Unescape page title after md->html conversion
Running locally website with https://github.com/perkeep/perkeep/pull/1744 I see that the title of the page is `<title>Perkeep 0.12 (&amp;ldquo;Toronto&amp;rdquo;) - Perkeep</title>`

We're using https://github.com/russross/blackfriday to convert markdown to HTML -- I have a small PR unescaping the title specifically. 

Discussion:

Solution patch:
diff --git a/app/publisher/js/file.go b/app/publisher/js/file.go
index 0c89c25dd..9f3b5b2cd 100644
--- a/app/publisher/js/file.go
+++ b/app/publisher/js/file.go
@@ -526,7 +526,7 @@ func (fic *fileItemContainer) render() error {
 
 	err := <-c
 	if err != nil {
-		return fmt.Errorf("cannot get peers of %v: %v", fit.pn, err)
+		return fmt.Errorf("cannot get peers of %v: %w", fit.pn, err)
 	}
 
 	if needsUpdate {
diff --git a/app/publisher/main.go b/app/publisher/main.go
index 34b4fe265..269db4c6e 100644
--- a/app/publisher/main.go
+++ b/app/publisher/main.go
@@ -23,6 +23,7 @@ import (
 	"context"
 	"crypto/tls"
 	"encoding/json"
+	"errors"
 	"flag"
 	"fmt"
 	"html"
@@ -97,7 +98,7 @@ func appConfig() (*config, error) {
 	}
 	cl, err := app.Client()
 	if err != nil {
-		return nil, fmt.Errorf("could not get a client to fetch extra config: %v", err)
+		return nil, fmt.Errorf("could not get a client to fetch extra config: %w", err)
 	}
 	conf := &config{}
 	pause := time.Second
@@ -415,12 +416,12 @@ func newPublishHandler(conf *config) *publishHandler {
 func goTemplate(files fs.FS, templateFile string) (*template.Template, error) {
 	f, err := files.Open(templateFile)
 	if err != nil {
-		return nil, fmt.Errorf("Could not open template %v: %v", templateFile, err)
+		return nil, fmt.Errorf("Could not open template %v: %w", templateFile, err)
 	}
 	defer f.Close()
 	templateBytes, err := io.ReadAll(f)
 	if err != nil {
-		return nil, fmt.Errorf("Could not read template %v: %v", templateFile, err)
+		return nil, fmt.Errorf("Could not read template %v: %w", templateFile, err)
 	}
 	return template.Must(template.New("subject").Parse(string(templateBytes))), nil
 }
@@ -466,7 +467,7 @@ func (ph *publishHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		// (by e.g. the owner) since last time.
 		err := ph.initRootNode()
 		if err != nil {
-			httputil.ServeError(w, r, fmt.Errorf("No publish root node: %v", err))
+			httputil.ServeError(w, r, fmt.Errorf("No publish root node: %w", err))
 			ph.rootNodeMu.Unlock()
 			return
 		}
@@ -475,7 +476,7 @@ func (ph *publishHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ph.masterQueryMu.Lock()
 	if !ph.masterQueryDone {
 		if err := ph.setMasterQuery(ph.rootNode); err != nil {
-			httputil.ServeError(w, r, fmt.Errorf("master query not set: %v", err))
+			httputil.ServeError(w, r, fmt.Errorf("master query not set: %w", err))
 			ph.masterQueryMu.Unlock()
 			return
 		}
@@ -485,7 +486,7 @@ func (ph *publishHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	preq, err := ph.NewRequest(w, r)
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Could not create publish request: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Could not create publish request: %w", err))
 		return
 	}
 	preq.serveHTTP()
@@ -495,7 +496,7 @@ func (ph *publishHandler) initRootNode() error {
 	var getRootNode = func() (blob.Ref, error) {
 		result, err := ph.camliRootQuery()
 		if err != nil {
-			return blob.Ref{}, fmt.Errorf("could not find permanode for root %q of publish handler: %v", ph.rootName, err)
+			return blob.Ref{}, fmt.Errorf("could not find permanode for root %q of publish handler: %w", ph.rootName, err)
 		}
 		if len(result.Blobs) == 0 || !result.Blobs[0].Blob.Valid() {
 			return blob.Ref{}, fmt.Errorf("could not find permanode for root %q of publish handler: %v", ph.rootName, os.ErrNotExist)
@@ -607,7 +608,7 @@ func (ph *publishHandler) describe(br blob.Ref) (*search.DescribedBlob, error) {
 		Depth:   1,
 	})
 	if err != nil {
-		return nil, fmt.Errorf("Could not describe %v: %v", br, err)
+		return nil, fmt.Errorf("Could not describe %v: %w", br, err)
 	}
 	// TODO(mpl): check why Describe is not giving us an error when br is invalid.
 	if res == nil || res.Meta == nil || res.Meta[br.String()] == nil {
@@ -633,7 +634,7 @@ func (ph *publishHandler) deepDescribe(br blob.Ref) (*search.DescribeResponse, e
 		Limit: -1,
 	})
 	if err != nil {
-		return nil, fmt.Errorf("Could not deep describe %v: %v", br, err)
+		return nil, fmt.Errorf("Could not deep describe %v: %w", br, err)
 	}
 	if res == nil || res.Describe == nil {
 		return nil, fmt.Errorf("no describe result for %v", br)
@@ -698,7 +699,7 @@ func (pr *publishRequest) serveHTTP() {
 	}
 
 	if err := pr.findSubject(); err != nil {
-		if err == os.ErrNotExist {
+		if errors.Is(err, os.ErrNotExist) {
 			http.NotFound(pr.rw, pr.req)
 			return
 		}
@@ -1176,7 +1177,7 @@ func (ph *publishHandler) describeMembers(br blob.Ref) (*search.SearchResult, er
 		Limit: -1,
 	})
 	if err != nil {
-		return nil, fmt.Errorf("Could not describe members of %v: %v", br, err)
+		return nil, fmt.Errorf("Could not describe members of %v: %w", br, err)
 	}
 	return res, nil
 }
diff --git a/app/publisher/zip.go b/app/publisher/zip.go
index 700ed169d..6406f33df 100644
--- a/app/publisher/zip.go
+++ b/app/publisher/zip.go
@@ -77,7 +77,7 @@ func (zh *zipHandler) describeMembers(br blob.Ref) (*search.DescribeResponse, er
 		Limit: -1,
 	})
 	if err != nil {
-		return nil, fmt.Errorf("Could not describe %v: %v", br, err)
+		return nil, fmt.Errorf("Could not describe %v: %w", br, err)
 	}
 	if res == nil || res.Describe == nil {
 		return nil, fmt.Errorf("no describe result for %v", br)
@@ -92,7 +92,7 @@ func (zh *zipHandler) blobList(dirPath string, dirBlob blob.Ref) ([]*blobFile, e
 	//	dr.Describe(dirBlob, 3)
 	//	res, err := dr.Result()
 	//	if err != nil {
-	//		return nil, fmt.Errorf("Could not describe %v: %v", dirBlob, err)
+	//		return nil, fmt.Errorf("Could not describe %v: %w", dirBlob, err)
 	//	}
 	res, err := zh.describeMembers(dirBlob)
 	if err != nil {
@@ -110,7 +110,7 @@ func (zh *zipHandler) blobList(dirPath string, dirBlob blob.Ref) ([]*blobFile, e
 		dirRoot := dirBlobPath[1]
 		children, err := zh.blobsFromDir("", dirRoot)
 		if err != nil {
-			return nil, fmt.Errorf("Could not get list of blobs from %v: %v", dirRoot, err)
+			return nil, fmt.Errorf("Could not get list of blobs from %v: %w", dirRoot, err)
 		}
 		list = append(list, children...)
 		return list, nil
@@ -128,7 +128,7 @@ func (zh *zipHandler) blobList(dirPath string, dirBlob blob.Ref) ([]*blobFile, e
 			children, err := zh.blobsFromDir(
 				path.Join(dirPath, dirInfo.FileName), newZipRoot)
 			if err != nil {
-				return nil, fmt.Errorf("Could not get list of blobs from %v: %v", newZipRoot, err)
+				return nil, fmt.Errorf("Could not get list of blobs from %v: %w", newZipRoot, err)
 			}
 			list = append(list, children...)
 			// TODO(mpl): we assume a directory permanode does not also have members.
@@ -147,7 +147,7 @@ func (zh *zipHandler) blobList(dirPath string, dirBlob blob.Ref) ([]*blobFile, e
 		fullpath := path.Join(dirPath, pseudoDirName)
 		moreMembers, err := zh.blobList(fullpath, member.BlobRef)
 		if err != nil {
-			return nil, fmt.Errorf("Could not get list of blobs from %v: %v", member.BlobRef, err)
+			return nil, fmt.Errorf("Could not get list of blobs from %v: %w", member.BlobRef, err)
 		}
 		list = append(list, moreMembers...)
 	}
@@ -160,11 +160,11 @@ func (zh *zipHandler) blobsFromDir(dirPath string, dirBlob blob.Ref) ([]*blobFil
 	var list []*blobFile
 	dr, err := schema.NewDirReader(context.TODO(), zh.fetcher, dirBlob)
 	if err != nil {
-		return nil, fmt.Errorf("Could not read dir blob %v: %v", dirBlob, err)
+		return nil, fmt.Errorf("Could not read dir blob %v: %w", dirBlob, err)
 	}
 	ent, err := dr.Readdir(context.TODO(), -1)
 	if err != nil {
-		return nil, fmt.Errorf("Could not read dir entries: %v", err)
+		return nil, fmt.Errorf("Could not read dir entries: %w", err)
 	}
 	for _, v := range ent {
 		fullpath := path.Join(dirPath, v.FileName())
@@ -174,7 +174,7 @@ func (zh *zipHandler) blobsFromDir(dirPath string, dirBlob blob.Ref) ([]*blobFil
 		case schema.TypeDirectory:
 			children, err := zh.blobsFromDir(fullpath, v.BlobRef())
 			if err != nil {
-				return nil, fmt.Errorf("Could not get list of blobs from %v: %v", v.BlobRef(), err)
+				return nil, fmt.Errorf("Could not get list of blobs from %v: %w", v.BlobRef(), err)
 			}
 			list = append(list, children...)
 		}
diff --git a/app/scanningcabinet/datastore.go b/app/scanningcabinet/datastore.go
index c2bb54bc2..96023b887 100644
--- a/app/scanningcabinet/datastore.go
+++ b/app/scanningcabinet/datastore.go
@@ -189,7 +189,7 @@ func (h *handler) fetchScans(limit int) ([]mediaObject, error) {
 		}
 		scan, err := h.describeScan(des)
 		if err != nil {
-			return nil, fmt.Errorf("error describing scan %v: %v", br, err)
+			return nil, fmt.Errorf("error describing scan %v: %w", br, err)
 		}
 		scans = append(scans, scan)
 	}
@@ -225,7 +225,7 @@ func (h *handler) fetchScanFunc(br blob.Ref, searchFunc func(blob.Ref) (*search.
 		}
 		scan, err := h.describeScan(des)
 		if err != nil {
-			return mo, fmt.Errorf("error describing scan %v: %v", des.BlobRef, err)
+			return mo, fmt.Errorf("error describing scan %v: %w", des.BlobRef, err)
 		}
 		return scan, nil
 	}
@@ -238,10 +238,10 @@ func (h *handler) signAndSend(ctx context.Context, json string) error {
 	// TODO(mpl): sign things ourselves if we can.
 	scl, err := h.cl.Sign(ctx, h.server, strings.NewReader("json="+json))
 	if err != nil {
-		return fmt.Errorf("could not get signed claim %v: %v", json, err)
+		return fmt.Errorf("could not get signed claim %v: %w", json, err)
 	}
 	if _, err := h.cl.Upload(ctx, client.NewUploadHandleFromString(string(scl))); err != nil {
-		return fmt.Errorf("could not upload signed claim %v: %v", json, err)
+		return fmt.Errorf("could not upload signed claim %v: %w", json, err)
 	}
 	return nil
 }
@@ -249,7 +249,7 @@ func (h *handler) signAndSend(ctx context.Context, json string) error {
 func (h *handler) setAttribute(ctx context.Context, pn blob.Ref, attr, val string) error {
 	ucl, err := schema.NewSetAttributeClaim(pn, attr, val).SetSigner(h.signer).JSON()
 	if err != nil {
-		return fmt.Errorf("could not create claim to set %v:%v on %v: %v", attr, val, pn, err)
+		return fmt.Errorf("could not create claim to set %v:%v on %v: %w", attr, val, pn, err)
 	}
 	return h.signAndSend(ctx, ucl)
 }
@@ -257,7 +257,7 @@ func (h *handler) setAttribute(ctx context.Context, pn blob.Ref, attr, val strin
 func (h *handler) delAttribute(ctx context.Context, pn blob.Ref, attr, val string) error {
 	ucl, err := schema.NewDelAttributeClaim(pn, attr, val).SetSigner(h.signer).JSON()
 	if err != nil {
-		return fmt.Errorf("could not create claim to delete %v:%v on %v: %v", attr, val, pn, err)
+		return fmt.Errorf("could not create claim to delete %v:%v on %v: %w", attr, val, pn, err)
 	}
 	return h.signAndSend(ctx, ucl)
 }
@@ -265,7 +265,7 @@ func (h *handler) delAttribute(ctx context.Context, pn blob.Ref, attr, val strin
 func (h *handler) addAttribute(ctx context.Context, pn blob.Ref, attr, val string) error {
 	ucl, err := schema.NewAddAttributeClaim(pn, attr, val).SetSigner(h.signer).JSON()
 	if err != nil {
-		return fmt.Errorf("could not create claim to add %v:%v on %v: %v", attr, val, pn, err)
+		return fmt.Errorf("could not create claim to add %v:%v on %v: %w", attr, val, pn, err)
 	}
 	return h.signAndSend(ctx, ucl)
 }
@@ -273,7 +273,7 @@ func (h *handler) addAttribute(ctx context.Context, pn blob.Ref, attr, val strin
 func (h *handler) deleteNode(ctx context.Context, node blob.Ref) error {
 	ucl, err := schema.NewDeleteClaim(node).SetSigner(h.signer).JSON()
 	if err != nil {
-		return fmt.Errorf("could not create delete claim for %v: %v", node, err)
+		return fmt.Errorf("could not create delete claim for %v: %w", node, err)
 	}
 	return h.signAndSend(ctx, ucl)
 }
@@ -285,15 +285,15 @@ func (h *handler) newPermanode() (blob.Ref, error) {
 	var pn blob.Ref
 	upn, err := schema.NewUnsignedPermanode().SetSigner(h.signer).JSON()
 	if err != nil {
-		return pn, fmt.Errorf("could not create unsigned permanode: %v", err)
+		return pn, fmt.Errorf("could not create unsigned permanode: %w", err)
 	}
 	spn, err := h.cl.Sign(context.TODO(), h.server, strings.NewReader("json="+upn))
 	if err != nil {
-		return pn, fmt.Errorf("could not get signed permanode: %v", err)
+		return pn, fmt.Errorf("could not get signed permanode: %w", err)
 	}
 	sbr, err := h.cl.Upload(context.TODO(), client.NewUploadHandleFromString(string(spn)))
 	if err != nil {
-		return pn, fmt.Errorf("could not upload permanode: %v", err)
+		return pn, fmt.Errorf("could not upload permanode: %w", err)
 	}
 	return sbr.BlobRef, nil
 }
@@ -301,22 +301,22 @@ func (h *handler) newPermanode() (blob.Ref, error) {
 func (h *handler) createScan(ctx context.Context, mo mediaObject) (blob.Ref, error) {
 	pn, err := h.newPermanode()
 	if err != nil {
-		return pn, fmt.Errorf("could not create scan: %v", err)
+		return pn, fmt.Errorf("could not create scan: %w", err)
 	}
 
 	// make it a scan
 	if err := h.setAttribute(ctx, pn, nodeattr.Type, scanNodeType); err != nil {
-		return pn, fmt.Errorf("could not set %v as a scan: %v", pn, err)
+		return pn, fmt.Errorf("could not set %v as a scan: %w", pn, err)
 	}
 
 	// give it content
 	if err := h.setAttribute(ctx, pn, nodeattr.CamliContent, mo.contentRef.String()); err != nil {
-		return pn, fmt.Errorf("could not set content for scan %v: %v", pn, err)
+		return pn, fmt.Errorf("could not set content for scan %v: %w", pn, err)
 	}
 
 	// set creationTime
 	if err := h.setAttribute(ctx, pn, nodeattr.DateCreated, mo.creation.UTC().Format(time.RFC3339)); err != nil {
-		return pn, fmt.Errorf("could not set creationTime for scan %v: %v", pn, err)
+		return pn, fmt.Errorf("could not set creationTime for scan %v: %w", pn, err)
 	}
 
 	return pn, nil
@@ -328,28 +328,28 @@ func (h *handler) updateScan(ctx context.Context, pn blob.Ref, new, old *mediaOb
 	if old == nil {
 		mo, err := h.fetchScan(pn)
 		if err != nil {
-			return fmt.Errorf("scan %v not found: %v", pn, err)
+			return fmt.Errorf("scan %v not found: %w", pn, err)
 		}
 		old = &mo
 	}
 	if new.contentRef.Valid() && old.contentRef != new.contentRef {
 		if err := h.setAttribute(ctx, pn, "camliContent", new.contentRef.String()); err != nil {
-			return fmt.Errorf("could not set contentRef for scan %v: %v", pn, err)
+			return fmt.Errorf("could not set contentRef for scan %v: %w", pn, err)
 		}
 	}
 	if new.documentRef.Valid() && old.documentRef != new.documentRef {
 		if err := h.setAttribute(ctx, pn, "document", new.documentRef.String()); err != nil {
-			return fmt.Errorf("could not set documentRef for scan %v: %v", pn, err)
+			return fmt.Errorf("could not set documentRef for scan %v: %w", pn, err)
 		}
 	}
 	if !old.creation.Equal(new.creation) {
 		if new.creation.IsZero() {
 			if err := h.delAttribute(ctx, pn, nodeattr.DateCreated, ""); err != nil {
-				return fmt.Errorf("could not delete creation date for scan %v: %v", pn, err)
+				return fmt.Errorf("could not delete creation date for scan %v: %w", pn, err)
 			}
 		} else {
 			if err := h.setAttribute(ctx, pn, nodeattr.DateCreated, new.creation.UTC().Format(time.RFC3339)); err != nil {
-				return fmt.Errorf("could not set creation date for scan %v: %v", pn, err)
+				return fmt.Errorf("could not set creation date for scan %v: %w", pn, err)
 			}
 		}
 	}
@@ -359,28 +359,28 @@ func (h *handler) updateScan(ctx context.Context, pn blob.Ref, new, old *mediaOb
 func (h *handler) updateDocument(ctx context.Context, pn blob.Ref, new *document) error {
 	old, err := h.fetchDocument(pn)
 	if err != nil {
-		return fmt.Errorf("document %v not found: %v", pn, err)
+		return fmt.Errorf("document %v not found: %w", pn, err)
 	}
 	if old.physicalLocation != new.physicalLocation {
 		if err := h.setAttribute(ctx, pn, nodeattr.LocationText, new.physicalLocation); err != nil {
-			return fmt.Errorf("could not set physicalLocation for document %v: %v", pn, err)
+			return fmt.Errorf("could not set physicalLocation for document %v: %w", pn, err)
 		}
 	}
 
 	if old.title != new.title {
 		if err := h.setAttribute(ctx, pn, nodeattr.Title, new.title); err != nil {
-			return fmt.Errorf("could not set title for document %v: %v", pn, err)
+			return fmt.Errorf("could not set title for document %v: %w", pn, err)
 		}
 	}
 
 	if !old.docDate.Equal(new.docDate) {
 		if new.docDate.IsZero() {
 			if err := h.delAttribute(ctx, pn, nodeattr.StartDate, ""); err != nil {
-				return fmt.Errorf("could not delete document date for document %v: %v", pn, err)
+				return fmt.Errorf("could not delete document date for document %v: %w", pn, err)
 			}
 		} else {
 			if err := h.setAttribute(ctx, pn, nodeattr.StartDate, new.docDate.UTC().Format(time.RFC3339)); err != nil {
-				return fmt.Errorf("could not set document date for document %v: %v", pn, err)
+				return fmt.Errorf("could not set document date for document %v: %w", pn, err)
 			}
 		}
 	}
@@ -388,18 +388,18 @@ func (h *handler) updateDocument(ctx context.Context, pn blob.Ref, new *document
 	if !old.dueDate.Equal(new.dueDate) {
 		if new.dueDate.IsZero() {
 			if err := h.delAttribute(ctx, pn, nodeattr.PaymentDueDate, ""); err != nil {
-				return fmt.Errorf("could not delete due date for document %v: %v", pn, err)
+				return fmt.Errorf("could not delete due date for document %v: %w", pn, err)
 			}
 		} else {
 			if err := h.setAttribute(ctx, pn, nodeattr.PaymentDueDate, new.dueDate.UTC().Format(time.RFC3339)); err != nil {
-				return fmt.Errorf("could not set due date for document %v: %v", pn, err)
+				return fmt.Errorf("could not set due date for document %v: %w", pn, err)
 			}
 		}
 	}
 
 	if !old.tags.equal(new.tags) {
 		if err := h.updateTags(ctx, pn, old.tags, new.tags); err != nil {
-			return fmt.Errorf("could not update tags for document %v: %v", pn, err)
+			return fmt.Errorf("could not update tags for document %v: %w", pn, err)
 		}
 	}
 	return nil
@@ -419,7 +419,7 @@ func (h *handler) updateTags(ctx context.Context, pn blob.Ref, old, new separate
 			continue
 		}
 		if err := h.delAttribute(ctx, pn, "tag", o); err != nil {
-			return fmt.Errorf("could not delete tag %v: %v", o, err)
+			return fmt.Errorf("could not delete tag %v: %w", o, err)
 		}
 	}
 	// then, add the ones that previously didn't  exist
@@ -435,7 +435,7 @@ func (h *handler) updateTags(ctx context.Context, pn blob.Ref, old, new separate
 			continue
 		}
 		if err := h.addAttribute(ctx, pn, "tag", n); err != nil {
-			return fmt.Errorf("could not add tag %v: %v", n, err)
+			return fmt.Errorf("could not add tag %v: %w", n, err)
 		}
 	}
 	return nil
@@ -444,17 +444,17 @@ func (h *handler) updateTags(ctx context.Context, pn blob.Ref, old, new separate
 func (h *handler) createDocument(ctx context.Context, doc document) (blob.Ref, error) {
 	pn, err := h.newPermanode()
 	if err != nil {
-		return pn, fmt.Errorf("could not create document: %v", err)
+		return pn, fmt.Errorf("could not create document: %w", err)
 	}
 
 	// make it a document
 	if err := h.setAttribute(ctx, pn, nodeattr.Type, documentNodeType); err != nil {
-		return pn, fmt.Errorf("could not set %v as a document: %v", pn, err)
+		return pn, fmt.Errorf("could not set %v as a document: %w", pn, err)
 	}
 
 	// set creationTime
 	if err := h.setAttribute(ctx, pn, nodeattr.DateCreated, doc.creation.UTC().Format(time.RFC3339)); err != nil {
-		return pn, fmt.Errorf("could not set creationTime for document %v: %v", pn, err)
+		return pn, fmt.Errorf("could not set creationTime for document %v: %w", pn, err)
 	}
 
 	// set its pages
@@ -462,7 +462,7 @@ func (h *handler) createDocument(ctx context.Context, doc document) (blob.Ref, e
 	// https://groups.google.com/d/msg/camlistore/xApHFjJKn3M/9Q5BfNbbptkJ
 	for pageNumber, pageRef := range doc.pages {
 		if err := h.setAttribute(ctx, pn, fmt.Sprintf("camliPath:%d", pageNumber), pageRef.String()); err != nil {
-			return pn, fmt.Errorf("could not set document %v page %d to scan %q: %v", doc.permanode, pageNumber, pageRef, err)
+			return pn, fmt.Errorf("could not set document %v page %d to scan %q: %w", doc.permanode, pageNumber, pageRef, err)
 		}
 	}
 
@@ -697,7 +697,7 @@ func (h *handler) fetchDocuments(limit int, opts searchOpts) ([]*document, error
 		}
 		doc, err := h.describeDocument(des)
 		if err != nil {
-			return nil, fmt.Errorf("error describing document %v: %v", br, err)
+			return nil, fmt.Errorf("error describing document %v: %w", br, err)
 		}
 		documents = append(documents, doc)
 	}
@@ -709,7 +709,7 @@ func (h *handler) fetchTags() (map[string]int, error) {
 	// it out when a document adds or removes a tag.
 	docs, err := h.fetchDocuments(-1, searchOpts{tagged: true})
 	if err != nil {
-		return nil, fmt.Errorf("could not fetch all tagged documents: %v", err)
+		return nil, fmt.Errorf("could not fetch all tagged documents: %w", err)
 	}
 	// Dedupe tags
 	count := make(map[string]int)
@@ -752,7 +752,7 @@ func (h *handler) fetchDocumentFunc(blobs []blob.Ref,
 		}
 		doc, err := h.describeDocument(des)
 		if err != nil {
-			return nil, fmt.Errorf("error describing document %v: %v", des.BlobRef, err)
+			return nil, fmt.Errorf("error describing document %v: %w", des.BlobRef, err)
 		}
 		return doc, nil
 	}
@@ -789,7 +789,7 @@ func (h *handler) describeDocument(b *search.DescribedBlob) (*document, error) {
 		if strings.HasPrefix(key, "camliPath:") {
 			pageNumber, err := strconv.ParseInt(strings.TrimPrefix(key, "camliPath:"), 10, 64)
 			if err != nil {
-				return nil, fmt.Errorf("invalid page number %q in document %v: %v", key, b.BlobRef, err)
+				return nil, fmt.Errorf("invalid page number %q in document %v: %w", key, b.BlobRef, err)
 			}
 			pageRef := val[0]
 			br, ok := blob.Parse(pageRef)
@@ -836,14 +836,14 @@ func (h *handler) describeDocument(b *search.DescribedBlob) (*document, error) {
 func (h *handler) breakAndDeleteDoc(ctx context.Context, docRef blob.Ref) error {
 	doc, err := h.fetchDocument(docRef)
 	if err != nil {
-		return fmt.Errorf("document %v not found: %v", docRef, err)
+		return fmt.Errorf("document %v not found: %w", docRef, err)
 	}
 	if err := h.deleteNode(ctx, docRef); err != nil {
-		return fmt.Errorf("could not delete document %v: %v", docRef, err)
+		return fmt.Errorf("could not delete document %v: %w", docRef, err)
 	}
 	for _, page := range doc.pages {
 		if err := h.delAttribute(ctx, page, "document", ""); err != nil {
-			return fmt.Errorf("could not unset document of scan %v: %v", page, err)
+			return fmt.Errorf("could not unset document of scan %v: %w", page, err)
 		}
 	}
 	return nil
@@ -854,14 +854,14 @@ func (h *handler) breakAndDeleteDoc(ctx context.Context, docRef blob.Ref) error
 func (h *handler) deleteDocAndImages(ctx context.Context, docRef blob.Ref) error {
 	doc, err := h.fetchDocument(docRef)
 	if err != nil {
-		return fmt.Errorf("document %v not found: %v", docRef, err)
+		return fmt.Errorf("document %v not found: %w", docRef, err)
 	}
 	if err := h.deleteNode(ctx, docRef); err != nil {
-		return fmt.Errorf("could not delete document %v: %v", docRef, err)
+		return fmt.Errorf("could not delete document %v: %w", docRef, err)
 	}
 	for _, page := range doc.pages {
 		if err := h.deleteNode(ctx, page); err != nil {
-			return fmt.Errorf("could not delete scan %v: %v", page, err)
+			return fmt.Errorf("could not delete scan %v: %w", page, err)
 		}
 	}
 	return nil
diff --git a/app/scanningcabinet/handler.go b/app/scanningcabinet/handler.go
index 0b493f758..074c54e5d 100644
--- a/app/scanningcabinet/handler.go
+++ b/app/scanningcabinet/handler.go
@@ -18,6 +18,7 @@ package main
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"html/template"
 	"io"
@@ -76,11 +77,11 @@ func appConfig() (*extraConfig, error) {
 	}
 	cl, err := app.Client()
 	if err != nil {
-		return nil, fmt.Errorf("could not get a client to fetch extra config: %v", err)
+		return nil, fmt.Errorf("could not get a client to fetch extra config: %w", err)
 	}
 	conf := &extraConfig{}
 	if err := cl.GetJSON(context.Background(), configURL, conf); err != nil {
-		return nil, fmt.Errorf("could not get app extra config at %v: %v", configURL, err)
+		return nil, fmt.Errorf("could not get app extra config at %v: %w", configURL, err)
 	}
 	return conf, nil
 }
@@ -103,7 +104,7 @@ type handler struct {
 func newHandler() (*handler, error) {
 	cl, err := app.Client()
 	if err != nil {
-		return nil, fmt.Errorf("could not initialize a client: %v", err)
+		return nil, fmt.Errorf("could not initialize a client: %w", err)
 	}
 	h := &handler{
 		sh:      cl,
@@ -198,7 +199,7 @@ func (h *handler) disco() error {
 	if server == "" {
 		server, err = h.cl.BlobRoot()
 		if err != nil {
-			return fmt.Errorf("CAMLI_API_HOST var not set, and client could not discover server blob root: %v", err)
+			return fmt.Errorf("CAMLI_API_HOST var not set, and client could not discover server blob root: %w", err)
 		}
 	}
 	h.server = server
@@ -206,7 +207,7 @@ func (h *handler) disco() error {
 	// TODO(mpl): setup our own signer if we got our own key and stuff.
 	signer, err := h.cl.ServerPublicKeyBlobRef()
 	if err != nil {
-		return fmt.Errorf("client has no signing capability and server can't sign for us either: %v", err)
+		return fmt.Errorf("client has no signing capability and server can't sign for us either: %w", err)
 	}
 	h.signer = signer
 	return nil
@@ -321,7 +322,7 @@ func (h *handler) handleUpload(w http.ResponseWriter, r *http.Request) {
 	cr := countingReader{}
 	for {
 		part, err := mr.NextPart()
-		if err == io.EOF {
+		if errors.Is(err, io.EOF) {
 			break
 		}
 		if err != nil {
@@ -337,12 +338,12 @@ func (h *handler) handleUpload(w http.ResponseWriter, r *http.Request) {
 			}
 			creationParam, err := io.ReadAll(part)
 			if err != nil {
-				httputil.ServeError(w, r, fmt.Errorf("could not read provided creation time: %v", err))
+				httputil.ServeError(w, r, fmt.Errorf("could not read provided creation time: %w", err))
 				return
 			}
 			creation, err = time.Parse(time.RFC3339, string(creationParam))
 			if err != nil {
-				httputil.ServeError(w, r, fmt.Errorf("could not parse provided creation time %q: %v", creationParam, err))
+				httputil.ServeError(w, r, fmt.Errorf("could not parse provided creation time %q: %w", creationParam, err))
 				return
 			}
 
@@ -352,7 +353,7 @@ func (h *handler) handleUpload(w http.ResponseWriter, r *http.Request) {
 		cr.r = part
 		br, err = h.cl.UploadFile(ctx, fileName, &cr, nil)
 		if err != nil {
-			httputil.ServeError(w, r, fmt.Errorf("could not write %v to blobserver: %v", fileName, err))
+			httputil.ServeError(w, r, fmt.Errorf("could not write %v to blobserver: %w", fileName, err))
 			return
 		}
 	}
@@ -369,7 +370,7 @@ func (h *handler) handleUpload(w http.ResponseWriter, r *http.Request) {
 			newScan := scan
 			newScan.creation = creation
 			if err := h.updateScan(ctx, scan.permanode, &newScan, &scan); err != nil {
-				httputil.ServeError(w, r, fmt.Errorf("could not update scan: %v", err))
+				httputil.ServeError(w, r, fmt.Errorf("could not update scan: %w", err))
 				return
 			}
 		}
@@ -377,7 +378,7 @@ func (h *handler) handleUpload(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	if !os.IsNotExist(err) {
-		httputil.ServeError(w, r, fmt.Errorf("could not check if scan with %v already exists: %v", fileName, err))
+		httputil.ServeError(w, r, fmt.Errorf("could not check if scan with %v already exists: %w", fileName, err))
 		return
 	}
 
@@ -387,7 +388,7 @@ func (h *handler) handleUpload(w http.ResponseWriter, r *http.Request) {
 	})
 
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("could not create scan object for %v: %v", fileName, err))
+		httputil.ServeError(w, r, fmt.Errorf("could not create scan object for %v: %w", fileName, err))
 		return
 	}
 	io.WriteString(w, scanRef.String())
@@ -426,11 +427,11 @@ func (h *handler) handleResource(w http.ResponseWriter, r *http.Request) {
 
 	mediaObject, err := h.fetchScan(scanRef)
 	if err != nil {
-		if err == os.ErrNotExist {
+		if errors.Is(err, os.ErrNotExist) {
 			http.Error(w, fmt.Sprintf("%v not found", scanRef), http.StatusNotFound)
 			return
 		}
-		httputil.ServeError(w, r, fmt.Errorf("resource %v not found: %v", scanRef, err))
+		httputil.ServeError(w, r, fmt.Errorf("resource %v not found: %w", scanRef, err))
 		return
 	}
 
@@ -447,7 +448,7 @@ func (h *handler) handleResource(w http.ResponseWriter, r *http.Request) {
 	if resizeParam := r.FormValue("resize"); resizeParam != "" {
 		resized, err := strconv.Atoi(resizeParam)
 		if err != nil {
-			httputil.ServeError(w, r, fmt.Errorf("bogus resize param %q: %v", resizeParam, err))
+			httputil.ServeError(w, r, fmt.Errorf("bogus resize param %q: %w", resizeParam, err))
 			return
 		}
 		ih.MaxWidth = resized
@@ -483,7 +484,7 @@ func (h *handler) handleMakedoc(w http.ResponseWriter, r *http.Request) {
 	doc, err = h.fetchDocumentByPages(pages)
 	if err != nil {
 		if !os.IsNotExist(err) {
-			httputil.ServeError(w, r, fmt.Errorf("could not check if document already existed: %v", err))
+			httputil.ServeError(w, r, fmt.Errorf("could not check if document already existed: %w", err))
 			return
 		}
 		newDoc := document{
@@ -492,7 +493,7 @@ func (h *handler) handleMakedoc(w http.ResponseWriter, r *http.Request) {
 		}
 		docRef, err := h.persistDocAndPages(ctx, newDoc)
 		if err != nil {
-			httputil.ServeError(w, r, fmt.Errorf("could not create new document: %v", err))
+			httputil.ServeError(w, r, fmt.Errorf("could not create new document: %w", err))
 			return
 		}
 		newDoc.permanode = docRef
@@ -533,7 +534,7 @@ func (h *handler) handleDoc(w http.ResponseWriter, r *http.Request) {
 		// TODO(mpl): group fetch ?
 		page, err := h.fetchScan(v)
 		if err != nil {
-			httputil.ServeError(w, r, fmt.Errorf("could not fetch page %v for document %v: %v", v, document.permanode, err))
+			httputil.ServeError(w, r, fmt.Errorf("could not fetch page %v for document %v: %w", v, document.permanode, err))
 			return
 		}
 		pages = append(pages, page)
@@ -543,7 +544,7 @@ func (h *handler) handleDoc(w http.ResponseWriter, r *http.Request) {
 	if sizeParam := r.FormValue("size"); sizeParam != "" {
 		sizeint, err := strconv.Atoi(sizeParam)
 		if err != nil {
-			httputil.ServeError(w, r, fmt.Errorf("invalid size param %q: %v", sizeParam, err))
+			httputil.ServeError(w, r, fmt.Errorf("invalid size param %q: %w", sizeParam, err))
 			return
 		}
 		size = sizeint
@@ -572,7 +573,7 @@ func (h *handler) handleDoc(w http.ResponseWriter, r *http.Request) {
 		AllTags:        allTags,
 	}
 	if err := docTemplate.Execute(w, d); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("could not serve doc template: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("could not serve doc template: %w", err))
 		return
 	}
 }
@@ -593,7 +594,7 @@ func (h *handler) handleChangedoc(w http.ResponseWriter, r *http.Request) {
 	mode := r.FormValue("mode")
 	if mode == "break" {
 		if err := h.breakAndDeleteDoc(ctx, docRef); err != nil {
-			httputil.ServeError(w, r, fmt.Errorf("could not delete document %v: %v", docRef, err))
+			httputil.ServeError(w, r, fmt.Errorf("could not delete document %v: %w", docRef, err))
 			return
 		}
 		fmt.Fprintf(w, "<html><body>[&lt;&lt; <a href='%s'>Back</a>] Doc %s deleted and images broken out as un-annotated.</body></html>", baseURL(r), docRef)
@@ -601,7 +602,7 @@ func (h *handler) handleChangedoc(w http.ResponseWriter, r *http.Request) {
 	}
 	if mode == "delete" {
 		if err := h.deleteDocAndImages(ctx, docRef); err != nil {
-			httputil.ServeError(w, r, fmt.Errorf("could not do full delete of %v: %v", docRef, err))
+			httputil.ServeError(w, r, fmt.Errorf("could not do full delete of %v: %w", docRef, err))
 			return
 		}
 		fmt.Fprintf(w, "<html><body>[&lt;&lt; <a href='%s'>Back</a>] Doc %s and its images deleted.</body></html>", baseURL(r), docRef)
@@ -615,20 +616,20 @@ func (h *handler) handleChangedoc(w http.ResponseWriter, r *http.Request) {
 
 	docDate, err := dateOrZero(r.FormValue("date"), dateformatYyyyMmDd)
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("could not assign new date to document: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("could not assign new date to document: %w", err))
 		return
 	}
 	document.docDate = docDate
 
 	duedate, err := dateOrZero(r.FormValue("due_date"), dateformatYyyyMmDd)
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("could not assign new due date to document: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("could not assign new due date to document: %w", err))
 		return
 	}
 	document.dueDate = duedate
 
 	if err := h.updateDocument(ctx, docRef, document); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("could not update document %v: %v", docRef, err))
+		httputil.ServeError(w, r, fmt.Errorf("could not update document %v: %w", docRef, err))
 		return
 	}
 
diff --git a/app/scanningcabinet/scancab/pdf.go b/app/scanningcabinet/scancab/pdf.go
index c638cc263..0fc50851b 100644
--- a/app/scanningcabinet/scancab/pdf.go
+++ b/app/scanningcabinet/scancab/pdf.go
@@ -38,7 +38,7 @@ func pageCountPoppler(filename string) (cnt int, err error) {
 	cmd := exec.Command("pdfinfo", filename)
 	out, err := cmd.CombinedOutput()
 	if err != nil {
-		return cnt, fmt.Errorf("could not get page count with pdfinfo: %v, %v", err, string(out))
+		return cnt, fmt.Errorf("could not get page count with pdfinfo: %w, %v", err, string(out))
 	}
 	sc := bufio.NewScanner(bytes.NewReader(out))
 	for sc.Scan() {
diff --git a/app/scanningcabinet/scancab/scancab.go b/app/scanningcabinet/scancab/scancab.go
index 07e79a2f3..d1127824e 100644
--- a/app/scanningcabinet/scancab/scancab.go
+++ b/app/scanningcabinet/scancab/scancab.go
@@ -98,7 +98,7 @@ func getUploadURL() (string, error) {
 	}
 	uploadURL := string(body)
 	if _, err := url.ParseRequestURI(uploadURL); err != nil {
-		return "", fmt.Errorf("could not parse upload URL returned by server (%q): %v", uploadURL, err)
+		return "", fmt.Errorf("could not parse upload URL returned by server (%q): %w", uploadURL, err)
 	}
 	return uploadURL, nil
 }
diff --git a/clients/web/embed/closure/updatelibrary.go b/clients/web/embed/closure/updatelibrary.go
index cd76b776a..a9d303232 100644
--- a/clients/web/embed/closure/updatelibrary.go
+++ b/clients/web/embed/closure/updatelibrary.go
@@ -205,7 +205,7 @@ func cpDir(src, dst string) error {
 		}
 		suffix, err := filepath.Rel(closureGitDir, path)
 		if err != nil {
-			return fmt.Errorf("Failed to find Rel(%q, %q): %v", closureGitDir, path, err)
+			return fmt.Errorf("Failed to find Rel(%q, %q): %w", closureGitDir, path, err)
 		}
 		base := fi.Name()
 		if fi.IsDir() {
diff --git a/cmd/pk-get/get.go b/cmd/pk-get/get.go
index 93be76346..8c5ad4c0e 100644
--- a/cmd/pk-get/get.go
+++ b/cmd/pk-get/get.go
@@ -196,7 +196,7 @@ func fetch(ctx context.Context, src blob.Fetcher, br blob.Ref) (r io.ReadCloser,
 	}
 	r, _, err = src.Fetch(ctx, br)
 	if err != nil {
-		return nil, fmt.Errorf("Failed to fetch %s: %s", br, err)
+		return nil, fmt.Errorf("Failed to fetch %s: %w", br, err)
 	}
 	return r, err
 }
@@ -230,7 +230,7 @@ func smartFetch(ctx context.Context, src blob.Fetcher, targ string, br blob.Ref)
 		// opaque data - put it in a file
 		f, err := os.Create(targ)
 		if err != nil {
-			return fmt.Errorf("opaque: %v", err)
+			return fmt.Errorf("opaque: %w", err)
 		}
 		defer f.Close()
 		body, _ := sniffer.Body()
@@ -294,7 +294,7 @@ func smartFetch(ctx context.Context, src blob.Fetcher, targ string, br blob.Ref)
 	case schema.TypeFile:
 		fr, err := schema.NewFileReader(ctx, src, br)
 		if err != nil {
-			return fmt.Errorf("NewFileReader: %v", err)
+			return fmt.Errorf("NewFileReader: %w", err)
 		}
 		fr.LoadAllChunks()
 		defer fr.Close()
@@ -314,11 +314,11 @@ func smartFetch(ctx context.Context, src blob.Fetcher, targ string, br blob.Ref)
 
 		f, err := os.Create(name)
 		if err != nil {
-			return fmt.Errorf("file type: %v", err)
+			return fmt.Errorf("file type: %w", err)
 		}
 		defer f.Close()
 		if _, err := io.Copy(f, fr); err != nil {
-			return fmt.Errorf("Copying %s to %s: %v", br, name, err)
+			return fmt.Errorf("Copying %s to %s: %w", br, name, err)
 		}
 		if err := setFileMeta(name, b); err != nil {
 			log.Print(err)
@@ -378,12 +378,12 @@ func smartFetch(ctx context.Context, src blob.Fetcher, targ string, br blob.Ref)
 		}
 
 		err = osutil.Mkfifo(name, 0600)
-		if err == osutil.ErrNotSupported {
+		if errors.Is(err, osutil.ErrNotSupported) {
 			log.Printf("Skipping FIFO %s: Unsupported filetype", name)
 			return nil
 		}
 		if err != nil {
-			return fmt.Errorf("%s: osutil.Mkfifo(): %v", name, err)
+			return fmt.Errorf("%s: osutil.Mkfifo(): %w", name, err)
 		}
 
 		if err := setFileMeta(name, b); err != nil {
@@ -412,12 +412,12 @@ func smartFetch(ctx context.Context, src blob.Fetcher, targ string, br blob.Ref)
 		}
 
 		err = osutil.Mksocket(name)
-		if err == osutil.ErrNotSupported {
+		if errors.Is(err, osutil.ErrNotSupported) {
 			log.Printf("Skipping socket %s: Unsupported filetype", name)
 			return nil
 		}
 		if err != nil {
-			return fmt.Errorf("%s: %v", name, err)
+			return fmt.Errorf("%s: %w", name, err)
 		}
 
 		if err := setFileMeta(name, b); err != nil {
diff --git a/cmd/pk-put/blobs.go b/cmd/pk-put/blobs.go
index 76fa7c166..950c1b6e3 100644
--- a/cmd/pk-put/blobs.go
+++ b/cmd/pk-put/blobs.go
@@ -113,7 +113,7 @@ func (c *blobCmd) RunCommand(args []string) error {
 	if c.makePermanode {
 		permaNode, err = up.UploadNewPermanode(ctxbg)
 		if err != nil {
-			return fmt.Errorf("Uploading permanode: %v", err)
+			return fmt.Errorf("Uploading permanode: %w", err)
 		}
 	}
 
diff --git a/cmd/pk-put/files.go b/cmd/pk-put/files.go
index eff513fc3..41f57e926 100644
--- a/cmd/pk-put/files.go
+++ b/cmd/pk-put/files.go
@@ -181,7 +181,7 @@ func (c *fileCmd) RunCommand(args []string) error {
 		}
 		permaNode, err = up.UploadNewPermanode(ctxbg)
 		if err != nil {
-			return fmt.Errorf("Uploading permanode: %v", err)
+			return fmt.Errorf("Uploading permanode: %w", err)
 		}
 	}
 	if c.diskUsage {
@@ -218,7 +218,7 @@ func (c *fileCmd) RunCommand(args []string) error {
 			if path = strings.TrimSpace(path); path != "" {
 				tu.Enqueue(path)
 			}
-			if err == io.EOF {
+			if errors.Is(err, io.EOF) {
 				android.PreExit()
 				os.Exit(0)
 			}
@@ -361,7 +361,7 @@ func (n *node) directoryStaticSet() ([]*schema.Blob, error) {
 	for _, c := range n.children {
 		pr, err := c.PutResult()
 		if err != nil {
-			return nil, fmt.Errorf("Error populating directory static set for child %q: %v", c.fullPath, err)
+			return nil, fmt.Errorf("Error populating directory static set for child %q: %w", c.fullPath, err)
 		}
 		members = append(members, pr.BlobRef)
 	}
@@ -677,7 +677,7 @@ func (up *Uploader) uploadNodeRegularFile(ctx context.Context, n *node) (*client
 		}
 		err = up.uploadFilePermanode(ctx, wholeRef[0].String(), br, claimTime)
 		if err != nil {
-			return nil, fmt.Errorf("Error uploading permanode for node %v: %v", n, err)
+			return nil, fmt.Errorf("Error uploading permanode for node %v: %w", n, err)
 		}
 	}
 
@@ -704,7 +704,7 @@ func (up *Uploader) uploadFilePermanode(ctx context.Context, sum string, fileRef
 	permaNodeSigTime := time.Unix(0, 0)
 	permaNode, err := up.UploadPlannedPermanode(ctx, sum, permaNodeSigTime)
 	if err != nil {
-		return fmt.Errorf("Error uploading planned permanode: %v", err)
+		return fmt.Errorf("Error uploading planned permanode: %w", err)
 	}
 	handleResult("node-permanode", permaNode, nil)
 
@@ -716,11 +716,11 @@ func (up *Uploader) uploadFilePermanode(ctx context.Context, sum string, fileRef
 	}
 	signed, err := contentAttr.SignAt(ctx, signer, claimTime)
 	if err != nil {
-		return fmt.Errorf("Failed to sign content claim: %v", err)
+		return fmt.Errorf("Failed to sign content claim: %w", err)
 	}
 	put, err := up.uploadString(ctx, signed)
 	if err != nil {
-		return fmt.Errorf("Error uploading permanode's attribute: %v", err)
+		return fmt.Errorf("Error uploading permanode's attribute: %w", err)
 	}
 
 	handleResult("node-permanode-contentattr", put, nil)
@@ -732,12 +732,12 @@ func (up *Uploader) uploadFilePermanode(ctx context.Context, sum string, fileRef
 				m.SetClaimDate(claimTime)
 				signed, err := m.SignAt(ctx, signer, claimTime)
 				if err != nil {
-					errch <- fmt.Errorf("Failed to sign tag claim: %v", err)
+					errch <- fmt.Errorf("Failed to sign tag claim: %w", err)
 					return
 				}
 				put, err := up.uploadString(ctx, signed)
 				if err != nil {
-					errch <- fmt.Errorf("Error uploading permanode's tag attribute %v: %v", tag, err)
+					errch <- fmt.Errorf("Error uploading permanode's tag attribute %v: %w", tag, err)
 					return
 				}
 				handleResult("node-permanode-tag", put, nil)
diff --git a/cmd/pk-put/init.go b/cmd/pk-put/init.go
index 758eb2acb..4b7af9a5f 100644
--- a/cmd/pk-put/init.go
+++ b/cmd/pk-put/init.go
@@ -106,7 +106,7 @@ func (c *initCmd) initSecretRing() error {
 	}
 	if _, err := os.Stat(c.secretRing); err != nil {
 		hint := "\nA GPG key is required, please use 'pk-put init --newkey'.\n\nOr if you know what you're doing, you can set the global pk-put flag --secret-keyring, or the CAMLI_SECRET_RING env var, to use your own GPG ring. And --gpgkey=<pubid> or GPGKEY to select which key ID to use."
-		return fmt.Errorf("Could not use secret ring file %v: %v.\n%v", c.secretRing, err, hint)
+		return fmt.Errorf("Could not use secret ring file %v: %w.\n%v", c.secretRing, err, hint)
 	}
 	return nil
 }
@@ -122,7 +122,7 @@ func (c *initCmd) initKeyId() error {
 		log.Printf("Re-using identity with keyId %q found in file %s", c.keyId, c.secretRing)
 	} else {
 		hint := "You can set --gpgkey=<pubid> or the GPGKEY env var to select which key ID to use.\n"
-		return fmt.Errorf("No suitable gpg key was found in %v: %v.\n%v", c.secretRing, err, hint)
+		return fmt.Errorf("No suitable gpg key was found in %v: %w.\n%v", c.secretRing, err, hint)
 	}
 	c.keyId = strings.TrimPrefix(
 		strings.ReplaceAll(c.keyId, " ", ""),
@@ -134,11 +134,11 @@ func (c *initCmd) initKeyId() error {
 func (c *initCmd) getPublicKeyArmored() ([]byte, error) {
 	entity, err := jsonsign.EntityFromSecring(c.keyId, c.secretRing)
 	if err != nil {
-		return nil, fmt.Errorf("Could not find keyId %v in ring %v: %v", c.keyId, c.secretRing, err)
+		return nil, fmt.Errorf("Could not find keyId %v in ring %v: %w", c.keyId, c.secretRing, err)
 	}
 	pubArmor, err := jsonsign.ArmoredPublicKey(entity)
 	if err != nil {
-		return nil, fmt.Errorf("failed to export armored public key ID %q from %v: %v", c.keyId, c.secretRing, err)
+		return nil, fmt.Errorf("failed to export armored public key ID %q from %v: %w", c.keyId, c.secretRing, err)
 	}
 	return []byte(pubArmor), nil
 }
@@ -192,7 +192,7 @@ func (c *initCmd) writeConfig(cc *clientconfig.Config) error {
 		log.Fatalf("JSON serialization error: %v", err)
 	}
 	if err := os.WriteFile(configFilePath, jsonBytes, 0600); err != nil {
-		return fmt.Errorf("could not write client config file %v: %v", configFilePath, err)
+		return fmt.Errorf("could not write client config file %v: %w", configFilePath, err)
 	}
 	log.Printf("Wrote %q; modify as necessary.", configFilePath)
 	return nil
diff --git a/cmd/pk-put/kvcache.go b/cmd/pk-put/kvcache.go
index de0b40437..19cbacc67 100644
--- a/cmd/pk-put/kvcache.go
+++ b/cmd/pk-put/kvcache.go
@@ -76,11 +76,11 @@ func NewKvHaveCache(gen string) *KvHaveCache {
 func maybeRunCompaction(dbname string, db *leveldb.DB) error {
 	val, err := db.GetProperty("leveldb.num-files-at-level0")
 	if err != nil {
-		return fmt.Errorf("could not get number of level-0 files of %v's LevelDB: %v", dbname, err)
+		return fmt.Errorf("could not get number of level-0 files of %v's LevelDB: %w", dbname, err)
 	}
 	nbFiles, err := strconv.Atoi(val)
 	if err != nil {
-		return fmt.Errorf("could not convert number of level-0 files to int: %v", err)
+		return fmt.Errorf("could not convert number of level-0 files to int: %w", err)
 	}
 	// Only force compaction if we're at the default trigger (4), see
 	// github.com/syndtr/goleveldb/leveldb/opt.DefaultCompactionL0Trigger
@@ -88,7 +88,7 @@ func maybeRunCompaction(dbname string, db *leveldb.DB) error {
 		return nil
 	}
 	if err := db.CompactRange(util.Range{Start: nil, Limit: nil}); err != nil {
-		return fmt.Errorf("could not run compaction on %v's LevelDB: %v", dbname, err)
+		return fmt.Errorf("could not run compaction on %v's LevelDB: %w", dbname, err)
 	}
 	return nil
 }
@@ -106,7 +106,7 @@ func (c *KvHaveCache) StatBlobCache(br blob.Ref) (size uint32, ok bool) {
 	binBr, _ := br.MarshalBinary()
 	binVal, err := c.db.Get(binBr, nil)
 	if err != nil {
-		if err == leveldb.ErrNotFound {
+		if errors.Is(err, leveldb.ErrNotFound) {
 			cachelog.Printf("have cache MISS on %v", br)
 			return
 		}
@@ -180,7 +180,7 @@ func (c *KvStatCache) CachedPutResult(pwd, filename string, fi os.FileInfo, with
 	}
 	binVal, err := c.db.Get(binKey, nil)
 	if err != nil {
-		if err == leveldb.ErrNotFound {
+		if errors.Is(err, leveldb.ErrNotFound) {
 			cachelog.Printf("stat cache MISS on %q", binKey)
 			return nil, errCacheMiss
 		}
@@ -188,7 +188,7 @@ func (c *KvStatCache) CachedPutResult(pwd, filename string, fi os.FileInfo, with
 	}
 	val := &statCacheValue{}
 	if err = val.unmarshalBinary(binVal); err != nil {
-		return nil, fmt.Errorf("Bogus stat cached value for %q: %v", binKey, err)
+		return nil, fmt.Errorf("Bogus stat cached value for %q: %w", binKey, err)
 	}
 	fp := fileInfoToFingerprint(fi)
 	if val.Fingerprint != fp {
@@ -261,23 +261,23 @@ func (scv *statCacheValue) marshalBinary() ([]byte, error) {
 	buf := bytes.NewBuffer(data)
 	_, err := buf.WriteString(string(scv.Fingerprint))
 	if err != nil {
-		return nil, fmt.Errorf("Could not write fingerprint %v: %v", scv.Fingerprint, err)
+		return nil, fmt.Errorf("Could not write fingerprint %v: %w", scv.Fingerprint, err)
 	}
 	err = buf.WriteByte('|')
 	if err != nil {
-		return nil, fmt.Errorf("Could not write '|': %v", err)
+		return nil, fmt.Errorf("Could not write '|': %w", err)
 	}
 	err = binary.Write(buf, binary.BigEndian, int32(scv.Result.Size))
 	if err != nil {
-		return nil, fmt.Errorf("Could not write blob size %d: %v", scv.Result.Size, err)
+		return nil, fmt.Errorf("Could not write blob size %d: %w", scv.Result.Size, err)
 	}
 	err = buf.WriteByte('|')
 	if err != nil {
-		return nil, fmt.Errorf("Could not write '|': %v", err)
+		return nil, fmt.Errorf("Could not write '|': %w", err)
 	}
 	_, err = buf.Write(binBr)
 	if err != nil {
-		return nil, fmt.Errorf("Could not write binary blobref %q: %v", binBr, err)
+		return nil, fmt.Errorf("Could not write binary blobref %q: %w", binBr, err)
 	}
 	return buf.Bytes(), nil
 }
@@ -301,11 +301,11 @@ func (scv *statCacheValue) unmarshalBinary(data []byte) error {
 	var size int32
 	err := binary.Read(buf, binary.BigEndian, &size)
 	if err != nil {
-		return fmt.Errorf("Could not decode blob size from stat cache value part %q: %v", parts[1], err)
+		return fmt.Errorf("Could not decode blob size from stat cache value part %q: %w", parts[1], err)
 	}
 	br := new(blob.Ref)
 	if err := br.UnmarshalBinary(parts[2]); err != nil {
-		return fmt.Errorf("Could not unmarshalBinary for %q: %v", parts[2], err)
+		return fmt.Errorf("Could not unmarshalBinary for %q: %w", parts[2], err)
 	}
 
 	scv.Fingerprint = statFingerprint(fingerprint)
diff --git a/cmd/pk-put/put.go b/cmd/pk-put/put.go
index 1a26c1ec4..edce8af24 100644
--- a/cmd/pk-put/put.go
+++ b/cmd/pk-put/put.go
@@ -153,7 +153,7 @@ func proxyFromEnvironment(req *http.Request) (*url.URL, error) {
 		}
 	}
 	if err != nil {
-		return nil, fmt.Errorf("invalid proxy address %q: %v", proxy, err)
+		return nil, fmt.Errorf("invalid proxy address %q: %w", proxy, err)
 	}
 	return proxyURL, nil
 }
diff --git a/cmd/pk/dbinit.go b/cmd/pk/dbinit.go
index 44313bdd3..a8cb9b921 100644
--- a/cmd/pk/dbinit.go
+++ b/cmd/pk/dbinit.go
@@ -162,7 +162,7 @@ func (c *dbinitCmd) RunCommand(args []string) error {
 		db, err = sql.Open("mysql", c.mysqlDSN(dbname))
 	}
 	if err != nil {
-		return fmt.Errorf("Connecting to the %s %s database: %v", dbname, c.dbType, err)
+		return fmt.Errorf("Connecting to the %s %s database: %w", dbname, c.dbType, err)
 	}
 	defer db.Close()
 
diff --git a/cmd/pk/describe.go b/cmd/pk/describe.go
index b33f74f2d..fd03b2b1f 100644
--- a/cmd/pk/describe.go
+++ b/cmd/pk/describe.go
@@ -75,7 +75,7 @@ func (c *desCmd) RunCommand(args []string) error {
 		var err error
 		at, err = time.Parse(time.RFC3339, c.at)
 		if err != nil {
-			return fmt.Errorf("error parsing --at value %q: %v", c.at, err)
+			return fmt.Errorf("error parsing --at value %q: %w", c.at, err)
 		}
 	}
 
diff --git a/cmd/pk/googinit.go b/cmd/pk/googinit.go
index bd9d398b4..09c843319 100644
--- a/cmd/pk/googinit.go
+++ b/cmd/pk/googinit.go
@@ -96,7 +96,7 @@ func (c *googinitCmd) RunCommand(args []string) error {
 		},
 	}).Token()
 	if err != nil {
-		return fmt.Errorf("could not acquire token: %v", err)
+		return fmt.Errorf("could not acquire token: %w", err)
 	}
 
 	fmt.Fprintf(cmdmain.Stdout, "\nYour Google auth object:\n\n")
diff --git a/cmd/pk/index.go b/cmd/pk/index.go
index 30aa0b8eb..40a588f96 100644
--- a/cmd/pk/index.go
+++ b/cmd/pk/index.go
@@ -58,13 +58,13 @@ func (c *indexCmd) RunCommand(args []string) error {
 	dc := c.discoClient()
 	syncHandlers, err := dc.SyncHandlers()
 	if err != nil {
-		return fmt.Errorf("sync handlers discovery failed: %v", err)
+		return fmt.Errorf("sync handlers discovery failed: %w", err)
 	}
 
 	for _, sh := range syncHandlers {
 		if sh.ToIndex {
 			if err := c.sync(sh.From, sh.To); err != nil {
-				return fmt.Errorf("Error while indexing from %v to %v: %v", sh.From, sh.To, err)
+				return fmt.Errorf("Error while indexing from %v to %v: %w", sh.From, sh.To, err)
 			}
 		}
 	}
diff --git a/cmd/pk/list.go b/cmd/pk/list.go
index eecdb7710..873c87821 100644
--- a/cmd/pk/list.go
+++ b/cmd/pk/list.go
@@ -81,7 +81,7 @@ func (c *listCmd) RunCommand(args []string) error {
 	defer func() { cmdmain.Stdout = stdout }()
 	pr, pw, err := os.Pipe()
 	if err != nil {
-		return fmt.Errorf("Could not create pipe to read from stdout: %v", err)
+		return fmt.Errorf("Could not create pipe to read from stdout: %w", err)
 	}
 	defer pr.Close()
 	cmdmain.Stdout = pw
@@ -110,7 +110,7 @@ func (c *listCmd) RunCommand(args []string) error {
 			Depth:    1,
 		})
 		if err != nil {
-			return fmt.Errorf("Error when describing blobs %v: %v", blobRefs, err)
+			return fmt.Errorf("Error when describing blobs %v: %w", blobRefs, err)
 		}
 		for _, v := range blobRefs {
 			blob, ok := described.Meta[v.String()]
@@ -147,7 +147,7 @@ func (c *listCmd) RunCommand(args []string) error {
 		return err
 	}
 	if err := scanner.Err(); err != nil {
-		return fmt.Errorf("Error reading on pipe from stdout: %v", err)
+		return fmt.Errorf("Error reading on pipe from stdout: %w", err)
 	}
 	return nil
 }
@@ -156,7 +156,7 @@ func (c *listCmd) RunCommand(args []string) error {
 func (c *listCmd) setClient() error {
 	ss, err := c.syncCmd.storageFromParam("src", c.syncCmd.src)
 	if err != nil {
-		return fmt.Errorf("Could not set client for describe requests: %v", err)
+		return fmt.Errorf("Could not set client for describe requests: %w", err)
 	}
 	var ok bool
 	c.cl, ok = ss.(*client.Client)
diff --git a/cmd/pk/packblobs.go b/cmd/pk/packblobs.go
index 0ade2f92d..48db3f41d 100644
--- a/cmd/pk/packblobs.go
+++ b/cmd/pk/packblobs.go
@@ -18,6 +18,7 @@ package main
 
 import (
 	"bytes"
+	"errors"
 	"flag"
 	"fmt"
 	"io"
@@ -72,7 +73,7 @@ func (c *packBlobsCmd) RunCommand(args []string) error {
 	cl := newClient(c.server)
 	looseClient, err := cl.NewPathClient("/bs-loose/")
 	if err != nil {
-		return fmt.Errorf("NewPathClient: %v", err)
+		return fmt.Errorf("NewPathClient: %w", err)
 	}
 
 	res, err := cl.Query(ctxbg, req)
@@ -86,7 +87,7 @@ func (c *packBlobsCmd) RunCommand(args []string) error {
 		n++
 		fileRef := sr.Blob
 		rc, _, err := looseClient.Fetch(ctxbg, fileRef)
-		if err == os.ErrNotExist {
+		if errors.Is(err, os.ErrNotExist) {
 			fmt.Printf("%d/%d: %v already done\n", n, total, fileRef)
 			continue
 		}
diff --git a/cmd/pk/search.go b/cmd/pk/search.go
index cb9fd55d9..c152e2f99 100644
--- a/cmd/pk/search.go
+++ b/cmd/pk/search.go
@@ -18,6 +18,7 @@ package main
 
 import (
 	"encoding/json"
+	"errors"
 	"flag"
 	"fmt"
 	"io"
@@ -92,7 +93,8 @@ func (c *searchCmd) RunCommand(args []string) error {
 		req.Continue = "" // clear this as well
 
 		if err := json.NewDecoder(strings.NewReader(q)).Decode(&req); err != nil {
-			if se, ok := err.(*json.SyntaxError); ok {
+			var se *json.SyntaxError
+			if errors.As(err, &se) {
 				line, col, msg := errorutil.HighlightBytePosition(strings.NewReader(q), se.Offset)
 				fmt.Fprintf(os.Stderr, "JSON syntax error at line %d, column %d parsing SearchQuery (https://godoc.org/perkeep.org/pkg/search#SearchQuery):\n%s\n", line, col, msg)
 			}
@@ -107,7 +109,8 @@ func (c *searchCmd) RunCommand(args []string) error {
 	} else if strutil.IsPlausibleJSON(q) {
 		cs := new(search.Constraint)
 		if err := json.NewDecoder(strings.NewReader(q)).Decode(&cs); err != nil {
-			if se, ok := err.(*json.SyntaxError); ok {
+			var se *json.SyntaxError
+			if errors.As(err, &se) {
 				line, col, msg := errorutil.HighlightBytePosition(strings.NewReader(q), se.Offset)
 				fmt.Fprintf(os.Stderr, "JSON syntax error at line %d, column %d parsing Constraint (https://godoc.org/perkeep.org/pkg/search#Constraint):\n%s\n", line, col, msg)
 			}
diff --git a/cmd/pk/splits.go b/cmd/pk/splits.go
index f48e8a0c6..7f9c47686 100644
--- a/cmd/pk/splits.go
+++ b/cmd/pk/splits.go
@@ -18,6 +18,7 @@ package main
 
 import (
 	"bufio"
+	"errors"
 	"fmt"
 	"io"
 	"log"
@@ -48,7 +49,7 @@ func showSplits(file string) {
 	for {
 		c, err := bufr.ReadByte()
 		if err != nil {
-			if err == io.EOF {
+			if errors.Is(err, io.EOF) {
 				if n != last {
 					spans = append(spans, span{from: last, to: n})
 				}
diff --git a/cmd/pk/sync.go b/cmd/pk/sync.go
index 2a1c85286..25a52bc49 100644
--- a/cmd/pk/sync.go
+++ b/cmd/pk/sync.go
@@ -98,14 +98,14 @@ func (c *syncCmd) RunCommand(args []string) error {
 	if c.dumpConfigFlag {
 		err := c.dumpConfig()
 		if err != nil {
-			return fmt.Errorf("dumb-config failed: %v", err)
+			return fmt.Errorf("dumb-config failed: %w", err)
 		}
 		return nil
 	}
 	if c.all {
 		err := c.syncAll()
 		if err != nil {
-			return fmt.Errorf("sync all failed: %v", err)
+			return fmt.Errorf("sync all failed: %w", err)
 		}
 		return nil
 	}
@@ -138,7 +138,7 @@ func (c *syncCmd) RunCommand(args []string) error {
 		stats, err := c.doPass(ss, ds, ts)
 		cmdmain.Logf("sync stats - pass: %d, blobs: %d, bytes %d\n", passNum, stats.BlobsCopied, stats.BytesCopied)
 		if err != nil {
-			return fmt.Errorf("sync failed: %v", err)
+			return fmt.Errorf("sync failed: %w", err)
 		}
 		if !c.loop {
 			break
@@ -168,7 +168,7 @@ func (c *syncCmd) storageFromParam(which storageType, val string) (blobserver.St
 			discl := c.discoClient()
 			src, err := discl.BlobRoot()
 			if err != nil {
-				return nil, fmt.Errorf("Failed to discover source server's blob path: %v", err)
+				return nil, fmt.Errorf("Failed to discover source server's blob path: %w", err)
 			}
 			val = src
 			httpClient = discl.HTTPClient()
@@ -183,20 +183,20 @@ func (c *syncCmd) storageFromParam(which storageType, val string) (blobserver.St
 	if looksLikePath(val) {
 		disk, err := localdisk.New(val)
 		if err != nil {
-			return nil, fmt.Errorf("Interpreted --%v=%q as a local disk path, but got error: %v", which, val, err)
+			return nil, fmt.Errorf("Interpreted --%v=%q as a local disk path, but got error: %w", which, val, err)
 		}
 		c.oneIsDisk = true
 		return disk, nil
 	}
 	cl, err := client.New(client.OptionServer(val), client.OptionInsecure(c.insecureTLS))
 	if err != nil {
-		return nil, fmt.Errorf("creating client for %q: %v", val, err)
+		return nil, fmt.Errorf("creating client for %q: %w", val, err)
 	}
 	if httpClient != nil {
 		cl.SetHTTPClient(httpClient)
 	}
 	if err := cl.SetupAuth(); err != nil {
-		return nil, fmt.Errorf("could not setup auth for connecting to %v: %v", val, err)
+		return nil, fmt.Errorf("could not setup auth for connecting to %v: %w", val, err)
 	}
 	cl.Verbose = *cmdmain.FlagVerbose
 	cl.Logger = log.New(cmdmain.Stderr, "", log.LstdFlags)
@@ -239,7 +239,7 @@ func (c *syncCmd) dumpConfig() error {
 	dc.Logger = log.New(cmdmain.Stderr, "", log.LstdFlags)
 	syncHandlers, err := dc.SyncHandlers()
 	if err != nil {
-		return fmt.Errorf("sync handlers discovery failed: %v", err)
+		return fmt.Errorf("sync handlers discovery failed: %w", err)
 	}
 	for _, sh := range syncHandlers {
 		fmt.Printf("%v -> %v\n", sh.From, sh.To)
@@ -261,7 +261,7 @@ func (c *syncCmd) syncAll() error {
 	dc := c.discoClient()
 	syncHandlers, err := dc.SyncHandlers()
 	if err != nil {
-		return fmt.Errorf("sync handlers discovery failed: %v", err)
+		return fmt.Errorf("sync handlers discovery failed: %w", err)
 	}
 	cmdmain.Logf("To be synced:\n")
 	for _, sh := range syncHandlers {
@@ -270,22 +270,22 @@ func (c *syncCmd) syncAll() error {
 	for _, sh := range syncHandlers {
 		from, err := client.New(client.OptionServer(sh.From), client.OptionInsecure(c.insecureTLS))
 		if err != nil {
-			return fmt.Errorf("creating source client from %q: %v", sh.From, err)
+			return fmt.Errorf("creating source client from %q: %w", sh.From, err)
 		}
 		from.Verbose = *cmdmain.FlagVerbose
 		from.Logger = log.New(cmdmain.Stderr, "", log.LstdFlags)
 		if err := from.SetupAuth(); err != nil {
-			return fmt.Errorf("could not setup auth for connecting to %v: %v", sh.From, err)
+			return fmt.Errorf("could not setup auth for connecting to %v: %w", sh.From, err)
 		}
 
 		to, err := client.New(client.OptionServer(sh.To), client.OptionInsecure(c.insecureTLS))
 		if err != nil {
-			return fmt.Errorf("creating destination client to %q: %v", sh.To, err)
+			return fmt.Errorf("creating destination client to %q: %w", sh.To, err)
 		}
 		to.Verbose = *cmdmain.FlagVerbose
 		to.Logger = log.New(cmdmain.Stderr, "", log.LstdFlags)
 		if err := to.SetupAuth(); err != nil {
-			return fmt.Errorf("could not setup auth for connecting to %v: %v", sh.To, err)
+			return fmt.Errorf("could not setup auth for connecting to %v: %w", sh.To, err)
 		}
 		cmdmain.Logf("Now syncing: %v -> %v", sh.From, sh.To)
 		stats, err := c.doPass(from, to, nil)
@@ -356,7 +356,7 @@ func (c *syncCmd) doPass(src, dest, thirdLeg blobserver.Storage) (stats SyncStat
 	go enumerate(srcErr, src, srcBlobs)
 	checkSourceError := func() {
 		if err := <-srcErr; err != nil && err != context.Canceled {
-			retErr = fmt.Errorf("Enumerate error from source: %v", err)
+			retErr = fmt.Errorf("Enumerate error from source: %w", err)
 		}
 	}
 
@@ -377,7 +377,7 @@ func (c *syncCmd) doPass(src, dest, thirdLeg blobserver.Storage) (stats SyncStat
 	go enumerate(destErr, dest, destBlobs)
 	checkDestError := func() {
 		if err := <-destErr; err != nil && err != context.Canceled {
-			retErr = fmt.Errorf("Enumerate error from destination: %v", err)
+			retErr = fmt.Errorf("Enumerate error from destination: %w", err)
 		}
 	}
 
@@ -415,7 +415,7 @@ func (c *syncCmd) doPass(src, dest, thirdLeg blobserver.Storage) (stats SyncStat
 		go enumerate(thirdErr, thirdLeg, thirdBlobs)
 		checkThirdError = func() {
 			if err := <-thirdErr; err != nil && err != context.Canceled {
-				retErr = fmt.Errorf("Enumerate error from third leg: %v", err)
+				retErr = fmt.Errorf("Enumerate error from third leg: %w", err)
 			}
 		}
 		thirdNeedBlobs := make(chan blob.SizedRef)
diff --git a/cmd/pk/whoami.go b/cmd/pk/whoami.go
index a2e55119b..aea2d58b9 100644
--- a/cmd/pk/whoami.go
+++ b/cmd/pk/whoami.go
@@ -61,11 +61,11 @@ func (c *whoamiCmd) RunCommand(args []string) error {
 
 	cc, err := client.New()
 	if err != nil {
-		return fmt.Errorf("creating Client: %v", err)
+		return fmt.Errorf("creating Client: %w", err)
 	}
 	signer, err := cc.Signer()
 	if err != nil {
-		return fmt.Errorf("no configured Signer: %v", err)
+		return fmt.Errorf("no configured Signer: %w", err)
 	}
 
 	cmd, ok := whoCmds[args[0]]
diff --git a/dev/devcam/devcam.go b/dev/devcam/devcam.go
index 17cf5d705..37ead76e4 100644
--- a/dev/devcam/devcam.go
+++ b/dev/devcam/devcam.go
@@ -64,7 +64,7 @@ func runExec(bin string, args []string, env *Env) error {
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 	if err := cmd.Start(); err != nil {
-		return fmt.Errorf("Could not run %v: %v", bin, err)
+		return fmt.Errorf("Could not run %v: %w", bin, err)
 	}
 	go handleSignals(cmd.Process)
 	return cmd.Wait()
@@ -84,7 +84,7 @@ func cpDir(src, dst string, filter []string) error {
 		}
 		suffix, err := filepath.Rel(src, fullpath)
 		if err != nil {
-			return fmt.Errorf("Failed to find Rel(%q, %q): %v", src, fullpath, err)
+			return fmt.Errorf("Failed to find Rel(%q, %q): %w", src, fullpath, err)
 		}
 		if fi.IsDir() {
 			return nil
@@ -175,7 +175,7 @@ func checkPerkeepSrcRoot() {
 func repoRoot() (string, error) {
 	dir, err := os.Getwd()
 	if err != nil {
-		return "", fmt.Errorf("could not get current directory: %v", err)
+		return "", fmt.Errorf("could not get current directory: %w", err)
 	}
 	rootlen := 1
 	if runtime.GOOS == "windows" {
@@ -208,18 +208,18 @@ func selfModTime() (time.Time, error) {
 func checkModtime() error {
 	binModtime, err := selfModTime()
 	if err != nil {
-		return fmt.Errorf("could not get ModTime of current devcam executable: %v", err)
+		return fmt.Errorf("could not get ModTime of current devcam executable: %w", err)
 	}
 
 	devcamDir := filepath.Join(camliSrcRoot, "dev", "devcam")
 	d, err := os.Open(devcamDir)
 	if err != nil {
-		return fmt.Errorf("could not read devcam source dir %v: %v", devcamDir, err)
+		return fmt.Errorf("could not read devcam source dir %v: %w", devcamDir, err)
 	}
 	defer d.Close()
 	fis, err := d.Readdir(-1)
 	if err != nil {
-		return fmt.Errorf("could not read devcam source dir %v: %v", devcamDir, err)
+		return fmt.Errorf("could not read devcam source dir %v: %w", devcamDir, err)
 	}
 	for _, fi := range fis {
 		if fi.ModTime().After(binModtime) {
@@ -258,7 +258,7 @@ func build(targets ...string) error {
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("error building %v: %v", targetsComma, err)
+		return fmt.Errorf("error building %v: %w", targetsComma, err)
 	}
 	return nil
 }
diff --git a/dev/devcam/get.go b/dev/devcam/get.go
index fc4ab4d52..20f9c3102 100644
--- a/dev/devcam/get.go
+++ b/dev/devcam/get.go
@@ -76,7 +76,7 @@ func (c *getCmd) RunCommand(args []string) error {
 	}
 	if !*noBuild {
 		if err := build(filepath.Join("cmd", "pk-get")); err != nil {
-			return fmt.Errorf("Could not build pk-get: %v", err)
+			return fmt.Errorf("Could not build pk-get: %w", err)
 		}
 	}
 	c.env.SetCamdevVars(c.altkey)
diff --git a/dev/devcam/hook.go b/dev/devcam/hook.go
index efcd8bceb..90f06890c 100644
--- a/dev/devcam/hook.go
+++ b/dev/devcam/hook.go
@@ -69,11 +69,11 @@ func (c *hookCmd) installHook() error {
 		}
 
 		if !os.IsNotExist(err) {
-			return fmt.Errorf("checking hook: %v", err)
+			return fmt.Errorf("checking hook: %w", err)
 		}
 		c.verbosef("installing %s hook", hookFile)
 		if err := os.WriteFile(filename, []byte(hookContent), 0700); err != nil {
-			return fmt.Errorf("writing hook: %v", err)
+			return fmt.Errorf("writing hook: %w", err)
 		}
 	}
 	return nil
@@ -215,9 +215,9 @@ func (c *hookCmd) runGofmt() (files []string, err error) {
 	if err != nil {
 		// Error but no stderr: usually can't find gofmt.
 		if stderr.Len() == 0 {
-			return nil, fmt.Errorf("invoking gofmt: %v", err)
+			return nil, fmt.Errorf("invoking gofmt: %w", err)
 		}
-		return nil, fmt.Errorf("%s: %v", stderr.String(), err)
+		return nil, fmt.Errorf("%s: %w", stderr.String(), err)
 	}
 
 	// Build file list.
diff --git a/dev/devcam/mount.go b/dev/devcam/mount.go
index d39cd0363..124b1f62e 100644
--- a/dev/devcam/mount.go
+++ b/dev/devcam/mount.go
@@ -89,7 +89,7 @@ func (c *mountCmd) RunCommand(args []string) error {
 	}
 	if !*noBuild {
 		if err := build(filepath.Join("cmd", "pk-mount")); err != nil {
-			return fmt.Errorf("Could not build pk-mount: %v", err)
+			return fmt.Errorf("Could not build pk-mount: %w", err)
 		}
 	}
 	c.env.SetCamdevVars(c.altkey)
@@ -101,7 +101,7 @@ func (c *mountCmd) RunCommand(args []string) error {
 
 	tryUnmount(mountpoint)
 	if err := os.Mkdir(mountpoint, 0700); err != nil && !os.IsExist(err) {
-		return fmt.Errorf("Could not make mount point: %v", err)
+		return fmt.Errorf("Could not make mount point: %w", err)
 	}
 
 	blobserver := "http://localhost:" + c.port + c.path
diff --git a/dev/devcam/pk.go b/dev/devcam/pk.go
index fa6b1fd6e..2b69954fc 100644
--- a/dev/devcam/pk.go
+++ b/dev/devcam/pk.go
@@ -63,7 +63,7 @@ func (c *toolCmd) Describe() string {
 func (c *toolCmd) RunCommand(args []string) error {
 	if !*noBuild {
 		if err := build(filepath.Join("cmd", "pk")); err != nil {
-			return fmt.Errorf("Could not build pk: %v", err)
+			return fmt.Errorf("Could not build pk: %w", err)
 		}
 	}
 	c.env.SetCamdevVars(c.altkey)
diff --git a/dev/devcam/put.go b/dev/devcam/put.go
index ced4c5d4a..7ebf3e462 100644
--- a/dev/devcam/put.go
+++ b/dev/devcam/put.go
@@ -74,7 +74,7 @@ func (c *putCmd) RunCommand(args []string) error {
 	}
 	if !*noBuild {
 		if err := build(filepath.Join("cmd", "pk-put")); err != nil {
-			return fmt.Errorf("Could not build pk-put: %v", err)
+			return fmt.Errorf("Could not build pk-put: %w", err)
 		}
 	}
 	c.env.SetCamdevVars(c.altkey)
diff --git a/dev/devcam/server.go b/dev/devcam/server.go
index c061b9dc0..ea0be4d2e 100644
--- a/dev/devcam/server.go
+++ b/dev/devcam/server.go
@@ -191,7 +191,7 @@ func (c *serverCmd) setRoot() error {
 	if c.wipe {
 		log.Printf("Wiping %v", c.root)
 		if err := os.RemoveAll(c.root); err != nil {
-			return fmt.Errorf("Could not wipe %v: %v", c.root, err)
+			return fmt.Errorf("Could not wipe %v: %w", c.root, err)
 		}
 	}
 	return nil
@@ -279,7 +279,7 @@ func (c *serverCmd) setEnvVars() error {
 		if c.hostname == "" {
 			hostname, err := os.Hostname()
 			if err != nil {
-				return fmt.Errorf("Could not get system hostname: %v", err)
+				return fmt.Errorf("Could not get system hostname: %w", err)
 			}
 			base = "http://" + hostname + ":" + c.port
 		} else {
@@ -383,7 +383,7 @@ func (c *serverCmd) setupIndexer() error {
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Could not run pk dbinit: %v", err)
+		return fmt.Errorf("Could not run pk dbinit: %w", err)
 	}
 	return nil
 }
@@ -399,7 +399,7 @@ func (c *serverCmd) syncTemplateBlobs() error {
 		}
 		blobsDir := filepath.Join(c.root, "sha1")
 		if err := cpDir(templateDir, blobsDir, nil); err != nil {
-			return fmt.Errorf("Could not cp template blobs: %v", err)
+			return fmt.Errorf("Could not cp template blobs: %w", err)
 		}
 	}
 	return nil
@@ -418,7 +418,7 @@ func (c *serverCmd) setFullClosure() error {
 		cmd.Stdout = os.Stdout
 		cmd.Stderr = os.Stderr
 		if err := cmd.Run(); err != nil {
-			return fmt.Errorf("Could not run updatelibrary.go: %v", err)
+			return fmt.Errorf("Could not run updatelibrary.go: %w", err)
 		}
 		c.env.Set("CAMLI_DEV_CLOSURE_DIR", "clients/web/embed/closure/lib/closure")
 	}
@@ -431,7 +431,7 @@ func (c *serverCmd) makeThings() error {
 	configFile := filepath.Join(camliSrcRoot, "config", "dev-server-config.json")
 	config, err := os.ReadFile(configFile)
 	if err != nil {
-		return fmt.Errorf("could not read config file %v: %v", configFile, err)
+		return fmt.Errorf("could not read config file %v: %w", configFile, err)
 	}
 	if !bytes.Contains(config, []byte(importerPrefix)) {
 		return fmt.Errorf("%s prefix not found in dev config. Did it change?", importerPrefix)
@@ -451,7 +451,7 @@ func (c *serverCmd) makeThings() error {
 
 	cl, err := client.New(client.OptionServer(baseURL))
 	if err != nil {
-		return fmt.Errorf("making client: %v", err)
+		return fmt.Errorf("making client: %w", err)
 	}
 	signer, err := cl.Signer()
 	if err != nil {
@@ -485,7 +485,7 @@ func (c *serverCmd) makeThings() error {
 		hc.HTTPClient = &http.Client{Transport: tr}
 		host, err := importer.NewHost(hc)
 		if err != nil {
-			return fmt.Errorf("could not obtain Host: %v", err)
+			return fmt.Errorf("could not obtain Host: %w", err)
 		}
 
 		rc, err := importer.CreateAccount(host, name)
@@ -494,7 +494,7 @@ func (c *serverCmd) makeThings() error {
 		}
 
 		if err := mk.SetTestAccount(rc.AccountNode()); err != nil {
-			return fmt.Errorf("could not set fake account node for importer %v: %v", name, err)
+			return fmt.Errorf("could not set fake account node for importer %v: %w", name, err)
 		}
 
 		if err := imp.Run(rc); err != nil {
@@ -527,10 +527,10 @@ func (c *serverCmd) RunCommand(args []string) error {
 		}
 	}
 	if err := c.setRoot(); err != nil {
-		return fmt.Errorf("Could not setup the camli root: %v", err)
+		return fmt.Errorf("Could not setup the camli root: %w", err)
 	}
 	if err := c.setEnvVars(); err != nil {
-		return fmt.Errorf("Could not setup the env vars: %v", err)
+		return fmt.Errorf("Could not setup the env vars: %w", err)
 	}
 	// wipeCacheDir needs to be called after setEnvVars, because that is where
 	// CAMLI_CACHE_DIR is defined.
@@ -538,13 +538,13 @@ func (c *serverCmd) RunCommand(args []string) error {
 		c.env.wipeCacheDir()
 	}
 	if err := c.setupIndexer(); err != nil {
-		return fmt.Errorf("Could not setup the indexer: %v", err)
+		return fmt.Errorf("Could not setup the indexer: %w", err)
 	}
 	if err := c.syncTemplateBlobs(); err != nil {
-		return fmt.Errorf("Could not copy the template blobs: %v", err)
+		return fmt.Errorf("Could not copy the template blobs: %w", err)
 	}
 	if err := c.setFullClosure(); err != nil {
-		return fmt.Errorf("Could not setup the closure lib: %v", err)
+		return fmt.Errorf("Could not setup the closure lib: %w", err)
 	}
 
 	log.Printf("Starting dev server on %v/ui/ with password \"pass3179\"\n",
@@ -552,7 +552,7 @@ func (c *serverCmd) RunCommand(args []string) error {
 
 	pkBin, err := exec.LookPath("perkeepd")
 	if err != nil {
-		return fmt.Errorf("could not find perkeepd path: %v", err)
+		return fmt.Errorf("could not find perkeepd path: %w", err)
 	}
 	cmdArgs := []string{
 		"-configfile=" + filepath.Join(camliSrcRoot, "config", "dev-server-config.json"),
diff --git a/dev/envvardoc/envvardoc.go b/dev/envvardoc/envvardoc.go
index 2e36fb392..dc25f8658 100644
--- a/dev/envvardoc/envvardoc.go
+++ b/dev/envvardoc/envvardoc.go
@@ -4,6 +4,7 @@ package main // import "perkeep.org/dev/envvardoc"
 
 import (
 	"bufio"
+	"errors"
 	"flag"
 	"fmt"
 	"io"
@@ -86,7 +87,7 @@ func (ec *envCollector) findEnvVars(path string, r io.Reader) error {
 	}
 
 	err := scanner.Err()
-	if err == bufio.ErrTooLong {
+	if errors.Is(err, bufio.ErrTooLong) {
 		// Happens only for unreasonably long lines.
 		// In our case the webui's embedded stuff.
 		return nil
diff --git a/internal/closure/gendeps.go b/internal/closure/gendeps.go
index d0db0c03d..965f9c365 100644
--- a/internal/closure/gendeps.go
+++ b/internal/closure/gendeps.go
@@ -47,7 +47,7 @@ func GenDeps(root http.FileSystem) ([]byte, error) {
 func GenDepsWithPath(pathPrefix string, root http.FileSystem) ([]byte, error) {
 	d, err := root.Open("/")
 	if err != nil {
-		return nil, fmt.Errorf("Failed to open root of %v: %v", root, err)
+		return nil, fmt.Errorf("Failed to open root of %v: %w", root, err)
 	}
 	fi, err := d.Stat()
 	if err != nil {
@@ -58,7 +58,7 @@ func GenDepsWithPath(pathPrefix string, root http.FileSystem) ([]byte, error) {
 	}
 	ent, err := d.Readdir(-1)
 	if err != nil {
-		return nil, fmt.Errorf("Could not read dir entries of root: %v", err)
+		return nil, fmt.Errorf("Could not read dir entries of root: %w", err)
 	}
 	var buf bytes.Buffer
 	for _, info := range ent {
@@ -72,12 +72,12 @@ func GenDepsWithPath(pathPrefix string, root http.FileSystem) ([]byte, error) {
 		}
 		f, err := root.Open(name)
 		if err != nil {
-			return nil, fmt.Errorf("Could not open %v: %v", name, err)
+			return nil, fmt.Errorf("Could not open %v: %w", name, err)
 		}
 		prov, req, err := parseProvidesRequires(info, name, f)
 		f.Close()
 		if err != nil {
-			return nil, fmt.Errorf("Could not parse deps for %v: %v", name, err)
+			return nil, fmt.Errorf("Could not parse deps for %v: %w", name, err)
 		}
 		if len(prov) > 0 {
 			fmt.Fprintf(&buf, "goog.addDependency(%q, %v, %v);\n", pathPrefix+name, jsList(prov), jsList(req))
diff --git a/internal/geocode/geocode.go b/internal/geocode/geocode.go
index de406433a..825ec9558 100644
--- a/internal/geocode/geocode.go
+++ b/internal/geocode/geocode.go
@@ -81,7 +81,7 @@ var (
 func GetAPIKeyPath() (string, error) {
 	dir, err := osutil.PerkeepConfigDir()
 	if err != nil {
-		return "", fmt.Errorf("could not get config dir: %v", err)
+		return "", fmt.Errorf("could not get config dir: %w", err)
 	}
 	return filepath.Join(dir, apiKeyName), nil
 }
diff --git a/internal/httputil/certs.go b/internal/httputil/certs.go
index 59b6a0147..631d1bd34 100644
--- a/internal/httputil/certs.go
+++ b/internal/httputil/certs.go
@@ -54,7 +54,7 @@ var (
 func GenSelfTLS(hostname string) (certPEM, keyPEM []byte, err error) {
 	priv, err := rsa.GenerateKey(rand.Reader, 2048)
 	if err != nil {
-		return certPEM, keyPEM, fmt.Errorf("failed to generate private key: %s", err)
+		return certPEM, keyPEM, fmt.Errorf("failed to generate private key: %w", err)
 	}
 
 	now := time.Now()
@@ -83,17 +83,17 @@ func GenSelfTLS(hostname string) (certPEM, keyPEM []byte, err error) {
 
 	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
 	if err != nil {
-		return certPEM, keyPEM, fmt.Errorf("failed to create certificate: %s", err)
+		return certPEM, keyPEM, fmt.Errorf("failed to create certificate: %w", err)
 	}
 	var certBuf bytes.Buffer
 	if err := pem.Encode(&certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
-		return certPEM, keyPEM, fmt.Errorf("error writing self-signed HTTPS cert: %v", err)
+		return certPEM, keyPEM, fmt.Errorf("error writing self-signed HTTPS cert: %w", err)
 	}
 	certPEM = certBuf.Bytes()
 
 	var keyBuf bytes.Buffer
 	if err := pem.Encode(&keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
-		return certPEM, keyPEM, fmt.Errorf("error writing self-signed HTTPS private key: %v", err)
+		return certPEM, keyPEM, fmt.Errorf("error writing self-signed HTTPS private key: %w", err)
 	}
 	keyPEM = keyBuf.Bytes()
 	return certPEM, keyPEM, nil
@@ -107,7 +107,7 @@ func CertFingerprint(certPEM []byte) (string, error) {
 	}
 	cert, err := x509.ParseCertificate(p.Bytes)
 	if err != nil {
-		return "", fmt.Errorf("failed to parse certificate: %v", err)
+		return "", fmt.Errorf("failed to parse certificate: %w", err)
 	}
 	return hashutil.SHA256Prefix(cert.Raw), nil
 }
@@ -122,13 +122,13 @@ func GenSelfTLSFiles(hostname, certPath, keyPath string) (fingerprint string, er
 	}
 	sig, err := CertFingerprint(cert)
 	if err != nil {
-		return "", fmt.Errorf("could not get SHA-256 fingerprint of certificate: %v", err)
+		return "", fmt.Errorf("could not get SHA-256 fingerprint of certificate: %w", err)
 	}
 	if err := wkfs.WriteFile(certPath, cert, 0666); err != nil {
-		return "", fmt.Errorf("failed to write self-signed TLS cert: %v", err)
+		return "", fmt.Errorf("failed to write self-signed TLS cert: %w", err)
 	}
 	if err := wkfs.WriteFile(keyPath, key, 0600); err != nil {
-		return "", fmt.Errorf("failed to write self-signed TLS key: %v", err)
+		return "", fmt.Errorf("failed to write self-signed TLS key: %w", err)
 	}
 	return sig, nil
 }
@@ -234,7 +234,8 @@ yE+vPxsiUkvQHdO2fojCkY8jg70jxM+gu59tPDNbw3Uh/2Ij310FgTHsnGQMyA==
 		panic("failed to parse dummy certificate: " + err.Error())
 	}
 	_, err = dummyCert.Verify(x509.VerifyOptions{})
-	_, isSysRootError := err.(x509.SystemRootsError)
+	var sre x509.SystemRootsError
+	isSysRootError := errors.As(err, &sre)
 	sysRootsGood = !isSysRootError
 }
 
diff --git a/internal/httputil/httputil.go b/internal/httputil/httputil.go
index 3439038ed..4a8390924 100644
--- a/internal/httputil/httputil.go
+++ b/internal/httputil/httputil.go
@@ -273,7 +273,7 @@ func ServeJSONError(rw http.ResponseWriter, err interface{}) {
 func DecodeJSON(res *http.Response, dest interface{}) error {
 	defer res.Body.Close()
 	if err := json.NewDecoder(res.Body).Decode(dest); err != nil {
-		return fmt.Errorf("httputil.DecodeJSON: %v", err)
+		return fmt.Errorf("httputil.DecodeJSON: %w", err)
 	}
 	return nil
 }
diff --git a/internal/images/docker.go b/internal/images/docker.go
index 667363b1d..c420a4525 100644
--- a/internal/images/docker.go
+++ b/internal/images/docker.go
@@ -49,11 +49,11 @@ func setUpThumbnailContainer() error {
 	}
 	if ok, err := haveImageID(thumbnailImage, thumbnailImageID); !ok || err != nil {
 		if err != nil {
-			return fmt.Errorf("error running docker to check for %s: %v", thumbnailImage, err)
+			return fmt.Errorf("error running docker to check for %s: %w", thumbnailImage, err)
 		}
 		log.Printf("pulling docker image %s ...", thumbnailImage)
 		if err := pull(thumbnailImage); err != nil {
-			return fmt.Errorf("error pulling %s: %v", thumbnailImage, err)
+			return fmt.Errorf("error pulling %s: %w", thumbnailImage, err)
 		}
 	}
 	haveThumbnailImage = true
@@ -101,7 +101,7 @@ func pull(image string) error {
 	// "Authentication is required" message does come from stderr, then quit
 	// checking stdout.
 	if err != nil || stderr.Len() != 0 || strings.Contains(out, "Authentication is required") {
-		return fmt.Errorf("docker pull failed: stdout: %s, stderr: %s, err: %v", out, stderr.String(), err)
+		return fmt.Errorf("docker pull failed: stdout: %s, stderr: %s, err: %w", out, stderr.String(), err)
 	}
 	return nil
 }
diff --git a/internal/images/fastjpeg/fastjpeg.go b/internal/images/fastjpeg/fastjpeg.go
index a3c07c3d3..3b62d2d3d 100644
--- a/internal/images/fastjpeg/fastjpeg.go
+++ b/internal/images/fastjpeg/fastjpeg.go
@@ -216,13 +216,14 @@ func DecodeDownsample(r io.Reader, factor int) (image.Image, error) {
 		// If djpeg finished with warnings (exit code 2)
 		// the output is still usable most of the time.
 		// If not `readPNM` will fail its sanity checks
-		if exiterr, ok := err.(*exec.ExitError); ok {
+		var exiterr *exec.ExitError
+		if errors.As(err, &exiterr) {
 			if exiterr.ExitCode() == 2 {
 				goto Success
 			}
 		}
 		djpegFailureVar.Add(1)
-		return nil, DjpegFailedError{Err: fmt.Errorf("%v: %s", err, stderrW)}
+		return nil, DjpegFailedError{Err: fmt.Errorf("%w: %s", err, stderrW)}
 	}
 
 Success:
diff --git a/internal/images/images.go b/internal/images/images.go
index 4a0b1d92a..0b2cddd40 100644
--- a/internal/images/images.go
+++ b/internal/images/images.go
@@ -492,16 +492,16 @@ func decode(r io.Reader, opts *DecodeOpts, swapDimensions bool) (im image.Image,
 			var buf bytes.Buffer
 			tr := io.TeeReader(mr, &buf)
 			im, err = fastjpeg.DecodeDownsample(tr, factor)
-			switch err.(type) {
-			case fastjpeg.DjpegFailedError:
+			var derr fastjpeg.DjpegFailedError
+			if errors.As(err, &derr) {
 				log.Printf("Retrying with jpeg.Decode, because djpeg failed with: %v", err)
 				im, err = jpeg.Decode(io.MultiReader(&buf, mr))
 				if err != nil {
 					return nil, format, err, false
 				}
-			case nil:
+			} else if err == nil {
 				// fallthrough to rescale() below.
-			default:
+			} else {
 				return nil, format, err, false
 			}
 			return rescale(im, sw, sh), format, err, true
@@ -671,7 +671,7 @@ func HEIFToJPEG(fr io.Reader, maxSize *Dimensions) ([]byte, error) {
 	bin := localImageMagick()
 	if bin == "" {
 		if err := setUpThumbnailContainer(); err != nil {
-			return nil, NoHEICTOJPEGError{fmt.Errorf("recent ImageMagick magick binary not found in PATH, and could not fallback on docker image because %v. Install a modern ImageMagick or install docker.", err)}
+			return nil, NoHEICTOJPEGError{fmt.Errorf("recent ImageMagick magick binary not found in PATH, and could not fallback on docker image because %w. Install a modern ImageMagick or install docker.", err)}
 		}
 		bin = "docker"
 		useDocker = true
@@ -728,7 +728,7 @@ func HEIFToJPEG(fr io.Reader, maxSize *Dimensions) ([]byte, error) {
 		if debug {
 			log.Printf("internal/images: error running imagemagick heic conversion: %s", buf.Bytes())
 		}
-		return nil, fmt.Errorf("error running imagemagick: %v, %s", err, buf.Bytes())
+		return nil, fmt.Errorf("error running imagemagick: %w, %s", err, buf.Bytes())
 	}
 	if debug {
 		log.Printf("internal/images: ran imagemagick heic conversion in %v", time.Since(t0))
diff --git a/internal/media/audio.go b/internal/media/audio.go
index cfbbd90b1..2c32dbf27 100644
--- a/internal/media/audio.go
+++ b/internal/media/audio.go
@@ -42,7 +42,7 @@ func HasID3v1Tag(r readerutil.SizeReaderAt) (bool, error) {
 
 	buf := make([]byte, len(id3v1Magic))
 	if _, err := r.ReadAt(buf, r.Size()-ID3v1TagLength); err != nil {
-		return false, fmt.Errorf("Failed to read ID3v1 data: %v", err)
+		return false, fmt.Errorf("Failed to read ID3v1 data: %w", err)
 	}
 	if bytes.Equal(buf, id3v1Magic) {
 		return true, nil
@@ -132,7 +132,7 @@ var infoHeaderName = []byte("Info")
 func GetMPEGAudioDuration(r readerutil.SizeReaderAt) (time.Duration, error) {
 	var header uint32
 	if err := binary.Read(io.NewSectionReader(r, 0, r.Size()), binary.BigEndian, &header); err != nil {
-		return 0, fmt.Errorf("Failed to read MPEG frame header: %v", err)
+		return 0, fmt.Errorf("Failed to read MPEG frame header: %w", err)
 	}
 	getBits := func(startBit, numBits uint) uint32 {
 		return (header << startBit) >> (32 - numBits)
@@ -174,7 +174,7 @@ func GetMPEGAudioDuration(r readerutil.SizeReaderAt) (time.Duration, error) {
 
 	b := make([]byte, 12)
 	if _, err := r.ReadAt(b, xingHeaderStart); err != nil {
-		return 0, fmt.Errorf("Unable to read Xing header at %d: %v", xingHeaderStart, err)
+		return 0, fmt.Errorf("Unable to read Xing header at %d: %w", xingHeaderStart, err)
 	}
 	var ms int64
 	if bytes.Equal(b[0:4], xingHeaderName) || bytes.Equal(b[0:4], infoHeaderName) {
diff --git a/internal/netutil/ident.go b/internal/netutil/ident.go
index 9b6926138..34bd55fd5 100644
--- a/internal/netutil/ident.go
+++ b/internal/netutil/ident.go
@@ -100,7 +100,7 @@ func AddrPairUserid(local, remote net.Addr) (uid int, err error) {
 		}
 		f, err := os.Open(file)
 		if err != nil {
-			return 0, fmt.Errorf("Error opening %s: %v", file, err)
+			return 0, fmt.Errorf("Error opening %s: %w", file, err)
 		}
 		defer f.Close()
 		return uidFromProcReader(lAddr.IP, lAddr.Port, rAddr.IP, rAddr.Port, f)
@@ -179,7 +179,7 @@ func uidFromLsof(lip net.IP, lport int, rip net.IP, rport int) (uid int, err err
 	br := bufio.NewReader(stdout)
 	for {
 		line, err := br.ReadSlice('\n')
-		if err == io.EOF {
+		if errors.Is(err, io.EOF) {
 			return 0, ErrNotFound
 		}
 		if err != nil {
diff --git a/internal/netutil/netutil.go b/internal/netutil/netutil.go
index 1cdae8717..aca221490 100644
--- a/internal/netutil/netutil.go
+++ b/internal/netutil/netutil.go
@@ -48,7 +48,7 @@ func HostPort(urlStr string) (string, error) {
 	// ListenHostPort below.
 	u, err := url.Parse(urlStr)
 	if err != nil {
-		return "", fmt.Errorf("could not parse %q as a url: %v", urlStr, err)
+		return "", fmt.Errorf("could not parse %q as a url: %w", urlStr, err)
 	}
 	if u.Scheme == "" {
 		return "", fmt.Errorf("url %q has no scheme", urlStr)
@@ -149,11 +149,11 @@ func RandPort() (int, error) {
 	}
 	listener, err := net.ListenTCP("tcp", addr)
 	if err != nil {
-		return port, fmt.Errorf("could not listen to find random port: %v", err)
+		return port, fmt.Errorf("could not listen to find random port: %w", err)
 	}
 	randAddr := listener.Addr().(*net.TCPAddr)
 	if err := listener.Close(); err != nil {
-		return port, fmt.Errorf("could not close random listener: %v", err)
+		return port, fmt.Errorf("could not close random listener: %w", err)
 	}
 	return randAddr.Port, nil
 }
diff --git a/internal/osutil/gce/gce.go b/internal/osutil/gce/gce.go
index c6b0e4853..a68fcef54 100644
--- a/internal/osutil/gce/gce.go
+++ b/internal/osutil/gce/gce.go
@@ -64,7 +64,7 @@ func init() {
 		}
 		val, err := metadata.InstanceAttributeValue(attr)
 		if err != nil {
-			return nil, fmt.Errorf("error reading GCE instance attribute %q: %v", attr, err)
+			return nil, fmt.Errorf("error reading GCE instance attribute %q: %w", attr, err)
 		}
 		return val, nil
 	})
@@ -130,10 +130,10 @@ func LogWriter() (w io.WriteCloser, err error) {
 	ctx := context.Background()
 	logc, err := logging.NewClient(ctx, projID)
 	if err != nil {
-		return nil, fmt.Errorf("error creating Google logging client: %v", err)
+		return nil, fmt.Errorf("error creating Google logging client: %w", err)
 	}
 	if err := logc.Ping(ctx); err != nil {
-		return nil, fmt.Errorf("Google logging client not ready (ping failed): %v", err)
+		return nil, fmt.Errorf("Google logging client not ready (ping failed): %w", err)
 	}
 	logw := writer{
 		severity: logging.Debug,
@@ -157,24 +157,24 @@ func gceInstance() (*gceInst, error) {
 	ctx := context.Background()
 	hc, err := google.DefaultClient(ctx)
 	if err != nil {
-		return nil, fmt.Errorf("error getting a default http client: %v", err)
+		return nil, fmt.Errorf("error getting a default http client: %w", err)
 	}
 	cs, err := compute.NewService(ctx, option.WithHTTPClient(hc))
 	if err != nil {
-		return nil, fmt.Errorf("error getting a compute service: %v", err)
+		return nil, fmt.Errorf("error getting a compute service: %w", err)
 	}
 	cis := compute.NewInstancesService(cs)
 	projectID, err := metadata.ProjectID()
 	if err != nil {
-		return nil, fmt.Errorf("error getting projectID: %v", err)
+		return nil, fmt.Errorf("error getting projectID: %w", err)
 	}
 	zone, err := metadata.Zone()
 	if err != nil {
-		return nil, fmt.Errorf("error getting zone: %v", err)
+		return nil, fmt.Errorf("error getting zone: %w", err)
 	}
 	name, err := metadata.InstanceName()
 	if err != nil {
-		return nil, fmt.Errorf("error getting instance name: %v", err)
+		return nil, fmt.Errorf("error getting instance name: %w", err)
 	}
 	return &gceInst{
 		cs:        cs,
@@ -205,7 +205,7 @@ func resetInstance() error {
 		if googleapi.IsNotModified(err) {
 			return nil
 		}
-		return fmt.Errorf("error resetting instance: %v", err)
+		return fmt.Errorf("error resetting instance: %w", err)
 	}
 	// TODO(mpl): refactor this whole pattern below into a func
 	opName := op.Name
@@ -217,7 +217,7 @@ func resetInstance() error {
 		}
 		op, err := inst.cs.ZoneOperations.Get(projectID, zone, opName).Context(ctx).Do()
 		if err != nil {
-			return fmt.Errorf("failed to get op %s: %v", opName, err)
+			return fmt.Errorf("failed to get op %s: %w", opName, err)
 		}
 		switch op.Status {
 		case "PENDING", "RUNNING":
@@ -248,8 +248,9 @@ func SetInstanceHostname(camliNetHostName string) error {
 
 	hostname, err := metadata.InstanceAttributeValue("camlistore-hostname")
 	if err != nil {
-		if _, ok := err.(metadata.NotDefinedError); !ok {
-			return fmt.Errorf("error getting existing camlistore-hostname: %v", err)
+		var nde metadata.NotDefinedError
+		if !errors.As(err, &nde) {
+			return fmt.Errorf("error getting existing camlistore-hostname: %w", err)
 		}
 	}
 	if err == nil && hostname != "" {
@@ -267,7 +268,7 @@ func SetInstanceHostname(camliNetHostName string) error {
 
 	instance, err := cs.Get(projectID, zone, name).Context(ctx).Do()
 	if err != nil {
-		return fmt.Errorf("error getting instance: %v", err)
+		return fmt.Errorf("error getting instance: %w", err)
 	}
 	items := instance.Metadata.Items
 	items = append(items, &compute.MetadataItems{
@@ -285,7 +286,7 @@ func SetInstanceHostname(camliNetHostName string) error {
 		if googleapi.IsNotModified(err) {
 			return nil
 		}
-		return fmt.Errorf("error setting instance hostname: %v", err)
+		return fmt.Errorf("error setting instance hostname: %w", err)
 	}
 	// TODO(mpl): refactor this whole pattern below into a func
 	opName := op.Name
@@ -298,7 +299,7 @@ func SetInstanceHostname(camliNetHostName string) error {
 		}
 		op, err := inst.cs.ZoneOperations.Get(projectID, zone, opName).Context(ctx).Do()
 		if err != nil {
-			return fmt.Errorf("failed to get op %s: %v", opName, err)
+			return fmt.Errorf("failed to get op %s: %w", opName, err)
 		}
 		switch op.Status {
 		case "PENDING", "RUNNING":
@@ -352,8 +353,9 @@ func fixUserDataForPerkeepRename() (needsRestart bool, err error) {
 
 	userData, err := metadata.InstanceAttributeValue(metadataKey)
 	if err != nil {
-		if _, ok := err.(metadata.NotDefinedError); !ok {
-			return false, fmt.Errorf("error getting existing user-data: %v", err)
+		var nde metadata.NotDefinedError
+		if !errors.As(err, &nde) {
+			return false, fmt.Errorf("error getting existing user-data: %w", err)
 		}
 	}
 
@@ -390,7 +392,7 @@ func fixUserDataForPerkeepRename() (needsRestart bool, err error) {
 
 	instance, err := cs.Get(projectID, zone, name).Context(ctx).Do()
 	if err != nil {
-		return false, fmt.Errorf("error getting instance: %v", err)
+		return false, fmt.Errorf("error getting instance: %w", err)
 	}
 	items := instance.Metadata.Items
 	for k, v := range items {
@@ -413,7 +415,7 @@ func fixUserDataForPerkeepRename() (needsRestart bool, err error) {
 		if googleapi.IsNotModified(err) {
 			return false, nil
 		}
-		return false, fmt.Errorf("error setting instance user-data: %v", err)
+		return false, fmt.Errorf("error setting instance user-data: %w", err)
 	}
 	// TODO(mpl): refactor this whole pattern below into a func
 	opName := op.Name
@@ -425,7 +427,7 @@ func fixUserDataForPerkeepRename() (needsRestart bool, err error) {
 		}
 		op, err := inst.cs.ZoneOperations.Get(projectID, zone, opName).Context(ctx).Do()
 		if err != nil {
-			return false, fmt.Errorf("failed to get op %s: %v", opName, err)
+			return false, fmt.Errorf("failed to get op %s: %w", opName, err)
 		}
 		switch op.Status {
 		case "PENDING", "RUNNING":
@@ -453,7 +455,8 @@ func fixUserDataForPerkeepRename() (needsRestart bool, err error) {
 func BlobpackedRecoveryValue() int {
 	recovery, err := metadata.InstanceAttributeValue("camlistore-recovery")
 	if err != nil {
-		if _, ok := err.(metadata.NotDefinedError); !ok {
+		var nde metadata.NotDefinedError
+		if !errors.As(err, &nde) {
 			log.Printf("error getting camlistore-recovery: %v", err)
 		}
 		return 0
diff --git a/internal/osutil/paths.go b/internal/osutil/paths.go
index bffe9b5b8..1a86da3d9 100644
--- a/internal/osutil/paths.go
+++ b/internal/osutil/paths.go
@@ -376,7 +376,7 @@ func GoPackagePath(pkg string) (path string, err error) {
 		cmd := exec.Command("go", "env", "GOPATH")
 		out, err := cmd.Output()
 		if err != nil {
-			return "", fmt.Errorf("could not run 'go env GOPATH': %v, %s", err, out)
+			return "", fmt.Errorf("could not run 'go env GOPATH': %w, %s", err, out)
 		}
 		gp = strings.TrimSpace(string(out))
 		if gp == "" {
@@ -407,7 +407,7 @@ func GoModPackagePath() (string, error) {
 		cmd := exec.Command("go", "env", "GOMOD")
 		out, err := cmd.Output()
 		if err != nil {
-			return "", fmt.Errorf("could not run 'go env GOMOD': %v, %s", err, out)
+			return "", fmt.Errorf("could not run 'go env GOMOD': %w, %s", err, out)
 		}
 		gmp = strings.TrimSuffix(strings.TrimSpace(string(out)), "go.mod")
 		if gmp == "" {
@@ -434,7 +434,7 @@ func goPathBinDir() (string, error) {
 	cmd := exec.Command("go", "env", "GOPATH")
 	out, err := cmd.Output()
 	if err != nil {
-		return "", fmt.Errorf("could not get GOPATH: %v, %s", err, out)
+		return "", fmt.Errorf("could not get GOPATH: %w, %s", err, out)
 	}
 	paths := filepath.SplitList(strings.TrimSpace(string(out)))
 	if len(paths) < 1 {
@@ -452,7 +452,7 @@ func LookPathGopath(binName string) (string, error) {
 	}
 	binDir, err := goPathBinDir()
 	if err != nil {
-		return "", fmt.Errorf("command %q not found in $PATH, and could not look in $GOPATH/bin because %v", binName, err)
+		return "", fmt.Errorf("command %q not found in $PATH, and could not look in $GOPATH/bin because %w", binName, err)
 	}
 	binPath = filepath.Join(binDir, binName)
 	if runtime.GOOS == "windows" {
diff --git a/internal/osutil/restart_unix.go b/internal/osutil/restart_unix.go
index 1ea64ba60..d3e6935c5 100644
--- a/internal/osutil/restart_unix.go
+++ b/internal/osutil/restart_unix.go
@@ -62,7 +62,7 @@ func SelfPath() (string, error) {
 func RestartProcess(arg ...string) error {
 	path, err := SelfPath()
 	if err != nil {
-		return fmt.Errorf("RestartProcess failed: %v", err)
+		return fmt.Errorf("RestartProcess failed: %w", err)
 	}
 
 	var args []string
diff --git a/internal/osutil/syscall_posix.go b/internal/osutil/syscall_posix.go
index 1e70b8512..576b8e4ca 100644
--- a/internal/osutil/syscall_posix.go
+++ b/internal/osutil/syscall_posix.go
@@ -19,6 +19,7 @@ limitations under the License.
 package osutil
 
 import (
+	"errors"
 	"fmt"
 	"net"
 	"os"
@@ -54,11 +55,11 @@ func mksocket(path string) error {
 func maxFD() (uint64, error) {
 	var rlim syscall.Rlimit
 	if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlim); err != nil {
-		if err == syscall.ENOSYS {
+		if errors.Is(err, syscall.ENOSYS) {
 			// syscall.Getrlimit() not implemented in ARMv5, and it returns this string
 			return 0, ErrNotSupported
 		}
-		return 0, fmt.Errorf("ulimit error: %v", err)
+		return 0, fmt.Errorf("ulimit error: %w", err)
 	}
 	// On FreeBSD Getrlimit returns an int64, because (among other things) the
 	// maximum value for the Rlimit struct fields, called RLIM_INFINITY used to
diff --git a/internal/osutil/syscall_solaris.go b/internal/osutil/syscall_solaris.go
index 18953a48a..9ae79ebee 100644
--- a/internal/osutil/syscall_solaris.go
+++ b/internal/osutil/syscall_solaris.go
@@ -55,7 +55,7 @@ func mksocket(path string) error {
 func maxFD() (uint64, error) {
 	var rlim syscall.Rlimit
 	if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlim); err != nil {
-		return 0, fmt.Errorf("ulimit error: %v", err)
+		return 0, fmt.Errorf("ulimit error: %w", err)
 	}
 	return rlim.Cur, nil
 }
diff --git a/internal/pinentry/pinentry.go b/internal/pinentry/pinentry.go
index 40e68e3aa..8484be67e 100644
--- a/internal/pinentry/pinentry.go
+++ b/internal/pinentry/pinentry.go
@@ -118,7 +118,7 @@ func (r *Request) GetPIN() (pin string, outerr error) {
 	fmt.Fprintf(stdin, "GETPIN\n")
 	lineb, _, err = br.ReadLine()
 	if err != nil {
-		return "", fmt.Errorf("Failed to read line after GETPIN: %v", err)
+		return "", fmt.Errorf("Failed to read line after GETPIN: %w", err)
 	}
 	line = string(lineb)
 	if strings.HasPrefix(line, "D ") {
@@ -134,7 +134,7 @@ func runPass(bin string, args ...string) error {
 	cmd := exec.Command(bin, args...)
 	cmd.Stdin = os.Stdin
 	if out, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("%v error: %v, %s", bin, err, out)
+		return fmt.Errorf("%v error: %w, %s", bin, err, out)
 	}
 	return nil
 }
diff --git a/make.go b/make.go
index 9b550bf26..3eb8e5562 100644
--- a/make.go
+++ b/make.go
@@ -203,7 +203,7 @@ func goPathBinDir() (string, error) {
 	cmd := exec.Command("go", "env", "GOPATH")
 	out, err := cmd.Output()
 	if err != nil {
-		return "", fmt.Errorf("could not get GOPATH: %v, %s", err, out)
+		return "", fmt.Errorf("could not get GOPATH: %w, %s", err, out)
 	}
 	paths := filepath.SplitList(strings.TrimSpace(string(out)))
 	if len(paths) < 1 {
diff --git a/misc/docker/dock.go b/misc/docker/dock.go
index 797ad5bab..b9aab391d 100644
--- a/misc/docker/dock.go
+++ b/misc/docker/dock.go
@@ -361,7 +361,7 @@ func ProjectTokenSource(proj string, scopes ...string) (oauth2.TokenSource, erro
 	}
 	conf, err := google.JWTConfigFromJSON(jsonConf, scopes...)
 	if err != nil {
-		return nil, fmt.Errorf("reading JSON config from %s: %v", fileName, err)
+		return nil, fmt.Errorf("reading JSON config from %s: %w", fileName, err)
 	}
 	return conf.TokenSource(context.TODO()), nil
 }
diff --git a/misc/docker/server/build-perkeep-server.go b/misc/docker/server/build-perkeep-server.go
index 9fba3d902..d29aca597 100644
--- a/misc/docker/server/build-perkeep-server.go
+++ b/misc/docker/server/build-perkeep-server.go
@@ -113,7 +113,7 @@ func fetchCamliSrc() {
 	var prefix string
 	for {
 		h, err := tr.Next()
-		if err == io.EOF {
+		if errors.Is(err, io.EOF) {
 			break
 		}
 		check(err)
diff --git a/misc/release/make-release.go b/misc/release/make-release.go
index 431409ae8..2f3933ecc 100644
--- a/misc/release/make-release.go
+++ b/misc/release/make-release.go
@@ -29,6 +29,7 @@ import (
 	"compress/gzip"
 	"context"
 	"crypto/sha256"
+	"errors"
 	"flag"
 	"fmt"
 	"html/template"
@@ -603,11 +604,11 @@ func listDownloads() (*ReleaseData, error) {
 	objIt := stoClient.Bucket(bucket).Objects(ctx, &storage.Query{Prefix: objPrefix})
 	for {
 		attrs, err := objIt.Next()
-		if err == iterator.Done {
+		if errors.Is(err, iterator.Done) {
 			break
 		}
 		if err != nil {
-			return nil, fmt.Errorf("error listing objects in %s: %v", bucket, err)
+			return nil, fmt.Errorf("error listing objects in %s: %w", bucket, err)
 		}
 		if !strings.Contains(attrs.Name, fileVersion) {
 			continue
@@ -627,11 +628,11 @@ func listDownloads() (*ReleaseData, error) {
 	objIt = stoClient.Bucket(bucket).Objects(ctx, &storage.Query{Prefix: objPrefix})
 	for {
 		attrs, err := objIt.Next()
-		if err == iterator.Done {
+		if errors.Is(err, iterator.Done) {
 			break
 		}
 		if err != nil {
-			return nil, fmt.Errorf("error listing objects in %s: %v", bucket, err)
+			return nil, fmt.Errorf("error listing objects in %s: %w", bucket, err)
 		}
 		if !strings.Contains(attrs.Name, fileVersion) {
 			continue
@@ -661,12 +662,12 @@ func listDownloads() (*ReleaseData, error) {
 func genReleasePage(releaseData *ReleaseData) error {
 	tpl, err := template.New("release").Parse(releaseTemplate)
 	if err != nil {
-		return fmt.Errorf("could not parse template: %v", err)
+		return fmt.Errorf("could not parse template: %w", err)
 	}
 
 	var buf bytes.Buffer
 	if err := tpl.ExecuteTemplate(&buf, "release", releaseData); err != nil {
-		return fmt.Errorf("could not execute template: %v", err)
+		return fmt.Errorf("could not execute template: %w", err)
 	}
 
 	releaseDocDir := filepath.Join(srcRoot, filepath.FromSlash("doc/release"))
@@ -675,7 +676,7 @@ func genReleasePage(releaseData *ReleaseData) error {
 	}
 	releaseDocPage := filepath.Join(releaseDocDir, "release.html")
 	if err := os.WriteFile(releaseDocPage, buf.Bytes(), 0700); err != nil {
-		return fmt.Errorf("could not write template to file %v: %v", releaseDocPage, err)
+		return fmt.Errorf("could not write template to file %v: %w", releaseDocPage, err)
 	}
 	return nil
 }
@@ -693,7 +694,7 @@ func committers() (map[string]string, error) {
 	cmd := exec.Command("git", "shortlog", "-n", "-e", "-s", *flagStatsFrom+".."+revOrHEAD())
 	out, err := cmd.CombinedOutput()
 	if err != nil {
-		return nil, fmt.Errorf("%v; %v", err, string(out))
+		return nil, fmt.Errorf("%w; %v", err, string(out))
 	}
 	rxp := regexp.MustCompile(`^\s+(\d+)\s+(.*)<(.*)>.*$`)
 	sc := bufio.NewScanner(bytes.NewReader(out))
@@ -708,7 +709,7 @@ func committers() (map[string]string, error) {
 		}
 		count, err := strconv.Atoi(m[1])
 		if err != nil {
-			return nil, fmt.Errorf("couldn't convert %q as a number of commits: %v", m[1], err)
+			return nil, fmt.Errorf("couldn't convert %q as a number of commits: %w", m[1], err)
 		}
 		name := strings.TrimSpace(m[2])
 		email := m[3]
@@ -744,7 +745,7 @@ func countCommits() (int, error) {
 	cmd := exec.Command("git", "log", "--format=oneline", *flagStatsFrom+".."+revOrHEAD())
 	out, err := cmd.CombinedOutput()
 	if err != nil {
-		return 0, fmt.Errorf("%v; %v", err, string(out))
+		return 0, fmt.Errorf("%w; %v", err, string(out))
 	}
 	sc := bufio.NewScanner(bytes.NewReader(out))
 	var sum int
@@ -760,11 +761,11 @@ func countCommits() (int, error) {
 func genCommitStats() (*stats, error) {
 	committers, err := committers()
 	if err != nil {
-		return nil, fmt.Errorf("Could not count number of committers: %v", err)
+		return nil, fmt.Errorf("Could not count number of committers: %w", err)
 	}
 	commits, err := countCommits()
 	if err != nil {
-		return nil, fmt.Errorf("Could not count number of commits: %v", err)
+		return nil, fmt.Errorf("Could not count number of commits: %w", err)
 	}
 	var names []string
 	for _, v := range committers {
@@ -783,7 +784,7 @@ func genReleaseNotes() (map[string][]string, error) {
 	cmd := exec.Command("git", "log", "--format=oneline", "--no-merges", *flagStatsFrom+".."+revOrHEAD())
 	out, err := cmd.CombinedOutput()
 	if err != nil {
-		return nil, fmt.Errorf("%v; %v", err, string(out))
+		return nil, fmt.Errorf("%w; %v", err, string(out))
 	}
 
 	// We define the "context" of a commit message as the very first
@@ -922,7 +923,7 @@ func ProjectTokenSource(proj string, scopes ...string) (oauth2.TokenSource, erro
 	}
 	conf, err := google.JWTConfigFromJSON(jsonConf, scopes...)
 	if err != nil {
-		return nil, fmt.Errorf("reading JSON config from %s: %v", fileName, err)
+		return nil, fmt.Errorf("reading JSON config from %s: %w", fileName, err)
 	}
 	return conf.TokenSource(context.TODO()), nil
 }
diff --git a/misc/release/zip-source.go b/misc/release/zip-source.go
index 1cedd61b0..b8b8846cd 100644
--- a/misc/release/zip-source.go
+++ b/misc/release/zip-source.go
@@ -140,7 +140,7 @@ func fetchCamliSrc() {
 	var prefix string
 	for {
 		h, err := tr.Next()
-		if err == io.EOF {
+		if errors.Is(err, io.EOF) {
 			break
 		}
 		check(err)
diff --git a/pkg/blob/blob.go b/pkg/blob/blob.go
index bda3c3fda..62dd5fab1 100644
--- a/pkg/blob/blob.go
+++ b/pkg/blob/blob.go
@@ -136,7 +136,7 @@ func FromReader(ctx context.Context, br Ref, r io.Reader, size uint32) (*Blob, e
 	buf := make([]byte, size)
 	// TODO: use ctx here during ReadFull? add context-checking Reader wrapper?
 	if n, err := io.ReadFull(r, buf); err != nil {
-		return nil, fmt.Errorf("blob: after reading %d bytes of %v: %v", n, br, err)
+		return nil, fmt.Errorf("blob: after reading %d bytes of %v: %w", n, br, err)
 	}
 	n, _ := io.CopyN(io.Discard, r, 1)
 	if n > 0 {
diff --git a/pkg/blobserver/archiver/archiver.go b/pkg/blobserver/archiver/archiver.go
index 40f6dc398..d8d8f3cf0 100644
--- a/pkg/blobserver/archiver/archiver.go
+++ b/pkg/blobserver/archiver/archiver.go
@@ -94,7 +94,7 @@ func (a *Archiver) RunOnce(ctx context.Context) error {
 		}
 		return nil
 	})
-	if err == errStopEnumerate {
+	if errors.Is(err, errStopEnumerate) {
 		err = nil
 	}
 	if err != nil {
diff --git a/pkg/blobserver/azure/azure.go b/pkg/blobserver/azure/azure.go
index ad0a67bb6..74ec5d936 100644
--- a/pkg/blobserver/azure/azure.go
+++ b/pkg/blobserver/azure/azure.go
@@ -35,9 +35,11 @@ package azure
 import (
 	"context"
 	"encoding/base64"
+	"errors"
 	"fmt"
 
 	"go4.org/jsonconfig"
+
 	"perkeep.org/internal/azure/storage"
 	"perkeep.org/pkg/blob"
 	"perkeep.org/pkg/blobserver"
@@ -92,13 +94,12 @@ func newFromConfig(_ blobserver.Loader, config jsonconfig.Obj) (blobserver.Stora
 	}
 	if !skipStartupCheck {
 		_, err := client.ListBlobs(context.TODO(), sto.container, 1)
-		if serr, ok := err.(*storage.Error); ok {
-			if serr.AzureError.Code == "ContainerNotFound" {
-				return nil, fmt.Errorf("container %q doesn't exist", sto.container)
-			}
-			return nil, fmt.Errorf("error listing container %s: %v", sto.container, serr)
-		} else if err != nil {
-			return nil, fmt.Errorf("error listing container %s: %v", sto.container, err)
+		var serr *storage.Error
+		if errors.As(err, &serr) && serr.AzureError.Code == "ContainerNotFound" {
+			return nil, fmt.Errorf("container %q doesn't exist", sto.container)
+		}
+		if err != nil {
+			return nil, fmt.Errorf("error listing container %s: %w", sto.container, err)
 		}
 	}
 	return sto, nil
diff --git a/pkg/blobserver/azure/stat.go b/pkg/blobserver/azure/stat.go
index ea5fb02b9..251021e71 100644
--- a/pkg/blobserver/azure/stat.go
+++ b/pkg/blobserver/azure/stat.go
@@ -18,9 +18,11 @@ package azure
 
 import (
 	"context"
+	"errors"
 	"os"
 
 	"go4.org/syncutil"
+
 	"perkeep.org/pkg/blob"
 	"perkeep.org/pkg/blobserver"
 )
@@ -34,7 +36,7 @@ func (sto *azureStorage) StatBlobs(ctx context.Context, blobs []blob.Ref, fn fun
 		if err == nil {
 			return blob.SizedRef{Ref: br, Size: uint32(size)}, nil
 		}
-		if err == os.ErrNotExist {
+		if errors.Is(err, os.ErrNotExist) {
 			return sb, nil
 		}
 		return sb, err
diff --git a/pkg/blobserver/blobpacked/blobpacked.go b/pkg/blobserver/blobpacked/blobpacked.go
index 84a89b429..86d6417b8 100644
--- a/pkg/blobserver/blobpacked/blobpacked.go
+++ b/pkg/blobserver/blobpacked/blobpacked.go
@@ -281,11 +281,11 @@ func newFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (blobserver.Storag
 	}
 	small, err := ld.GetStorage(smallPrefix)
 	if err != nil {
-		return nil, fmt.Errorf("failed to load smallBlobs at %s: %v", smallPrefix, err)
+		return nil, fmt.Errorf("failed to load smallBlobs at %s: %w", smallPrefix, err)
 	}
 	large, err := ld.GetStorage(largePrefix)
 	if err != nil {
-		return nil, fmt.Errorf("failed to load largeBlobs at %s: %v", largePrefix, err)
+		return nil, fmt.Errorf("failed to load largeBlobs at %s: %w", largePrefix, err)
 	}
 	largeSubber, ok := large.(subFetcherStorage)
 	if !ok {
@@ -294,7 +294,7 @@ func newFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (blobserver.Storag
 	}
 	meta, err := sorted.NewKeyValueMaybeWipe(metaConf)
 	if err != nil {
-		return nil, fmt.Errorf("failed to setup blobpacked metaIndex: %v", err)
+		return nil, fmt.Errorf("failed to setup blobpacked metaIndex: %w", err)
 	}
 	sto := &storage{
 		small: small,
@@ -329,7 +329,7 @@ func newFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (blobserver.Storag
 				return nil, fmt.Errorf("blobpacked meta index of type %T needs to be wiped, but does not support automatic wiping. It should be removed manually.", kv)
 			}
 			if err := wiper.Wipe(); err != nil {
-				return nil, fmt.Errorf("blobpacked meta index of type %T could not be wiped: %v", kv, err)
+				return nil, fmt.Errorf("blobpacked meta index of type %T could not be wiped: %w", kv, err)
 			}
 			return kv, nil
 		}
@@ -414,7 +414,7 @@ func (s *storage) checkLargeIntegrity() (RecoveryMode, error) {
 			return enumFunc(sb)
 		}
 		if _, err := parseZipMetaRow(t.ValueBytes()); err != nil {
-			return fmt.Errorf("error parsing row from meta: %v", err)
+			return fmt.Errorf("error parsing row from meta: %w", err)
 		}
 		inLarge++
 		return nil
@@ -432,7 +432,7 @@ func (s *storage) checkLargeIntegrity() (RecoveryMode, error) {
 		return FullRecovery, fmt.Errorf("%d large blobs in index but not actually in storage", len(extra))
 	}
 	if err := t.Close(); err != nil {
-		return FullRecovery, fmt.Errorf("error reading or closing index: %v", err)
+		return FullRecovery, fmt.Errorf("error reading or closing index: %w", err)
 	}
 	if len(missing) > 0 {
 		return FastRecovery, fmt.Errorf("%d large blobs missing from index", len(missing))
@@ -500,7 +500,7 @@ func (s *storage) fileName(ctx context.Context, zipRef blob.Ref) (string, error)
 func (s *storage) reindex(ctx context.Context, newMeta func() (sorted.KeyValue, error)) error {
 	meta, err := newMeta()
 	if err != nil {
-		return fmt.Errorf("failed to create new blobpacked meta index: %v", err)
+		return fmt.Errorf("failed to create new blobpacked meta index: %w", err)
 	}
 
 	zipMetaByWholeRef := make(map[blob.Ref][]zipMetaInfo)
@@ -645,7 +645,7 @@ func (s *storage) reindex(ctx context.Context, newMeta func() (sorted.KeyValue,
 			// missing at least one full zip for the whole file to be complete.
 			fileName, err := s.fileName(ctx, zipMetas[0].zipRef)
 			if err != nil {
-				return fmt.Errorf("could not get filename of file in zip %v: %v", zipMetas[0].zipRef, err)
+				return fmt.Errorf("could not get filename of file in zip %v: %w", zipMetas[0].zipRef, err)
 			}
 			log.Printf(
 				"blobpacked: file %q (wholeRef %v) is incomplete: sum of all zips (%d bytes) does not match manifest's WholeSize (%d bytes)",
@@ -840,11 +840,11 @@ func (m *meta) isPacked() bool { return m.largeRef.Valid() }
 // if not found, err == nil.
 func (s *storage) getMetaRow(br blob.Ref) (meta, error) {
 	v, err := s.meta.Get(blobMetaPrefix + br.String())
-	if err == sorted.ErrNotFound {
+	if errors.Is(err, sorted.ErrNotFound) {
 		return meta{}, nil
 	}
 	if err != nil {
-		return meta{}, fmt.Errorf("blobpacked.getMetaRow(%v) = %v", br, err)
+		return meta{}, fmt.Errorf("blobpacked.getMetaRow(%v) = %w", br, err)
 	}
 	return parseMetaRow([]byte(v))
 }
@@ -879,7 +879,7 @@ func parseMetaRow(v []byte) (m meta, err error) {
 	m.largeRef = largeRef
 	off, err := strutil.ParseUintBytes(v[sp+1:], 10, 32)
 	if err != nil {
-		return meta{}, fmt.Errorf("invalid metarow %q: bad offset: %v", row, err)
+		return meta{}, fmt.Errorf("invalid metarow %q: bad offset: %w", row, err)
 	}
 	m.largeOff = uint32(off)
 	return m, nil
@@ -1226,7 +1226,7 @@ func (pk *packer) pack(ctx context.Context) error {
 	if err == nil {
 		// Nil error means there was some knowledge of this wholeref.
 		return fmt.Errorf("already have wholeref %v packed; not packing again", pk.wholeRef)
-	} else if err != sorted.ErrNotFound {
+	} else if !errors.Is(err, sorted.ErrNotFound) {
 		return err
 	}
 
@@ -1235,8 +1235,9 @@ func (pk *packer) pack(ctx context.Context) error {
 MakingZips:
 	for len(pk.chunksRemain) > 0 {
 		if err := pk.writeAZip(ctx, trunc); err != nil {
-			if needTrunc, ok := err.(needsTruncatedAfterError); ok {
-				trunc = needTrunc.Ref
+			var needTruncErr needsTruncatedAfterError
+			if errors.As(err, &needTruncErr) {
+				trunc = needTruncErr.Ref
 				if fn := testHookSawTruncate; fn != nil {
 					fn(trunc)
 				}
@@ -1250,7 +1251,7 @@ MakingZips:
 	// Record the final wholeMetaPrefix record:
 	err = pk.s.meta.Set(wholeKey, fmt.Sprintf("%d %d", pk.wholeSize, len(pk.zips)))
 	if err != nil {
-		return fmt.Errorf("Error setting %s: %v", wholeKey, err)
+		return fmt.Errorf("Error setting %s: %w", wholeKey, err)
 	}
 
 	return nil
@@ -1389,7 +1390,7 @@ func (pk *packer) writeAZip(ctx context.Context, trunc blob.Ref) (err error) {
 		}
 		if n, err := io.Copy(io.MultiWriter(fw, chunkWholeHash), rc); err != nil || n != int64(size) {
 			rc.Close()
-			return fmt.Errorf("copy to zip = %v, %v; want %v bytes", n, err, size)
+			return fmt.Errorf("copy to zip = %v, %w; want %v bytes", n, err, size)
 		}
 		rc.Close()
 
diff --git a/pkg/blobserver/blobpacked/subfetch.go b/pkg/blobserver/blobpacked/subfetch.go
index 7bd770c1d..0ffee5f60 100644
--- a/pkg/blobserver/blobpacked/subfetch.go
+++ b/pkg/blobserver/blobpacked/subfetch.go
@@ -18,6 +18,7 @@ package blobpacked
 
 import (
 	"context"
+	"errors"
 	"io"
 
 	"perkeep.org/pkg/blob"
@@ -47,7 +48,7 @@ func (s *storage) SubFetch(ctx context.Context, ref blob.Ref, offset, length int
 	}
 	if sf, ok := s.small.(blob.SubFetcher); ok {
 		rc, err := sf.SubFetch(ctx, ref, offset, length)
-		if err != blob.ErrUnimplemented {
+		if !errors.Is(err, blob.ErrUnimplemented) {
 			return rc, err
 		}
 	}
diff --git a/pkg/blobserver/blobpacked/wholefetch.go b/pkg/blobserver/blobpacked/wholefetch.go
index 8c0c98e8d..9cb9edc5b 100644
--- a/pkg/blobserver/blobpacked/wholefetch.go
+++ b/pkg/blobserver/blobpacked/wholefetch.go
@@ -83,13 +83,13 @@ func (s *storage) OpenWholeRef(wholeRef blob.Ref, offset int64) (rc io.ReadClose
 		}
 		if err := conv.ParseFields(k[len(startKey)+len(":"):], &zp.idx); err != nil {
 			it.Close()
-			return nil, 0, fmt.Errorf("blobpacked: error parsing meta key %q: %v", k, err)
+			return nil, 0, fmt.Errorf("blobpacked: error parsing meta key %q: %w", k, err)
 		}
 		// "<zipchunk-blobref> <offset-in-zipchunk-blobref> <offset-in-whole_u64> <length_u32>"
 		var ignore uint64
 		if err := conv.ParseFields(it.ValueBytes(), &zp.zipRef, &zp.zipOff, &ignore, &zp.len); err != nil {
 			it.Close()
-			return nil, 0, fmt.Errorf("blobpacked: error parsing meta key %q = %q: %v", k, it.ValueBytes(), err)
+			return nil, 0, fmt.Errorf("blobpacked: error parsing meta key %q = %q: %w", k, it.ValueBytes(), err)
 		}
 		parts = append(parts, zp)
 	}
@@ -163,7 +163,7 @@ func (zr *wholeFromZips) Read(p []byte) (n int, err error) {
 		zr.cur.Close()
 		zr.cur = nil
 	}
-	if err == io.EOF && zr.remain > 0 {
+	if errors.Is(err, io.EOF) && zr.remain > 0 {
 		err = nil
 	} else if err == nil && zr.remain == 0 {
 		err = io.EOF
@@ -187,8 +187,8 @@ func (zr *wholeFromZips) initCur() {
 	zr.zp = zr.zp[1:]
 	rc, err := zr.src.SubFetch(zr.ctx, zp.zipRef, int64(zp.zipOff), int64(zp.len))
 	if err != nil {
-		if err == os.ErrNotExist {
-			err = fmt.Errorf("blobpacked: error opening next part of file: %v", err)
+		if errors.Is(err, os.ErrNotExist) {
+			err = fmt.Errorf("blobpacked: error opening next part of file: %w", err)
 		}
 		zr.err = err
 		return
diff --git a/pkg/blobserver/encrypt/encrypt.go b/pkg/blobserver/encrypt/encrypt.go
index 3d32e4a5e..98eecc45b 100644
--- a/pkg/blobserver/encrypt/encrypt.go
+++ b/pkg/blobserver/encrypt/encrypt.go
@@ -178,7 +178,7 @@ func (s *storage) ReceiveBlob(ctx context.Context, plainBR blob.Ref, source io.R
 	encBR := blob.RefFromBytes(encBytes.Bytes())
 
 	if _, err = blobserver.ReceiveNoHash(ctx, s.blobs, encBR, encBytes); err != nil {
-		return sb, fmt.Errorf("encrypt: error writing encrypted blob %v (plaintext %v): %v", encBR, plainBR, err)
+		return sb, fmt.Errorf("encrypt: error writing encrypted blob %v (plaintext %v): %w", encBR, plainBR, err)
 	}
 
 	metaBytes, err := s.makeSingleMetaBlob(plainBR, encBR, uint32(plainSize))
@@ -189,13 +189,13 @@ func (s *storage) ReceiveBlob(ctx context.Context, plainBR blob.Ref, source io.R
 	metaBR := blob.RefFromBytes(metaBytes)
 	metaSB, err := blobserver.ReceiveNoHash(ctx, s.meta, metaBR, bytes.NewReader(metaBytes))
 	if err != nil {
-		return sb, fmt.Errorf("encrypt: error writing encrypted meta for plaintext %v (encrypted blob %v): %v", plainBR, encBR, err)
+		return sb, fmt.Errorf("encrypt: error writing encrypted meta for plaintext %v (encrypted blob %v): %w", plainBR, encBR, err)
 	}
 	s.recordMeta(&metaBlob{br: metaSB.Ref, plains: []blob.Ref{plainBR}})
 
 	err = s.index.Set(plainBR.String(), packIndexEntry(uint32(plainSize), encBR))
 	if err != nil {
-		return sb, fmt.Errorf("encrypt: error updating index for encrypted %v (plaintext %v): %v", encBR, plainBR, err)
+		return sb, fmt.Errorf("encrypt: error updating index for encrypted %v (plaintext %v): %w", encBR, plainBR, err)
 	}
 
 	return blob.SizedRef{Ref: plainBR, Size: uint32(plainSize)}, nil
@@ -208,7 +208,7 @@ func (s *storage) Fetch(ctx context.Context, plainBR blob.Ref) (io.ReadCloser, u
 	}
 	encData, _, err := s.blobs.Fetch(ctx, encBR)
 	if err != nil {
-		return nil, 0, fmt.Errorf("encrypt: error fetching plaintext %s's encrypted %v blob: %v", plainBR, encBR, err)
+		return nil, 0, fmt.Errorf("encrypt: error fetching plaintext %s's encrypted %v blob: %w", plainBR, encBR, err)
 	}
 	defer encData.Close()
 
@@ -232,7 +232,7 @@ func (s *storage) Fetch(ctx context.Context, plainBR blob.Ref) (io.ReadCloser, u
 	// Using the pool here would be racy since the caller will read this asynchronously
 	plainBytes := bytes.NewBuffer(nil)
 	if err := s.decryptBlob(plainBytes, encBytes); err != nil {
-		return nil, 0, fmt.Errorf("encrypt: encrypted blob %s failed validation: %s", encBR, err)
+		return nil, 0, fmt.Errorf("encrypt: encrypted blob %s failed validation: %w", encBR, err)
 	}
 
 	return io.NopCloser(plainBytes), plainSize, nil
@@ -250,7 +250,7 @@ func (s *storage) EnumerateBlobs(ctx context.Context, dest chan<- blob.SizedRef,
 		br := blob.MustParse(iter.Key())
 		plainSize, _, err := unpackIndexEntry(iter.Value())
 		if err != nil {
-			return fmt.Errorf("bogus encrypt index value %q: %s", iter.Value(), err)
+			return fmt.Errorf("bogus encrypt index value %q: %w", iter.Value(), err)
 		}
 		select {
 		case dest <- blob.SizedRef{Ref: br, Size: plainSize}:
@@ -314,7 +314,7 @@ func newFromConfig(ld blobserver.Loader, config jsonconfig.Obj) (bs blobserver.S
 	log.Printf("Reading encryption metadata...")
 	sto.smallMeta = &metaBlobHeap{}
 	if err := sto.readAllMetaBlobs(); err != nil {
-		return nil, fmt.Errorf("error scanning metadata on start-up: %v", err)
+		return nil, fmt.Errorf("error scanning metadata on start-up: %w", err)
 	}
 	log.Printf("Read all encryption metadata in %.3f seconds", time.Since(start).Seconds())
 
diff --git a/pkg/blobserver/encrypt/meta.go b/pkg/blobserver/encrypt/meta.go
index 203e67d6e..6fde0e2a9 100644
--- a/pkg/blobserver/encrypt/meta.go
+++ b/pkg/blobserver/encrypt/meta.go
@@ -181,13 +181,13 @@ func unpackIndexEntry(s string) (plainSize uint32, encBR blob.Ref, err error) {
 	}
 	size, err := strconv.ParseUint(parts[0], 10, 32)
 	if err != nil {
-		err = fmt.Errorf("malformed index entry %q: %s", s, err)
+		err = fmt.Errorf("malformed index entry %q: %w", s, err)
 		return
 	}
 	plainSize = uint32(size)
 	encBR = blob.ParseOrZero(parts[1])
 	if !encBR.Valid() {
-		err = fmt.Errorf("malformed index entry %q: %s", s, err)
+		err = fmt.Errorf("malformed index entry %q: %w", s, err)
 	}
 	return
 }
@@ -195,7 +195,7 @@ func unpackIndexEntry(s string) (plainSize uint32, encBR blob.Ref, err error) {
 // fetchMeta returns os.ErrNotExist if the plaintext blob is not in the index.
 func (s *storage) fetchMeta(ctx context.Context, b blob.Ref) (plainSize uint32, encBR blob.Ref, err error) {
 	v, err := s.index.Get(b.String())
-	if err == sorted.ErrNotFound {
+	if errors.Is(err, sorted.ErrNotFound) {
 		err = os.ErrNotExist
 	}
 	if err != nil {
@@ -289,13 +289,13 @@ func (s *storage) readAllMetaBlobs() error {
 				defer func() { <-gate }()
 				rc, _, err := s.meta.Fetch(ctx, sb.Ref)
 				if err != nil {
-					metac <- encMB{sb.Ref, nil, fmt.Errorf("fetch failed: %v", err)}
+					metac <- encMB{sb.Ref, nil, fmt.Errorf("fetch failed: %w", err)}
 					return
 				}
 				defer rc.Close()
 				all, err := io.ReadAll(rc)
 				if err != nil {
-					metac <- encMB{sb.Ref, nil, fmt.Errorf("read failed: %v", err)}
+					metac <- encMB{sb.Ref, nil, fmt.Errorf("read failed: %w", err)}
 					return
 				}
 				metac <- encMB{sb.Ref, all, nil}
@@ -320,7 +320,7 @@ func (s *storage) readAllMetaBlobs() error {
 			// TODO: advertise in this error message a new option or environment variable
 			// to skip a certain or all meta blobs, to allow partial recovery, if some
 			// are corrupt. For now, require all to be correct.
-			return fmt.Errorf("error with meta blob %v: %v", mi.br, err)
+			return fmt.Errorf("error with meta blob %v: %w", mi.br, err)
 		}
 	}
 
diff --git a/pkg/blobserver/encrypt/permissions_posix.go b/pkg/blobserver/encrypt/permissions_posix.go
index 65bdb8b5c..0372afdad 100644
--- a/pkg/blobserver/encrypt/permissions_posix.go
+++ b/pkg/blobserver/encrypt/permissions_posix.go
@@ -25,7 +25,7 @@ import (
 
 func checkKeyFilePermissions(keyFile string) error {
 	if fileInfo, err := os.Stat(keyFile); err != nil {
-		return fmt.Errorf("Checking for key file permissions %v: %v", keyFile, err)
+		return fmt.Errorf("Checking for key file permissions %v: %w", keyFile, err)
 	} else if fileInfo.Mode().Perm()&0077 != 0 {
 		return fmt.Errorf("Key file permissions are too permissive (%o), they should be 600", fileInfo.Mode().Perm())
 	}
diff --git a/pkg/blobserver/files/files.go b/pkg/blobserver/files/files.go
index 4a3a37ed2..9b6ea6f99 100644
--- a/pkg/blobserver/files/files.go
+++ b/pkg/blobserver/files/files.go
@@ -165,7 +165,7 @@ func (ds *Storage) fetch(ctx context.Context, br blob.Ref, offset, length int64)
 	if offset != 0 {
 		if at, err := file.Seek(offset, io.SeekStart); err != nil || at != offset {
 			file.Close()
-			return nil, 0, fmt.Errorf("localdisk: error seeking to %d: got %v, %v", offset, at, err)
+			return nil, 0, fmt.Errorf("localdisk: error seeking to %d: got %v, %w", offset, at, err)
 		}
 	}
 	return struct {
diff --git a/pkg/blobserver/files/osfs_windows.go b/pkg/blobserver/files/osfs_windows.go
index 1ec2ea575..6dffb410a 100644
--- a/pkg/blobserver/files/osfs_windows.go
+++ b/pkg/blobserver/files/osfs_windows.go
@@ -17,6 +17,7 @@ limitations under the License.
 package files
 
 import (
+	"errors"
 	"fmt"
 	"os"
 	"syscall"
@@ -27,8 +28,8 @@ import (
 // a file over one that already exists
 // 2) oldfile and newfile are the same files (i.e have the same size)
 func mapRenameError(err error, oldfile, newfile string) error {
-	linkErr, ok := err.(*os.LinkError)
-	if !ok {
+	var linkErr *os.LinkError
+	if !errors.As(err, &linkErr) {
 		return err
 	}
 	if linkErr.Err != error(syscall.ERROR_ALREADY_EXISTS) {
diff --git a/pkg/blobserver/google/cloudstorage/storage.go b/pkg/blobserver/google/cloudstorage/storage.go
index 361f648dd..6e220b9f8 100644
--- a/pkg/blobserver/google/cloudstorage/storage.go
+++ b/pkg/blobserver/google/cloudstorage/storage.go
@@ -157,7 +157,7 @@ func newFromConfig(_ blobserver.Loader, config jsonconfig.Obj) (blobserver.Stora
 
 	ba, err := gs.client.Bucket(gs.bucket).Attrs(ctx)
 	if err != nil {
-		return nil, fmt.Errorf("error statting bucket %q: %v", gs.bucket, err)
+		return nil, fmt.Errorf("error statting bucket %q: %w", gs.bucket, err)
 	}
 	hash := sha1.New()
 	fmt.Fprintf(hash, "%v%v", ba.Created, ba.MetaGeneration)
@@ -226,7 +226,7 @@ func (s *Storage) StatBlobs(ctx context.Context, blobs []blob.Ref, fn func(blob.
 	// TODO: use cache
 	return blobserver.StatBlobsParallelHelper(ctx, blobs, fn, statGate, func(br blob.Ref) (sb blob.SizedRef, err error) {
 		attrs, err := s.client.Bucket(s.bucket).Object(s.dirPrefix + br.String()).Attrs(ctx)
-		if err == storage.ErrObjectNotExist {
+		if errors.Is(err, storage.ErrObjectNotExist) {
 			return sb, nil
 		}
 		if err != nil {
@@ -247,7 +247,7 @@ func (s *Storage) Fetch(ctx context.Context, br blob.Ref) (rc io.ReadCloser, siz
 		}
 	}
 	r, err := s.client.Bucket(s.bucket).Object(s.dirPrefix + br.String()).NewReader(ctx)
-	if err == storage.ErrObjectNotExist {
+	if errors.Is(err, storage.ErrObjectNotExist) {
 		return nil, 0, os.ErrNotExist
 	}
 	if err != nil {
@@ -271,10 +271,10 @@ func (s *Storage) SubFetch(ctx context.Context, br blob.Ref, offset, length int6
 	}
 	ctx = context.WithValue(ctx, ctxutil.HTTPClient, s.baseHTTPClient)
 	rc, err = gcsutil.GetPartialObject(ctx, gcsutil.Object{Bucket: s.bucket, Key: s.dirPrefix + br.String()}, offset, length)
-	if err == gcsutil.ErrInvalidRange {
+	if errors.Is(err, gcsutil.ErrInvalidRange) {
 		return nil, blob.ErrOutOfRangeOffsetSubFetch
 	}
-	if err == storage.ErrObjectNotExist {
+	if errors.Is(err, storage.ErrObjectNotExist) {
 		return nil, os.ErrNotExist
 	}
 	return rc, err
@@ -292,7 +292,7 @@ func (s *Storage) RemoveBlobs(ctx context.Context, blobs []blob.Ref) error {
 		grp.Go(func() error {
 			defer gate.Done()
 			err := s.client.Bucket(s.bucket).Object(s.dirPrefix + br.String()).Delete(ctx)
-			if err == storage.ErrObjectNotExist {
+			if errors.Is(err, storage.ErrObjectNotExist) {
 				return nil
 			}
 			return err
diff --git a/pkg/blobserver/handlers/upload.go b/pkg/blobserver/handlers/upload.go
index a173b7f5a..62c341665 100644
--- a/pkg/blobserver/handlers/upload.go
+++ b/pkg/blobserver/handlers/upload.go
@@ -75,7 +75,7 @@ func CreatePutUploadHandler(storage blobserver.BlobReceiver) http.Handler {
 			return
 		}
 		_, err := blobserver.Receive(ctx, storage, br, req.Body)
-		if err == blobserver.ErrCorruptBlob {
+		if errors.Is(err, blobserver.ErrCorruptBlob) {
 			httputil.BadRequestError(rw, "data doesn't match declared digest")
 			return
 		}
@@ -97,14 +97,14 @@ func vivify(ctx context.Context, blobReceiver blobserver.BlobReceiveConfiger, fi
 	}
 	fr, err := schema.NewFileReader(ctx, sf, fileblob.Ref)
 	if err != nil {
-		return fmt.Errorf("Filereader error for blobref %v: %v", fileblob.Ref.String(), err)
+		return fmt.Errorf("Filereader error for blobref %v: %w", fileblob.Ref.String(), err)
 	}
 	defer fr.Close()
 
 	h := blob.NewHash()
 	n, err := io.Copy(h, fr)
 	if err != nil {
-		return fmt.Errorf("Could not read all file of blobref %v: %v", fileblob.Ref.String(), err)
+		return fmt.Errorf("Could not read all file of blobref %v: %w", fileblob.Ref.String(), err)
 	}
 	if n != fr.Size() {
 		return fmt.Errorf("Could not read all file of blobref %v. Wanted %v, got %v", fileblob.Ref.String(), fr.Size(), n)
@@ -137,7 +137,7 @@ func vivify(ctx context.Context, blobReceiver blobserver.BlobReceiveConfiger, fi
 	// 3) the signature time of 2)
 	claimDate := fr.UnixMtime()
 	if claimDate.IsZero() {
-		return fmt.Errorf("While parsing modtime for file %v: %v", fr.FileName(), err)
+		return fmt.Errorf("While parsing modtime for file %v: %w", fr.FileName(), err)
 	}
 
 	permanodeBB := schema.NewHashPlannedPermanode(h)
@@ -145,12 +145,12 @@ func vivify(ctx context.Context, blobReceiver blobserver.BlobReceiveConfiger, fi
 	permanodeBB.SetClaimDate(claimDate)
 	permanodeSigned, err := sigHelper.Sign(ctx, permanodeBB)
 	if err != nil {
-		return fmt.Errorf("signing permanode %v: %v", permanodeSigned, err)
+		return fmt.Errorf("signing permanode %v: %w", permanodeSigned, err)
 	}
 	permanodeRef := blob.RefFromString(permanodeSigned)
 	_, err = blobserver.ReceiveNoHash(ctx, blobReceiver, permanodeRef, strings.NewReader(permanodeSigned))
 	if err != nil {
-		return fmt.Errorf("while uploading signed permanode %v, %v: %v", permanodeRef, permanodeSigned, err)
+		return fmt.Errorf("while uploading signed permanode %v, %v: %w", permanodeRef, permanodeSigned, err)
 	}
 
 	contentClaimBB := schema.NewSetAttributeClaim(permanodeRef, "camliContent", fileblob.Ref.String())
@@ -158,12 +158,12 @@ func vivify(ctx context.Context, blobReceiver blobserver.BlobReceiveConfiger, fi
 	contentClaimBB.SetClaimDate(claimDate)
 	contentClaimSigned, err := sigHelper.Sign(ctx, contentClaimBB)
 	if err != nil {
-		return fmt.Errorf("signing camliContent claim: %v", err)
+		return fmt.Errorf("signing camliContent claim: %w", err)
 	}
 	contentClaimRef := blob.RefFromString(contentClaimSigned)
 	_, err = blobserver.ReceiveNoHash(ctx, blobReceiver, contentClaimRef, strings.NewReader(contentClaimSigned))
 	if err != nil {
-		return fmt.Errorf("while uploading signed camliContent claim %v, %v: %v", contentClaimRef, contentClaimSigned, err)
+		return fmt.Errorf("while uploading signed camliContent claim %v, %v: %w", contentClaimRef, contentClaimSigned, err)
 	}
 	return nil
 }
@@ -197,7 +197,7 @@ func handleMultiPartUpload(rw http.ResponseWriter, req *http.Request, blobReceiv
 
 	for {
 		mimePart, err := multipart.NextPart()
-		if err == io.EOF {
+		if errors.Is(err, io.EOF) {
 			break
 		}
 		if err != nil {
diff --git a/pkg/blobserver/localdisk/localdisk.go b/pkg/blobserver/localdisk/localdisk.go
index 356640173..577294e38 100644
--- a/pkg/blobserver/localdisk/localdisk.go
+++ b/pkg/blobserver/localdisk/localdisk.go
@@ -31,6 +31,7 @@ package localdisk // import "perkeep.org/pkg/blobserver/localdisk"
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"log"
 	"os"
@@ -98,7 +99,7 @@ func New(root string) (*DiskStorage, error) {
 		// As a special case, we auto-created the "packed" directory for subpacked.
 		if filepath.Base(root) == "packed" {
 			if err := os.Mkdir(root, 0700); err != nil {
-				return nil, fmt.Errorf("failed to mkdir packed directory: %v", err)
+				return nil, fmt.Errorf("failed to mkdir packed directory: %w", err)
 			}
 			fi, err = os.Stat(root)
 		} else {
@@ -106,7 +107,7 @@ func New(root string) (*DiskStorage, error) {
 		}
 	}
 	if err != nil {
-		return nil, fmt.Errorf("Failed to stat directory %q: %v", root, err)
+		return nil, fmt.Errorf("Failed to stat directory %q: %w", root, err)
 	}
 	if !fi.IsDir() {
 		return nil, fmt.Errorf("storage root %q exists but is not a directory", root)
@@ -119,11 +120,11 @@ func New(root string) (*DiskStorage, error) {
 		gen:        local.NewGenerationer(root),
 	}
 	if _, _, err := ds.StorageGeneration(); err != nil {
-		return nil, fmt.Errorf("Error initialization generation for %q: %v", root, err)
+		return nil, fmt.Errorf("Error initialization generation for %q: %w", root, err)
 	}
 	ul, err := osutil.MaxFD()
 	if err != nil {
-		if err == osutil.ErrNotSupported {
+		if errors.Is(err, osutil.ErrNotSupported) {
 			// Do not set the gate on Windows, since we don't know the ulimit.
 			return ds, nil
 		}
@@ -162,12 +163,12 @@ func init() {
 func (ds *DiskStorage) checkFS() (ret error) {
 	tempdir, err := os.MkdirTemp(ds.root, "")
 	if err != nil {
-		return fmt.Errorf("localdisk check: unable to create tempdir in %s, err=%v", ds.root, err)
+		return fmt.Errorf("localdisk check: unable to create tempdir in %s, err=%w", ds.root, err)
 	}
 	defer func() {
 		err := os.RemoveAll(tempdir)
 		if err != nil {
-			cleanErr := fmt.Errorf("localdisk check: unable to clean temp dir: %v", err)
+			cleanErr := fmt.Errorf("localdisk check: unable to clean temp dir: %w", err)
 			if ret == nil {
 				ret = cleanErr
 			} else {
@@ -181,24 +182,24 @@ func (ds *DiskStorage) checkFS() (ret error) {
 	data := []byte("perkeep rocks")
 	err = os.WriteFile(tempfile, data, 0644)
 	if err != nil {
-		return fmt.Errorf("localdisk check: unable to write into %s, err=%v", ds.root, err)
+		return fmt.Errorf("localdisk check: unable to write into %s, err=%w", ds.root, err)
 	}
 
 	out, err := os.ReadFile(tempfile)
 	if err != nil {
-		return fmt.Errorf("localdisk check: unable to read from %s, err=%v", tempfile, err)
+		return fmt.Errorf("localdisk check: unable to read from %s, err=%w", tempfile, err)
 	}
 	if !bytes.Equal(out, data) {
 		return fmt.Errorf("localdisk check: tempfile contents didn't match, got=%q", out)
 	}
 	if _, err := os.Lstat(filename); !os.IsNotExist(err) {
-		return fmt.Errorf("localdisk check: didn't expect file to exist, Lstat had other error, err=%v", err)
+		return fmt.Errorf("localdisk check: didn't expect file to exist, Lstat had other error, err=%w", err)
 	}
 	if err := os.Rename(tempfile, filename); err != nil {
-		return fmt.Errorf("localdisk check: rename failed, err=%v", err)
+		return fmt.Errorf("localdisk check: rename failed, err=%w", err)
 	}
 	if _, err := os.Lstat(filename); err != nil {
-		return fmt.Errorf("localdisk check: after rename passed Lstat had error, err=%v", err)
+		return fmt.Errorf("localdisk check: after rename passed Lstat had error, err=%w", err)
 	}
 	return nil
 }
diff --git a/pkg/blobserver/mongo/receive.go b/pkg/blobserver/mongo/receive.go
index f8b5aefd0..ad04fa1fd 100644
--- a/pkg/blobserver/mongo/receive.go
+++ b/pkg/blobserver/mongo/receive.go
@@ -18,6 +18,7 @@ package mongo
 
 import (
 	"context"
+	"errors"
 	"io"
 
 	"perkeep.org/pkg/blob"
@@ -42,7 +43,8 @@ func (m *mongoStorage) ReceiveBlob(ctx context.Context, ref blob.Ref, source io.
 	if err = m.c.Insert(b); err != nil {
 		// Unique key violation?
 		// Then the blob already exists, no need to throw an error
-		if mongoErr, isMongoErr := err.(*mgo.LastError); isMongoErr && mongoErr.Code == mgoUniqueKeyErr {
+		var mongoErr *mgo.LastError
+		if errors.As(err, &mongoErr) && mongoErr.Code == mgoUniqueKeyErr {
 			return blob.SizedRef{Ref: ref, Size: b.Size}, nil
 		}
 		return blob.SizedRef{}, err
diff --git a/pkg/blobserver/mongo/remove.go b/pkg/blobserver/mongo/remove.go
index 9cee89d96..4ee1b0eff 100644
--- a/pkg/blobserver/mongo/remove.go
+++ b/pkg/blobserver/mongo/remove.go
@@ -18,10 +18,12 @@ package mongo
 
 import (
 	"context"
+	"errors"
 
 	"go4.org/syncutil"
 	"gopkg.in/mgo.v2"
 	"gopkg.in/mgo.v2/bson"
+
 	"perkeep.org/pkg/blob"
 )
 
@@ -36,7 +38,7 @@ func (m *mongoStorage) RemoveBlobs(ctx context.Context, blobs []blob.Ref) error
 		wg.Go(func() error {
 			defer removeGate.Done()
 			err := m.c.Remove(bson.M{"key": blob.String()})
-			if err == mgo.ErrNotFound {
+			if errors.Is(err, mgo.ErrNotFound) {
 				return nil
 			}
 			return err
diff --git a/pkg/blobserver/mongo/stat.go b/pkg/blobserver/mongo/stat.go
index 4d94b8c20..ee9f03f49 100644
--- a/pkg/blobserver/mongo/stat.go
+++ b/pkg/blobserver/mongo/stat.go
@@ -40,7 +40,7 @@ func (m *mongoStorage) StatBlobs(ctx context.Context, blobs []blob.Ref, fn func(
 		case mgo.ErrNotFound:
 			return sb, nil
 		default:
-			return sb, fmt.Errorf("mongo: error statting %v: %v", b, err)
+			return sb, fmt.Errorf("mongo: error statting %v: %w", b, err)
 		}
 	})
 }
diff --git a/pkg/blobserver/namespace/ns.go b/pkg/blobserver/namespace/ns.go
index a70278b62..880f044ca 100644
--- a/pkg/blobserver/namespace/ns.go
+++ b/pkg/blobserver/namespace/ns.go
@@ -25,6 +25,7 @@ package namespace // import "perkeep.org/pkg/blobserver/namespace"
 import (
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"log"
@@ -32,6 +33,7 @@ import (
 	"strconv"
 
 	"go4.org/jsonconfig"
+
 	"perkeep.org/pkg/blob"
 	"perkeep.org/pkg/blobserver"
 	"perkeep.org/pkg/sorted"
@@ -57,11 +59,11 @@ func newFromConfig(ld blobserver.Loader, config jsonconfig.Obj) (storage blobser
 	}
 	sto.inventory, err = sorted.NewKeyValue(invConf)
 	if err != nil {
-		return nil, fmt.Errorf("Invalid 'inventory' configuration: %v", err)
+		return nil, fmt.Errorf("Invalid 'inventory' configuration: %w", err)
 	}
 	sto.master, err = ld.GetStorage(masterName)
 	if err != nil {
-		return nil, fmt.Errorf("Invalid 'storage' configuration: %v", err)
+		return nil, fmt.Errorf("Invalid 'storage' configuration: %w", err)
 	}
 	return sto, nil
 }
@@ -100,7 +102,7 @@ func (ns *nsto) EnumerateBlobs(ctx context.Context, dest chan<- blob.SizedRef, a
 
 func (ns *nsto) Fetch(ctx context.Context, br blob.Ref) (rc io.ReadCloser, size uint32, err error) {
 	invSizeStr, err := ns.inventory.Get(br.String())
-	if err == sorted.ErrNotFound {
+	if errors.Is(err, sorted.ErrNotFound) {
 		err = os.ErrNotExist
 		return
 	}
@@ -155,7 +157,7 @@ func (ns *nsto) RemoveBlobs(ctx context.Context, blobs []blob.Ref) error {
 func (ns *nsto) StatBlobs(ctx context.Context, blobs []blob.Ref, fn func(blob.SizedRef) error) error {
 	for _, br := range blobs {
 		invSizeStr, err := ns.inventory.Get(br.String())
-		if err == sorted.ErrNotFound {
+		if errors.Is(err, sorted.ErrNotFound) {
 			continue
 		}
 		if err != nil {
diff --git a/pkg/blobserver/overlay/overlay.go b/pkg/blobserver/overlay/overlay.go
index b104498ea..580d1a881 100644
--- a/pkg/blobserver/overlay/overlay.go
+++ b/pkg/blobserver/overlay/overlay.go
@@ -41,6 +41,7 @@ package overlay // import "perkeep.org/pkg/blobserver/overlay"
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"log"
@@ -48,6 +49,7 @@ import (
 	"time"
 
 	"go4.org/jsonconfig"
+
 	"perkeep.org/pkg/blob"
 	"perkeep.org/pkg/blobserver"
 	"perkeep.org/pkg/sorted"
@@ -86,17 +88,17 @@ func newFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (blobserver.Storag
 
 	lower, err := ld.GetStorage(lowerPrefix)
 	if err != nil {
-		return nil, fmt.Errorf("failed to load lower at %s: %v", lowerPrefix, err)
+		return nil, fmt.Errorf("failed to load lower at %s: %w", lowerPrefix, err)
 	}
 	upper, err := ld.GetStorage(upperPrefix)
 	if err != nil {
-		return nil, fmt.Errorf("failed to load upper at %s: %v", upperPrefix, err)
+		return nil, fmt.Errorf("failed to load upper at %s: %w", upperPrefix, err)
 	}
 	var deleted sorted.KeyValue
 	if len(deletedConf) != 0 {
 		deleted, err = sorted.NewKeyValueMaybeWipe(deletedConf)
 		if err != nil {
-			return nil, fmt.Errorf("failed to setup deleted: %v", err)
+			return nil, fmt.Errorf("failed to setup deleted: %w", err)
 		}
 	}
 
@@ -153,7 +155,7 @@ func (sto *overlayStorage) isDeleted(br blob.Ref) bool {
 		return true
 	}
 
-	if err != sorted.ErrNotFound {
+	if !errors.Is(err, sorted.ErrNotFound) {
 		log.Printf("overlayStorage error accessing deleted: %v", err)
 	}
 
@@ -168,7 +170,7 @@ func (sto *overlayStorage) Fetch(ctx context.Context, br blob.Ref) (file io.Read
 	}
 
 	file, size, err = sto.upper.Fetch(ctx, br)
-	if err != os.ErrNotExist {
+	if !errors.Is(err, os.ErrNotExist) {
 		return file, size, err
 	}
 
diff --git a/pkg/blobserver/proxycache/proxycache.go b/pkg/blobserver/proxycache/proxycache.go
index 6eb5c067b..7d0c2da35 100644
--- a/pkg/blobserver/proxycache/proxycache.go
+++ b/pkg/blobserver/proxycache/proxycache.go
@@ -37,6 +37,7 @@ package proxycache // import "perkeep.org/pkg/blobserver/proxycache"
 import (
 	"bytes"
 	"context"
+	"errors"
 	"io"
 	"log"
 	"os"
@@ -44,6 +45,7 @@ import (
 
 	"go4.org/jsonconfig"
 	"go4.org/syncutil"
+
 	"perkeep.org/internal/lru"
 	"perkeep.org/pkg/blob"
 	"perkeep.org/pkg/blobserver"
@@ -155,7 +157,7 @@ func (sto *Storage) Fetch(ctx context.Context, b blob.Ref) (rc io.ReadCloser, si
 		sto.touch(blob.SizedRef{Ref: b, Size: size})
 		return
 	}
-	if err != os.ErrNotExist {
+	if !errors.Is(err, os.ErrNotExist) {
 		log.Printf("warning: proxycache cache fetch error for %v: %v", b, err)
 	}
 	rc, size, err = sto.origin.Fetch(ctx, b)
@@ -180,7 +182,7 @@ func (sto *Storage) SubFetch(ctx context.Context, ref blob.Ref, offset, length i
 		if err == nil {
 			return rc, nil
 		}
-		if err != os.ErrNotExist && err != blob.ErrUnimplemented {
+		if !errors.Is(err, os.ErrNotExist) && !errors.Is(err, blob.ErrUnimplemented) {
 			log.Printf("proxycache: error fetching from cache %T: %v", sto.cache, err)
 		}
 	}
diff --git a/pkg/blobserver/receive.go b/pkg/blobserver/receive.go
index c627dc088..b5fe10c35 100644
--- a/pkg/blobserver/receive.go
+++ b/pkg/blobserver/receive.go
@@ -81,7 +81,7 @@ type checkHashReader struct {
 func (c *checkHashReader) Read(p []byte) (n int, err error) {
 	n, err = c.src.Read(p)
 	c.h.Write(p[:n])
-	if err == io.EOF && !c.br.HashMatches(c.h) {
+	if errors.Is(err, io.EOF) && !c.br.HashMatches(c.h) {
 		err = ErrCorruptBlob
 		c.corrupt = true
 	}
diff --git a/pkg/blobserver/s3/enumerate.go b/pkg/blobserver/s3/enumerate.go
index 03687e91c..6d560cf59 100644
--- a/pkg/blobserver/s3/enumerate.go
+++ b/pkg/blobserver/s3/enumerate.go
@@ -85,7 +85,7 @@ func (sto *s3Storage) EnumerateBlobs(ctx context.Context, dest chan<- blob.Sized
 	}
 
 	if err != nil {
-		return fmt.Errorf("s3 EnumerateBlobs: %v", err)
+		return fmt.Errorf("s3 EnumerateBlobs: %w", err)
 	}
 	return nil
 }
diff --git a/pkg/blobserver/s3/fetch.go b/pkg/blobserver/s3/fetch.go
index ff05b094c..2668d17a0 100644
--- a/pkg/blobserver/s3/fetch.go
+++ b/pkg/blobserver/s3/fetch.go
@@ -18,6 +18,7 @@ package s3
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -25,6 +26,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/service/s3"
+
 	"perkeep.org/pkg/blob"
 )
 
@@ -58,7 +60,8 @@ func (sto *s3Storage) fetch(ctx context.Context, br blob.Ref, objRange *string)
 	if isNotFound(err) {
 		return nil, 0, os.ErrNotExist
 	}
-	if aerr, ok := err.(awserr.Error); ok {
+	var aerr awserr.Error
+	if errors.As(err, &aerr) {
 		if aerr.Code() == "InvalidRange" {
 			return nil, 0, blob.ErrOutOfRangeOffsetSubFetch
 		}
diff --git a/pkg/blobserver/s3/s3.go b/pkg/blobserver/s3/s3.go
index 1a70ed485..8ec4f12e0 100644
--- a/pkg/blobserver/s3/s3.go
+++ b/pkg/blobserver/s3/s3.go
@@ -35,6 +35,7 @@ package s3 // import "perkeep.org/pkg/blobserver/s3"
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 	"strings"
@@ -178,7 +179,8 @@ func isNotFound(err error) bool {
 	if err == nil {
 		return false
 	}
-	if aerr, ok := err.(awserr.Error); ok {
+	var aerr awserr.Error
+	if errors.As(err, &aerr) {
 		return aerr.Code() == s3.ErrCodeNoSuchKey ||
 			// Check 'NotFound' as well because it's returned for some requests, even
 			// though the API model doesn't include it (hence why there isn't an
diff --git a/pkg/blobserver/s3/s3_preflight.go b/pkg/blobserver/s3/s3_preflight.go
index b25636aeb..740851d45 100644
--- a/pkg/blobserver/s3/s3_preflight.go
+++ b/pkg/blobserver/s3/s3_preflight.go
@@ -20,6 +20,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/xml"
+	"errors"
 	"fmt"
 	"io"
 	"log"
@@ -126,7 +127,7 @@ func determineEndpoint(ctx context.Context, svc s3iface.S3API, endpoint, bucket,
 		}
 		var b bytes.Buffer
 		if _, err := io.Copy(&b, r.HTTPResponse.Body); err != nil {
-			r.Error = fmt.Errorf("error reading body: %v", err)
+			r.Error = fmt.Errorf("error reading body: %w", err)
 			return
 		}
 
@@ -152,7 +153,7 @@ func determineEndpoint(ctx context.Context, svc s3iface.S3API, endpoint, bucket,
 				return determineEndpoint(ctx, svc, endpoint, bucket, newRegion)
 			}
 		}
-		return "", "", fmt.Errorf("s3: could not determine endpoint: %v", err)
+		return "", "", fmt.Errorf("s3: could not determine endpoint: %w", err)
 	}
 	// this indicates the UnmarshalError handler wasn't called, and since the
 	// above branch didn't happen there wasn't an error. That means our current
@@ -209,7 +210,8 @@ var malformedAuthHeaderMessageRegexp = regexp.MustCompile("region '[^']+' is wro
 // In an attempt to retain that functionality, we parse the error message
 // telling us we hit the wrong region and auto-correct it.
 func regionFromMalformedAuthHeaderError(err error) string {
-	if aerr, ok := err.(awserr.Error); ok && aerr.Code() == "AuthorizationHeaderMalformed" {
+	var aerr awserr.Error
+	if errors.As(err, &aerr) && aerr.Code() == "AuthorizationHeaderMalformed" {
 		matches := malformedAuthHeaderMessageRegexp.FindStringSubmatch(aerr.Message())
 		if len(matches) == 2 {
 			return matches[1]
diff --git a/pkg/blobserver/s3/stat.go b/pkg/blobserver/s3/stat.go
index 1a5cc1f0f..ad2e96743 100644
--- a/pkg/blobserver/s3/stat.go
+++ b/pkg/blobserver/s3/stat.go
@@ -46,6 +46,6 @@ func (sto *s3Storage) StatBlobs(ctx context.Context, blobs []blob.Ref, fn func(b
 		if isNotFound(err) {
 			return sb, nil
 		}
-		return sb, fmt.Errorf("error statting %v: %v", br, err)
+		return sb, fmt.Errorf("error statting %v: %w", br, err)
 	})
 }
diff --git a/pkg/client/client.go b/pkg/client/client.go
index deeba9805..2ecb17152 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -229,7 +229,7 @@ func NewOrFail(opts ...ClientOption) *Client {
 func (c *Client) NewPathClient(path string) (*Client, error) {
 	u, err := url.Parse(c.server)
 	if err != nil {
-		return nil, fmt.Errorf("bogus server %q for NewPathClient receiver: %v", c.server, err)
+		return nil, fmt.Errorf("bogus server %q for NewPathClient receiver: %w", c.server, err)
 	}
 	u.Path = path
 	pc, err := New(OptionServer(u.String()))
@@ -449,7 +449,7 @@ func NewFromShareRoot(ctx context.Context, shareBlobURL string, opts ...ClientOp
 	req := c.newRequest(ctx, "GET", shareBlobURL, nil)
 	res, err := c.expect2XX(req)
 	if err != nil {
-		return nil, blob.Ref{}, fmt.Errorf("error fetching %s: %v", shareBlobURL, err)
+		return nil, blob.Ref{}, fmt.Errorf("error fetching %s: %w", shareBlobURL, err)
 	}
 	defer res.Body.Close()
 	var buf bytes.Buffer
@@ -459,7 +459,7 @@ func NewFromShareRoot(ctx context.Context, shareBlobURL string, opts ...ClientOp
 	}
 	b, err := schema.BlobFromReader(rootbr, io.TeeReader(res.Body, &buf))
 	if err != nil {
-		return nil, blob.Ref{}, fmt.Errorf("error parsing JSON from %s: %v , with response: %q", shareBlobURL, err, buf.Bytes())
+		return nil, blob.Ref{}, fmt.Errorf("error parsing JSON from %s: %w, with response: %q", shareBlobURL, err, buf.Bytes())
 	}
 	if b.ShareAuthType() != schema.ShareHaveRef {
 		return nil, blob.Ref{}, fmt.Errorf("unknown share authType of %q", b.ShareAuthType())
@@ -846,7 +846,7 @@ func (c *Client) SearchExistingFileSchema(ctx context.Context, wholeRef ...blob.
 		} else if mismatch {
 			return blob.Ref{}, fmt.Errorf("Client is too recent for this server. Use a client built before 2018-01-13-6e8a5930c9, or upgrade the server to after that revision.")
 		}
-		return blob.Ref{}, fmt.Errorf("client: error parsing JSON from URL %s: %v", url, err)
+		return blob.Ref{}, fmt.Errorf("client: error parsing JSON from URL %s: %w", url, err)
 	}
 	if len(ress.Files) == 0 {
 		return blob.Ref{}, nil
@@ -872,7 +872,7 @@ func (c *Client) versionMismatch(ctx context.Context) (bool, error) {
 	version = version[:10] // keep only the date part
 	clientDate, err := time.Parse(shortRFC3339, version)
 	if err != nil {
-		return false, fmt.Errorf("could not parse date from version %q: %v", version, err)
+		return false, fmt.Errorf("could not parse date from version %q: %w", version, err)
 	}
 	apiChangeDate, _ := time.Parse(shortRFC3339, "2018-01-13")
 	if !clientDate.After(apiChangeDate) {
@@ -894,12 +894,12 @@ func (c *Client) versionMismatch(ctx context.Context) (bool, error) {
 		Version string `json:"version"`
 	}
 	if err := httputil.DecodeJSON(res, &status); err != nil {
-		return false, fmt.Errorf("error parsing JSON from URL %s: %v", url, err)
+		return false, fmt.Errorf("error parsing JSON from URL %s: %w", url, err)
 	}
 	serverVersion := status.Version[:10]
 	serverDate, err := time.Parse(shortRFC3339, serverVersion)
 	if err != nil {
-		return false, fmt.Errorf("could not parse date from server version %q: %v", status.Version, err)
+		return false, fmt.Errorf("could not parse date from server version %q: %w", status.Version, err)
 	}
 	if serverDate.After(apiChangeDate) {
 		// server is recent enough, all good.
@@ -1098,7 +1098,7 @@ func (c *Client) doDiscovery() error {
 
 	u, err = root.Parse(disco.BlobRoot)
 	if err != nil {
-		return fmt.Errorf("client: error resolving blobRoot: %v", err)
+		return fmt.Errorf("client: error resolving blobRoot: %w", err)
 	}
 	c.prefixv = strings.TrimRight(u.String(), "/")
 
diff --git a/pkg/client/config.go b/pkg/client/config.go
index 42b68a55c..0a5c69116 100644
--- a/pkg/client/config.go
+++ b/pkg/client/config.go
@@ -326,8 +326,8 @@ func (c *Client) SetupAuth() error {
 			c.authMode = authMode
 			return nil
 		}
-		if err != auth.ErrNoAuth {
-			return fmt.Errorf("Could not set up auth from env var CAMLI_AUTH: %v", err)
+		if !errors.Is(err, auth.ErrNoAuth) {
+			return fmt.Errorf("Could not set up auth from env var CAMLI_AUTH: %w", err)
 		}
 	}
 	if c.server == "" {
diff --git a/pkg/client/get.go b/pkg/client/get.go
index 2744ab4e8..f30eeda5a 100644
--- a/pkg/client/get.go
+++ b/pkg/client/get.go
@@ -131,7 +131,7 @@ func (c *Client) fetchVia(ctx context.Context, b blob.Ref, v []blob.Ref) (body i
 		}
 		if err == nil {
 			panic("unexpected")
-		} else if err == io.EOF {
+		} else if errors.Is(err, io.EOF) {
 			size = uint32(n)
 			reader, closer = &buf, types.NopCloser
 		} else {
@@ -141,7 +141,7 @@ func (c *Client) fetchVia(ctx context.Context, b blob.Ref, v []blob.Ref) (body i
 
 	var buf bytes.Buffer
 	if err := c.UpdateShareChain(b, io.TeeReader(reader, &buf)); err != nil {
-		if err != ErrNotSharing {
+		if !errors.Is(err, ErrNotSharing) {
 			return nil, 0, err
 		}
 	}
diff --git a/pkg/client/remove.go b/pkg/client/remove.go
index 3e42d1d3f..16a4ec778 100644
--- a/pkg/client/remove.go
+++ b/pkg/client/remove.go
@@ -54,7 +54,7 @@ func (c *Client) RemoveBlobs(ctx context.Context, blobs []blob.Ref) error {
 
 	req, err := http.NewRequest("POST", url_, strings.NewReader(params.Encode()))
 	if err != nil {
-		return fmt.Errorf("Error creating RemoveBlobs POST request: %v", err)
+		return fmt.Errorf("Error creating RemoveBlobs POST request: %w", err)
 	}
 	req = req.WithContext(ctx)
 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
diff --git a/pkg/client/upload.go b/pkg/client/upload.go
index 581c35c57..2f0db9268 100644
--- a/pkg/client/upload.go
+++ b/pkg/client/upload.go
@@ -257,7 +257,7 @@ func (c *Client) Upload(ctx context.Context, h *UploadHandle) (*PutResult, error
 
 	bodyReader, bodySize, err := h.readerAndSize()
 	if err != nil {
-		return nil, fmt.Errorf("client: error slurping upload handle to find its length: %v", err)
+		return nil, fmt.Errorf("client: error slurping upload handle to find its length: %w", err)
 	}
 	if bodySize > constants.MaxBlobSize {
 		return nil, errors.New("client: body is bigger then max blob size")
@@ -491,7 +491,7 @@ func (c *Client) UploadFile(ctx context.Context, filename string, contents io.Re
 func (c *Client) wholeRef(contents io.Reader) ([]blob.Ref, error) {
 	hasLegacySHA1, err := c.HasLegacySHA1()
 	if err != nil {
-		return nil, fmt.Errorf("cannot discover if server has legacy sha1: %v", err)
+		return nil, fmt.Errorf("cannot discover if server has legacy sha1: %w", err)
 	}
 	td := hashutil.NewTrackDigestReader(contents)
 	td.DoLegacySHA1 = hasLegacySHA1
@@ -522,12 +522,12 @@ func (c *Client) fileMapFromDuplicate(ctx context.Context, fileMap *schema.Build
 	}
 	dupMap, err := c.FetchSchemaBlob(ctx, dupFileRef)
 	if err != nil {
-		return blob.Ref{}, fmt.Errorf("could not find existing file blob for wholeRef %q: %v", wholeRef, err)
+		return blob.Ref{}, fmt.Errorf("could not find existing file blob for wholeRef %q: %w", wholeRef, err)
 	}
 	fileMap.PopulateParts(dupMap.PartsSize(), dupMap.ByteParts())
 	json, err := fileMap.JSON()
 	if err != nil {
-		return blob.Ref{}, fmt.Errorf("could not write file map for wholeRef %q: %v", wholeRef, err)
+		return blob.Ref{}, fmt.Errorf("could not write file map for wholeRef %q: %w", wholeRef, err)
 	}
 	bref := blob.RefFromString(json)
 	if bref == dupFileRef {
diff --git a/pkg/cmdmain/cmdmain.go b/pkg/cmdmain/cmdmain.go
index daaeb6d9d..c9e043cef 100644
--- a/pkg/cmdmain/cmdmain.go
+++ b/pkg/cmdmain/cmdmain.go
@@ -19,6 +19,7 @@ limitations under the License.
 package cmdmain // import "perkeep.org/pkg/cmdmain"
 
 import (
+	"errors"
 	"flag"
 	"fmt"
 	"io"
@@ -296,7 +297,7 @@ func Main() {
 		// We want -h to behave as -help, but without having to define another flag for
 		// it, so we handle it here.
 		// TODO(mpl): maybe even remove -help and just let them both be handled here?
-		if err == flag.ErrHelp {
+		if errors.Is(err, flag.ErrHelp) {
 			help(mode)
 			return
 		}
@@ -308,10 +309,9 @@ func Main() {
 		}
 		err = cmd.RunCommand(cmdFlags.Args())
 	}
-	if ue, isUsage := err.(UsageError); isUsage {
-		if isUsage {
-			Errorf("%s\n", ue)
-		}
+	var ue UsageError
+	if errors.As(err, &ue) {
+		Errorf("%s\n", ue)
 		cmd.Usage()
 		Errorf("\nGlobal options:\n")
 		flag.PrintDefaults()
diff --git a/pkg/fs/fs.go b/pkg/fs/fs.go
index 9644d6f34..b9354c08c 100644
--- a/pkg/fs/fs.go
+++ b/pkg/fs/fs.go
@@ -22,6 +22,7 @@ package fs // import "perkeep.org/pkg/fs"
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"log"
@@ -223,7 +224,7 @@ func (nr *nodeReader) Read(ctx context.Context, req *fuse.ReadRequest, res *fuse
 	}
 	buf := make([]byte, size)
 	n, err := nr.fr.ReadAt(buf, req.Offset)
-	if err == io.EOF {
+	if errors.Is(err, io.EOF) {
 		err = nil
 	}
 	if err != nil {
diff --git a/pkg/fs/mut.go b/pkg/fs/mut.go
index cdfbf1092..b9717c229 100644
--- a/pkg/fs/mut.go
+++ b/pkg/fs/mut.go
@@ -782,7 +782,7 @@ func (h *mutFileHandle) Read(ctx context.Context, req *fuse.ReadRequest, res *fu
 
 	buf := make([]byte, toRead)
 	n, err := h.tmp.ReadAt(buf, req.Offset)
-	if err == io.EOF {
+	if errors.Is(err, io.EOF) {
 		err = nil
 	}
 	if err != nil {
diff --git a/pkg/fs/time.go b/pkg/fs/time.go
index 193da1e11..a9e9f19c9 100644
--- a/pkg/fs/time.go
+++ b/pkg/fs/time.go
@@ -74,32 +74,32 @@ func parseCanonicalTime(in string) (time.Time, error) {
 
 	year, err := strconv.Atoi(in[0:4])
 	if err != nil {
-		return time.Time{}, fmt.Errorf("error parsing year: %v", err)
+		return time.Time{}, fmt.Errorf("error parsing year: %w", err)
 	}
 
 	month, err := strconv.Atoi(in[5:7])
 	if err != nil {
-		return time.Time{}, fmt.Errorf("error parsing month: %v", err)
+		return time.Time{}, fmt.Errorf("error parsing month: %w", err)
 	}
 
 	day, err := strconv.Atoi(in[8:10])
 	if err != nil {
-		return time.Time{}, fmt.Errorf("error parsing day: %v", err)
+		return time.Time{}, fmt.Errorf("error parsing day: %w", err)
 	}
 
 	hour, err := strconv.Atoi(in[11:13])
 	if err != nil {
-		return time.Time{}, fmt.Errorf("error parsing hour: %v", err)
+		return time.Time{}, fmt.Errorf("error parsing hour: %w", err)
 	}
 
 	minute, err := strconv.Atoi(in[14:16])
 	if err != nil {
-		return time.Time{}, fmt.Errorf("error parsing minute: %v", err)
+		return time.Time{}, fmt.Errorf("error parsing minute: %w", err)
 	}
 
 	second, err := strconv.Atoi(in[17:19])
 	if err != nil {
-		return time.Time{}, fmt.Errorf("error parsing second: %v", err)
+		return time.Time{}, fmt.Errorf("error parsing second: %w", err)
 	}
 
 	var nsecstr string
@@ -111,7 +111,7 @@ func parseCanonicalTime(in string) (time.Time, error) {
 	if nsecstr != "" {
 		nsec, err = strconv.Atoi(nsecstr)
 		if err != nil {
-			return time.Time{}, fmt.Errorf("error parsing nanoseconds: %v", err)
+			return time.Time{}, fmt.Errorf("error parsing nanoseconds: %w", err)
 		}
 	}
 
diff --git a/pkg/gc/gc.go b/pkg/gc/gc.go
index 562b496cd..28d6f3d8f 100644
--- a/pkg/gc/gc.go
+++ b/pkg/gc/gc.go
@@ -166,7 +166,7 @@ func (c *Collector) Collect(ctx context.Context) (err error) {
 		return c.Roots.Enumerate(markCtx, roots)
 	})
 	if err := marker.Err(); err != nil {
-		return fmt.Errorf("Mark failure: %v", err)
+		return fmt.Errorf("Mark failure: %w", err)
 	}
 
 	// Sweep.
@@ -192,7 +192,7 @@ func (c *Collector) Collect(ctx context.Context) (err error) {
 		return nil
 	})
 	if err := sweeper.Err(); err != nil {
-		return fmt.Errorf("sweep failure: %v", err)
+		return fmt.Errorf("sweep failure: %w", err)
 	}
 	return nil
 }
diff --git a/pkg/importer/dummy/dummy.go b/pkg/importer/dummy/dummy.go
index 0ca77f8f5..765a600ce 100644
--- a/pkg/importer/dummy/dummy.go
+++ b/pkg/importer/dummy/dummy.go
@@ -147,7 +147,7 @@ func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *import
 		acctAttrUsername, name,
 		acctAttrToken, code,
 	); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error setting attributes: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error setting attributes: %w", err))
 		return
 	}
 	http.Redirect(w, r, ctx.AccountURL(), http.StatusFound)
diff --git a/pkg/importer/feed/feed.go b/pkg/importer/feed/feed.go
index 9fbebe3b9..927553416 100644
--- a/pkg/importer/feed/feed.go
+++ b/pkg/importer/feed/feed.go
@@ -269,7 +269,7 @@ func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *import
 	if err := ctx.AccountNode.SetAttrs(
 		acctAttrFeedURL, feed.String(),
 	); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %w", err))
 		return
 	}
 	http.Redirect(w, r, ctx.AccountURL(), http.StatusFound)
diff --git a/pkg/importer/flickr/flickr.go b/pkg/importer/flickr/flickr.go
index 2d7cec2ed..97fe1d901 100644
--- a/pkg/importer/flickr/flickr.go
+++ b/pkg/importer/flickr/flickr.go
@@ -119,7 +119,7 @@ type run struct {
 func (imp) Run(ctx *importer.RunContext) error {
 	clientID, secret, err := ctx.Credentials()
 	if err != nil {
-		return fmt.Errorf("no API credentials: %v", err)
+		return fmt.Errorf("no API credentials: %w", err)
 	}
 	accountNode := ctx.AccountNode()
 	accessToken := accountNode.Attr(importer.AcctAttrAccessToken)
@@ -385,7 +385,7 @@ func (r *run) importPhoto(parent *importer.Object, photo *photosSearchItem) erro
 		log.Printf("Flickr importer: problem with date taken of photo %v, defaulting to published date instead.", photo.Id)
 		seconds, err := strconv.ParseInt(photo.DateUpload, 10, 64)
 		if err != nil {
-			return fmt.Errorf("could not parse date upload time %q for image %v: %v", photo.DateUpload, photo.Id, err)
+			return fmt.Errorf("could not parse date upload time %q for image %v: %w", photo.DateUpload, photo.Id, err)
 		}
 		dateTaken = time.Unix(seconds, 0)
 	}
@@ -408,13 +408,13 @@ func (r *run) importPhoto(parent *importer.Object, photo *photosSearchItem) erro
 	// lastupdate is a Unix timestamp according to https://www.flickr.com/services/api/flickr.photos.getInfo.html
 	seconds, err := strconv.ParseInt(photo.LastUpdate, 10, 64)
 	if err != nil {
-		return fmt.Errorf("could not parse lastupdate time for image %v: %v", photo.Id, err)
+		return fmt.Errorf("could not parse lastupdate time for image %v: %w", photo.Id, err)
 	}
 	lastUpdate := time.Unix(seconds, 0)
 	if lastUpdateString := photoNode.Attr(nodeattr.DateModified); lastUpdateString != "" {
 		oldLastUpdate, err := time.Parse(time.RFC3339, lastUpdateString)
 		if err != nil {
-			return fmt.Errorf("could not parse last stored update time for image %v: %v", photo.Id, err)
+			return fmt.Errorf("could not parse last stored update time for image %v: %w", photo.Id, err)
 		}
 		if lastUpdate.Equal(oldLastUpdate) {
 			if err := r.updatePrimaryPhoto(photoNode); err != nil {
@@ -460,18 +460,18 @@ func (r *run) updatePrimaryPhoto(photoNode *importer.Object) error {
 		}
 		setsNode, err := r.getTopLevelNode("sets", "Sets")
 		if err != nil {
-			return fmt.Errorf("could not set %v as primary photo of %v, no root sets: %v", photoId, album, err)
+			return fmt.Errorf("could not set %v as primary photo of %v, no root sets: %w", photoId, album, err)
 		}
 		setNode, err := setsNode.ChildPathObject(album)
 		if err != nil {
-			return fmt.Errorf("could not set %v as primary photo of %v, no album: %v", photoId, album, err)
+			return fmt.Errorf("could not set %v as primary photo of %v, no album: %w", photoId, album, err)
 		}
 		fileRef := photoNode.Attr(nodeattr.CamliContent)
 		if fileRef == "" {
 			return fmt.Errorf("could not set %v as primary photo of %v: fileRef of photo is unknown", photoId, album)
 		}
 		if err := setNode.SetAttr(nodeattr.CamliContentImage, fileRef); err != nil {
-			return fmt.Errorf("could not set %v as primary photo of %v: %v", photoId, album, err)
+			return fmt.Errorf("could not set %v as primary photo of %v: %w", photoId, album, err)
 		}
 		delete(r.primaryPhoto, album)
 	}
@@ -515,13 +515,13 @@ func (r *run) fetch(url string, form url.Values) (*http.Response, error) {
 func (imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error {
 	oauthClient, err := ctx.NewOAuthClient(oAuthURIs)
 	if err != nil {
-		err = fmt.Errorf("error getting OAuth client: %v", err)
+		err = fmt.Errorf("error getting OAuth client: %w", err)
 		httputil.ServeError(w, r, err)
 		return err
 	}
 	tempCred, err := oauthClient.RequestTemporaryCredentials(ctxutil.Client(ctx), ctx.CallbackURL(), nil)
 	if err != nil {
-		err = fmt.Errorf("Error getting temp cred: %v", err)
+		err = fmt.Errorf("Error getting temp cred: %w", err)
 		httputil.ServeError(w, r, err)
 		return err
 	}
@@ -529,7 +529,7 @@ func (imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.Setu
 		importer.AcctAttrTempToken, tempCred.Token,
 		importer.AcctAttrTempSecret, tempCred.Secret,
 	); err != nil {
-		err = fmt.Errorf("Error saving temp creds: %v", err)
+		err = fmt.Errorf("Error saving temp creds: %w", err)
 		httputil.ServeError(w, r, err)
 		return err
 	}
@@ -554,7 +554,7 @@ func (imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.S
 	}
 	oauthClient, err := ctx.NewOAuthClient(oAuthURIs)
 	if err != nil {
-		err = fmt.Errorf("error getting OAuth client: %v", err)
+		err = fmt.Errorf("error getting OAuth client: %w", err)
 		httputil.ServeError(w, r, err)
 		return
 	}
@@ -567,17 +567,17 @@ func (imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.S
 		r.FormValue("oauth_verifier"),
 	)
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error getting request token: %v ", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error getting request token: %w", err))
 		return
 	}
 	userID := vals.Get("user_nsid")
 	if userID == "" {
-		httputil.ServeError(w, r, fmt.Errorf("Couldn't get user id: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Couldn't get user id: %w", err))
 		return
 	}
 	username := vals.Get("username")
 	if username == "" {
-		httputil.ServeError(w, r, fmt.Errorf("Couldn't get user name: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Couldn't get user name: %w", err))
 		return
 	}
 
@@ -588,7 +588,7 @@ func (imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.S
 		importer.AcctAttrUserID, userID,
 		importer.AcctAttrUserName, username,
 	); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error setting basic account attributes: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error setting basic account attributes: %w", err))
 		return
 	}
 	http.Redirect(w, r, ctx.AccountURL(), http.StatusFound)
diff --git a/pkg/importer/gphotos/download.go b/pkg/importer/gphotos/download.go
index 84cebb267..5ae669208 100644
--- a/pkg/importer/gphotos/download.go
+++ b/pkg/importer/gphotos/download.go
@@ -336,8 +336,8 @@ func (dl *downloader) rateLimit(ctx context.Context, f func() error) error {
 		if err == nil {
 			return nil
 		}
-		ge, ok := err.(*googleapi.Error)
-		if !ok || ge.Code != http.StatusForbidden {
+		var ge *googleapi.Error
+		if !errors.As(err, &ge) || ge.Code != http.StatusForbidden {
 			return err
 		}
 		if ge.Message == "" {
diff --git a/pkg/importer/gphotos/gphotos.go b/pkg/importer/gphotos/gphotos.go
index af27c9315..46bce070c 100644
--- a/pkg/importer/gphotos/gphotos.go
+++ b/pkg/importer/gphotos/gphotos.go
@@ -153,7 +153,7 @@ func (im imp) CallbackURLParameters(acctRef blob.Ref) url.Values {
 func (im imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) {
 	oauthConfig, err := im.auth(ctx)
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error getting oauth config: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error getting oauth config: %w", err))
 		return
 	}
 
@@ -170,7 +170,7 @@ func (im imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importe
 	token, err := oauthConfig.Exchange(ctx, code)
 	if err != nil {
 		logf("token exchange error: %v", err)
-		httputil.ServeError(w, r, fmt.Errorf("token exchange error: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("token exchange error: %w", err))
 		return
 	}
 
@@ -179,7 +179,7 @@ func (im imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importe
 	userInfo, err := im.getUserInfo(gphotosCtx)
 	if err != nil {
 		logf("couldn't get username: %v", err)
-		httputil.ServeError(w, r, fmt.Errorf("can't get username: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("can't get username: %w", err))
 		return
 	}
 
@@ -189,7 +189,7 @@ func (im imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importe
 		importer.AcctAttrUserName, userInfo.Email,
 		acctAttrOAuthToken, encodeToken(token),
 	); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %w", err))
 		return
 	}
 	http.Redirect(w, r, ctx.AccountURL(), http.StatusFound)
@@ -333,7 +333,7 @@ func (imp) Run(rctx *importer.RunContext) error {
 func (r *run) importPhotos(ctx context.Context, sinceToken string) error {
 	photosNode, err := r.getTopLevelNode("photos")
 	if err != nil {
-		return fmt.Errorf("gphotos importer: get top level node: %v", err)
+		return fmt.Errorf("gphotos importer: get top level node: %w", err)
 	}
 
 	grp, grpCtx := errgroup.WithContext(ctx)
@@ -359,7 +359,7 @@ func (r *run) importPhotos(ctx context.Context, sinceToken string) error {
 		}
 	}
 	if err != nil {
-		return fmt.Errorf("gphotos importer: %v", err)
+		return fmt.Errorf("gphotos importer: %w", err)
 	}
 	if r.setNextToken != nil {
 		r.setNextToken(nextToken)
@@ -448,8 +448,8 @@ func (r *run) updatePhoto(ctx context.Context, parent *importer.Object, ph *phot
 		wholeRef := blob.RefFromHash(h)
 		pn, attrs, err := findExistingPermanode(r.Context(), r.Host.Searcher(), wholeRef)
 		if err != nil {
-			if err != os.ErrNotExist {
-				return nil, fmt.Errorf("could not look for permanode with %v as camliContent : %v", fileRefStr, err)
+			if !errors.Is(err, os.ErrNotExist) {
+				return nil, fmt.Errorf("could not look for permanode with %v as camliContent: %w", fileRefStr, err)
 			}
 			return r.Host.NewObject()
 		}
@@ -460,9 +460,9 @@ func (r *run) updatePhoto(ctx context.Context, parent *importer.Object, ph *phot
 	})
 	if err != nil {
 		if fileRefStr != "" {
-			return fmt.Errorf("error getting permanode for photo %q, with content %v: %v", ph.ID, fileRefStr, err)
+			return fmt.Errorf("error getting permanode for photo %q, with content %v: %w", ph.ID, fileRefStr, err)
 		}
-		return fmt.Errorf("error getting permanode for photo %q: %v", ph.ID, err)
+		return fmt.Errorf("error getting permanode for photo %q: %w", ph.ID, err)
 	}
 
 	if fileRefStr == "" {
diff --git a/pkg/importer/importer.go b/pkg/importer/importer.go
index ab9ec4225..20ce7d80c 100644
--- a/pkg/importer/importer.go
+++ b/pkg/importer/importer.go
@@ -289,7 +289,7 @@ func newFromConfig(ld blobserver.Loader, cfg jsonconfig.Obj) (http.Handler, erro
 				}
 			}
 			if err := impConf.Validate(); err != nil {
-				return nil, fmt.Errorf("Invalid static configuration for importer %q: %v", k, err)
+				return nil, fmt.Errorf("Invalid static configuration for importer %q: %w", k, err)
 			}
 			ClientId[k] = clientId
 			ClientSecret[k] = clientSecret
@@ -371,7 +371,7 @@ func CreateAccount(h *Host, impl string) (*RunContext, error) {
 	}
 	ia, err := imp.newAccount()
 	if err != nil {
-		return nil, fmt.Errorf("could not create new account for importer %v: %v", impl, err)
+		return nil, fmt.Errorf("could not create new account for importer %v: %w", impl, err)
 	}
 	rc := &RunContext{
 		Host: ia.im.host,
diff --git a/pkg/importer/instapaper/instapaper.go b/pkg/importer/instapaper/instapaper.go
index 81112a366..2f034c196 100644
--- a/pkg/importer/instapaper/instapaper.go
+++ b/pkg/importer/instapaper/instapaper.go
@@ -170,7 +170,7 @@ func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *import
 
 	clientID, secret, err := ctx.Credentials()
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Credentials error: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Credentials error: %w", err))
 		return
 	}
 
@@ -183,13 +183,13 @@ func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *import
 	}
 	creds, _, err := oauthClient.RequestTokenXAuth(ctxutil.Client(ctx), nil, username, password)
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Failed to get access token: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Failed to get access token: %w", err))
 		return
 	}
 
 	user, err := getUserInfo(importer.OAuthContext{Ctx: ctx.Context, Client: oauthClient, Creds: creds})
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Failed to verify credentials: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Failed to verify credentials: %w", err))
 		return
 	}
 
@@ -200,7 +200,7 @@ func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *import
 		importer.AcctAttrUserName, user.Username,
 		importer.AcctAttrUserID, fmt.Sprint(user.UserId),
 	); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error setting attributes: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error setting attributes: %w", err))
 		return
 	}
 	http.Redirect(w, r, ctx.AccountURL(), http.StatusFound)
@@ -209,7 +209,7 @@ func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *import
 func (im *imp) Run(ctx *importer.RunContext) (err error) {
 	clientId, secret, err := ctx.Credentials()
 	if err != nil {
-		return fmt.Errorf("no API credentials: %v", err)
+		return fmt.Errorf("no API credentials: %w", err)
 	}
 	acctNode := ctx.AccountNode()
 	accessToken := acctNode.Attr(importer.AcctAttrAccessToken)
@@ -400,8 +400,8 @@ func (r *run) importBookmark(parent *importer.Object, b *bookmark, folder string
 		func() (*importer.Object, error) {
 			found, err := r.findExistingBookmark(fmt.Sprint(b.BookmarkId))
 			if err != nil {
-				if err != os.ErrNotExist {
-					return nil, fmt.Errorf("searching for node with %v %v: %v", attrBookmarkId, b.BookmarkId, err)
+				if !errors.Is(err, os.ErrNotExist) {
+					return nil, fmt.Errorf("searching for node with %v %v: %w", attrBookmarkId, b.BookmarkId, err)
 				}
 				return r.Host.NewObject()
 			}
@@ -461,7 +461,7 @@ func (r *run) importBookmarkText(req txtReq) error {
 	defer resp.Body.Close()
 	fileRef, err := schema.WriteFileFromReader(r.Context(), r.Host.Target(), filename, resp.Body)
 	if err != nil {
-		return fmt.Errorf("error storing bookmark content: %v", err)
+		return fmt.Errorf("error storing bookmark content: %w", err)
 	}
 	err = req.bmNode.SetAttr("camliContent", fileRef.String())
 	if err == nil {
diff --git a/pkg/importer/mastodon/mastodon.go b/pkg/importer/mastodon/mastodon.go
index d7dd77479..2915e31d5 100644
--- a/pkg/importer/mastodon/mastodon.go
+++ b/pkg/importer/mastodon/mastodon.go
@@ -352,7 +352,7 @@ func (r *run) importStatuses(userID mastodon.ID) error {
 				defer gate.Done()
 				alreadyHad, err := r.importStatus(statusesNode, st)
 				if err != nil {
-					return fmt.Errorf("error importing status %s: %v", st.URI, err)
+					return fmt.Errorf("error importing status %s: %w", st.URI, err)
 				}
 
 				if !alreadyHad {
diff --git a/pkg/importer/oauth.go b/pkg/importer/oauth.go
index f44051342..d1aa8b3c4 100644
--- a/pkg/importer/oauth.go
+++ b/pkg/importer/oauth.go
@@ -173,7 +173,7 @@ func (octx OAuthContext) do(method string, url string, form url.Values) (*http.R
 		res, err = octx.Client.Get(ctxutil.Client(octx.Ctx), octx.Creds, url, form)
 	}
 	if err != nil {
-		return nil, fmt.Errorf("error fetching %s: %v", url, err)
+		return nil, fmt.Errorf("error fetching %s: %w", url, err)
 	}
 	if res.StatusCode != http.StatusOK {
 		return res, fmt.Errorf("%s request on %s failed with: %s", method, url, res.Status)
@@ -208,7 +208,7 @@ func (octx OAuthContext) PopulateJSONFromURL(result interface{}, method string,
 	}
 	err = httputil.DecodeJSON(hres, result)
 	if err != nil {
-		return fmt.Errorf("could not parse response for %s: %v", apiURL, err)
+		return fmt.Errorf("could not parse response for %s: %w", apiURL, err)
 	}
 	return err
 }
diff --git a/pkg/importer/picasa/picasa.go b/pkg/importer/picasa/picasa.go
index 075c474ad..eb33edaf7 100644
--- a/pkg/importer/picasa/picasa.go
+++ b/pkg/importer/picasa/picasa.go
@@ -148,7 +148,7 @@ func (im imp) CallbackURLParameters(acctRef blob.Ref) url.Values {
 func (im imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) {
 	oauthConfig, err := im.auth(ctx)
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error getting oauth config: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error getting oauth config: %w", err))
 		return
 	}
 
@@ -165,7 +165,7 @@ func (im imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importe
 	token, err := oauthConfig.Exchange(ctx, code)
 	if err != nil {
 		log.Printf("importer/picasa: token exchange error: %v", err)
-		httputil.ServeError(w, r, fmt.Errorf("token exchange error: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("token exchange error: %w", err))
 		return
 	}
 
@@ -175,7 +175,7 @@ func (im imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importe
 	userInfo, err := im.getUserInfo(picagoCtx)
 	if err != nil {
 		log.Printf("Couldn't get username: %v", err)
-		httputil.ServeError(w, r, fmt.Errorf("can't get username: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("can't get username: %w", err))
 		return
 	}
 
@@ -184,7 +184,7 @@ func (im imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importe
 		importer.AcctAttrName, userInfo.Name,
 		acctAttrOAuthToken, encodeToken(token),
 	); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %w", err))
 		return
 	}
 	http.Redirect(w, r, ctx.AccountURL(), http.StatusFound)
@@ -321,7 +321,7 @@ func (imp) Run(rctx *importer.RunContext) error {
 func (r *run) importAlbums(ctx context.Context) error {
 	albums, err := picago.GetAlbums(ctxutil.Client(ctx), "default")
 	if err != nil {
-		return fmt.Errorf("importAlbums: error listing albums: %v", err)
+		return fmt.Errorf("importAlbums: error listing albums: %w", err)
 	}
 	albumsNode, err := r.getTopLevelNode("albums", "Albums")
 	if err != nil {
@@ -334,7 +334,7 @@ func (r *run) importAlbums(ctx context.Context) error {
 		default:
 		}
 		if err := r.importAlbum(ctx, albumsNode, album); err != nil {
-			return fmt.Errorf("picasa importer: error importing album %s: %v", album, err)
+			return fmt.Errorf("picasa importer: error importing album %s: %w", album, err)
 		}
 	}
 	return nil
@@ -346,7 +346,7 @@ func (r *run) importAlbum(ctx context.Context, albumsNode *importer.Object, albu
 	}
 	albumNode, err := albumsNode.ChildPathObject(album.ID)
 	if err != nil {
-		return fmt.Errorf("importAlbum: error listing album: %v", err)
+		return fmt.Errorf("importAlbum: error listing album: %w", err)
 	}
 
 	dateMod := schema.RFC3339FromTime(album.Updated)
@@ -363,7 +363,7 @@ func (r *run) importAlbum(ctx context.Context, albumsNode *importer.Object, albu
 		nodeattr.URL, album.URL,
 	)
 	if err != nil {
-		return fmt.Errorf("error setting album attributes: %v", err)
+		return fmt.Errorf("error setting album attributes: %w", err)
 	}
 	if !changes && r.incremental && albumNode.Attr(nodeattr.DateModified) == dateMod {
 		return nil
@@ -416,7 +416,7 @@ func (r *run) updatePhotoInAlbum(ctx context.Context, albumNode *importer.Object
 		log.Printf("Importing media from %v", photo.URL)
 		resp, err := ctxutil.Client(ctx).Get(photo.URL)
 		if err != nil {
-			return nil, fmt.Errorf("importing photo %s: %v", photo.ID, err)
+			return nil, fmt.Errorf("importing photo %s: %w", photo.ID, err)
 		}
 		if resp.StatusCode != http.StatusOK {
 			resp.Body.Close()
diff --git a/pkg/importer/pinboard/pinboard.go b/pkg/importer/pinboard/pinboard.go
index 0b4d3d97a..62633aab6 100644
--- a/pkg/importer/pinboard/pinboard.go
+++ b/pkg/importer/pinboard/pinboard.go
@@ -170,7 +170,7 @@ func (im imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importe
 	if err := ctx.AccountNode.SetAttrs(
 		attrAuthToken, t,
 	); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %w", err))
 		return
 	}
 	http.Redirect(w, r, ctx.AccountURL(), http.StatusFound)
diff --git a/pkg/importer/plaid/plaid.go b/pkg/importer/plaid/plaid.go
index 0dfbafaa4..2d6b6cb1f 100644
--- a/pkg/importer/plaid/plaid.go
+++ b/pkg/importer/plaid/plaid.go
@@ -124,14 +124,14 @@ func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *import
 
 	clientID, secret, err := ctx.Credentials()
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Credentials error: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Credentials error: %w", err))
 		return
 	}
 
 	client := plaid.NewClient(clientID, secret, plaid.Tartan)
 	res, _, err := client.ConnectAddUser(username, password, "", institution, nil)
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("ConnectAddUser error: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("ConnectAddUser error: %w", err))
 		return
 	}
 
@@ -141,7 +141,7 @@ func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *import
 		acctAttrToken, res.AccessToken,
 		acctInstitution, institution,
 	); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error setting attributes: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error setting attributes: %w", err))
 		return
 	}
 	http.Redirect(w, r, ctx.AccountURL(), http.StatusFound)
@@ -166,7 +166,7 @@ func (im *imp) Run(ctx *importer.RunContext) (err error) {
 	client := plaid.NewClient(clientID, secret, plaid.Tartan)
 	resp, _, err := client.ConnectGet(ctx.AccountNode().Attr(acctAttrToken), &opt)
 	if err != nil {
-		return fmt.Errorf("connectGet: %s", err)
+		return fmt.Errorf("connectGet: %w", err)
 	}
 
 	var latestTrans string
diff --git a/pkg/importer/swarm/swarm.go b/pkg/importer/swarm/swarm.go
index e14420e48..f0e46c3d9 100644
--- a/pkg/importer/swarm/swarm.go
+++ b/pkg/importer/swarm/swarm.go
@@ -593,7 +593,7 @@ func (im *imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.
 func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) {
 	oauthConfig, err := auth(ctx)
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error getting oauth config: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error getting oauth config: %w", err))
 		return
 	}
 
@@ -626,7 +626,7 @@ func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *import
 		acctAttrUserLast, u.LastName,
 		acctAttrAccessToken, token.AccessToken,
 	); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %w", err))
 		return
 	}
 	http.Redirect(w, r, ctx.AccountURL(), http.StatusFound)
diff --git a/pkg/importer/twitter/twitter.go b/pkg/importer/twitter/twitter.go
index 6a972f293..244321491 100644
--- a/pkg/importer/twitter/twitter.go
+++ b/pkg/importer/twitter/twitter.go
@@ -208,7 +208,7 @@ var forceFullImport, _ = strconv.ParseBool(os.Getenv("CAMLI_TWITTER_FULL_IMPORT"
 func (im *imp) Run(ctx *importer.RunContext) error {
 	clientId, secret, err := ctx.Credentials()
 	if err != nil {
-		return fmt.Errorf("no API credentials: %v", err)
+		return fmt.Errorf("no API credentials: %w", err)
 	}
 	acctNode := ctx.AccountNode()
 	accessToken := acctNode.Attr(importer.AcctAttrAccessToken)
@@ -250,7 +250,7 @@ func (im *imp) Run(ctx *importer.RunContext) error {
 
 	acctNode, err = ctx.Host.ObjectFromRef(acctNode.PermanodeRef())
 	if err != nil {
-		return fmt.Errorf("error reloading account node: %v", err)
+		return fmt.Errorf("error reloading account node: %w", err)
 	}
 	importLikes, err := strconv.ParseBool(acctNode.Attr(acctAttrImportLikes))
 	if err == nil && importLikes {
@@ -268,12 +268,12 @@ func (im *imp) Run(ctx *importer.RunContext) error {
 		}
 		fr, err := schema.NewFileReader(r.Context(), r.Host.BlobSource(), zipbr)
 		if err != nil {
-			return fmt.Errorf("error opening zip %v: %v", zipbr, err)
+			return fmt.Errorf("error opening zip %v: %w", zipbr, err)
 		}
 		defer fr.Close()
 		zr, err := zip.NewReader(fr, fr.Size())
 		if err != nil {
-			return fmt.Errorf("Error opening twitter zip file %v: %v", zipRef, err)
+			return fmt.Errorf("Error opening twitter zip file %v: %w", zipRef, err)
 		}
 		if err := r.importTweetsFromZip(userID, zr); err != nil {
 			return err
@@ -483,7 +483,7 @@ func tweetsFromZipFile(zf *zip.File) (tweets []*zipTweetItem, err error) {
 	}
 	slurp = slurp[i:]
 	if err := json.Unmarshal(slurp, &tweets); err != nil {
-		return nil, fmt.Errorf("JSON error: %v", err)
+		return nil, fmt.Errorf("JSON error: %w", err)
 	}
 	return
 }
@@ -507,7 +507,7 @@ func (r *run) importTweetsFromZip(userID string, zr *zip.Reader) error {
 		}
 		tweets, err := tweetsFromZipFile(zf)
 		if err != nil {
-			return fmt.Errorf("error reading tweets from %s: %v", zf.Name, err)
+			return fmt.Errorf("error reading tweets from %s: %w", zf.Name, err)
 		}
 
 		for i := range tweets {
@@ -566,7 +566,7 @@ func (r *run) importTweet(parent *importer.Object, tweet tweetItem, viaAPI bool)
 	// e.g. "2014-06-12 19:11:51 +0000"
 	createdTime, err := timeParseFirstFormat(tweet.CreatedAt(), time.RubyDate, "2006-01-02 15:04:05 -0700")
 	if err != nil {
-		return false, fmt.Errorf("could not parse time %q: %v", tweet.CreatedAt(), err)
+		return false, fmt.Errorf("could not parse time %q: %w", tweet.CreatedAt(), err)
 	}
 
 	url := fmt.Sprintf("https://twitter.com/%s/status/%v",
@@ -608,7 +608,7 @@ func (r *run) importTweet(parent *importer.Object, tweet tweetItem, viaAPI bool)
 			tried++
 			res, err := ctxutil.Client(r.Context()).Get(mediaURL)
 			if err != nil {
-				return false, fmt.Errorf("Error fetching %s for tweet %s : %v", mediaURL, url, err)
+				return false, fmt.Errorf("Error fetching %s for tweet %s: %w", mediaURL, url, err)
 			}
 			if res.StatusCode == http.StatusNotFound {
 				continue
@@ -622,7 +622,7 @@ func (r *run) importTweet(parent *importer.Object, tweet tweetItem, viaAPI bool)
 			fileRef, err := schema.WriteFileFromReader(r.Context(), r.Host.Target(), filename, res.Body)
 			res.Body.Close()
 			if err != nil {
-				return false, fmt.Errorf("Error fetching media %s for tweet %s: %v", mediaURL, url, err)
+				return false, fmt.Errorf("Error fetching media %s for tweet %s: %w", mediaURL, url, err)
 			}
 			attrs = append(attrs, "camliPath:"+filename, fileRef.String())
 			if i == 0 {
@@ -688,13 +688,13 @@ func getUserInfo(ctx importer.OAuthContext) (userInfo, error) {
 func (im *imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error {
 	oauthClient, err := ctx.NewOAuthClient(oAuthURIs)
 	if err != nil {
-		err = fmt.Errorf("error getting OAuth client: %v", err)
+		err = fmt.Errorf("error getting OAuth client: %w", err)
 		httputil.ServeError(w, r, err)
 		return err
 	}
 	tempCred, err := oauthClient.RequestTemporaryCredentials(ctxutil.Client(ctx), ctx.CallbackURL(), nil)
 	if err != nil {
-		err = fmt.Errorf("Error getting temp cred: %v", err)
+		err = fmt.Errorf("Error getting temp cred: %w", err)
 		httputil.ServeError(w, r, err)
 		return err
 	}
@@ -702,7 +702,7 @@ func (im *imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.
 		importer.AcctAttrTempToken, tempCred.Token,
 		importer.AcctAttrTempSecret, tempCred.Secret,
 	); err != nil {
-		err = fmt.Errorf("Error saving temp creds: %v", err)
+		err = fmt.Errorf("Error saving temp creds: %w", err)
 		httputil.ServeError(w, r, err)
 		return err
 	}
@@ -727,7 +727,7 @@ func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *import
 	}
 	oauthClient, err := ctx.NewOAuthClient(oAuthURIs)
 	if err != nil {
-		err = fmt.Errorf("error getting OAuth client: %v", err)
+		err = fmt.Errorf("error getting OAuth client: %w", err)
 		httputil.ServeError(w, r, err)
 		return
 	}
@@ -740,25 +740,25 @@ func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *import
 		r.FormValue("oauth_verifier"),
 	)
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error getting request token: %v ", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error getting request token: %w", err))
 		return
 	}
 	userid := vals.Get("user_id")
 	if userid == "" {
-		httputil.ServeError(w, r, fmt.Errorf("Couldn't get user id: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Couldn't get user id: %w", err))
 		return
 	}
 	if err := ctx.AccountNode.SetAttrs(
 		importer.AcctAttrAccessToken, tokenCred.Token,
 		importer.AcctAttrAccessTokenSecret, tokenCred.Secret,
 	); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error setting token attributes: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error setting token attributes: %w", err))
 		return
 	}
 
 	u, err := getUserInfo(importer.OAuthContext{Ctx: ctx.Context, Client: oauthClient, Creds: tokenCred})
 	if err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Couldn't get user info: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Couldn't get user info: %w", err))
 		return
 	}
 	if err := ctx.AccountNode.SetAttrs(
@@ -767,7 +767,7 @@ func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *import
 		importer.AcctAttrUserName, u.ScreenName,
 		nodeattr.Title, fmt.Sprintf("%s's Twitter Account", u.ScreenName),
 	); err != nil {
-		httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err))
+		httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %w", err))
 		return
 	}
 	http.Redirect(w, r, ctx.AccountURL(), http.StatusFound)
diff --git a/pkg/index/corpus.go b/pkg/index/corpus.go
index 08861c04b..11de84764 100644
--- a/pkg/index/corpus.go
+++ b/pkg/index/corpus.go
@@ -485,7 +485,7 @@ func (c *Corpus) scanFromStorage(s sorted.KeyValue) error {
 	// log.V(1).Printf("interned blob.Ref = %d", c.brInterns)
 
 	if err := c.initDeletes(s); err != nil {
-		return fmt.Errorf("Could not populate the corpus deletes: %v", err)
+		return fmt.Errorf("Could not populate the corpus deletes: %w", err)
 	}
 
 	if logCorpusStats {
@@ -618,7 +618,7 @@ func (c *Corpus) addBlob(ctx context.Context, br blob.Ref, mm *mutationMap) erro
 	}
 	for _, cl := range mm.deletes {
 		if err := c.updateDeletes(cl); err != nil {
-			return fmt.Errorf("Could not update the deletes cache after deletion from %v: %v", cl, err)
+			return fmt.Errorf("Could not update the deletes cache after deletion from %v: %w", cl, err)
 		}
 	}
 	return nil
@@ -631,7 +631,7 @@ func (c *Corpus) updateDeletes(deleteClaim schema.Claim) error {
 	deleter := deleteClaim.Blob()
 	when, err := deleter.ClaimDate()
 	if err != nil {
-		return fmt.Errorf("Could not get date of delete claim %v: %v", deleteClaim, err)
+		return fmt.Errorf("Could not get date of delete claim %v: %w", deleteClaim, err)
 	}
 	del := deletion{
 		deleter: c.br(deleter.BlobRef()),
diff --git a/pkg/index/enumstat.go b/pkg/index/enumstat.go
index a93a27858..38b610ac8 100644
--- a/pkg/index/enumstat.go
+++ b/pkg/index/enumstat.go
@@ -18,6 +18,7 @@ package index
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"strconv"
 	"strings"
@@ -67,11 +68,11 @@ func (ix *Index) StatBlobs(ctx context.Context, blobs []blob.Ref, fn func(blob.S
 	for _, br := range blobs {
 		key := "have:" + br.String()
 		v, err := ix.s.Get(key)
-		if err == sorted.ErrNotFound {
+		if errors.Is(err, sorted.ErrNotFound) {
 			continue
 		}
 		if err != nil {
-			return fmt.Errorf("error looking up key %q: %v", key, err)
+			return fmt.Errorf("error looking up key %q: %w", key, err)
 		}
 		size, err := parseHaveVal(v)
 		if err != nil {
diff --git a/pkg/index/index.go b/pkg/index/index.go
index ec8077176..f40634677 100644
--- a/pkg/index/index.go
+++ b/pkg/index/index.go
@@ -204,7 +204,7 @@ func New(s sorted.KeyValue) (*Index, error) {
 		// New index.
 		err := idx.s.Set(keySchemaVersion.name, fmt.Sprint(requiredSchemaVersion))
 		if err != nil {
-			return nil, fmt.Errorf("Could not write index schema version %q: %v", requiredSchemaVersion, err)
+			return nil, fmt.Errorf("Could not write index schema version %q: %w", requiredSchemaVersion, err)
 		}
 	case schemaVersion != requiredSchemaVersion:
 		tip := ""
@@ -222,10 +222,10 @@ func New(s sorted.KeyValue) (*Index, error) {
 			schemaVersion, requiredSchemaVersion, tip)
 	}
 	if err := idx.initDeletesCache(); err != nil {
-		return nil, fmt.Errorf("Could not initialize index's deletes cache: %v", err)
+		return nil, fmt.Errorf("Could not initialize index's deletes cache: %w", err)
 	}
 	if err := idx.initNeededMaps(); err != nil {
-		return nil, fmt.Errorf("Could not initialize index's missing blob maps: %v", err)
+		return nil, fmt.Errorf("Could not initialize index's missing blob maps: %w", err)
 	}
 	return idx, nil
 }
@@ -318,7 +318,7 @@ func (x *Index) fixMissingWholeRef(fetcher blob.Fetcher) (err error) {
 		}
 		size, err := strconv.Atoi(size_s)
 		if err != nil {
-			return fmt.Errorf("bogus size in keyFileInfo value %v: %v", it.Value(), err)
+			return fmt.Errorf("bogus size in keyFileInfo value %v: %w", it.Value(), err)
 		}
 		mutations[keyFileInfo.Key(br)] = keyFileInfo.Val(size, filename, mimetype, wholeRef)
 		fixedEntries++
@@ -353,7 +353,8 @@ func newFromConfig(ld blobserver.Loader, config jsonconfig.Obj) (blobserver.Stor
 
 	kv, err := sorted.NewKeyValue(kvConfig)
 	if err != nil {
-		if _, ok := err.(sorted.NeedWipeError); !ok {
+		var nwe sorted.NeedWipeError
+		if !errors.As(err, &nwe) {
 			return nil, err
 		}
 		if !reindex {
@@ -367,7 +368,7 @@ func newFromConfig(ld blobserver.Loader, config jsonconfig.Obj) (blobserver.Stor
 			return nil, fmt.Errorf("index's storage type %T doesn't support sorted.Wiper", kv)
 		}
 		if err := wiper.Wipe(); err != nil {
-			return nil, fmt.Errorf("error wiping index's sorted key/value type %T: %v", kv, err)
+			return nil, fmt.Errorf("error wiping index's sorted key/value type %T: %w", kv, err)
 		}
 		log.Printf("Index wiped.")
 	}
@@ -381,12 +382,12 @@ func newFromConfig(ld blobserver.Loader, config jsonconfig.Obj) (blobserver.Stor
 	// TODO(mpl): next time we need to do another fix, make a new error
 	// type that lets us apply the needed fix depending on its value or
 	// something. For now just one value/fix.
-	if err == errMissingWholeRef {
+	if errors.Is(err, errMissingWholeRef) {
 		// TODO: maybe we don't want to do that automatically. Brad says
 		// we have to think about the case on GCE/CoreOS in particular.
 		if err := ix.fixMissingWholeRef(sto); err != nil {
 			ix.Close()
-			return nil, fmt.Errorf("could not fix missing wholeRef entries: %v", err)
+			return nil, fmt.Errorf("could not fix missing wholeRef entries: %w", err)
 		}
 		ix, err = New(kv)
 	}
@@ -467,7 +468,7 @@ func (x *Index) Reindex() error {
 		}
 		log.Printf("Wiping index storage type %T ...", x.s)
 		if err := wiper.Wipe(); err != nil {
-			return fmt.Errorf("error wiping index's sorted key/value type %T: %v", x.s, err)
+			return fmt.Errorf("error wiping index's sorted key/value type %T: %w", x.s, err)
 		}
 		log.Printf("Index wiped.")
 	}
@@ -654,7 +655,7 @@ func closeIterator(it sorted.Iterator, perr *error) {
 func (x *Index) schemaVersion() int {
 	schemaVersionStr, err := x.s.Get(keySchemaVersion.name)
 	if err != nil {
-		if err == sorted.ErrNotFound {
+		if errors.Is(err, sorted.ErrNotFound) {
 			return 0
 		}
 		panic(fmt.Sprintf("Could not get index schema version: %v", err))
@@ -803,7 +804,7 @@ func (x *Index) GetRecentPermanodes(ctx context.Context, dest chan<- camtypes.Re
 	defer close(dest)
 
 	keyId, err := x.KeyId(ctx, owner)
-	if err == sorted.ErrNotFound {
+	if errors.Is(err, sorted.ErrNotFound) {
 		x.logf("no recent permanodes because keyId for owner %v not found", owner)
 		return nil
 	}
@@ -956,7 +957,7 @@ func (x *Index) GetBlobMeta(ctx context.Context, br blob.Ref) (camtypes.BlobMeta
 	}
 	key := "meta:" + br.String()
 	meta, err := x.s.Get(key)
-	if err == sorted.ErrNotFound {
+	if errors.Is(err, sorted.ErrNotFound) {
 		err = os.ErrNotExist
 	}
 	if err != nil {
@@ -1019,7 +1020,7 @@ func (x *Index) signerRefs(ctx context.Context, keyID string) (SignerRefSet, err
 
 func (x *Index) PermanodeOfSignerAttrValue(ctx context.Context, signer blob.Ref, attr, val string) (permaNode blob.Ref, err error) {
 	keyId, err := x.KeyId(ctx, signer)
-	if err == sorted.ErrNotFound {
+	if errors.Is(err, sorted.ErrNotFound) {
 		return blob.Ref{}, os.ErrNotExist
 	}
 	if err != nil {
@@ -1053,7 +1054,7 @@ func (x *Index) SearchPermanodesWithAttr(ctx context.Context, dest chan<- blob.R
 	}
 
 	keyId, err := x.KeyId(ctx, request.Signer)
-	if err == sorted.ErrNotFound {
+	if errors.Is(err, sorted.ErrNotFound) {
 		return nil
 	}
 	if err != nil {
@@ -1139,7 +1140,7 @@ func (x *Index) PathsOfSignerTarget(ctx context.Context, signer, target blob.Ref
 	paths = []*camtypes.Path{}
 	keyId, err := x.KeyId(ctx, signer)
 	if err != nil {
-		if err == sorted.ErrNotFound {
+		if errors.Is(err, sorted.ErrNotFound) {
 			err = nil
 		}
 		return
@@ -1226,7 +1227,7 @@ func (x *Index) PathsLookup(ctx context.Context, signer, base blob.Ref, suffix s
 	paths = []*camtypes.Path{}
 	keyId, err := x.KeyId(ctx, signer)
 	if err != nil {
-		if err == sorted.ErrNotFound {
+		if errors.Is(err, sorted.ErrNotFound) {
 			err = nil
 		}
 		return
@@ -1387,7 +1388,7 @@ func (x *Index) GetFileInfo(ctx context.Context, fileRef blob.Ref) (camtypes.Fil
 	go x.loadKey(tkey, &tv, &terr, wg)
 	wg.Wait()
 
-	if ierr == sorted.ErrNotFound {
+	if errors.Is(ierr, sorted.ErrNotFound) {
 		return camtypes.FileInfo{}, os.ErrNotExist
 	}
 	if ierr != nil {
@@ -1460,7 +1461,7 @@ func (x *Index) GetImageInfo(ctx context.Context, fileRef blob.Ref) (camtypes.Im
 	// (because of unsupported JPEG features like progressive mode).
 	key := keyImageSize.Key(fileRef.String())
 	v, err := x.s.Get(key)
-	if err == sorted.ErrNotFound {
+	if errors.Is(err, sorted.ErrNotFound) {
 		err = os.ErrNotExist
 	}
 	if err != nil {
@@ -1728,7 +1729,7 @@ func (x *Index) EnumerateBlobMeta(ctx context.Context, fn func(camtypes.BlobMeta
 			return nil
 		}
 	})
-	if err == errStopIteration {
+	if errors.Is(err, errStopIteration) {
 		err = nil
 	}
 	return err
diff --git a/pkg/index/receive.go b/pkg/index/receive.go
index ae07bd426..004cb3945 100644
--- a/pkg/index/receive.go
+++ b/pkg/index/receive.go
@@ -109,7 +109,7 @@ func blobsFilteringOut(v []blob.Ref, x blob.Ref) []blob.Ref {
 func (ix *Index) indexBlob(ctx context.Context, br blob.Ref) error {
 	rc, _, err := ix.blobSource.Fetch(ctx, br)
 	if err != nil {
-		return fmt.Errorf("index: failed to fetch %v for reindexing: %v", br, err)
+		return fmt.Errorf("index: failed to fetch %v for reindexing: %w", br, err)
 	}
 	defer rc.Close()
 	if _, err := blobserver.Receive(ctx, ix, br, rc); err != nil {
@@ -243,7 +243,7 @@ func (ix *Index) ReceiveBlob(ctx context.Context, blobRef blob.Ref, source io.Re
 		log.Printf("index of %v: mm=%v, err=%v", blobRef, mm, err)
 	}
 	if err != nil {
-		if err != errMissingDep {
+		if !errors.Is(err, errMissingDep) {
 			return blob.SizedRef{}, err
 		}
 		fetcher.mu.Lock()
@@ -307,7 +307,7 @@ func (ix *Index) commit(mm *mutationMap) error {
 	}
 	for _, cl := range mm.deletes {
 		if err := ix.updateDeletesCache(cl); err != nil {
-			return fmt.Errorf("Could not update the deletes cache after deletion from %v: %v", cl, err)
+			return fmt.Errorf("Could not update the deletes cache after deletion from %v: %w", cl, err)
 		}
 	}
 	return nil
@@ -370,7 +370,7 @@ func (ix *Index) populateMutationMap(ctx context.Context, fetcher *missTrackFetc
 		return nil, err
 	}
 	var haveVal string
-	if err == errMissingDep {
+	if errors.Is(err, errMissingDep) {
 		haveVal = fmt.Sprintf("%d", sniffer.Size())
 	} else {
 		haveVal = fmt.Sprintf("%d|indexed", sniffer.Size())
@@ -414,7 +414,7 @@ type missTrackFetcher struct {
 
 func (f *missTrackFetcher) Fetch(ctx context.Context, br blob.Ref) (blob io.ReadCloser, size uint32, err error) {
 	blob, size, err = f.fetcher.Fetch(ctx, br)
-	if err == os.ErrNotExist {
+	if errors.Is(err, os.ErrNotExist) {
 		f.mu.Lock()
 		defer f.mu.Unlock()
 		f.missing = append(f.missing, br)
@@ -467,7 +467,7 @@ type filePrefixReader interface {
 func readPrefixOrFile(prefix []byte, fetcher blob.Fetcher, b *schema.Blob, fn func(filePrefixReader) error) (err error) {
 	pr := bytes.NewReader(prefix)
 	err = fn(pr)
-	if err == io.EOF || err == io.ErrUnexpectedEOF {
+	if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
 		var fr *schema.FileReader
 		fr, err = b.NewFileReader(fetcher)
 		if err == nil {
@@ -839,13 +839,13 @@ func (ix *Index) populateDeleteClaim(ctx context.Context, cl schema.Claim, vr *j
 	}
 	meta, err := ix.GetBlobMeta(ctx, target)
 	if err != nil {
-		if err == os.ErrNotExist {
+		if errors.Is(err, os.ErrNotExist) {
 			if err := ix.noteNeeded(br, target); err != nil {
-				return fmt.Errorf("could not note that delete claim %v depends on %v: %v", br, target, err)
+				return fmt.Errorf("could not note that delete claim %v depends on %v: %w", br, target, err)
 			}
 			return errMissingDep
 		}
-		log.Print(fmt.Errorf("Could not get mime type of target blob %v: %v", target, err))
+		log.Print(fmt.Errorf("Could not get mime type of target blob %v: %w", target, err))
 		return nil
 	}
 
@@ -948,7 +948,7 @@ func (ix *Index) updateDeletesCache(deleteClaim schema.Claim) error {
 	deleter := deleteClaim.Blob()
 	when, err := deleter.ClaimDate()
 	if err != nil {
-		return fmt.Errorf("Could not get date of delete claim %v: %v", deleteClaim, err)
+		return fmt.Errorf("Could not get date of delete claim %v: %w", deleteClaim, err)
 	}
 	targetDeletions := append(ix.deletes.m[target],
 		deletion{
diff --git a/pkg/jsonsign/keys.go b/pkg/jsonsign/keys.go
index d5e4e2ffc..93f715a8c 100644
--- a/pkg/jsonsign/keys.go
+++ b/pkg/jsonsign/keys.go
@@ -67,7 +67,7 @@ func openArmoredPublicKeyFile(reader io.ReadCloser) (*packet.PublicKey, error) {
 	}
 	p, err := packet.Read(block.Body)
 	if err != nil {
-		return nil, fmt.Errorf("Invalid public key blob: %v", err)
+		return nil, fmt.Errorf("Invalid public key blob: %w", err)
 	}
 
 	pk, ok := p.(*packet.PublicKey)
@@ -89,13 +89,13 @@ func EntityFromSecring(keyID, keyFile string) (*openpgp.Entity, error) {
 	}
 	secring, err := wkfs.Open(keyFile)
 	if err != nil {
-		return nil, fmt.Errorf("jsonsign: failed to open keyring: %v", err)
+		return nil, fmt.Errorf("jsonsign: failed to open keyring: %w", err)
 	}
 	defer secring.Close()
 
 	el, err := readKeyRing(secring)
 	if err != nil {
-		return nil, fmt.Errorf("readKeyRing of %q: %v", keyFile, err)
+		return nil, fmt.Errorf("readKeyRing of %q: %w", keyFile, err)
 	}
 	var entity *openpgp.Entity
 	for _, e := range el {
@@ -176,12 +176,12 @@ func readKeyRing(r io.Reader) (openpgp.EntityList, error) {
 func KeyIdFromRing(secRing string) (keyID string, err error) {
 	f, err := wkfs.Open(secRing)
 	if err != nil {
-		return "", fmt.Errorf("Could not open secret ring file %v: %v", secRing, err)
+		return "", fmt.Errorf("Could not open secret ring file %v: %w", secRing, err)
 	}
 	defer f.Close()
 	el, err := readKeyRing(f)
 	if err != nil {
-		return "", fmt.Errorf("Could not read secret ring file %s: %v", secRing, err)
+		return "", fmt.Errorf("Could not read secret ring file %s: %w", secRing, err)
 	}
 	if len(el) != 1 {
 		return "", fmt.Errorf("Secret ring file %v contained %d identities; expected 1", secRing, len(el))
@@ -196,7 +196,7 @@ func KeyIdFromRing(secRing string) (keyID string, err error) {
 func GenerateNewSecRing(secRing string) (keyID string, err error) {
 	ent, err := NewEntity()
 	if err != nil {
-		return "", fmt.Errorf("generating new identity: %v", err)
+		return "", fmt.Errorf("generating new identity: %w", err)
 	}
 	if err := os.MkdirAll(filepath.Dir(secRing), 0700); err != nil {
 		return "", err
@@ -208,10 +208,10 @@ func GenerateNewSecRing(secRing string) (keyID string, err error) {
 	err = WriteKeyRing(f, openpgp.EntityList([]*openpgp.Entity{ent}))
 	if err != nil {
 		f.Close()
-		return "", fmt.Errorf("Could not write new key ring to %s: %v", secRing, err)
+		return "", fmt.Errorf("Could not write new key ring to %s: %w", secRing, err)
 	}
 	if err := f.Close(); err != nil {
-		return "", fmt.Errorf("Could not close %v: %v", secRing, err)
+		return "", fmt.Errorf("Could not close %v: %w", secRing, err)
 	}
 	return ent.PrimaryKey.KeyIdString(), nil
 }
diff --git a/pkg/jsonsign/sign.go b/pkg/jsonsign/sign.go
index 7d7bff0b7..51e4ab7ee 100644
--- a/pkg/jsonsign/sign.go
+++ b/pkg/jsonsign/sign.go
@@ -81,12 +81,12 @@ func (ce *CachingEntityFetcher) FetchEntity(fingerprint string) (*openpgp.Entity
 func (fe *FileEntityFetcher) FetchEntity(fingerprint string) (*openpgp.Entity, error) {
 	f, err := wkfs.Open(fe.File)
 	if err != nil {
-		return nil, fmt.Errorf("jsonsign: FetchEntity: %v", err)
+		return nil, fmt.Errorf("jsonsign: FetchEntity: %w", err)
 	}
 	defer f.Close()
 	el, err := readKeyRing(f)
 	if err != nil {
-		return nil, fmt.Errorf("jsonsign: readKeyRing of %q: %v", fe.File, err)
+		return nil, fmt.Errorf("jsonsign: readKeyRing of %q: %w", fe.File, err)
 	}
 	for _, e := range el {
 		pubk := &e.PrivateKey.PublicKey
@@ -185,7 +185,7 @@ func (sr *SignRequest) Sign(ctx context.Context) (signedJSON string, err error)
 		}
 		secring, err := wkfs.Open(sr.secretRingPath())
 		if err != nil {
-			return "", fmt.Errorf("jsonsign: failed to open secret ring file %q: %v", sr.secretRingPath(), err)
+			return "", fmt.Errorf("jsonsign: failed to open secret ring file %q: %w", sr.secretRingPath(), err)
 		}
 		secring.Close() // just opened to see if it's readable
 		entityFetcher = &FileEntityFetcher{File: file}
diff --git a/pkg/jsonsign/sign_normal.go b/pkg/jsonsign/sign_normal.go
index 4063e162d..3fa55508a 100644
--- a/pkg/jsonsign/sign_normal.go
+++ b/pkg/jsonsign/sign_normal.go
@@ -55,7 +55,7 @@ func (fe *FileEntityFetcher) decryptEntity(e *openpgp.Entity) error {
 				conn.RemoveFromCache(req.CacheKey)
 				continue
 			}
-			if err == gpgagent.ErrCancel {
+			if errors.Is(err, gpgagent.ErrCancel) {
 				return errors.New("jsonsign: failed to decrypt key; action canceled")
 			}
 			log.Printf("jsonsign: gpgagent: %v", err)
@@ -75,7 +75,7 @@ func (fe *FileEntityFetcher) decryptEntity(e *openpgp.Entity) error {
 			pinReq.Error = "Passphrase failed to decrypt: " + err.Error()
 			continue
 		}
-		if err == pinentry.ErrCancel {
+		if errors.Is(err, pinentry.ErrCancel) {
 			return errors.New("jsonsign: failed to decrypt key; action canceled")
 		}
 		log.Printf("jsonsign: pinentry: %v", err)
diff --git a/pkg/jsonsign/signhandler/sig.go b/pkg/jsonsign/signhandler/sig.go
index 0dd78f8f7..2a6dfe4c7 100644
--- a/pkg/jsonsign/signhandler/sig.go
+++ b/pkg/jsonsign/signhandler/sig.go
@@ -110,7 +110,7 @@ func newJSONSignFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Hand
 	ms := &memory.Storage{}
 	h.pubKeyBlobRef = blob.RefFromString(h.pubKey)
 	if _, err := ms.ReceiveBlob(ctx, h.pubKeyBlobRef, strings.NewReader(h.pubKey)); err != nil {
-		return nil, fmt.Errorf("could not store pub key blob: %v", err)
+		return nil, fmt.Errorf("could not store pub key blob: %w", err)
 	}
 	h.pubKeyFetcher = ms
 
diff --git a/pkg/jsonsign/verify.go b/pkg/jsonsign/verify.go
index ddec0cb43..f63e14184 100644
--- a/pkg/jsonsign/verify.go
+++ b/pkg/jsonsign/verify.go
@@ -141,7 +141,7 @@ func (vr *VerifyRequest) ParsePayloadMap() bool {
 func (vr *VerifyRequest) FindAndParsePublicKeyBlob(ctx context.Context) error {
 	reader, _, err := vr.fetcher.Fetch(ctx, vr.CamliSigner)
 	if err != nil {
-		if err == os.ErrNotExist {
+		if errors.Is(err, os.ErrNotExist) {
 			return camerrors.ErrMissingKeyBlob
 		}
 		log.Printf("error fetching public key blob %v: %v", vr.CamliSigner, err)
@@ -150,7 +150,7 @@ func (vr *VerifyRequest) FindAndParsePublicKeyBlob(ctx context.Context) error {
 	defer reader.Close()
 	pk, err := openArmoredPublicKeyFile(reader)
 	if err != nil {
-		return fmt.Errorf("error opening public key file: %v", err)
+		return fmt.Errorf("error opening public key file: %w", err)
 	}
 	vr.PublicKeyPacket = pk
 	return nil
diff --git a/pkg/kvutil/kvutil.go b/pkg/kvutil/kvutil.go
index 95cc41eee..3e99446a4 100644
--- a/pkg/kvutil/kvutil.go
+++ b/pkg/kvutil/kvutil.go
@@ -45,7 +45,7 @@ func Open(dbFile string, opts *kv.Options) (*kv.DB, error) {
 			lkfile := dbFile + ".lock"
 			cl, err := lock.Lock(lkfile)
 			if err != nil {
-				return nil, fmt.Errorf("failed to acquire lock on %s: %v", lkfile, err)
+				return nil, fmt.Errorf("failed to acquire lock on %s: %w", lkfile, err)
 			}
 			return cl, nil
 		}
@@ -58,7 +58,7 @@ func Open(dbFile string, opts *kv.Options) (*kv.DB, error) {
 	}
 	db, err := createOpen(dbFile, opts)
 	if err != nil {
-		return nil, fmt.Errorf("error %s %s: %v", verb, dbFile, err)
+		return nil, fmt.Errorf("error %s %s: %w", verb, dbFile, err)
 	}
 	return db, nil
 }
diff --git a/pkg/schema/blob.go b/pkg/schema/blob.go
index f28400062..ff44dffeb 100644
--- a/pkg/schema/blob.go
+++ b/pkg/schema/blob.go
@@ -19,6 +19,7 @@ package schema
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"path/filepath"
@@ -38,8 +39,8 @@ func (e MissingFieldError) Error() string {
 
 // IsMissingField returns whether error is of type MissingFieldError.
 func IsMissingField(err error) bool {
-	_, ok := err.(MissingFieldError)
-	return ok
+	var mfe MissingFieldError
+	return errors.As(err, &mfe)
 }
 
 // AnyBlob represents any type of schema blob.
diff --git a/pkg/schema/dirreader.go b/pkg/schema/dirreader.go
index 8bec19077..444cb0036 100644
--- a/pkg/schema/dirreader.go
+++ b/pkg/schema/dirreader.go
@@ -50,7 +50,7 @@ func NewDirReader(ctx context.Context, fetcher blob.Fetcher, dirBlobRef blob.Ref
 	}
 	dr, err := ss.NewDirReader(fetcher)
 	if err != nil {
-		return nil, fmt.Errorf("schema/dirreader: creating DirReader for %s: %v", dirBlobRef, err)
+		return nil, fmt.Errorf("schema/dirreader: creating DirReader for %s: %w", dirBlobRef, err)
 	}
 	dr.current = 0
 	return dr, nil
@@ -74,11 +74,11 @@ func (ss *superset) setFromBlobRef(ctx context.Context, fetcher blob.Fetcher, bl
 	ss.BlobRef = blobRef
 	rc, _, err := fetcher.Fetch(ctx, blobRef)
 	if err != nil {
-		return fmt.Errorf("schema/dirreader: fetching schema blob %s: %v", blobRef, err)
+		return fmt.Errorf("schema/dirreader: fetching schema blob %s: %w", blobRef, err)
 	}
 	defer rc.Close()
 	if err := json.NewDecoder(rc).Decode(ss); err != nil {
-		return fmt.Errorf("schema/dirreader: decoding schema blob %s: %v", blobRef, err)
+		return fmt.Errorf("schema/dirreader: decoding schema blob %s: %w", blobRef, err)
 	}
 	return nil
 }
@@ -103,12 +103,12 @@ func (dr *DirReader) StaticSet(ctx context.Context) ([]blob.Ref, error) {
 func staticSet(ctx context.Context, staticSetBlobref blob.Ref, fetcher blob.Fetcher) ([]blob.Ref, error) {
 	rsc, _, err := fetcher.Fetch(ctx, staticSetBlobref)
 	if err != nil {
-		return nil, fmt.Errorf("schema/dirreader: fetching schema blob %s: %v", staticSetBlobref, err)
+		return nil, fmt.Errorf("schema/dirreader: fetching schema blob %s: %w", staticSetBlobref, err)
 	}
 	defer rsc.Close()
 	ss, err := parseSuperset(rsc)
 	if err != nil {
-		return nil, fmt.Errorf("schema/dirreader: decoding schema blob %s: %v", staticSetBlobref, err)
+		return nil, fmt.Errorf("schema/dirreader: decoding schema blob %s: %w", staticSetBlobref, err)
 	}
 	if ss.Type != "static-set" {
 		return nil, fmt.Errorf("schema/dirreader: expected \"static-set\" schema blob for %s, got %q", staticSetBlobref, ss.Type)
@@ -134,7 +134,7 @@ func staticSet(ctx context.Context, staticSetBlobref blob.Ref, fetcher blob.Fetc
 		// TODO(mpl): do it concurrently
 		subset, err := staticSet(ctx, toMerge, fetcher)
 		if err != nil {
-			return nil, fmt.Errorf("schema/dirreader: could not get members of %q, subset of %v: %v", toMerge, staticSetBlobref, err)
+			return nil, fmt.Errorf("schema/dirreader: could not get members of %q, subset of %v: %w", toMerge, staticSetBlobref, err)
 		}
 		members = append(members, subset...)
 	}
@@ -145,7 +145,7 @@ func staticSet(ctx context.Context, staticSetBlobref blob.Ref, fetcher blob.Fetc
 func (dr *DirReader) Readdir(ctx context.Context, n int) (entries []DirectoryEntry, err error) {
 	sts, err := dr.StaticSet(ctx)
 	if err != nil {
-		return nil, fmt.Errorf("schema/dirreader: can't get StaticSet: %v", err)
+		return nil, fmt.Errorf("schema/dirreader: can't get StaticSet: %w", err)
 	}
 	up := dr.current + n
 	if n <= 0 {
diff --git a/pkg/schema/filereader.go b/pkg/schema/filereader.go
index 1cb7faa26..fb7d8b7e2 100644
--- a/pkg/schema/filereader.go
+++ b/pkg/schema/filereader.go
@@ -187,7 +187,7 @@ func (fr *FileReader) ReadAt(p []byte, offset int64) (n int, err error) {
 		var n1 int
 		n1, err = io.ReadFull(rc, p)
 		rc.Close()
-		if err == io.EOF || err == io.ErrUnexpectedEOF {
+		if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
 			err = nil
 		}
 		if n1 == 0 {
diff --git a/pkg/schema/filewriter.go b/pkg/schema/filewriter.go
index 8960a2ff2..b24d8aa3e 100644
--- a/pkg/schema/filewriter.go
+++ b/pkg/schema/filewriter.go
@@ -20,6 +20,7 @@ import (
 	"bufio"
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -27,6 +28,7 @@ import (
 	"time"
 
 	"go4.org/rollsum"
+
 	"perkeep.org/pkg/blob"
 	"perkeep.org/pkg/blobserver"
 
@@ -97,7 +99,7 @@ func serverHasBlob(ctx context.Context, bs blobserver.BlobStatter, br blob.Ref)
 	_, err = blobserver.StatBlob(ctx, bs, br)
 	if err == nil {
 		have = true
-	} else if err == os.ErrNotExist {
+	} else if errors.Is(err, os.ErrNotExist) {
 		err = nil
 	}
 	return
@@ -131,7 +133,7 @@ type noteEOFReader struct {
 
 func (r *noteEOFReader) Read(p []byte) (n int, err error) {
 	n, err = r.r.Read(p)
-	if err == io.EOF {
+	if errors.Is(err, io.EOF) {
 		r.sawEOF = true
 	}
 	return
@@ -330,7 +332,7 @@ func writeFileChunks(ctx context.Context, bs blobserver.StatReceiver, file *Buil
 
 	for {
 		c, err := bufr.ReadByte()
-		if err == io.EOF {
+		if errors.Is(err, io.EOF) {
 			if n != last {
 				spans = append(spans, span{from: last, to: n})
 				if !uploadLastSpan() {
diff --git a/pkg/schema/lookup.go b/pkg/schema/lookup.go
index 177e6fefa..e7797bf95 100644
--- a/pkg/schema/lookup.go
+++ b/pkg/schema/lookup.go
@@ -18,6 +18,7 @@ package schema
 
 import (
 	"bufio"
+	"errors"
 	"os"
 	"os/user"
 	"strconv"
@@ -113,7 +114,8 @@ func lookupUserid(id int) string {
 	if err == nil {
 		return u.Username
 	}
-	if _, ok := err.(user.UnknownUserIdError); ok {
+	var ue user.UnknownUserIdError
+	if errors.As(err, &ue) {
 		return ""
 	}
 	if parsedPasswd {
diff --git a/pkg/search/describe.go b/pkg/search/describe.go
index f63f73839..b1b6f7e70 100644
--- a/pkg/search/describe.go
+++ b/pkg/search/describe.go
@@ -20,6 +20,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"log"
 	"net/http"
@@ -573,7 +574,7 @@ func (dr *DescribeRequest) metaMap() (map[string]*DescribedBlob, error) {
 	defer dr.mu.Unlock()
 	for k, err := range dr.errs {
 		// TODO: include all?
-		return nil, fmt.Errorf("error populating %s: %v", k, err)
+		return nil, fmt.Errorf("error populating %s: %w", k, err)
 	}
 	m := make(map[string]*DescribedBlob)
 	for k, desb := range dr.m {
@@ -738,7 +739,7 @@ func (dr *DescribeRequest) addError(br blob.Ref, err error) {
 
 func (dr *DescribeRequest) doDescribe(ctx context.Context, br blob.Ref, depth int) {
 	meta, err := dr.sh.index.GetBlobMeta(ctx, br)
-	if err == os.ErrNotExist {
+	if errors.Is(err, os.ErrNotExist) {
 		return
 	}
 	if err != nil {
@@ -766,7 +767,7 @@ func (dr *DescribeRequest) doDescribe(ctx context.Context, br blob.Ref, depth in
 		if loc, err := dr.sh.lh.PermanodeLocation(ctx, br, at, dr.sh.owner); err == nil {
 			des.Location = &loc
 		} else {
-			if err != os.ErrNotExist {
+			if !errors.Is(err, os.ErrNotExist) {
 				log.Printf("PermanodeLocation(permanode %s): %v", br, err)
 			}
 		}
@@ -797,7 +798,7 @@ func (dr *DescribeRequest) doDescribe(ctx context.Context, br blob.Ref, depth in
 		if loc, err := dr.sh.index.GetFileLocation(ctx, br); err == nil {
 			des.Location = &loc
 		} else {
-			if err != os.ErrNotExist {
+			if !errors.Is(err, os.ErrNotExist) {
 				log.Printf("index.GetFileLocation(file %s): %v", br, err)
 			}
 		}
@@ -830,7 +831,7 @@ func (dr *DescribeRequest) populatePermanodeFields(ctx context.Context, pi *Desc
 	claims, err := dr.sh.index.AppendClaims(ctx, nil, pn, dr.sh.owner.KeyID(), "")
 	if err != nil {
 		log.Printf("Error getting claims of %s: %v", pn.String(), err)
-		dr.addError(pn, fmt.Errorf("Error getting claims of %s: %v", pn.String(), err))
+		dr.addError(pn, fmt.Errorf("Error getting claims of %s: %w", pn.String(), err))
 		return
 	}
 
diff --git a/pkg/search/handler.go b/pkg/search/handler.go
index 40a4026cb..4d37c124d 100644
--- a/pkg/search/handler.go
+++ b/pkg/search/handler.go
@@ -154,7 +154,7 @@ func newHandlerFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Handl
 	if devBlockStartupPrefix != "" {
 		_, err := ld.GetHandler(devBlockStartupPrefix)
 		if err != nil {
-			return nil, fmt.Errorf("search handler references bogus devBlockStartupOn handler %s: %v", devBlockStartupPrefix, err)
+			return nil, fmt.Errorf("search handler references bogus devBlockStartupOn handler %s: %w", devBlockStartupPrefix, err)
 		}
 	}
 
@@ -172,7 +172,7 @@ func newHandlerFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Handl
 		SecringFile: ownerSecring,
 	})
 	if err != nil {
-		return nil, fmt.Errorf("could not create Owner %v", err)
+		return nil, fmt.Errorf("could not create Owner %w", err)
 	}
 	h := NewHandler(indexer, owner)
 
@@ -182,7 +182,7 @@ func newHandlerFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Handl
 		corpus, err := ii.KeepInMemory()
 		if err != nil {
 			ii.Unlock()
-			return nil, fmt.Errorf("error slurping index to memory: %v", err)
+			return nil, fmt.Errorf("error slurping index to memory: %w", err)
 		}
 		h.SetCorpus(corpus)
 		ii.Unlock()
@@ -650,7 +650,7 @@ func (h *Handler) GetClaims(req *ClaimsRequest) (*ClaimsResponse, error) {
 	var claims []camtypes.Claim
 	claims, err := h.index.AppendClaims(ctx, claims, req.Permanode, h.owner.KeyID(), req.AttrFilter)
 	if err != nil {
-		return nil, fmt.Errorf("Error getting claims of %s: %v", req.Permanode.String(), err)
+		return nil, fmt.Errorf("Error getting claims of %s: %w", req.Permanode.String(), err)
 	}
 	sort.Sort(camtypes.ClaimsByDate(claims))
 	var jclaims []*ClaimsItem
@@ -889,7 +889,7 @@ func (h *Handler) GetSignerPaths(req *SignerPathsRequest) (*SignerPathsResponse,
 
 	paths, err := h.index.PathsOfSignerTarget(ctx, req.Signer, req.Target)
 	if err != nil {
-		return nil, fmt.Errorf("Error getting paths of %s: %v", req.Target.String(), err)
+		return nil, fmt.Errorf("Error getting paths of %s: %w", req.Target.String(), err)
 	}
 	var jpaths []*SignerPathsItem
 	for _, path := range paths {
diff --git a/pkg/search/predicate.go b/pkg/search/predicate.go
index 8a44e2d68..72be69cf3 100644
--- a/pkg/search/predicate.go
+++ b/pkg/search/predicate.go
@@ -652,7 +652,7 @@ func (l location) Predicate(ctx context.Context, args []string) (*Constraint, er
 	for k, v := range coords {
 		coo, err := strconv.ParseFloat(v, 64)
 		if err != nil {
-			return nil, fmt.Errorf("could not convert location area coordinate as a float: %v", err)
+			return nil, fmt.Errorf("could not convert location area coordinate as a float: %w", err)
 		}
 		asFloat[k] = coo
 	}
diff --git a/pkg/search/query.go b/pkg/search/query.go
index 5f595cc6b..1f1f6ae25 100644
--- a/pkg/search/query.go
+++ b/pkg/search/query.go
@@ -252,10 +252,10 @@ func (q *SearchQuery) checkValid(ctx context.Context) (sq *SearchQuery, err erro
 	expr := q.Expression
 	sq, err = parseExpression(ctx, expr)
 	if err != nil {
-		return nil, fmt.Errorf("Error parsing search expression %q: %v", expr, err)
+		return nil, fmt.Errorf("Error parsing search expression %q: %w", expr, err)
 	}
 	if err := sq.Constraint.checkValid(); err != nil {
-		return nil, fmt.Errorf("Internal error: parseExpression(%q) returned invalid constraint: %v", expr, err)
+		return nil, fmt.Errorf("Internal error: parseExpression(%q) returned invalid constraint: %w", expr, err)
 	}
 	return sq, nil
 }
@@ -990,7 +990,7 @@ func (h *Handler) Query(ctx context.Context, rawq *SearchQuery) (ret_ *SearchRes
 	}
 	exprResult, err := rawq.checkValid(ctx)
 	if err != nil {
-		return nil, fmt.Errorf("Invalid SearchQuery: %v", err)
+		return nil, fmt.Errorf("Invalid SearchQuery: %w", err)
 	}
 	q := rawq.plannedQuery(exprResult)
 	res := new(SearchResult)
@@ -1761,7 +1761,7 @@ func (c *PermanodeConstraint) blobMatches(ctx context.Context, s *search, br blo
 		l, err := s.h.lh.PermanodeLocation(ctx, br, c.At, s.h.owner)
 		if c.Location != nil {
 			if err != nil {
-				if err != os.ErrNotExist {
+				if !errors.Is(err, os.ErrNotExist) {
 					log.Printf("PermanodeLocation(ref %s): %v", br, err)
 				}
 				return false, nil
@@ -1865,7 +1865,7 @@ func (c *PermanodeConstraint) permanodeMatchesAttrVal(ctx context.Context, s *se
 			return false, nil
 		}
 		meta, err := s.blobMeta(ctx, br)
-		if err == os.ErrNotExist {
+		if errors.Is(err, os.ErrNotExist) {
 			return false, nil
 		}
 		if err != nil {
@@ -1885,7 +1885,7 @@ func (c *FileConstraint) blobMatches(ctx context.Context, s *search, br blob.Ref
 		return false, nil
 	}
 	fi, err := s.fileInfo(ctx, br)
-	if err == os.ErrNotExist {
+	if errors.Is(err, os.ErrNotExist) {
 		return false, nil
 	}
 	if err != nil {
@@ -1915,7 +1915,7 @@ func (c *FileConstraint) blobMatches(ctx context.Context, s *search, br blob.Ref
 	}
 	if pc := c.ParentDir; pc != nil {
 		parents, err := s.parentDirs(ctx, br)
-		if err == os.ErrNotExist {
+		if errors.Is(err, os.ErrNotExist) {
 			return false, nil
 		}
 		if err != nil {
@@ -2097,7 +2097,7 @@ func (c *DirConstraint) blobMatches(ctx context.Context, s *search, br blob.Ref,
 		}
 	}
 	fi, err := s.fileInfo(ctx, br)
-	if err == os.ErrNotExist {
+	if errors.Is(err, os.ErrNotExist) {
 		return false, nil
 	}
 	if err != nil {
@@ -2108,7 +2108,7 @@ func (c *DirConstraint) blobMatches(ctx context.Context, s *search, br blob.Ref,
 	}
 	if pc := c.ParentDir; pc != nil {
 		parents, err := s.parentDirs(ctx, br)
-		if err == os.ErrNotExist {
+		if errors.Is(err, os.ErrNotExist) {
 			return false, nil
 		}
 		if err != nil {
diff --git a/pkg/search/websocket.go b/pkg/search/websocket.go
index 2277f37d7..cf7d992b3 100644
--- a/pkg/search/websocket.go
+++ b/pkg/search/websocket.go
@@ -20,6 +20,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"errors"
 	"log"
 	"net/http"
 	"os"
@@ -28,6 +29,7 @@ import (
 	"time"
 
 	"github.com/gorilla/websocket"
+
 	"perkeep.org/pkg/schema"
 )
 
@@ -309,7 +311,8 @@ var upgrader = websocket.Upgrader{
 
 func (h *Handler) serveWebSocket(rw http.ResponseWriter, req *http.Request) {
 	ws, err := upgrader.Upgrade(rw, req, nil)
-	if _, ok := err.(websocket.HandshakeError); ok {
+	var he websocket.HandshakeError
+	if errors.As(err, &he) {
 		http.Error(rw, "Not a websocket handshake", http.StatusBadRequest)
 		return
 	} else if err != nil {
diff --git a/pkg/server/app/app.go b/pkg/server/app/app.go
index 08a016fe0..0e0c3958d 100644
--- a/pkg/server/app/app.go
+++ b/pkg/server/app/app.go
@@ -122,7 +122,7 @@ func (a *Handler) handleMasterQuery(w http.ResponseWriter, r *http.Request) {
 	}
 	if refresh, _ := strconv.ParseBool(r.FormValue("refresh")); refresh {
 		if err := a.refreshDomainBlobs(); err != nil {
-			if err == errRefreshSuppress {
+			if errors.Is(err, errRefreshSuppress) {
 				http.Error(w, "too many refresh requests", http.StatusTooManyRequests)
 			} else {
 				http.Error(w, fmt.Sprintf("%v", err), 500)
@@ -171,7 +171,7 @@ func (a *Handler) refreshDomainBlobs() error {
 	sq.Describe = sq.Describe.Clone()
 	sr, err := a.sh.Query(context.TODO(), &sq)
 	if err != nil {
-		return fmt.Errorf("error running master query: %v", err)
+		return fmt.Errorf("error running master query: %w", err)
 	}
 	a.domainBlobs = make(map[blob.Ref]bool, len(sr.Describe.Meta))
 	for _, v := range sr.Describe.Meta {
@@ -269,7 +269,7 @@ var portMap = map[string]string{
 func baseURL(serverBaseURL, listenAddr string) (string, error) {
 	backendURL, err := url.Parse(serverBaseURL)
 	if err != nil {
-		return "", fmt.Errorf("invalid baseURL %q: %v", serverBaseURL, err)
+		return "", fmt.Errorf("invalid baseURL %q: %w", serverBaseURL, err)
 	}
 	scheme := backendURL.Scheme
 	host := backendURL.Host
@@ -375,7 +375,7 @@ func NewHandler(cfg HandlerConfig) (*Handler, error) {
 			}
 			parsedUrl, err := url.Parse(cfg.ServerBaseURL)
 			if err != nil {
-				return nil, fmt.Errorf("app: could not initialize Handler for %q: unparseable ServerBaseURL %q: %v", name, cfg.ServerBaseURL, err)
+				return nil, fmt.Errorf("app: could not initialize Handler for %q: unparseable ServerBaseURL %q: %w", name, cfg.ServerBaseURL, err)
 			}
 			serverListen = parsedUrl.Host
 		}
@@ -402,7 +402,7 @@ func NewHandler(cfg HandlerConfig) (*Handler, error) {
 
 	proxyURL, err := url.Parse(backendURL)
 	if err != nil {
-		return nil, fmt.Errorf("could not parse backendURL %q: %v", backendURL, err)
+		return nil, fmt.Errorf("could not parse backendURL %q: %w", backendURL, err)
 	}
 
 	username, password := auth.RandToken(20), auth.RandToken(20)
@@ -496,7 +496,7 @@ func (a *Handler) Start() error {
 	}
 	cmd.Env = env
 	if err := cmd.Start(); err != nil {
-		return fmt.Errorf("could not start app %v: %v", name, err)
+		return fmt.Errorf("could not start app %v: %w", name, err)
 	}
 	a.process = cmd.Process
 	return nil
diff --git a/pkg/server/download.go b/pkg/server/download.go
index cfad43627..002c257d0 100644
--- a/pkg/server/download.go
+++ b/pkg/server/download.go
@@ -100,12 +100,12 @@ var errNotDir = errors.New("not a directory")
 func (dh *DownloadHandler) dirInfo(ctx context.Context, dir blob.Ref) (fi fileInfo, err error) {
 	rc, _, err := dh.Fetcher.Fetch(ctx, dir)
 	if err != nil {
-		return fi, fmt.Errorf("could not fetch %v: %v", dir, err)
+		return fi, fmt.Errorf("could not fetch %v: %w", dir, err)
 	}
 	b, err := schema.BlobFromReader(dir, rc)
 	rc.Close()
 	if err != nil {
-		return fi, fmt.Errorf("could not read %v as blob: %v", dir, err)
+		return fi, fmt.Errorf("could not read %v as blob: %w", dir, err)
 	}
 	tp := b.Type()
 	if tp != "directory" {
@@ -113,11 +113,11 @@ func (dh *DownloadHandler) dirInfo(ctx context.Context, dir blob.Ref) (fi fileIn
 	}
 	dr, err := schema.NewDirReader(ctx, dh.Fetcher, dir)
 	if err != nil {
-		return fi, fmt.Errorf("could not open %v as directory: %v", dir, err)
+		return fi, fmt.Errorf("could not open %v as directory: %w", dir, err)
 	}
 	children, err := dr.StaticSet(ctx)
 	if err != nil {
-		return fi, fmt.Errorf("could not get dir entries of %v: %v", dir, err)
+		return fi, fmt.Errorf("could not get dir entries of %v: %w", dir, err)
 	}
 	return fileInfo{
 		isDir:    true,
@@ -132,12 +132,12 @@ func (dh *DownloadHandler) fileInfo(ctx context.Context, file blob.Ref) (fi file
 	// TODO(mpl): should we let NewFileReader be ok with non-regular files? and fail later when e.g. trying to read?
 	rc, _, err := dh.Fetcher.Fetch(ctx, file)
 	if err != nil {
-		return fi, false, fmt.Errorf("could not fetch %v: %v", file, err)
+		return fi, false, fmt.Errorf("could not fetch %v: %w", file, err)
 	}
 	b, err := schema.BlobFromReader(file, rc)
 	rc.Close()
 	if err != nil {
-		return fi, false, fmt.Errorf("could not read %v as blob: %v", file, err)
+		return fi, false, fmt.Errorf("could not read %v as blob: %w", file, err)
 	}
 	tp := b.Type()
 	if tp != schema.TypeFile {
@@ -223,7 +223,7 @@ func fileInfoPacked(ctx context.Context, sh *search.Handler, src blob.Fetcher, r
 
 	offset := int64(0)
 	rc, wholeSize, err := wf.OpenWholeRef(fi.WholeRef, offset)
-	if err == os.ErrNotExist {
+	if errors.Is(err, os.ErrNotExist) {
 		return fileInfo{whyNot: "WholeRefFetcher returned ErrNotexist"}, false
 	}
 	if wholeSize != fi.Size {
@@ -345,7 +345,7 @@ func (dh *DownloadHandler) ServeFile(w http.ResponseWriter, r *http.Request, fil
 			text, err := isText(fi.rs)
 			if err != nil {
 				// TODO: https://perkeep.org/issues/1060
-				httputil.ServeError(w, r, fmt.Errorf("cannot verify MIME type of file: %v", err))
+				httputil.ServeError(w, r, fmt.Errorf("cannot verify MIME type of file: %w", err))
 				return
 			}
 			if text {
@@ -388,7 +388,7 @@ func isText(rs io.ReadSeeker) (ok bool, err error) {
 	}()
 	var buf bytes.Buffer
 	if _, err := io.CopyN(&buf, rs, 1e6); err != nil {
-		if err != io.EOF {
+		if !errors.Is(err, io.EOF) {
 			return false, err
 		}
 	}
@@ -438,12 +438,12 @@ func (dh *DownloadHandler) checkFiles(ctx context.Context, parentPath string, fi
 	for _, br := range fileRefs {
 		rc, _, err := dh.Fetcher.Fetch(ctx, br)
 		if err != nil {
-			return fmt.Errorf("could not fetch %v: %v", br, err)
+			return fmt.Errorf("could not fetch %v: %w", br, err)
 		}
 		b, err := schema.BlobFromReader(br, rc)
 		rc.Close()
 		if err != nil {
-			return fmt.Errorf("could not read %v as blob: %v", br, err)
+			return fmt.Errorf("could not read %v as blob: %w", br, err)
 		}
 		tp := b.Type()
 		if _, ok := allowedFileTypes[tp]; !ok && tp != schema.TypeDirectory {
@@ -452,11 +452,11 @@ func (dh *DownloadHandler) checkFiles(ctx context.Context, parentPath string, fi
 		if tp == schema.TypeDirectory {
 			dr, err := b.NewDirReader(ctx, dh.Fetcher)
 			if err != nil {
-				return fmt.Errorf("could not open %v as directory: %v", br, err)
+				return fmt.Errorf("could not open %v as directory: %w", br, err)
 			}
 			children, err := dr.StaticSet(ctx)
 			if err != nil {
-				return fmt.Errorf("could not get dir entries of %v: %v", br, err)
+				return fmt.Errorf("could not get dir entries of %v: %w", br, err)
 			}
 			if err := dh.checkFiles(ctx, filepath.Join(parentPath, b.FileName()), children); err != nil {
 				return err
@@ -471,12 +471,12 @@ func (dh *DownloadHandler) checkFiles(ctx context.Context, parentPath string, fi
 		}
 		fr, err := b.NewFileReader(dh.Fetcher)
 		if err != nil {
-			return fmt.Errorf("could not open %v: %v", br, err)
+			return fmt.Errorf("could not open %v: %w", br, err)
 		}
 		_, err = io.Copy(io.Discard, fr)
 		fr.Close()
 		if err != nil {
-			return fmt.Errorf("could not read %v: %v", br, err)
+			return fmt.Errorf("could not read %v: %w", br, err)
 		}
 		dh.pathByRef[br] = filepath.Join(parentPath, b.FileName())
 	}
diff --git a/pkg/server/help.go b/pkg/server/help.go
index 30907df11..6f26b7b71 100644
--- a/pkg/server/help.go
+++ b/pkg/server/help.go
@@ -116,7 +116,7 @@ func (hh *HelpHandler) InitHandler(hl blobserver.FindHandlerByTyper) error {
 
 	clientConfig, err := clientconfig.GenerateClientConfig(hh.serverConfig)
 	if err != nil {
-		return fmt.Errorf("error generating client config: %v", err)
+		return fmt.Errorf("error generating client config: %w", err)
 	}
 	hh.clientConfig = clientConfig
 
@@ -125,7 +125,7 @@ func (hh *HelpHandler) InitHandler(hl blobserver.FindHandlerByTyper) error {
 
 	tmpl, err := template.New("help").Parse(helpHTML)
 	if err != nil {
-		return fmt.Errorf("error creating template: %v", err)
+		return fmt.Errorf("error creating template: %w", err)
 	}
 	hh.goTemplate = tmpl
 
@@ -164,7 +164,7 @@ func (hh *HelpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 func (hh *HelpHandler) serveHelpHTML(cc *clientconfig.Config, rw http.ResponseWriter, req *http.Request) {
 	jsonBytes, err := json.MarshalIndent(cc, "", "  ")
 	if err != nil {
-		httputil.ServeError(rw, req, fmt.Errorf("could not serialize client config JSON: %v", err))
+		httputil.ServeError(rw, req, fmt.Errorf("could not serialize client config JSON: %w", err))
 		return
 	}
 
diff --git a/pkg/server/image.go b/pkg/server/image.go
index 82d236984..b036be643 100644
--- a/pkg/server/image.go
+++ b/pkg/server/image.go
@@ -180,7 +180,7 @@ func cacheKey(bref string, width int, height int) string {
 func (ih *ImageHandler) scaledCached(ctx context.Context, buf *bytes.Buffer, file blob.Ref) (format string) {
 	key := cacheKey(file.String(), ih.MaxWidth, ih.MaxHeight)
 	br, err := ih.ThumbMeta.Get(key)
-	if err == errCacheMiss {
+	if errors.Is(err, errCacheMiss) {
 		return
 	}
 	if err != nil {
@@ -265,7 +265,7 @@ func (ih *ImageHandler) scaleImage(ctx context.Context, fileRef blob.Ref) (*form
 
 	sr := readerutil.NewStatsReader(imageBytesFetchedVar, fr)
 	sr, conf, err := imageConfigFromReader(sr)
-	if err == images.ErrHEIC {
+	if errors.Is(err, images.ErrHEIC) {
 		jpegBytes, err := images.HEIFToJPEG(sr, &images.Dimensions{MaxWidth: ih.MaxWidth, MaxHeight: ih.MaxHeight})
 		if err != nil {
 			log.Printf("cannot convert with heiftojpeg: %v", err)
diff --git a/pkg/server/import_share.go b/pkg/server/import_share.go
index 6f639f734..12696f123 100644
--- a/pkg/server/import_share.go
+++ b/pkg/server/import_share.go
@@ -151,7 +151,7 @@ func (si *shareImporter) imprt(ctx context.Context, br blob.Ref) error {
 		rcc.Close()
 		fr, err := schema.NewFileReader(ctx, src, br)
 		if err != nil {
-			return fmt.Errorf("NewFileReader: %v", err)
+			return fmt.Errorf("NewFileReader: %w", err)
 		}
 		defer fr.Close()
 		si.mu.Lock()
diff --git a/pkg/server/root.go b/pkg/server/root.go
index a13c8bc96..75f9fd7f4 100644
--- a/pkg/server/root.go
+++ b/pkg/server/root.go
@@ -121,7 +121,7 @@ func newRootFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handle
 	if root.BlobRoot != "" {
 		bs, err := ld.GetStorage(root.BlobRoot)
 		if err != nil {
-			return nil, fmt.Errorf("Root handler's blobRoot of %q error: %v", root.BlobRoot, err)
+			return nil, fmt.Errorf("Root handler's blobRoot of %q error: %w", root.BlobRoot, err)
 		}
 		root.Storage = bs
 	}
diff --git a/pkg/server/share.go b/pkg/server/share.go
index 2794cc47e..a7862e4c6 100644
--- a/pkg/server/share.go
+++ b/pkg/server/share.go
@@ -132,7 +132,7 @@ func newShareFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Handler
 
 	bs, err := ld.GetStorage(blobRoot)
 	if err != nil {
-		return nil, fmt.Errorf("failed to get share handler's storage at %q: %v", blobRoot, err)
+		return nil, fmt.Errorf("failed to get share handler's storage at %q: %w", blobRoot, err)
 	}
 	fetcher, ok := bs.(blob.Fetcher)
 	if !ok {
@@ -291,7 +291,8 @@ func (h *shareHandler) serveHTTP(rw http.ResponseWriter, req *http.Request) erro
 	} else {
 		err = h.handleGetViaSharing(rw, req, blobRef)
 	}
-	if se, ok := err.(*shareError); ok {
+	var se *shareError
+	if errors.As(err, &se) {
 		switch se.response {
 		case badRequest:
 			httputil.BadRequestError(rw, "%s", err)
diff --git a/pkg/server/status.go b/pkg/server/status.go
index 20915ed8b..a44c3d88e 100644
--- a/pkg/server/status.go
+++ b/pkg/server/status.go
@@ -70,7 +70,7 @@ func newStatusFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Hand
 
 func (sh *StatusHandler) InitHandler(hl blobserver.FindHandlerByTyper) error {
 	_, h, err := hl.FindHandlerByType("search")
-	if err == blobserver.ErrHandlerTypeNotFound {
+	if errors.Is(err, blobserver.ErrHandlerTypeNotFound) {
 		return nil
 	}
 	if err != nil {
@@ -220,7 +220,7 @@ func (sh *StatusHandler) googleCloudConsole() (string, error) {
 	}
 	projID, err := metadata.ProjectID()
 	if err != nil {
-		return "", fmt.Errorf("Error getting project ID: %v", err)
+		return "", fmt.Errorf("Error getting project ID: %w", err)
 	}
 	return "https://console.cloud.google.com/compute/instances?project=" + projID, nil
 }
diff --git a/pkg/server/sync.go b/pkg/server/sync.go
index 75d668a97..deb53795a 100644
--- a/pkg/server/sync.go
+++ b/pkg/server/sync.go
@@ -176,7 +176,7 @@ func newSyncFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Handler,
 	sh.toIndex = isToIndex
 	sh.copierPoolSize = copierPoolSize
 	if err := sh.readQueueToMemory(); err != nil {
-		return nil, fmt.Errorf("Error reading sync queue to memory: %v", err)
+		return nil, fmt.Errorf("Error reading sync queue to memory: %w", err)
 	}
 
 	if fullSync || blockFullSync {
@@ -222,7 +222,7 @@ func newSyncFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Handler,
 
 func (sh *SyncHandler) InitHandler(hl blobserver.FindHandlerByTyper) error {
 	_, h, err := hl.FindHandlerByType("root")
-	if err == blobserver.ErrHandlerTypeNotFound {
+	if errors.Is(err, blobserver.ErrHandlerTypeNotFound) {
 		// It's optional. We register ourselves if it's there.
 		return nil
 	}
@@ -639,7 +639,7 @@ func (sh *SyncHandler) copyBlob(ctx context.Context, sb blob.SizedRef) (err erro
 	cs.setStatus(statusFetching)
 	rc, fromSize, err := sh.from.Fetch(ctx, br)
 	if err != nil {
-		return fmt.Errorf("source fetch: %v", err)
+		return fmt.Errorf("source fetch: %w", err)
 	}
 	if fromSize != sb.Size {
 		rc.Close()
@@ -656,7 +656,7 @@ func (sh *SyncHandler) copyBlob(ctx context.Context, sb blob.SizedRef) (err erro
 		)), buf)
 	rc.Close()
 	if err != nil {
-		return fmt.Errorf("Read error after %d/%d bytes: %v", n, fromSize, err)
+		return fmt.Errorf("Read error after %d/%d bytes: %w", n, fromSize, err)
 	}
 	if !br.HashMatches(hash) {
 		return fmt.Errorf("Read data has unexpected digest %x", hash.Sum(nil))
@@ -665,7 +665,7 @@ func (sh *SyncHandler) copyBlob(ctx context.Context, sb blob.SizedRef) (err erro
 	cs.setStatus(statusWriting)
 	newsb, err := sh.to.ReceiveBlob(ctx, br, io.TeeReader(bytes.NewReader(buf), incrWriter{cs, &cs.nwrite}))
 	if err != nil {
-		return fmt.Errorf("dest write: %v", err)
+		return fmt.Errorf("dest write: %w", err)
 	}
 	if newsb.Size != sb.Size {
 		return fmt.Errorf("write size mismatch: source_read=%d but dest_write=%d", sb.Size, newsb.Size)
@@ -791,13 +791,13 @@ func (sh *SyncHandler) validateShardPrefix(pfx string) (err error) {
 	srcErr := &chanError{
 		C: serrc,
 		Wrap: func(err error) error {
-			return fmt.Errorf("Error enumerating source %s for validating shard %s: %v", sh.fromName, pfx, err)
+			return fmt.Errorf("Error enumerating source %s for validating shard %s: %w", sh.fromName, pfx, err)
 		},
 	}
 	dstErr := &chanError{
 		C: derrc,
 		Wrap: func(err error) error {
-			return fmt.Errorf("Error enumerating target %s for validating shard %s: %v", sh.toName, pfx, err)
+			return fmt.Errorf("Error enumerating target %s for validating shard %s: %w", sh.toName, pfx, err)
 		},
 	}
 
@@ -877,7 +877,7 @@ func (sh *SyncHandler) startValidatePrefix(ctx context.Context, pfx string, doDe
 				return ctx.Err()
 			}
 		})
-		if err == errNotPrefix {
+		if errors.Is(err, errNotPrefix) {
 			err = nil
 		}
 		if err != nil {
@@ -1098,14 +1098,14 @@ func (sh *SyncHandler) hourlyCompare(hourlyBytes uint64) {
 			}
 			blob, size, err := sh.to.(blob.Fetcher).Fetch(ctx, sr.Ref)
 			if err != nil {
-				return fmt.Errorf("error fetching %s: %v", sr.Ref, err)
+				return fmt.Errorf("error fetching %s: %w", sr.Ref, err)
 			}
 			if size != sr.Size {
 				return fmt.Errorf("%s: expected size %d, got %d", sr.Ref, sr.Size, size)
 			}
 			h := sr.Ref.Hash()
 			if _, err := io.Copy(h, blob); err != nil {
-				return fmt.Errorf("error while reading %s: %v", sr.Ref, err)
+				return fmt.Errorf("error while reading %s: %w", sr.Ref, err)
 			}
 			if !sr.HashMatches(h) {
 				return fmt.Errorf("expected %s, got %x", sr.Ref, h.Sum(nil))
diff --git a/pkg/server/thumbcache.go b/pkg/server/thumbcache.go
index d4eb90b9a..b9f1ca3f8 100644
--- a/pkg/server/thumbcache.go
+++ b/pkg/server/thumbcache.go
@@ -59,7 +59,7 @@ func (m *ThumbMeta) Get(key string) (blob.Ref, error) {
 	}
 	if m.kv != nil {
 		v, err := m.kv.Get(key)
-		if err == sorted.ErrNotFound {
+		if errors.Is(err, sorted.ErrNotFound) {
 			return br, errCacheMiss
 		}
 		if err != nil {
diff --git a/pkg/server/ui.go b/pkg/server/ui.go
index 337c8e157..0a0c6600e 100644
--- a/pkg/server/ui.go
+++ b/pkg/server/ui.go
@@ -157,7 +157,7 @@ func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, er
 
 	scaledImageKV, err := newKVOrNil(scaledImageConf)
 	if err != nil {
-		return nil, fmt.Errorf("in UI handler's scaledImage: %v", err)
+		return nil, fmt.Errorf("in UI handler's scaledImage: %w", err)
 	}
 	if scaledImageKV != nil && cachePrefix == "" {
 		return nil, fmt.Errorf("in UI handler, can't specify scaledImage without cache")
@@ -165,7 +165,7 @@ func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, er
 	if cachePrefix != "" {
 		bs, err := ld.GetStorage(cachePrefix)
 		if err != nil {
-			return nil, fmt.Errorf("UI handler's cache of %q error: %v", cachePrefix, err)
+			return nil, fmt.Errorf("UI handler's cache of %q error: %w", cachePrefix, err)
 		}
 		ui.Cache = bs
 		ui.thumbMeta = NewThumbMeta(scaledImageKV)
@@ -176,7 +176,7 @@ func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, er
 		if ui.sourceRoot == "" {
 			files, err := uistatic.Files.ReadDir(".")
 			if err != nil {
-				return nil, fmt.Errorf("Could not read static files: %v", err)
+				return nil, fmt.Errorf("Could not read static files: %w", err)
 			}
 			if len(files) == 0 {
 				ui.sourceRoot, err = osutil.PkSourceRoot()
@@ -197,33 +197,33 @@ func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, er
 
 	ui.closureHandler, err = ui.makeClosureHandler(ui.sourceRoot)
 	if err != nil {
-		return nil, fmt.Errorf(`Invalid "sourceRoot" value of %q: %v"`, ui.sourceRoot, err)
+		return nil, fmt.Errorf(`Invalid "sourceRoot" value of %q: %w"`, ui.sourceRoot, err)
 	}
 
 	if ui.sourceRoot != "" {
 		ui.fileReactHandler, err = makeFileServer(ui.sourceRoot, filepath.Join(vendorEmbed, "react"), "react-dom.min.js")
 		if err != nil {
-			return nil, fmt.Errorf("Could not make react handler: %s", err)
+			return nil, fmt.Errorf("Could not make react handler: %w", err)
 		}
 		ui.fileLeafletHandler, err = makeFileServer(ui.sourceRoot, filepath.Join(vendorEmbed, "leaflet"), "leaflet.js")
 		if err != nil {
-			return nil, fmt.Errorf("Could not make leaflet handler: %s", err)
+			return nil, fmt.Errorf("Could not make leaflet handler: %w", err)
 		}
 		ui.fileKeepyHandler, err = makeFileServer(ui.sourceRoot, filepath.Join(vendorEmbed, "keepy"), "keepy-dancing.png")
 		if err != nil {
-			return nil, fmt.Errorf("Could not make keepy handler: %s", err)
+			return nil, fmt.Errorf("Could not make keepy handler: %w", err)
 		}
 		ui.fileFontawesomeHandler, err = makeFileServer(ui.sourceRoot, filepath.Join(vendorEmbed, "fontawesome"), "css/font-awesome.css")
 		if err != nil {
-			return nil, fmt.Errorf("Could not make fontawesome handler: %s", err)
+			return nil, fmt.Errorf("Could not make fontawesome handler: %w", err)
 		}
 		ui.fileLessHandler, err = makeFileServer(ui.sourceRoot, filepath.Join(vendorEmbed, "less"), "less.js")
 		if err != nil {
-			return nil, fmt.Errorf("Could not make less handler: %s", err)
+			return nil, fmt.Errorf("Could not make less handler: %w", err)
 		}
 		ui.fileOpenSansHandler, err = makeFileServer(ui.sourceRoot, filepath.Join(vendorEmbed, "opensans"), "OpenSans.css")
 		if err != nil {
-			return nil, fmt.Errorf("Could not make Open Sans handler: %s", err)
+			return nil, fmt.Errorf("Could not make Open Sans handler: %w", err)
 		}
 	}
 
diff --git a/pkg/server/uploadhelper.go b/pkg/server/uploadhelper.go
index 002da0e7d..91f8a663a 100644
--- a/pkg/server/uploadhelper.go
+++ b/pkg/server/uploadhelper.go
@@ -17,11 +17,13 @@ limitations under the License.
 package server
 
 import (
+	"errors"
 	"io"
 	"log"
 	"net/http"
 
 	"go4.org/types"
+
 	"perkeep.org/internal/httputil"
 	"perkeep.org/pkg/blob"
 	"perkeep.org/pkg/schema"
@@ -56,7 +58,7 @@ func (ui *UIHandler) serveUploadHelper(rw http.ResponseWriter, req *http.Request
 	var modTime types.Time3339
 	for {
 		part, err := mr.NextPart()
-		if err == io.EOF {
+		if errors.Is(err, io.EOF) {
 			break
 		}
 		if err != nil {
diff --git a/pkg/serverinit/devmode.go b/pkg/serverinit/devmode.go
index c996478c6..d404ca80d 100644
--- a/pkg/serverinit/devmode.go
+++ b/pkg/serverinit/devmode.go
@@ -85,24 +85,24 @@ func (hl *handlerLoader) initPublisherRootNode(ah *app.Handler) error {
 	signUpload := func(bb *schema.Builder) (blob.Ref, error) {
 		signed, err := sigh.Sign(ctx, bb)
 		if err != nil {
-			return blob.Ref{}, fmt.Errorf("could not sign blob: %v", err)
+			return blob.Ref{}, fmt.Errorf("could not sign blob: %w", err)
 		}
 		br := blob.RefFromString(signed)
 		if _, err := blobserver.Receive(ctx, bs, br, strings.NewReader(signed)); err != nil {
-			return blob.Ref{}, fmt.Errorf("could not upload %v: %v", br.String(), err)
+			return blob.Ref{}, fmt.Errorf("could not upload %v: %w", br.String(), err)
 		}
 		return br, nil
 	}
 
 	pn, err := signUpload(schema.NewUnsignedPermanode())
 	if err != nil {
-		return fmt.Errorf("could not create new camliRoot node: %v", err)
+		return fmt.Errorf("could not create new camliRoot node: %w", err)
 	}
 	if _, err := signUpload(schema.NewSetAttributeClaim(pn, "camliRoot", camliRoot)); err != nil {
-		return fmt.Errorf("could not set camliRoot on new node %v: %v", pn, err)
+		return fmt.Errorf("could not set camliRoot on new node %v: %w", pn, err)
 	}
 	if _, err := signUpload(schema.NewSetAttributeClaim(pn, "title", "Publish root node for "+camliRoot)); err != nil {
-		return fmt.Errorf("could not set camliRoot on new node %v: %v", pn, err)
+		return fmt.Errorf("could not set camliRoot on new node %v: %w", pn, err)
 	}
 	return nil
 }
diff --git a/pkg/serverinit/env.go b/pkg/serverinit/env.go
index 639a9b1b9..0a5b617a8 100644
--- a/pkg/serverinit/env.go
+++ b/pkg/serverinit/env.go
@@ -47,11 +47,11 @@ func DefaultEnvConfig() (*Config, error) {
 	pass, _ := metadata.InstanceAttributeValue("camlistore-password")
 	confBucket, err := metadata.InstanceAttributeValue("camlistore-config-dir")
 	if confBucket == "" || err != nil {
-		return nil, fmt.Errorf("VM instance metadata key 'camlistore-config-dir' not set: %v", err)
+		return nil, fmt.Errorf("VM instance metadata key 'camlistore-config-dir' not set: %w", err)
 	}
 	blobBucket, err := metadata.InstanceAttributeValue("camlistore-blob-dir")
 	if blobBucket == "" || err != nil {
-		return nil, fmt.Errorf("VM instance metadata key 'camlistore-blob-dir' not set: %v", err)
+		return nil, fmt.Errorf("VM instance metadata key 'camlistore-blob-dir' not set: %w", err)
 	}
 	if user != "" && pass != "" {
 		auth = "userpass:" + user + ":" + pass
diff --git a/pkg/serverinit/genconfig.go b/pkg/serverinit/genconfig.go
index c69f3c629..a1f9ab353 100644
--- a/pkg/serverinit/genconfig.go
+++ b/pkg/serverinit/genconfig.go
@@ -209,7 +209,7 @@ func (b *lowBuilder) longIdentity() (string, error) {
 	}
 	keyID, err := jsonsign.KeyIdFromRing(b.high.IdentitySecretRing)
 	if err != nil {
-		return "", fmt.Errorf("could not find any keyID in file %q: %v", b.high.IdentitySecretRing, err)
+		return "", fmt.Errorf("could not find any keyID in file %q: %w", b.high.IdentitySecretRing, err)
 	}
 	if !strings.HasSuffix(keyID, b.high.Identity) {
 		return "", fmt.Errorf("%q identity not found in secret ring %v", b.high.Identity, b.high.IdentitySecretRing)
@@ -986,7 +986,7 @@ func (b *lowBuilder) build() (*Config, error) {
 	if conf.BaseURL != "" {
 		u, err := url.Parse(conf.BaseURL)
 		if err != nil {
-			return nil, fmt.Errorf("Error parsing baseURL %q as a URL: %v", conf.BaseURL, err)
+			return nil, fmt.Errorf("Error parsing baseURL %q as a URL: %w", conf.BaseURL, err)
 		}
 		if u.Path != "" && u.Path != "/" {
 			return nil, fmt.Errorf("baseURL can't have a path, only a scheme, host, and optional port")
@@ -1061,7 +1061,7 @@ func (b *lowBuilder) build() (*Config, error) {
 	}
 	if !noMkdir {
 		if err := os.MkdirAll(cacheDir, 0700); err != nil {
-			return nil, fmt.Errorf("Could not create blobs cache dir %s: %v", cacheDir, err)
+			return nil, fmt.Errorf("Could not create blobs cache dir %s: %w", cacheDir, err)
 		}
 	}
 
@@ -1083,7 +1083,7 @@ func (b *lowBuilder) build() (*Config, error) {
 			}
 		}
 		if err := b.addPublishedConfig(tlsO); err != nil {
-			return nil, fmt.Errorf("Could not generate config for published: %v", err)
+			return nil, fmt.Errorf("Could not generate config for published: %w", err)
 		}
 	}
 
@@ -1101,7 +1101,7 @@ func (b *lowBuilder) build() (*Config, error) {
 			}
 		}
 		if err := b.addScanCabConfig(tlsO); err != nil {
-			return nil, fmt.Errorf("Could not generate config for scanning cabinet: %v", err)
+			return nil, fmt.Errorf("Could not generate config for scanning cabinet: %w", err)
 		}
 	}
 
@@ -1179,7 +1179,7 @@ func WriteDefaultConfigFile(filePath string) error {
 		return err
 	}
 	if err := wkfs.MkdirAll(blobDir, 0700); err != nil {
-		return fmt.Errorf("Could not create default blobs directory: %v", err)
+		return fmt.Errorf("Could not create default blobs directory: %w", err)
 	}
 	conf.BlobPath = blobDir
 	conf.PackRelated = true
@@ -1195,11 +1195,11 @@ func WriteDefaultConfigFile(filePath string) error {
 
 	confData, err := json.MarshalIndent(conf, "", "    ")
 	if err != nil {
-		return fmt.Errorf("Could not json encode config file : %v", err)
+		return fmt.Errorf("Could not json encode config file: %w", err)
 	}
 
 	if err := wkfs.WriteFile(filePath, confData, 0600); err != nil {
-		return fmt.Errorf("Could not create or write default server config: %v", err)
+		return fmt.Errorf("Could not create or write default server config: %w", err)
 	}
 
 	return nil
@@ -1212,19 +1212,19 @@ func getOrMakeKeyring() (keyID, secRing string, err error) {
 	case err == nil:
 		keyID, err = jsonsign.KeyIdFromRing(secRing)
 		if err != nil {
-			err = fmt.Errorf("Could not find any keyID in file %q: %v", secRing, err)
+			err = fmt.Errorf("Could not find any keyID in file %q: %w", secRing, err)
 			return
 		}
 		log.Printf("Re-using identity with keyID %q found in file %s", keyID, secRing)
 	case os.IsNotExist(err):
 		keyID, err = jsonsign.GenerateNewSecRing(secRing)
 		if err != nil {
-			err = fmt.Errorf("Could not generate new secRing at file %q: %v", secRing, err)
+			err = fmt.Errorf("Could not generate new secRing at file %q: %w", secRing, err)
 			return
 		}
 		log.Printf("Generated new identity with keyID %q in file %s", keyID, secRing)
 	default:
-		err = fmt.Errorf("Could not stat secret ring %q: %v", secRing, err)
+		err = fmt.Errorf("Could not stat secret ring %q: %w", secRing, err)
 	}
 	return
 }
diff --git a/pkg/serverinit/serverinit.go b/pkg/serverinit/serverinit.go
index cf51776ee..65b487283 100644
--- a/pkg/serverinit/serverinit.go
+++ b/pkg/serverinit/serverinit.go
@@ -534,12 +534,12 @@ func load(filename string, opener func(filename string) (jsonconfig.File, error)
 	// struct later.
 	highExpandedJSON, err := json.Marshal(m)
 	if err != nil {
-		return nil, fmt.Errorf("Can't re-marshal high-level JSON config: %v", err)
+		return nil, fmt.Errorf("Can't re-marshal high-level JSON config: %w", err)
 	}
 
 	var hiLevelConf serverconfig.Config
 	if err := json.Unmarshal(highExpandedJSON, &hiLevelConf); err != nil {
-		return nil, fmt.Errorf("Could not unmarshal into a serverconfig.Config: %v", err)
+		return nil, fmt.Errorf("Could not unmarshal into a serverconfig.Config: %w", err)
 	}
 
 	// At this point, conf.jconf.UnknownKeys() contains all the names found in
@@ -662,11 +662,11 @@ func (c *Config) InstallHandlers(hi HandlerInstaller, baseURL string) (shutdown
 	}()
 
 	if err := config.checkValidAuth(); err != nil {
-		return nil, fmt.Errorf("error while configuring auth: %v", err)
+		return nil, fmt.Errorf("error while configuring auth: %w", err)
 	}
 	prefixes := config.jconf.RequiredObject("prefixes")
 	if err := config.jconf.Validate(); err != nil {
-		return nil, fmt.Errorf("configuration error in root object's keys: %v", err)
+		return nil, fmt.Errorf("configuration error in root object's keys: %w", err)
 	}
 
 	if v := os.Getenv("CAMLI_PPROF_START"); v != "" {
@@ -740,7 +740,7 @@ func (c *Config) InstallHandlers(hi HandlerInstaller, baseURL string) (shutdown
 		}
 		if in, ok := handler.(blobserver.HandlerIniter); ok {
 			if err := in.InitHandler(hl); err != nil {
-				return nil, fmt.Errorf("Error calling InitHandler on %s: %v", pfx, err)
+				return nil, fmt.Errorf("Error calling InitHandler on %s: %w", pfx, err)
 			}
 		}
 	}
@@ -772,7 +772,7 @@ func dumpGoroutines(w http.ResponseWriter, r *http.Request) {
 func (c *Config) StartApps() error {
 	for _, ap := range c.apps {
 		if err := ap.Start(); err != nil {
-			return fmt.Errorf("error starting app %v: %v", ap.ProgramName(), err)
+			return fmt.Errorf("error starting app %v: %w", ap.ProgramName(), err)
 		}
 	}
 	return nil
@@ -881,7 +881,7 @@ func logsHandler(w http.ResponseWriter, r *http.Request) {
 	case "perkeepd":
 		projID, err := metadata.ProjectID()
 		if err != nil {
-			httputil.ServeError(w, r, fmt.Errorf("Error getting project ID: %v", err))
+			httputil.ServeError(w, r, fmt.Errorf("Error getting project ID: %w", err))
 			return
 		}
 		http.Redirect(w, r,
diff --git a/pkg/sorted/kv.go b/pkg/sorted/kv.go
index 9ca660cb3..3f91268ea 100644
--- a/pkg/sorted/kv.go
+++ b/pkg/sorted/kv.go
@@ -236,9 +236,9 @@ func NewKeyValue(cfg jsonconfig.Obj) (KeyValue, error) {
 	if ok {
 		s, err = ctor(cfg)
 		if err != nil {
-			we, ok := err.(NeedWipeError)
-			if !ok {
-				return nil, fmt.Errorf("error from %q KeyValue: %v", typ, err)
+			var we NeedWipeError
+			if !errors.As(err, &we) {
+				return nil, fmt.Errorf("error from %q KeyValue: %w", typ, err)
 			}
 			if err := cfg.Validate(); err != nil {
 				return nil, err
@@ -257,16 +257,16 @@ func NewKeyValueMaybeWipe(cfg jsonconfig.Obj) (KeyValue, error) {
 	if err == nil {
 		return kv, nil
 	}
-	if _, ok := err.(NeedWipeError); !ok {
+	var we NeedWipeError
+	if !errors.As(err, &we) {
 		return nil, err
 	}
 	wiper, ok := kv.(Wiper)
 	if !ok {
-		return nil, fmt.Errorf("storage type %T needs wiping because %v. But it doesn't support sorted.Wiper", err, kv)
+		return nil, fmt.Errorf("storage type %T needs wiping because %w. But it doesn't support sorted.Wiper", kv, err)
 	}
-	we := err
 	if err := wiper.Wipe(); err != nil {
-		return nil, fmt.Errorf("sorted key/value type %T needed wiping because %v. But could not wipe: %v", kv, we, err)
+		return nil, fmt.Errorf("sorted key/value type %T needed wiping because %w. But could not wipe: %w", kv, we, err)
 	}
 	return kv, nil
 }
diff --git a/pkg/sorted/kvfile/kvfile.go b/pkg/sorted/kvfile/kvfile.go
index 2ec2efa3d..22a6dd9c4 100644
--- a/pkg/sorted/kvfile/kvfile.go
+++ b/pkg/sorted/kvfile/kvfile.go
@@ -127,7 +127,7 @@ func (is *kvis) Wipe() error {
 
 	db, err := kv.Create(is.path, is.opts)
 	if err != nil {
-		return fmt.Errorf("error creating %s: %v", is.path, err)
+		return fmt.Errorf("error creating %s: %w", is.path, err)
 	}
 	is.db = db
 	return nil
@@ -254,7 +254,7 @@ func (it *iter) Next() bool {
 	it.skey, it.sval = nil, nil
 	var err error
 	it.key, it.val, err = it.enum.Next()
-	if err == io.EOF {
+	if errors.Is(err, io.EOF) {
 		it.err = nil
 		return it.end()
 	}
diff --git a/pkg/sorted/leveldb/leveldb.go b/pkg/sorted/leveldb/leveldb.go
index baa4a052a..61d58bd6f 100644
--- a/pkg/sorted/leveldb/leveldb.go
+++ b/pkg/sorted/leveldb/leveldb.go
@@ -97,7 +97,7 @@ type kvis struct {
 func (is *kvis) Get(key string) (string, error) {
 	val, err := is.db.Get([]byte(key), is.readOpts)
 	if err != nil {
-		if err == leveldb.ErrNotFound {
+		if errors.Is(err, leveldb.ErrNotFound) {
 			return "", sorted.ErrNotFound
 		}
 		return "", err
@@ -150,7 +150,7 @@ func (is *kvis) Wipe() error {
 
 	db, err := leveldb.OpenFile(is.path, is.opts)
 	if err != nil {
-		return fmt.Errorf("error creating %s: %v", is.path, err)
+		return fmt.Errorf("error creating %s: %w", is.path, err)
 	}
 	is.db = db
 	return nil
diff --git a/pkg/sorted/mem.go b/pkg/sorted/mem.go
index 0c2be97a3..da83ca6f8 100644
--- a/pkg/sorted/mem.go
+++ b/pkg/sorted/mem.go
@@ -100,7 +100,7 @@ func (mk *memKeys) Get(key string) (string, error) {
 	mk.mu.Lock()
 	defer mk.mu.Unlock()
 	k, err := mk.db.Get([]byte(key))
-	if err == memdb.ErrNotFound {
+	if errors.Is(err, memdb.ErrNotFound) {
 		return "", ErrNotFound
 	}
 	return string(k), err
@@ -134,7 +134,7 @@ func (mk *memKeys) Delete(key string) error {
 	mk.mu.Lock()
 	defer mk.mu.Unlock()
 	err := mk.db.Delete([]byte(key))
-	if err == memdb.ErrNotFound {
+	if errors.Is(err, memdb.ErrNotFound) {
 		return nil
 	}
 	return err
diff --git a/pkg/sorted/mongo/mongokv.go b/pkg/sorted/mongo/mongokv.go
index fdbe2aac3..827ace445 100644
--- a/pkg/sorted/mongo/mongokv.go
+++ b/pkg/sorted/mongo/mongokv.go
@@ -129,7 +129,7 @@ func (kv *keyValue) Get(key string) (string, error) {
 	q := kv.db.Find(&bson.M{mgoKey: key})
 	err := q.One(&res)
 	if err != nil {
-		if err == mgo.ErrNotFound {
+		if errors.Is(err, mgo.ErrNotFound) {
 			return "", sorted.ErrNotFound
 		}
 		return "", err
@@ -160,7 +160,7 @@ func (kv *keyValue) Delete(key string) error {
 	kv.mu.Lock()
 	defer kv.mu.Unlock()
 	err := kv.db.Remove(&bson.M{mgoKey: key})
-	if err == mgo.ErrNotFound {
+	if errors.Is(err, mgo.ErrNotFound) {
 		return nil
 	}
 	return err
diff --git a/pkg/sorted/mysql/cloudsql.go b/pkg/sorted/mysql/cloudsql.go
index 6dffdd9c4..16c154562 100644
--- a/pkg/sorted/mysql/cloudsql.go
+++ b/pkg/sorted/mysql/cloudsql.go
@@ -43,14 +43,14 @@ func maybeRemapCloudSQL(host string) (out string, err error) {
 	}
 	proj, err := metadata.ProjectID()
 	if err != nil {
-		return "", fmt.Errorf("Failed to lookup GCE project ID: %v", err)
+		return "", fmt.Errorf("Failed to lookup GCE project ID: %w", err)
 	}
 
 	ctx := context.TODO()
 	admin, _ := sqladmin.NewService(ctx, option.WithHTTPClient(oauth2.NewClient(ctx, google.ComputeTokenSource(""))))
 	listRes, err := admin.Instances.List(proj).Do()
 	if err != nil {
-		return "", fmt.Errorf("error enumerating Cloud SQL instances: %v", err)
+		return "", fmt.Errorf("error enumerating Cloud SQL instances: %w", err)
 	}
 	for _, it := range listRes.Items {
 		if !strings.EqualFold(it.Name, inst) {
diff --git a/pkg/sorted/mysql/mysqlkv.go b/pkg/sorted/mysql/mysqlkv.go
index 678a3e8a3..a0a17e0dd 100644
--- a/pkg/sorted/mysql/mysqlkv.go
+++ b/pkg/sorted/mysql/mysqlkv.go
@@ -111,17 +111,17 @@ func (kv *keyValue) finalize() error {
 	}
 
 	if err := kv.ping(); err != nil {
-		return fmt.Errorf("MySQL db unreachable: %v", err)
+		return fmt.Errorf("MySQL db unreachable: %w", err)
 	}
 
 	version, err := kv.SchemaVersion()
 	if err != nil {
-		return fmt.Errorf("error getting current database schema version: %v", err)
+		return fmt.Errorf("error getting current database schema version: %w", err)
 	}
 	if version == 0 {
 		// Newly created table case
 		if _, err := kv.db.Exec(fmt.Sprintf(`REPLACE INTO %s.meta VALUES ('version', ?)`, kv.database), requiredSchemaVersion); err != nil {
-			return fmt.Errorf("error setting schema version: %v", err)
+			return fmt.Errorf("error setting schema version: %w", err)
 		}
 		return nil
 	}
@@ -166,7 +166,7 @@ func CreateDB(db *sql.DB, dbname string) error {
 		return errors.New("can not create database: database name is missing")
 	}
 	if _, err := db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", dbname)); err != nil {
-		return fmt.Errorf("error creating database %v: %v", dbname, err)
+		return fmt.Errorf("error creating database %v: %w", dbname, err)
 	}
 	return nil
 }
@@ -237,7 +237,7 @@ func (kv *keyValue) ping() error {
 // version is found it returns (0, nil), as the table should be considered empty.
 func (kv *keyValue) SchemaVersion() (version int, err error) {
 	err = kv.db.QueryRow("SELECT value FROM " + kv.KeyValue.TablePrefix + "meta WHERE metakey='version'").Scan(&version)
-	if err == sql.ErrNoRows {
+	if errors.Is(err, sql.ErrNoRows) {
 		return 0, nil
 	}
 	return
@@ -254,7 +254,7 @@ func serverVersion(db *sql.DB) ([]int, error) {
 	versionRx := regexp.MustCompile(`([0-9]+)\.([0-9]+)\.([0-9]+)-.*`)
 	var version string
 	if err := db.QueryRow("SELECT VERSION()").Scan(&version); err != nil {
-		return nil, fmt.Errorf("error getting MySQL server version: %v", err)
+		return nil, fmt.Errorf("error getting MySQL server version: %w", err)
 	}
 	m := versionRx.FindStringSubmatch(version)
 	if len(m) < 4 {
diff --git a/pkg/sorted/postgres/postgreskv.go b/pkg/sorted/postgres/postgreskv.go
index 44258a9e4..cf0bc0146 100644
--- a/pkg/sorted/postgres/postgreskv.go
+++ b/pkg/sorted/postgres/postgreskv.go
@@ -72,17 +72,17 @@ func newKeyValueFromJSONConfig(cfg jsonconfig.Obj) (sorted.KeyValue, error) {
 
 	for _, tableSQL := range SQLCreateTables() {
 		if _, err := db.Exec(tableSQL); err != nil {
-			return nil, fmt.Errorf("error creating table with %q: %v", tableSQL, err)
+			return nil, fmt.Errorf("error creating table with %q: %w", tableSQL, err)
 		}
 	}
 	for _, statement := range SQLDefineReplace() {
 		if _, err := db.Exec(statement); err != nil {
-			return nil, fmt.Errorf("error setting up replace statement with %q: %v", statement, err)
+			return nil, fmt.Errorf("error setting up replace statement with %q: %w", statement, err)
 		}
 	}
 	r, err := db.Query(fmt.Sprintf(`SELECT replaceintometa('version', '%d')`, SchemaVersion()))
 	if err != nil {
-		return nil, fmt.Errorf("error setting schema version: %v", err)
+		return nil, fmt.Errorf("error setting schema version: %w", err)
 	}
 	r.Close()
 
@@ -96,11 +96,11 @@ func newKeyValueFromJSONConfig(cfg jsonconfig.Obj) (sorted.KeyValue, error) {
 		},
 	}
 	if err := kv.ping(); err != nil {
-		return nil, fmt.Errorf("PostgreSQL db unreachable: %v", err)
+		return nil, fmt.Errorf("PostgreSQL db unreachable: %w", err)
 	}
 	version, err := kv.SchemaVersion()
 	if err != nil {
-		return nil, fmt.Errorf("error getting schema version (need to init database?): %v", err)
+		return nil, fmt.Errorf("error getting schema version (need to init database?): %w", err)
 	}
 	if version != requiredSchemaVersion {
 		if env.IsDev() {
diff --git a/pkg/sorted/sqlite/sqlitekv.go b/pkg/sorted/sqlite/sqlitekv.go
index 4b8d6c47f..806c0973d 100644
--- a/pkg/sorted/sqlite/sqlitekv.go
+++ b/pkg/sorted/sqlite/sqlitekv.go
@@ -51,7 +51,7 @@ func newKeyValueFromConfig(cfg jsonconfig.Obj) (sorted.KeyValue, error) {
 	fi, err := os.Stat(file)
 	if os.IsNotExist(err) || (err == nil && fi.Size() == 0) {
 		if err := InitDB(file); err != nil {
-			return nil, fmt.Errorf("could not initialize sqlite DB at %s: %v", file, err)
+			return nil, fmt.Errorf("could not initialize sqlite DB at %s: %w", file, err)
 		}
 	}
 	db, err := sql.Open("sqlite", file)
@@ -69,7 +69,7 @@ func newKeyValueFromConfig(cfg jsonconfig.Obj) (sorted.KeyValue, error) {
 
 	version, err := kv.SchemaVersion()
 	if err != nil {
-		return nil, fmt.Errorf("error getting schema version (need to init database with 'camtool dbinit %s'?): %v", file, err)
+		return nil, fmt.Errorf("error getting schema version (need to init database with 'camtool dbinit %s'?): %w", file, err)
 	}
 
 	if err := kv.ping(); err != nil {
diff --git a/pkg/sorted/sqlkv/sqlkv.go b/pkg/sorted/sqlkv/sqlkv.go
index 7f4b04e82..bfc7b793f 100644
--- a/pkg/sorted/sqlkv/sqlkv.go
+++ b/pkg/sorted/sqlkv/sqlkv.go
@@ -274,7 +274,7 @@ func find(kv *KeyValue, qobj queryObject, start, end string) *iter {
 // Common logic for KeyValue.Get and batchTx.Get
 func get(kv *KeyValue, qobj queryObject, key string) (value string, err error) {
 	err = qobj.QueryRow(kv.sql("SELECT v FROM /*TPRE*/rows WHERE k=?"), key).Scan(&value)
-	if err == sql.ErrNoRows {
+	if errors.Is(err, sql.ErrNoRows) {
 		err = sorted.ErrNotFound
 	}
 	return
diff --git a/pkg/types/clientconfig/config.go b/pkg/types/clientconfig/config.go
index 42a85dbbd..029128254 100644
--- a/pkg/types/clientconfig/config.go
+++ b/pkg/types/clientconfig/config.go
@@ -126,11 +126,11 @@ func GenerateClientConfig(serverConfig jsonconfig.Obj) (*Config, error) {
 	if https && httpsCert != "" {
 		certPEMBlock, err := wkfs.ReadFile(httpsCert)
 		if err != nil {
-			return nil, fmt.Errorf("could not read certificate: %v", err)
+			return nil, fmt.Errorf("could not read certificate: %w", err)
 		}
 		sig, err := httputil.CertFingerprint(certPEMBlock)
 		if err != nil {
-			return nil, fmt.Errorf("could not get fingerprints of certificate: %v", err)
+			return nil, fmt.Errorf("could not get fingerprints of certificate: %w", err)
 		}
 		trustedList = []string{sig}
 	}
diff --git a/pkg/webserver/listen/listen.go b/pkg/webserver/listen/listen.go
index a092380f8..62ce68eb8 100644
--- a/pkg/webserver/listen/listen.go
+++ b/pkg/webserver/listen/listen.go
@@ -82,7 +82,7 @@ func (a *Addr) Set(v string) error {
 		fdStr := v[len("FD:"):]
 		fd, err := strconv.ParseUint(fdStr, 10, 32)
 		if err != nil {
-			return fmt.Errorf("invalid file descriptor %q: %v", fdStr, err)
+			return fmt.Errorf("invalid file descriptor %q: %w", fdStr, err)
 		}
 		return a.listenOnFD(uintptr(fd))
 	}
@@ -94,7 +94,7 @@ func (a *Addr) Set(v string) error {
 
 	_, _, err := net.SplitHostPort(ipPort)
 	if err != nil {
-		return fmt.Errorf("invalid PORT or IP:PORT %q: %v", v, err)
+		return fmt.Errorf("invalid PORT or IP:PORT %q: %w", v, err)
 	}
 	a.ln, err = net.Listen("tcp", ipPort)
 	return err
diff --git a/pkg/webserver/webserver.go b/pkg/webserver/webserver.go
index 3cd572e26..beb2a6a50 100644
--- a/pkg/webserver/webserver.go
+++ b/pkg/webserver/webserver.go
@@ -206,7 +206,7 @@ func (s *Server) Listen(addr string) error {
 		s.listener, err = listen.Listen(addr)
 	}
 	if err != nil {
-		return fmt.Errorf("Failed to listen on %s: %v", addr, err)
+		return fmt.Errorf("Failed to listen on %s: %w", addr, err)
 	}
 	base := s.ListenURL()
 	s.printf("Starting to listen on %s\n", base)
@@ -240,7 +240,7 @@ func (s *Server) Listen(addr string) error {
 			config.Certificates = make([]tls.Certificate, 1)
 			config.Certificates[0], err = loadX509KeyPair(s.tlsCertFile, s.tlsKeyFile)
 			if err != nil {
-				return fmt.Errorf("Failed to load TLS cert: %v", err)
+				return fmt.Errorf("Failed to load TLS cert: %w", err)
 			}
 			s.listener = tls.NewListener(s.listener, config)
 			return nil
@@ -274,7 +274,7 @@ func (s *Server) listenTailscale(addr string, withTLS bool) (net.Listener, error
 	if dir == "" {
 		confDir, err := os.UserConfigDir()
 		if err != nil {
-			return nil, fmt.Errorf("failed to find user config dir: %v", err)
+			return nil, fmt.Errorf("failed to find user config dir: %w", err)
 		}
 		dir = filepath.Join(confDir, "tsnet-"+name)
 	}
diff --git a/server/perkeepd/perkeepd.go b/server/perkeepd/perkeepd.go
index 3997a04d0..e9590be25 100644
--- a/server/perkeepd/perkeepd.go
+++ b/server/perkeepd/perkeepd.go
@@ -319,14 +319,14 @@ func listen(ws *webserver.Server, config *serverinit.Config) (baseURL string, er
 	} else {
 		hostname, err := certHostname(listen, baseURL)
 		if err != nil {
-			return "", fmt.Errorf("Bad baseURL or listen address: %v", err)
+			return "", fmt.Errorf("Bad baseURL or listen address: %w", err)
 		}
 		setupTLS(ws, config, hostname)
 	}
 
 	err = ws.Listen(listen)
 	if err != nil {
-		return "", fmt.Errorf("Listen: %v", err)
+		return "", fmt.Errorf("Listen: %w", err)
 	}
 	if baseURL == "" {
 		baseURL = ws.ListenURL()
@@ -343,7 +343,7 @@ func certHostname(listen, baseURL string) (string, error) {
 	}
 	hostname, _, err := net.SplitHostPort(hostPort)
 	if err != nil {
-		return "", fmt.Errorf("failed to find hostname for cert from address %q (baseURL %q): %v", hostPort, baseURL, err)
+		return "", fmt.Errorf("failed to find hostname for cert from address %q (baseURL %q): %w", hostPort, baseURL, err)
 	}
 	return hostname, nil
 }
@@ -365,7 +365,7 @@ func checkGeoKey() error {
 	}
 	keyPath, err := geocode.GetAPIKeyPath()
 	if err != nil {
-		return fmt.Errorf("error getting Geocoding API key path: %v", err)
+		return fmt.Errorf("error getting Geocoding API key path: %w", err)
 	}
 	if env.OnGCE() {
 		keyPath = strings.TrimPrefix(keyPath, "/gcs/")
diff --git a/website/pk-web/contributors.go b/website/pk-web/contributors.go
index 9ade8afe2..601ac93ab 100644
--- a/website/pk-web/contributors.go
+++ b/website/pk-web/contributors.go
@@ -97,7 +97,7 @@ func genContribPage() ([]byte, error) {
 	}
 	err = shortlog.Start()
 	if err != nil {
-		return nil, fmt.Errorf("couldn't run git shortlog: %v", err)
+		return nil, fmt.Errorf("couldn't run git shortlog: %w", err)
 	}
 
 	scn := bufio.NewScanner(shortlogOut)
@@ -130,7 +130,7 @@ func genContribPage() ([]byte, error) {
 	}
 	err = shortlog.Wait()
 	if err != nil {
-		return nil, fmt.Errorf("git shortlog failed: %v", err)
+		return nil, fmt.Errorf("git shortlog failed: %w", err)
 	}
 
 	// Add URLs and roles
diff --git a/website/pk-web/godoc.go b/website/pk-web/godoc.go
index e53662fab..e4f4449b1 100644
--- a/website/pk-web/godoc.go
+++ b/website/pk-web/godoc.go
@@ -230,7 +230,8 @@ func (pi *PageInfo) populateDirs(diskPath string, depth int) {
 func getPageInfo(pkgName, diskPath string) (pi PageInfo, err error) {
 	bpkg, err := build.ImportDir(diskPath, 0)
 	if err != nil {
-		if _, ok := err.(*build.NoGoError); ok {
+		var nge *build.NoGoError
+		if errors.As(err, &nge) {
 			pi.populateDirs(diskPath, -1)
 			return pi, nil
 		}
diff --git a/website/pk-web/pkweb.go b/website/pk-web/pkweb.go
index 757850b9c..b446f3991 100644
--- a/website/pk-web/pkweb.go
+++ b/website/pk-web/pkweb.go
@@ -153,8 +153,8 @@ type pageParams struct {
 
 // pageTmplData is the template data passed to page.html.
 type pageTmplData struct {
-	Title    string
-	Subtitle string
+	Title    template.HTML
+	Subtitle template.HTML
 	Content  template.HTML
 
 	// For the "go-import" meta header:
@@ -178,8 +178,8 @@ func servePage(w http.ResponseWriter, r *http.Request, params pageParams) {
 		upstream = "https://github.com/camlistore/old-cam-snapshot"
 	}
 	if err := pageHTML.ExecuteTemplate(w, "page", &pageTmplData{
-		Title:            title,
-		Subtitle:         subtitle,
+		Title:            template.HTML(title),
+		Subtitle:         template.HTML(subtitle),
 		Content:          template.HTML(content),
 		GoImportDomain:   domain,
 		GoImportUpstream: upstream,
@@ -483,7 +483,7 @@ func runDemoBlobServerContainer(name string) error {
 	// stderr := &writerutil.PrefixSuffixSaver{N: 32 << 10}
 	// cmd.Stderr = stderr
 	// if err := cmd.Run(); err != nil {
-	// 	return fmt.Errorf("failed to run demo blob server: %v, stderr: %v", err, string(stderr.Bytes()))
+	// 	return fmt.Errorf("failed to run demo blob server: %w, stderr: %v", err, string(stderr.Bytes()))
 	// }
 	return nil
 }



Test patch:
diff --git a/dev/devcam/test.go b/dev/devcam/test.go
index 10b7db40f..e08386e06 100644
--- a/dev/devcam/test.go
+++ b/dev/devcam/test.go
@@ -115,7 +115,7 @@ func (c *testCmd) buildSelf() error {
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Error building devcam: %v", err)
+		return fmt.Errorf("Error building devcam: %w", err)
 	}
 	return nil
 }
diff --git a/internal/closure/jstest/jstest.go b/internal/closure/jstest/jstest.go
index 966802948..d36d2f2ad 100644
--- a/internal/closure/jstest/jstest.go
+++ b/internal/closure/jstest/jstest.go
@@ -65,7 +65,7 @@ func checkSystemRequirements() error {
 func getRepoRoot(target string) (string, error) {
 	dir, err := filepath.Abs(filepath.Dir(target))
 	if err != nil {
-		return "", fmt.Errorf("Could not get working directory: %v", err)
+		return "", fmt.Errorf("Could not get working directory: %w", err)
 	}
 	for ; dir != "" && filepath.Base(dir) != "camlistore.org"; dir = filepath.Dir(dir) {
 	}
@@ -80,21 +80,21 @@ func writeDeps(baseJS, targetDir string) (string, error) {
 	closureBaseDir := filepath.Dir(baseJS)
 	depPrefix, err := filepath.Rel(closureBaseDir, targetDir)
 	if err != nil {
-		return "", fmt.Errorf("Could not compute relative path from %q to %q: %v", baseJS, targetDir, err)
+		return "", fmt.Errorf("Could not compute relative path from %q to %q: %w", baseJS, targetDir, err)
 	}
 
 	depPrefix += string(os.PathSeparator)
 	b, err := closure.GenDepsWithPath(depPrefix, http.Dir(targetDir))
 	if err != nil {
-		return "", fmt.Errorf("GenDepsWithPath failed: %v", err)
+		return "", fmt.Errorf("GenDepsWithPath failed: %w", err)
 	}
 	depsFile, err := os.CreateTemp("", "camlistore_closure_test_runner")
 	if err != nil {
-		return "", fmt.Errorf("Could not create temp js deps file: %v", err)
+		return "", fmt.Errorf("Could not create temp js deps file: %w", err)
 	}
 	err = os.WriteFile(depsFile.Name(), b, 0644)
 	if err != nil {
-		return "", fmt.Errorf("Could not write js deps file: %v", err)
+		return "", fmt.Errorf("Could not write js deps file: %w", err)
 	}
 	return depsFile.Name(), nil
 }
diff --git a/internal/gpgagent/gpgagent_test.go b/internal/gpgagent/gpgagent_test.go
index 814788bea..3a8528df4 100644
--- a/internal/gpgagent/gpgagent_test.go
+++ b/internal/gpgagent/gpgagent_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package gpgagent
 
 import (
+	"errors"
 	"fmt"
 	"os"
 	"testing"
@@ -65,7 +66,7 @@ func TestPrompt(t *testing.T) {
 
 	req.NoAsk = true
 	s3, err := conn.GetPassphrase(req)
-	if err != ErrNoData {
+	if !errors.Is(err, ErrNoData) {
 		t.Errorf("after remove from cache, expected gpgagent.ErrNoData, got %q, %v", s3, err)
 	}
 
@@ -75,7 +76,7 @@ func TestPrompt(t *testing.T) {
 		Prompt:   "cancel!",
 		CacheKey: fmt.Sprintf("gpgagent_test-cachekey-%d", time.Now().UnixNano()),
 	})
-	if err != ErrCancel {
+	if !errors.Is(err, ErrCancel) {
 		t.Errorf("expected cancel, got %q, %v", s4, err)
 	}
 }
diff --git a/internal/images/fastjpeg/fastjpeg_test.go b/internal/images/fastjpeg/fastjpeg_test.go
index ec4481db3..d13bbe01b 100644
--- a/internal/images/fastjpeg/fastjpeg_test.go
+++ b/internal/images/fastjpeg/fastjpeg_test.go
@@ -18,6 +18,7 @@ package fastjpeg
 
 import (
 	"bytes"
+	"errors"
 	"image"
 	"image/jpeg"
 	"os"
@@ -132,7 +133,7 @@ func TestUnavailable(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if _, err := DecodeDownsample(bytes.NewReader(tis[0].buf), 2); err != ErrDjpegNotFound {
+	if _, err := DecodeDownsample(bytes.NewReader(tis[0].buf), 2); !errors.Is(err, ErrDjpegNotFound) {
 		t.Errorf("Wanted ErrDjpegNotFound, got %v", err)
 	}
 }
@@ -162,7 +163,8 @@ func TestFailed(t *testing.T) {
 		t.Fatal(err)
 	}
 	_, err = DecodeDownsample(bytes.NewReader(tis[0].buf), 2)
-	if _, ok := err.(DjpegFailedError); !ok {
+	var derr DjpegFailedError
+	if !errors.As(err, &derr) {
 		t.Errorf("Got err type %T want ErrDjpegFailed: %v", err, err)
 	}
 }
diff --git a/internal/images/images_test.go b/internal/images/images_test.go
index 45749e403..32c27acbf 100644
--- a/internal/images/images_test.go
+++ b/internal/images/images_test.go
@@ -18,6 +18,7 @@ package images
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"image"
 	"image/jpeg"
@@ -391,7 +392,8 @@ func TestHEIFToJPEG(t *testing.T) {
 	max := 1008
 	data, err := HEIFToJPEG(f, &Dimensions{MaxWidth: max, MaxHeight: max})
 	if err != nil {
-		if _, ok := err.(NoHEICTOJPEGError); ok {
+		var e NoHEICTOJPEGError
+		if errors.As(err, &e) {
 			t.Skipf("skipping test; missing program: %v", err)
 		}
 		t.Fatal(err)
diff --git a/internal/magic/magic_test.go b/internal/magic/magic_test.go
index 7ce6bf893..9bf88cb68 100644
--- a/internal/magic/magic_test.go
+++ b/internal/magic/magic_test.go
@@ -140,7 +140,7 @@ func TestMIMETypeFromReader(t *testing.T) {
 	if string(slurp) != "<html>foobar" {
 		t.Errorf("read = %q; want %q", slurp, content)
 	}
-	if err != someErr {
+	if !errors.Is(err, someErr) {
 		t.Errorf("read error = %v; want %v", err, someErr)
 	}
 }
diff --git a/internal/netutil/ident_test.go b/internal/netutil/ident_test.go
index 537d7bd79..d8a24dfa9 100644
--- a/internal/netutil/ident_test.go
+++ b/internal/netutil/ident_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package netutil
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"net"
@@ -82,7 +83,7 @@ func testLocalListener(t *testing.T, ln net.Listener) {
 	select {
 	case r := <-c:
 		if r.err != nil {
-			if r.err == ErrUnsupportedOS {
+			if errors.Is(r.err, ErrUnsupportedOS) {
 				t.Skipf("Skipping test; not implemented on " + runtime.GOOS)
 			}
 			t.Fatal(r.err)
diff --git a/internal/video/thumbnail/service_test.go b/internal/video/thumbnail/service_test.go
index 83e62bfd6..f69f825dd 100644
--- a/internal/video/thumbnail/service_test.go
+++ b/internal/video/thumbnail/service_test.go
@@ -19,6 +19,7 @@ package thumbnail
 import (
 	"bytes"
 	"context"
+	"errors"
 	"io"
 	"net/url"
 	"os"
@@ -147,7 +148,7 @@ func TestThumbnailGenerateTimeout(t *testing.T) {
 	service := NewService(sleepyThumbnailer{}, time.Duration(time.Millisecond), 5)
 	err := service.Generate(ref, io.Discard, store)
 
-	if err != errTimeout {
+	if !errors.Is(err, errTimeout) {
 		t.Errorf("expected to timeout: %v", err)
 	}
 }
diff --git a/pkg/blobserver/archiver/archiver_test.go b/pkg/blobserver/archiver/archiver_test.go
index 4bb9f5a1f..b9931694e 100644
--- a/pkg/blobserver/archiver/archiver_test.go
+++ b/pkg/blobserver/archiver/archiver_test.go
@@ -55,7 +55,7 @@ func TestArchiver(t *testing.T) {
 		return errors.New("Store shouldn't be called")
 	}
 	a.MinZipSize = 400 // empirically: the zip will be 416 bytes
-	if err := a.RunOnce(ctxbg); err != ErrSourceTooSmall {
+	if err := a.RunOnce(ctxbg); !errors.Is(err, ErrSourceTooSmall) {
 		t.Fatalf("RunOnce with just Hello = %v; want ErrSourceTooSmall", err)
 	}
 
@@ -126,7 +126,7 @@ func TestArchiverStress(t *testing.T) {
 	}
 	for {
 		err := a.RunOnce(ctxbg)
-		if err == ErrSourceTooSmall {
+		if errors.Is(err, ErrSourceTooSmall) {
 			break
 		}
 		if err != nil {
diff --git a/pkg/blobserver/blobpacked/blobpacked_test.go b/pkg/blobserver/blobpacked/blobpacked_test.go
index df38c5fe9..13a9e97c9 100644
--- a/pkg/blobserver/blobpacked/blobpacked_test.go
+++ b/pkg/blobserver/blobpacked/blobpacked_test.go
@@ -696,15 +696,15 @@ func testPack(t *testing.T,
 	if err := blobserver.EnumerateAll(ctx, logical, func(sb blob.SizedRef) error {
 		logBlobs++
 		v, err := pt.sto.meta.Get(blobMetaPrefix + sb.Ref.String())
-		if err == sorted.ErrNotFound && pt.okayNoMeta[sb.Ref] {
+		if errors.Is(err, sorted.ErrNotFound) && pt.okayNoMeta[sb.Ref] {
 			return nil
 		}
 		if err != nil {
-			return fmt.Errorf("error looking up logical blob %v in meta: %v", sb.Ref, err)
+			return fmt.Errorf("error looking up logical blob %v in meta: %w", sb.Ref, err)
 		}
 		m, err := parseMetaRow([]byte(v))
 		if err != nil {
-			return fmt.Errorf("error parsing logical blob %v meta %q: %v", sb.Ref, v, err)
+			return fmt.Errorf("error parsing logical blob %v meta %q: %w", sb.Ref, v, err)
 		}
 		if !m.exists || m.size != sb.Size || !zipSeen[m.largeRef] {
 			return fmt.Errorf("logical blob %v = %+v; want in zip", sb.Ref, m)
diff --git a/pkg/blobserver/google/cloudstorage/cloudstorage_test.go b/pkg/blobserver/google/cloudstorage/cloudstorage_test.go
index f4279fec1..4769dea5b 100644
--- a/pkg/blobserver/google/cloudstorage/cloudstorage_test.go
+++ b/pkg/blobserver/google/cloudstorage/cloudstorage_test.go
@@ -19,6 +19,7 @@ package cloudstorage
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"flag"
 	"io"
 	"log"
@@ -150,7 +151,7 @@ func testStorage(t *testing.T, bucketDir string) {
 			ctx := context.Background()
 			stor := sto.(*Storage)
 			it := stor.client.Bucket(stor.bucket).Objects(ctx, nil)
-			if _, err := it.Next(); err != iterator.Done {
+			if _, err := it.Next(); !errors.Is(err, iterator.Done) {
 				if err == nil {
 					t.Fatalf("Refusing to run test: bucket %v is not empty", *bucket)
 				}
diff --git a/pkg/blobserver/localdisk/localdisk_test.go b/pkg/blobserver/localdisk/localdisk_test.go
index 08a1405d3..77f385cd0 100644
--- a/pkg/blobserver/localdisk/localdisk_test.go
+++ b/pkg/blobserver/localdisk/localdisk_test.go
@@ -18,6 +18,7 @@ package localdisk
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"os"
 	"strconv"
@@ -134,7 +135,7 @@ func TestMissingGetReturnsNoEnt(t *testing.T) {
 	foo := &test.Blob{Contents: "foo"}
 
 	blob, _, err := ds.Fetch(context.Background(), foo.BlobRef())
-	if err != os.ErrNotExist {
+	if !errors.Is(err, os.ErrNotExist) {
 		t.Errorf("expected ErrNotExist; got %v", err)
 	}
 	if blob != nil {
diff --git a/pkg/blobserver/overlay/overlay_test.go b/pkg/blobserver/overlay/overlay_test.go
index db8cd3385..e8e7b8681 100644
--- a/pkg/blobserver/overlay/overlay_test.go
+++ b/pkg/blobserver/overlay/overlay_test.go
@@ -101,7 +101,7 @@ func TestDelete(t *testing.T) {
 		return func() error {
 			sb, err := sto.ReceiveBlob(ctxbg, tb.BlobRef(), tb.Reader())
 			if err != nil {
-				return fmt.Errorf("ReceiveBlob of %s: %v", sb, err)
+				return fmt.Errorf("ReceiveBlob of %s: %w", sb, err)
 			}
 			if sb != tb.SizedRef() {
 				return fmt.Errorf("Received %v; want %v", sb, tb.SizedRef())
@@ -127,7 +127,7 @@ func TestDelete(t *testing.T) {
 	stepDelete := func(tb *test.Blob) step { // delete the blob in sto
 		return func() error {
 			if err := sto.RemoveBlobs(ctx, []blob.Ref{tb.BlobRef()}); err != nil {
-				return fmt.Errorf("RemoveBlob(%s): %v", tb.BlobRef(), err)
+				return fmt.Errorf("RemoveBlob(%s): %w", tb.BlobRef(), err)
 			}
 			return nil
 		}
diff --git a/pkg/blobserver/proxycache/proxycache_test.go b/pkg/blobserver/proxycache/proxycache_test.go
index 573faf183..65abde0a5 100644
--- a/pkg/blobserver/proxycache/proxycache_test.go
+++ b/pkg/blobserver/proxycache/proxycache_test.go
@@ -18,6 +18,7 @@ package proxycache
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"log"
 	"os"
@@ -107,7 +108,7 @@ func TestMissingGetReturnsNoEnt(t *testing.T) {
 	foo := &test.Blob{Contents: "foo"}
 
 	blob, _, err := px.Fetch(ctxbg, foo.BlobRef())
-	if err != os.ErrNotExist {
+	if !errors.Is(err, os.ErrNotExist) {
 		t.Errorf("expected ErrNotExist; got %v", err)
 	}
 	if blob != nil {
diff --git a/pkg/blobserver/receive_test.go b/pkg/blobserver/receive_test.go
index 9f36d96b3..bc7e5925b 100644
--- a/pkg/blobserver/receive_test.go
+++ b/pkg/blobserver/receive_test.go
@@ -19,6 +19,7 @@ package blobserver_test
 import (
 	"bytes"
 	"context"
+	"errors"
 	"testing"
 	"time"
 
@@ -64,7 +65,7 @@ func TestReceiveCorrupt(t *testing.T) {
 	br := blob.RefFromBytes(data)
 	data[0] = 'X' // corrupt it
 	_, err := blobserver.Receive(ctxbg, sto, br, bytes.NewReader(data))
-	if err != blobserver.ErrCorruptBlob {
+	if !errors.Is(err, blobserver.ErrCorruptBlob) {
 		t.Errorf("Receive = %v; want ErrCorruptBlob", err)
 	}
 	if len(sto.BlobrefStrings()) > 0 {
diff --git a/pkg/blobserver/storagetest/storagetest.go b/pkg/blobserver/storagetest/storagetest.go
index b41557389..5e4bed522 100644
--- a/pkg/blobserver/storagetest/storagetest.go
+++ b/pkg/blobserver/storagetest/storagetest.go
@@ -202,7 +202,7 @@ func (r *run) testSubFetcher() {
 	}
 	for _, tt := range regions {
 		r, err := sf.SubFetch(context.Background(), big.BlobRef(), tt.off, tt.limit)
-		if err == blob.ErrUnimplemented {
+		if errors.Is(err, blob.ErrUnimplemented) {
 			t.Logf("%T implements SubFetcher but its wrapped value doesn't", sto)
 			return
 		}
@@ -232,7 +232,7 @@ func (r *run) testSubFetcher() {
 	}
 	for _, tt := range invalids {
 		r, err := sf.SubFetch(context.Background(), big.BlobRef(), tt.off, tt.limit)
-		if err == blob.ErrUnimplemented {
+		if errors.Is(err, blob.ErrUnimplemented) {
 			t.Logf("%T implements SubFetcher but its wrapped value doesn't", sto)
 			return
 		}
@@ -241,7 +241,7 @@ func (r *run) testSubFetcher() {
 			t.Errorf("No error fetching with off=%d limit=%d; wanted an error", tt.off, tt.limit)
 			continue
 		}
-		if err != blob.ErrNegativeSubFetch && err != blob.ErrOutOfRangeOffsetSubFetch {
+		if !errors.Is(err, blob.ErrNegativeSubFetch) && !errors.Is(err, blob.ErrOutOfRangeOffsetSubFetch) {
 			t.Errorf("Unexpected error fetching with off=%d limit=%d: %v", tt.off, tt.limit, err)
 		}
 	}
@@ -288,7 +288,7 @@ func CheckEnumerate(sto blobserver.Storage, wantUnsorted []blob.SizedRef, opts .
 		ctx, cancel := context.WithCancel(context.TODO())
 		defer cancel()
 		if err := sto.EnumerateBlobs(ctx, sbc, after, n); err != nil {
-			return fmt.Errorf("EnumerateBlobs(%q, %d): %v", after, n, err)
+			return fmt.Errorf("EnumerateBlobs(%q, %d): %w", after, n, err)
 		}
 		return nil
 	})
@@ -318,7 +318,7 @@ func CheckEnumerate(sto blobserver.Storage, wantUnsorted []blob.SizedRef, opts .
 
 	})
 	if err := grp.Err(); err != nil {
-		return fmt.Errorf("Enumerate error: %v", err)
+		return fmt.Errorf("Enumerate error: %w", err)
 	}
 	if len(got) == 0 && len(want) == 0 {
 		return nil
diff --git a/pkg/fs/fs_test.go b/pkg/fs/fs_test.go
index b79f3d194..86a262d23 100644
--- a/pkg/fs/fs_test.go
+++ b/pkg/fs/fs_test.go
@@ -880,7 +880,7 @@ func shortenString(v string) string {
 func ignoringEINTR(fn func() error) error {
 	for {
 		err := fn()
-		if err != syscall.EINTR {
+		if !errors.Is(err, syscall.EINTR) {
 			return err
 		}
 	}
diff --git a/pkg/importer/flickr/testdata.go b/pkg/importer/flickr/testdata.go
index f3f840a89..ce04eaabf 100644
--- a/pkg/importer/flickr/testdata.go
+++ b/pkg/importer/flickr/testdata.go
@@ -18,6 +18,7 @@ package flickr
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"log"
 	"net/http"
@@ -278,7 +279,7 @@ func fakePhotosPage(page, pages, perPage int, photoIds []string) string {
 
 func fakePicture() string {
 	srcRoot, err := osutil.PkSourceRoot()
-	if err == os.ErrNotExist {
+	if errors.Is(err, os.ErrNotExist) {
 		log.Fatal("Directory \"perkeep.org\" not found under GOPATH/src; are you not running with devcam?")
 	}
 	if err != nil {
diff --git a/pkg/importer/picasa/picasa_test.go b/pkg/importer/picasa/picasa_test.go
index f2e8e74b8..a32aa3b1e 100644
--- a/pkg/importer/picasa/picasa_test.go
+++ b/pkg/importer/picasa/picasa_test.go
@@ -208,7 +208,7 @@ func newSigner(bs blobserver.BlobReceiver) (signer *schema.Signer, owner blob.Re
 	}
 	pubRef := blob.RefFromString(armorPub)
 	if _, err := bs.ReceiveBlob(ctxbg, pubRef, strings.NewReader(armorPub)); err != nil {
-		return nil, owner, fmt.Errorf("could not store pub key blob: %v", err)
+		return nil, owner, fmt.Errorf("could not store pub key blob: %w", err)
 	}
 	sig, err := schema.NewSigner(pubRef, strings.NewReader(armorPub), ent)
 	if err != nil {
diff --git a/pkg/importer/picasa/testdata.go b/pkg/importer/picasa/testdata.go
index 2f9e36663..e939ad720 100644
--- a/pkg/importer/picasa/testdata.go
+++ b/pkg/importer/picasa/testdata.go
@@ -18,6 +18,7 @@ package picasa
 
 import (
 	"encoding/xml"
+	"errors"
 	"fmt"
 	"log"
 	"net/http"
@@ -229,7 +230,7 @@ func fakePhotoEntry(photoNbr int, albumNbr int) picago.Entry {
 // TODO(mpl): refactor with twitter
 func fakePhoto() string {
 	srcRoot, err := osutil.PkSourceRoot()
-	if err == os.ErrNotExist {
+	if errors.Is(err, os.ErrNotExist) {
 		log.Fatal("Directory \"perkeep.org\" not found under GOPATH/src; are you not running with devcam?")
 	}
 	if err != nil {
diff --git a/pkg/importer/swarm/testdata.go b/pkg/importer/swarm/testdata.go
index d5cbdd696..1e144d9df 100644
--- a/pkg/importer/swarm/testdata.go
+++ b/pkg/importer/swarm/testdata.go
@@ -18,6 +18,7 @@ package swarm
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"log"
 	"net/http"
@@ -270,7 +271,7 @@ func fakePhotoItem() *photoItem {
 // TODO(mpl): refactor with twitter
 func fakePhoto() string {
 	srcRoot, err := osutil.PkSourceRoot()
-	if err == os.ErrNotExist {
+	if errors.Is(err, os.ErrNotExist) {
 		log.Fatal("Directory \"perkeep.org\" not found under GOPATH/src; are you not running with devcam?")
 	}
 	if err != nil {
diff --git a/pkg/importer/twitter/testdata.go b/pkg/importer/twitter/testdata.go
index 0edb54d27..214e7d9f5 100644
--- a/pkg/importer/twitter/testdata.go
+++ b/pkg/importer/twitter/testdata.go
@@ -18,6 +18,7 @@ package twitter
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"log"
 	"net/http"
@@ -257,7 +258,7 @@ func fakeEntities(counter int) entities {
 
 func fakePicture() string {
 	camliDir, err := osutil.PkSourceRoot()
-	if err == os.ErrNotExist {
+	if errors.Is(err, os.ErrNotExist) {
 		log.Fatal("Directory \"perkeep.org\" not found under GOPATH/src; are you not running with devcam?")
 	}
 	if err != nil {
diff --git a/pkg/index/index_test.go b/pkg/index/index_test.go
index f60469eff..776972bd6 100644
--- a/pkg/index/index_test.go
+++ b/pkg/index/index_test.go
@@ -18,6 +18,7 @@ package index_test
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"go/ast"
 	"go/parser"
@@ -635,7 +636,7 @@ func TestFixMissingWholeref(t *testing.T) {
 
 	// init broken index
 	ix, err = index.New(s)
-	if err != index.Exp_ErrMissingWholeRef {
+	if !errors.Is(err, index.Exp_ErrMissingWholeRef) {
 		t.Fatalf("wrong error upon index initialization: got %v, wanted %v", err, index.Exp_ErrMissingWholeRef)
 	}
 	// and fix it
diff --git a/pkg/index/indextest/tests.go b/pkg/index/indextest/tests.go
index f30847ea7..2a5f6d888 100644
--- a/pkg/index/indextest/tests.go
+++ b/pkg/index/indextest/tests.go
@@ -698,7 +698,7 @@ func Index(t *testing.T, initIdx func() *index.Index) {
 			}
 		}
 		_, err = id.Index.GetBlobMeta(ctx, blob.ParseOrZero("abc-123"))
-		if err != os.ErrNotExist {
+		if !errors.Is(err, os.ErrNotExist) {
 			t.Errorf("GetBlobMeta(dummy blobref) = %v; want os.ErrNotExist", err)
 		}
 	}
diff --git a/pkg/schema/fileread_test.go b/pkg/schema/fileread_test.go
index 5f2d28040..53dde7332 100644
--- a/pkg/schema/fileread_test.go
+++ b/pkg/schema/fileread_test.go
@@ -333,7 +333,7 @@ func TestReaderForeachChunk(t *testing.T) {
 		}
 		rc, size, err := sto.Fetch(ctxbg, p.BlobRef)
 		if err != nil {
-			return fmt.Errorf("Error fetching blobref of chunk %+v: %v", p, err)
+			return fmt.Errorf("Error fetching blobref of chunk %+v: %w", p, err)
 		}
 		defer rc.Close()
 		totSize += p.Size
diff --git a/pkg/schema/schema_test.go b/pkg/schema/schema_test.go
index d3c859306..06145f36a 100644
--- a/pkg/schema/schema_test.go
+++ b/pkg/schema/schema_test.go
@@ -18,6 +18,7 @@ package schema
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -603,7 +604,7 @@ func TestStaticFIFO(t *testing.T) {
 
 	fifoPath := filepath.Join(tdir, "fifo")
 	err := osutil.Mkfifo(fifoPath, 0660)
-	if err == osutil.ErrNotSupported {
+	if errors.Is(err, osutil.ErrNotSupported) {
 		t.SkipNow()
 	}
 	if err != nil {
@@ -636,7 +637,7 @@ func TestStaticSocket(t *testing.T) {
 
 	sockPath := filepath.Join(tdir, "socket")
 	err := osutil.Mksocket(sockPath)
-	if err == osutil.ErrNotSupported {
+	if errors.Is(err, osutil.ErrNotSupported) {
 		t.SkipNow()
 	}
 	if err != nil {
diff --git a/pkg/search/handler_test.go b/pkg/search/handler_test.go
index 8bbcf837f..837127ae3 100644
--- a/pkg/search/handler_test.go
+++ b/pkg/search/handler_test.go
@@ -151,7 +151,7 @@ type fetcherIndex struct {
 func (fi *fetcherIndex) addBlob(b *test.Blob) error {
 	fi.tf.AddBlob(b)
 	if _, err := fi.idx.ReceiveBlob(ctxbg, b.BlobRef(), b.Reader()); err != nil {
-		return fmt.Errorf("ReceiveBlob(%v): %v", b.BlobRef(), err)
+		return fmt.Errorf("ReceiveBlob(%v): %w", b.BlobRef(), err)
 	}
 	return nil
 }
diff --git a/pkg/server/app/app_test.go b/pkg/server/app/app_test.go
index bc5d2da9f..47e9949c7 100644
--- a/pkg/server/app/app_test.go
+++ b/pkg/server/app/app_test.go
@@ -18,6 +18,7 @@ package app
 
 import (
 	"bufio"
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -198,7 +199,7 @@ func TestQuit(t *testing.T) {
 		process: pid,
 	}
 	err = h.Quit()
-	if err != errProcessTookTooLong {
+	if !errors.Is(err, errProcessTookTooLong) {
 		t.Errorf("got %v, wanted %v", err, errProcessTookTooLong)
 	}
 }
diff --git a/pkg/server/share_test.go b/pkg/server/share_test.go
index 34eec57af..698303d06 100644
--- a/pkg/server/share_test.go
+++ b/pkg/server/share_test.go
@@ -18,6 +18,7 @@ package server
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
@@ -130,7 +131,10 @@ func (st *shareTester) get(path string) *shareError {
 		st.t.Fatalf("NewRequest(path=%q): %v", path, err)
 	}
 	if err := st.handler.serveHTTP(st.rec, req); err != nil {
-		return err.(*shareError)
+		var se *shareError
+		if errors.As(err, &se) {
+			return se
+		}
 	}
 	return nil
 }
diff --git a/pkg/serverinit/serverinit_test.go b/pkg/serverinit/serverinit_test.go
index d397661f3..bdeabe931 100644
--- a/pkg/serverinit/serverinit_test.go
+++ b/pkg/serverinit/serverinit_test.go
@@ -135,7 +135,7 @@ func configParser() *jsonconfig.ConfigParser {
 func replaceRingPath(path string) ([]byte, error) {
 	secRing, err := filepath.Abs(relativeRing)
 	if err != nil {
-		return nil, fmt.Errorf("Could not get absolute path of %v: %v", relativeRing, err)
+		return nil, fmt.Errorf("Could not get absolute path of %v: %w", relativeRing, err)
 	}
 	secRing = strings.Replace(secRing, `\`, `\\`, -1)
 	slurpBytes, err := os.ReadFile(path)
diff --git a/pkg/sorted/kvtest/kvtest.go b/pkg/sorted/kvtest/kvtest.go
index 295d1482a..ac5463e61 100644
--- a/pkg/sorted/kvtest/kvtest.go
+++ b/pkg/sorted/kvtest/kvtest.go
@@ -18,6 +18,7 @@ limitations under the License.
 package kvtest // import "perkeep.org/pkg/sorted/kvtest"
 
 import (
+	"errors"
 	"reflect"
 	"testing"
 	"time"
@@ -43,7 +44,7 @@ func TestSorted(t *testing.T, kv sorted.KeyValue) {
 	if v, err := kv.Get("foo"); err != nil || v != "bar" {
 		t.Errorf("get(foo) = %q, %v; want bar", v, err)
 	}
-	if v, err := kv.Get("NOT_EXIST"); err != sorted.ErrNotFound {
+	if v, err := kv.Get("NOT_EXIST"); !errors.Is(err, sorted.ErrNotFound) {
 		t.Errorf("get(NOT_EXIST) = %q, %v; want error sorted.ErrNotFound", v, err)
 	}
 	for i := 0; i < 2; i++ {
@@ -111,7 +112,7 @@ func testDeletePartialNotFoundBatch(t *testing.T, kv sorted.KeyValue) {
 	if err := kv.CommitBatch(b); err != nil {
 		t.Fatalf("Batch deletion with one non existing key returned an error: %v", err)
 	}
-	if val, err := kv.Get(butIExistKey); err != sorted.ErrNotFound || val != "" {
+	if val, err := kv.Get(butIExistKey); !errors.Is(err, sorted.ErrNotFound) || val != "" {
 		t.Fatalf("Key %q should have been batch deleted", butIExistKey)
 	}
 }
diff --git a/pkg/test/dockertest/docker.go b/pkg/test/dockertest/docker.go
index c14c94b98..5a3de06cb 100644
--- a/pkg/test/dockertest/docker.go
+++ b/pkg/test/dockertest/docker.go
@@ -77,7 +77,7 @@ func loadCamliHubImage(image string) error {
 	imgURL := camliHub + strings.TrimPrefix(image, "camlistore/") + ".tar.gz"
 	resp, err := http.Get(imgURL)
 	if err != nil {
-		return fmt.Errorf("error fetching image %s: %v", image, err)
+		return fmt.Errorf("error fetching image %s: %w", image, err)
 	}
 	defer resp.Body.Close()
 
@@ -93,19 +93,19 @@ func loadCamliHubImage(image string) error {
 		defer tar.Close()
 		zr, err := gzip.NewReader(resp.Body)
 		if err != nil {
-			errc1 <- fmt.Errorf("gzip reader error for image %s: %v", image, err)
+			errc1 <- fmt.Errorf("gzip reader error for image %s: %w", image, err)
 			return
 		}
 		defer zr.Close()
 		if _, err = io.Copy(tar, zr); err != nil {
-			errc1 <- fmt.Errorf("error gunzipping image %s: %v", image, err)
+			errc1 <- fmt.Errorf("error gunzipping image %s: %w", image, err)
 			return
 		}
 		errc1 <- nil
 	}()
 	go func() {
 		if err := dockerLoad.Run(); err != nil {
-			errc2 <- fmt.Errorf("error running docker load %v: %v", image, err)
+			errc2 <- fmt.Errorf("error running docker load %v: %w", image, err)
 			return
 		}
 		errc2 <- nil
@@ -161,7 +161,7 @@ func run(args ...string) (containerID string, err error) {
 	var stdout, stderr bytes.Buffer
 	cmd.Stdout, cmd.Stderr = &stdout, &stderr
 	if err = cmd.Run(); err != nil {
-		err = fmt.Errorf("%v%v", stderr.String(), err)
+		err = fmt.Errorf("%v%w", stderr.String(), err)
 		return
 	}
 	containerID = strings.TrimSpace(stdout.String())
@@ -187,7 +187,7 @@ func Pull(image string) error {
 	// "Authentication is required" message does come from stderr, then quit
 	// checking stdout.
 	if err != nil || stderr.Len() != 0 || strings.Contains(out, "Authentication is required") {
-		return fmt.Errorf("docker pull failed: stdout: %s, stderr: %s, err: %v", out, stderr.String(), err)
+		return fmt.Errorf("docker pull failed: stdout: %s, stderr: %s, err: %w", out, stderr.String(), err)
 	}
 	return nil
 }
@@ -258,7 +258,7 @@ func (c ContainerID) KillRemove(t *testing.T) {
 func (c ContainerID) lookup(port int, timeout time.Duration) (ip string, err error) {
 	ip, err = c.IP()
 	if err != nil {
-		err = fmt.Errorf("error getting IP: %v", err)
+		err = fmt.Errorf("error getting IP: %w", err)
 		return
 	}
 	addr := fmt.Sprintf("%s:%d", ip, port)
@@ -333,12 +333,12 @@ func SetupPostgreSQLContainer(t *testing.T, dbname string) (c ContainerID, ip st
 	rootdb, err := sql.Open("postgres",
 		fmt.Sprintf("user=%s password=%s host=%s dbname=postgres sslmode=disable", PostgresUsername, PostgresPassword, ip))
 	if err != nil {
-		cleanupAndDie(fmt.Errorf("Could not open postgres rootdb: %v", err))
+		cleanupAndDie(fmt.Errorf("Could not open postgres rootdb: %w", err))
 	}
 	if _, err := sqlExecRetry(rootdb,
 		"CREATE DATABASE "+dbname+" LC_COLLATE = 'C' TEMPLATE = template0",
 		50); err != nil {
-		cleanupAndDie(fmt.Errorf("Could not create database %v: %v", dbname, err))
+		cleanupAndDie(fmt.Errorf("Could not create database %v: %w", dbname, err))
 	}
 	return
 }
@@ -367,5 +367,5 @@ func sqlExecRetry(db *sql.DB, stmt string, maxTry int) (sql.Result, error) {
 		time.Sleep(interval)
 		interval *= 2
 	}
-	return result, fmt.Errorf("failed %v times: %v", try, err)
+	return result, fmt.Errorf("failed %v times: %w", try, err)
 }
diff --git a/pkg/test/integration/non-utf8_test.go b/pkg/test/integration/non-utf8_test.go
index 5604687e5..2cfab963a 100644
--- a/pkg/test/integration/non-utf8_test.go
+++ b/pkg/test/integration/non-utf8_test.go
@@ -19,6 +19,7 @@ package integration
 import (
 	"bytes"
 	"encoding/hex"
+	"errors"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -122,7 +123,8 @@ func isBadFilenameError(err error) bool {
 	if runtime.GOOS != "darwin" {
 		return false
 	}
-	if pe, ok := err.(*os.PathError); ok && pe.Op == "open" && pe.Err == syscall.Errno(0x5c) {
+	var pe *os.PathError
+	if errors.As(err, &pe) && pe.Op == "open" && pe.Err == syscall.Errno(0x5c) {
 		return true
 	}
 	return false
diff --git a/pkg/test/world.go b/pkg/test/world.go
index 9c21aa067..a23f1ea4a 100644
--- a/pkg/test/world.go
+++ b/pkg/test/world.go
@@ -117,7 +117,7 @@ func (w *World) Build() error {
 		log.Print("Running make.go to build perkeep binaries for testing...")
 		out, err := cmd.CombinedOutput()
 		if err != nil {
-			return fmt.Errorf("Error building world: %v, %s", err, string(out))
+			return fmt.Errorf("Error building world: %w, %s", err, string(out))
 		}
 		if testing.Verbose() {
 			log.Printf("%s\n", out)
@@ -179,7 +179,7 @@ func (w *World) Start() error {
 		)
 
 		if err := w.server.Start(); err != nil {
-			w.serverErr = fmt.Errorf("starting perkeepd: %v", err)
+			w.serverErr = fmt.Errorf("starting perkeepd: %w", err)
 			return w.serverErr
 		}
 
@@ -187,7 +187,7 @@ func (w *World) Start() error {
 		waitc := make(chan error, 1)
 		go func() {
 			err := w.server.Wait()
-			w.serverErr = fmt.Errorf("%v: %s", err, buf.String())
+			w.serverErr = fmt.Errorf("%w: %s", err, buf.String())
 			atomic.StoreInt32(&w.isRunning, 0)
 			waitc <- w.serverErr
 		}()
@@ -196,14 +196,14 @@ func (w *World) Start() error {
 		go func() {
 			c, err := getPortListener.Accept()
 			if err != nil {
-				upErr <- fmt.Errorf("waiting for child to report its port: %v", err)
+				upErr <- fmt.Errorf("waiting for child to report its port: %w", err)
 				return
 			}
 			defer c.Close()
 			br := bufio.NewReader(c)
 			addr, err := br.ReadString('\n')
 			if err != nil {
-				upErr <- fmt.Errorf("ReadString: %v", err)
+				upErr <- fmt.Errorf("ReadString: %w", err)
 				return
 			}
 			w.addr = strings.TrimSpace(addr)