diff --git a/cmd/linters/main.go b/cmd/linters/main.go
index 35838441ac2..ddb71496355 100644
--- a/cmd/linters/main.go
+++ b/cmd/linters/main.go
@@ -16,6 +16,7 @@ package main
 import (
 	"golang.org/x/tools/go/analysis/multichecker"
 
+	"github.com/github/gh-aw/pkg/linters/appendbytestring"
 	"github.com/github/gh-aw/pkg/linters/contextcancelnotdeferred"
 	"github.com/github/gh-aw/pkg/linters/ctxbackground"
 	"github.com/github/gh-aw/pkg/linters/deferinloop"
@@ -61,6 +62,7 @@ import (
 
 func main() {
 	multichecker.Main(
+		appendbytestring.Analyzer,
 		contextcancelnotdeferred.Analyzer,
 		ctxbackground.Analyzer,
 		deferinloop.Analyzer,
diff --git a/docs/adr/43423-appendbytestring-linter-for-redundant-byte-conversion.md b/docs/adr/43423-appendbytestring-linter-for-redundant-byte-conversion.md
new file mode 100644
index 00000000000..b3984113800
--- /dev/null
+++ b/docs/adr/43423-appendbytestring-linter-for-redundant-byte-conversion.md
@@ -0,0 +1,44 @@
+# ADR-43423: Add appendbytestring Linter to Flag Redundant []byte Conversion in append Calls
+
+**Date**: 2026-07-05
+**Status**: Draft
+**Deciders**: pelikhan, linter-miner (automated)
+
+---
+
+### Context
+
+The gh-aw codebase maintains a suite of custom Go static analysis linters via `golang.org/x/tools/go/analysis` registered in a `multichecker` binary. A recurring anti-pattern was identified in `pkg/workflow/action_cache.go` and potentially elsewhere: `append(b, []byte(s)...)` where `b` is `[]byte` and `s` is a string. This conversion is unnecessary because Go natively allows `append(b, s...)` to append a string directly to a byte slice without allocating an intermediate `[]byte`. Each redundant conversion allocates a temporary byte slice on the heap, adding GC pressure in hot paths that construct byte buffers from string literals.
+
+### Decision
+
+We will add a new custom linter package `pkg/linters/appendbytestring` implementing a `golang.org/x/tools/go/analysis` pass that detects `append(b, []byte(s)...)` where the first argument is `[]byte` and the inner argument of the `[]byte(...)` conversion is a `string`. The linter provides a suggested fix that rewrites the expression to the idiomatic `append(b, s...)`. It is registered in the `cmd/linters/main.go` multichecker, respects `//nolint:appendbytestring` suppressions, and skips test files. This choice integrates naturally with the existing in-house linter framework already used for similar micro-pattern enforcement.
+
+### Alternatives Considered
+
+#### Alternative 1: Rely on an external linter (e.g., gocritic or staticcheck)
+
+`gocritic` and `staticcheck` are popular third-party linters that detect many Go anti-patterns. `gocritic` in particular has a `appendAssign` check and similar micro-pattern checkers. However, gh-aw's custom multichecker pipeline does not currently integrate these tools end-to-end, and adopting one would introduce a large external dependency for the sake of a single narrowly-scoped check. Furthermore, the existing in-house linter infrastructure (shared `astutil`, `filecheck`, `nolint` packages) already provides the scaffolding needed to write focused analyzers with minimal friction. The net cost of adding external tooling outweighs the benefit for a single pattern.
+
+#### Alternative 2: One-off manual cleanup without ongoing enforcement
+
+The specific instances in `pkg/workflow/action_cache.go` could be fixed manually in a single PR without adding a linter. This would remove the existing allocation overhead but would not prevent the pattern from re-appearing in future code. Given that the anti-pattern is easy to write by habit (especially when converting from C or Java backgrounds where explicit conversions are common), enforcement at CI time is preferable over relying solely on code review vigilance to catch regressions.
+
+### Consequences
+
+#### Positive
+- New instances of `append(b, []byte(s)...)` will be caught at CI analysis time, preventing performance regressions from this specific allocation pattern.
+- The suggested fix enables automated or semi-automated remediation, reducing the manual effort needed to bring existing code into compliance.
+- Consistent with the established pattern of in-house micro-linters, keeping the enforcement surface unified within a single multichecker binary.
+
+#### Negative
+- Adds one more analysis pass to the CI linter run, marginally increasing analysis time (though `golang.org/x/tools/go/analysis` passes are generally fast).
+- Developers unfamiliar with the Go idiom of appending a string directly to a `[]byte` may initially find the diagnostic confusing until they understand the underlying language semantics.
+
+#### Neutral
+- The linter skips test files by convention (consistent with other linters in this suite), meaning test code using the pattern will not be flagged even if it is technically wasteful.
+- The `//nolint:appendbytestring` escape hatch is available for the rare legitimate use case where the explicit `[]byte(...)` conversion aids readability at the cost of an allocation.
+
+---
+
+*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*
diff --git a/pkg/linters/appendbytestring/appendbytestring.go b/pkg/linters/appendbytestring/appendbytestring.go
new file mode 100644
index 00000000000..d8b874d4043
--- /dev/null
+++ b/pkg/linters/appendbytestring/appendbytestring.go
@@ -0,0 +1,152 @@
+// Package appendbytestring implements a Go analysis linter that flags
+// append(b, []byte(s)...) calls where b is []byte and s is a string,
+// which can be simplified to append(b, s...) without the redundant conversion.
+package appendbytestring
+
+import (
+	"fmt"
+	"go/ast"
+	"go/types"
+
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/go/analysis/passes/inspect"
+
+	"github.com/github/gh-aw/pkg/linters/internal/astutil"
+	"github.com/github/gh-aw/pkg/linters/internal/filecheck"
+	"github.com/github/gh-aw/pkg/linters/internal/nolint"
+)
+
+// Analyzer is the append-byte-string analysis pass.
+var Analyzer = &analysis.Analyzer{
+	Name:     "appendbytestring",
+	Doc:      "reports append(b, []byte(s)...) calls where s is a string that can be simplified to append(b, s...)",
+	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/appendbytestring",
+	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Run:      run,
+}
+
+func run(pass *analysis.Pass) (any, error) {
+	insp, err := astutil.Inspector(pass)
+	if err != nil {
+		return nil, err
+	}
+	noLintLinesByFile := nolint.BuildLineIndex(pass, "appendbytestring")
+
+	nodeFilter := []ast.Node{
+		(*ast.CallExpr)(nil),
+	}
+
+	insp.Preorder(nodeFilter, func(n ast.Node) {
+		call, ok := n.(*ast.CallExpr)
+		if !ok {
+			return
+		}
+
+		// Match append(b, x...) with exactly 2 arguments and an ellipsis.
+		ident, ok := call.Fun.(*ast.Ident)
+		if !ok || ident.Name != "append" {
+			return
+		}
+		if len(call.Args) != 2 || !call.Ellipsis.IsValid() {
+			return
+		}
+
+		pos := pass.Fset.PositionFor(call.Pos(), false)
+		if filecheck.IsTestFile(pos.Filename) {
+			return
+		}
+		if nolint.HasDirective(pos, noLintLinesByFile) {
+			return
+		}
+
+		// The first argument must be []byte.
+		if !isByteSlice(pass, call.Args[0]) {
+			return
+		}
+
+		// The second argument must be a []byte(s) conversion where s is a string.
+		conv, ok := call.Args[1].(*ast.CallExpr)
+		if !ok {
+			return
+		}
+		if !isByteSliceConversion(pass, conv) {
+			return
+		}
+		if len(conv.Args) != 1 {
+			return
+		}
+		strArg := conv.Args[0]
+		if !isStringType(pass, strArg) {
+			return
+		}
+
+		sText := astutil.NodeText(pass.Fset, strArg)
+		if sText == "" {
+			return
+		}
+
+		pass.Report(analysis.Diagnostic{
+			Pos:            call.Pos(),
+			End:            call.End(),
+			Message:        fmt.Sprintf("append(b, []byte(%s)...) can be simplified to append(b, %s...); the []byte conversion is unnecessary", sText, sText),
+			SuggestedFixes: buildFix(pass, conv, strArg),
+		})
+	})
+
+	return nil, nil
+}
+
+// isByteSlice reports whether expr has type []byte.
+func isByteSlice(pass *analysis.Pass, expr ast.Expr) bool {
+	t := pass.TypesInfo.TypeOf(expr)
+	if t == nil {
+		return false
+	}
+	sl, ok := t.Underlying().(*types.Slice)
+	if !ok {
+		return false
+	}
+	elem, ok := sl.Elem().(*types.Basic)
+	return ok && elem.Kind() == types.Byte
+}
+
+// isByteSliceConversion reports whether conv is a []byte/[]uint8 conversion expression.
+func isByteSliceConversion(pass *analysis.Pass, conv *ast.CallExpr) bool {
+	// A type conversion has a type expression as the call function.
+	funTypeInfo, ok := pass.TypesInfo.Types[conv.Fun]
+	if !ok || !funTypeInfo.IsType() {
+		return false
+	}
+	return isByteSlice(pass, conv)
+}
+
+// isStringType reports whether expr has type string.
+func isStringType(pass *analysis.Pass, expr ast.Expr) bool {
+	t := pass.TypesInfo.TypeOf(expr)
+	if t == nil {
+		return false
+	}
+	basic, ok := t.Underlying().(*types.Basic)
+	return ok && basic.Kind() == types.String
+}
+
+// buildFix returns a SuggestedFix rewriting append(b, []byte(s)...) to append(b, s...).
+func buildFix(pass *analysis.Pass, conv *ast.CallExpr, strArg ast.Expr) []analysis.SuggestedFix {
+	sText := astutil.NodeText(pass.Fset, strArg)
+	if sText == "" {
+		return nil
+	}
+	// Replace the entire second argument []byte(s) with just s.
+	// The ellipsis token follows the closing paren of the outer append call,
+	// so we only need to rewrite conv.Pos()..conv.End() to sText.
+	return []analysis.SuggestedFix{{
+		Message: "Replace []byte(s) with s in append",
+		TextEdits: []analysis.TextEdit{
+			{
+				Pos:     conv.Pos(),
+				End:     conv.End(),
+				NewText: []byte(sText),
+			},
+		},
+	}}
+}
diff --git a/pkg/linters/appendbytestring/appendbytestring_test.go b/pkg/linters/appendbytestring/appendbytestring_test.go
new file mode 100644
index 00000000000..6e882f2b372
--- /dev/null
+++ b/pkg/linters/appendbytestring/appendbytestring_test.go
@@ -0,0 +1,16 @@
+//go:build !integration
+
+package appendbytestring_test
+
+import (
+	"testing"
+
+	"golang.org/x/tools/go/analysis/analysistest"
+
+	"github.com/github/gh-aw/pkg/linters/appendbytestring"
+)
+
+func TestAnalyzer(t *testing.T) {
+	testdata := analysistest.TestData()
+	analysistest.RunWithSuggestedFixes(t, testdata, appendbytestring.Analyzer, "appendbytestring")
+}
diff --git a/pkg/linters/appendbytestring/testdata/src/appendbytestring/appendbytestring.go b/pkg/linters/appendbytestring/testdata/src/appendbytestring/appendbytestring.go
new file mode 100644
index 00000000000..5a9caeee2f1
--- /dev/null
+++ b/pkg/linters/appendbytestring/testdata/src/appendbytestring/appendbytestring.go
@@ -0,0 +1,61 @@
+package appendbytestring
+
+func bad() {
+	b := []byte("hello")
+	s := " world"
+	b = append(b, []byte(s)...) // want `append\(b, \[\]byte\(s\)\.\.\.\) can be simplified to append\(b, s\.\.\.\); the \[\]byte conversion is unnecessary`
+	_ = b
+}
+
+func badLiteral() {
+	b := []byte("hello")
+	b = append(b, []byte(" world")...) // want `append\(b, \[\]byte\(" world"\)\.\.\.\) can be simplified to append\(b, " world"\.\.\.\); the \[\]byte conversion is unnecessary`
+	_ = b
+}
+
+type myString string
+
+func badNamedString() {
+	b := []byte("hello")
+	s := myString(" world")
+	b = append(b, []byte(s)...) // want `append\(b, \[\]byte\(s\)\.\.\.\) can be simplified to append\(b, s\.\.\.\); the \[\]byte conversion is unnecessary`
+	_ = b
+}
+
+func badUint8() {
+	b := []byte("hello")
+	s := " world"
+	b = append(b, []uint8(s)...) // want `append\(b, \[\]byte\(s\)\.\.\.\) can be simplified to append\(b, s\.\.\.\); the \[\]byte conversion is unnecessary`
+	_ = b
+}
+
+func good() {
+	b := []byte("hello")
+	s := " world"
+	// Direct string append — already the idiomatic form.
+	b = append(b, s...)
+	_ = b
+}
+
+func goodAlreadyBytes() {
+	b := []byte("hello")
+	extra := []byte(" world")
+	// Appending []byte directly with ellipsis is fine; the conversion is necessary here.
+	b = append(b, extra...)
+	_ = b
+}
+
+func goodMultipleArgs() {
+	b := []byte("hello")
+	// append with multiple non-ellipsis args is fine.
+	b = append(b, 'a', 'b', 'c')
+	_ = b
+}
+
+func suppressed() {
+	b := []byte("hello")
+	s := " world"
+	//nolint:appendbytestring
+	b = append(b, []byte(s)...)
+	_ = b
+}
diff --git a/pkg/linters/appendbytestring/testdata/src/appendbytestring/appendbytestring.go.golden b/pkg/linters/appendbytestring/testdata/src/appendbytestring/appendbytestring.go.golden
new file mode 100644
index 00000000000..bb878d742fd
--- /dev/null
+++ b/pkg/linters/appendbytestring/testdata/src/appendbytestring/appendbytestring.go.golden
@@ -0,0 +1,61 @@
+package appendbytestring
+
+func bad() {
+	b := []byte("hello")
+	s := " world"
+	b = append(b, s...) // want `append\(b, \[\]byte\(s\)\.\.\.\) can be simplified to append\(b, s\.\.\.\); the \[\]byte conversion is unnecessary`
+	_ = b
+}
+
+func badLiteral() {
+	b := []byte("hello")
+	b = append(b, " world"...) // want `append\(b, \[\]byte\(" world"\)\.\.\.\) can be simplified to append\(b, " world"\.\.\.\); the \[\]byte conversion is unnecessary`
+	_ = b
+}
+
+type myString string
+
+func badNamedString() {
+	b := []byte("hello")
+	s := myString(" world")
+	b = append(b, s...) // want `append\(b, \[\]byte\(s\)\.\.\.\) can be simplified to append\(b, s\.\.\.\); the \[\]byte conversion is unnecessary`
+	_ = b
+}
+
+func badUint8() {
+	b := []byte("hello")
+	s := " world"
+	b = append(b, s...) // want `append\(b, \[\]byte\(s\)\.\.\.\) can be simplified to append\(b, s\.\.\.\); the \[\]byte conversion is unnecessary`
+	_ = b
+}
+
+func good() {
+	b := []byte("hello")
+	s := " world"
+	// Direct string append — already the idiomatic form.
+	b = append(b, s...)
+	_ = b
+}
+
+func goodAlreadyBytes() {
+	b := []byte("hello")
+	extra := []byte(" world")
+	// Appending []byte directly with ellipsis is fine; the conversion is necessary here.
+	b = append(b, extra...)
+	_ = b
+}
+
+func goodMultipleArgs() {
+	b := []byte("hello")
+	// append with multiple non-ellipsis args is fine.
+	b = append(b, 'a', 'b', 'c')
+	_ = b
+}
+
+func suppressed() {
+	b := []byte("hello")
+	s := " world"
+	//nolint:appendbytestring
+	b = append(b, []byte(s)...)
+	_ = b
+}
diff --git a/pkg/linters/spec_test.go b/pkg/linters/spec_test.go
index 14925b7939c..2e42a1431b1 100644
--- a/pkg/linters/spec_test.go
+++ b/pkg/linters/spec_test.go
@@ -11,6 +11,7 @@ import (
 	"golang.org/x/tools/go/analysis"
 
 	"github.com/github/gh-aw/pkg/linters"
+	"github.com/github/gh-aw/pkg/linters/appendbytestring"
 	"github.com/github/gh-aw/pkg/linters/contextcancelnotdeferred"
 	"github.com/github/gh-aw/pkg/linters/ctxbackground"
 	"github.com/github/gh-aw/pkg/linters/deferinloop"
@@ -63,13 +64,13 @@ type docAnalyzer struct {
 }
 
 // documentedAnalyzers returns the analyzer subpackages documented in the README
-// "Public API > Subpackages" table. The README documents 37 analyzers
+// "Public API > Subpackages" table. The README documents 38 analyzers
 // subpackages (the non-analyzer `internal` helper subpackage is excluded because
 // it exposes no Analyzer).
 //
 // Spec (README "Public API > Subpackages"):
 //
-//	contextcancelnotdeferred, ctxbackground, deferinloop, errorfwrapv, excessivefuncparams, errormessage,
+//	appendbytestring, contextcancelnotdeferred, ctxbackground, deferinloop, errorfwrapv, excessivefuncparams, errormessage,
 //	errortypeassertion, errstringmatch, execcommandwithoutcontext, fileclosenotdeferred, fmterrorfnoverbs, fprintlnsprintf,
 //	hardcodedfilepath, httpnoctx, jsonmarshalignoredeerror, largefunc, lenstringsplit, lenstringzero,
 //	manualmutexunlock, osexitinlibrary, ossetenvlibrary, panic-in-library-code, rawloginlib,
@@ -78,6 +79,7 @@ type docAnalyzer struct {
 //	tolowerequalfold, uncheckedtypeassertion, wgdonenotdeferred
 func documentedAnalyzers() []docAnalyzer {
 	return []docAnalyzer{
+		{"appendbytestring", appendbytestring.Analyzer},
 		{"contextcancelnotdeferred", contextcancelnotdeferred.Analyzer},
 		{"ctxbackground", ctxbackground.Analyzer},
 		{"deferinloop", deferinloop.Analyzer},
