diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml
index 9840a38941a..b670dfbb32e 100644
--- a/.github/workflows/release.lock.yml
+++ b/.github/workflows/release.lock.yml
@@ -1,5 +1,6 @@
 # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"2d6e82aec6fea9061e1ef1cbb085324e6ed6c079230d70bbbb96611bb18cf7cf","body_hash":"646353d7bb4e5523bc85349c2cce38188190095a303f83cc95961ff145a47043","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}}
-# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/login-action","sha":"af1e73f918a031802d376d3c8bbc3fe56130a9b0","version":"v4.4.0"},{"repo":"docker/metadata-action","sha":"dc802804100637a589fabce1cb79ff13a1411302","version":"v6.2.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}# This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/login-action","sha":"af1e73f918a031802d376d3c8bbc3fe56130a9b0","version":"v4.4.0"},{"repo":"docker/metadata-action","sha":"dc802804100637a589fabce1cb79ff13a1411302","version":"v6.2.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}
+# This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
 #
 #    ___                   _   _
 #   / _ \                 | | (_)
diff --git a/pkg/linters/appendbytestring/appendbytestring.go b/pkg/linters/appendbytestring/appendbytestring.go
index e0c6f4c303c..bb960e2d93b 100644
--- a/pkg/linters/appendbytestring/appendbytestring.go
+++ b/pkg/linters/appendbytestring/appendbytestring.go
@@ -6,7 +6,6 @@ package appendbytestring
 import (
 	"fmt"
 	"go/ast"
-	"go/types"
 
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/passes/inspect"
@@ -67,7 +66,7 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		// The first argument must be []byte.
-		if !isByteSlice(pass, call.Args[0]) {
+		if !astutil.IsByteSlice(pass, call.Args[0]) {
 			return
 		}
 
@@ -76,14 +75,14 @@ func run(pass *analysis.Pass) (any, error) {
 		if !ok {
 			return
 		}
-		if !isByteSliceConversion(pass, conv) {
+		if !astutil.IsByteSliceConversion(pass, conv) {
 			return
 		}
 		if len(conv.Args) != 1 {
 			return
 		}
 		strArg := conv.Args[0]
-		if !isStringType(pass, strArg) {
+		if !astutil.IsStringType(pass, strArg) {
 			return
 		}
 
@@ -103,40 +102,6 @@ func run(pass *analysis.Pass) (any, error) {
 	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)
diff --git a/pkg/linters/bytescomparestring/bytescomparestring.go b/pkg/linters/bytescomparestring/bytescomparestring.go
index b17f7b48bae..e34af17c94a 100644
--- a/pkg/linters/bytescomparestring/bytescomparestring.go
+++ b/pkg/linters/bytescomparestring/bytescomparestring.go
@@ -276,23 +276,9 @@ func extractByteSliceStringConv(pass *analysis.Pass, expr ast.Expr) (ast.Expr, b
 
 	// The argument must be []byte (or []uint8).
 	arg := call.Args[0]
-	if !isByteSlice(pass, arg) {
+	if !astutil.IsByteSlice(pass, arg) {
 		return nil, false
 	}
 
 	return arg, true
 }
-
-// isByteSlice reports whether expr has underlying type []byte ([]uint8).
-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
-}
diff --git a/pkg/linters/internal/astutil/astutil.go b/pkg/linters/internal/astutil/astutil.go
index 4aa9526f620..c9e524defa5 100644
--- a/pkg/linters/internal/astutil/astutil.go
+++ b/pkg/linters/internal/astutil/astutil.go
@@ -345,6 +345,39 @@ func QualifierShadowed(pkg *types.Package, pos token.Pos, name, importPath strin
 	return pkgName.Imported().Path() != importPath
 }
 
+// IsByteSlice reports whether expr has underlying type []byte ([]uint8).
+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 or []uint8 conversion expression.
+func IsByteSliceConversion(pass *analysis.Pass, conv *ast.CallExpr) bool {
+	funTypeInfo, ok := pass.TypesInfo.Types[conv.Fun]
+	if !ok || !funTypeInfo.IsType() {
+		return false
+	}
+	return IsByteSlice(pass, conv)
+}
+
+// IsStringType reports whether expr has underlying type string (or a named string type).
+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
+}
+
 // ConstIntValue returns the integer constant value of expr, if it is a
 // constant integer.
 func ConstIntValue(pass *analysis.Pass, expr ast.Expr) (int64, bool) {
diff --git a/pkg/linters/internal/astutil/astutil_test.go b/pkg/linters/internal/astutil/astutil_test.go
index 2350e9186d0..3a7cad06262 100644
--- a/pkg/linters/internal/astutil/astutil_test.go
+++ b/pkg/linters/internal/astutil/astutil_test.go
@@ -5,6 +5,8 @@ package astutil
 import (
 	"go/ast"
 	"go/constant"
+	"go/importer"
+	"go/parser"
 	"go/token"
 	"go/types"
 	"testing"
@@ -12,6 +14,28 @@ import (
 	"golang.org/x/tools/go/analysis"
 )
 
+func typecheckSnippet(t *testing.T, src string) (*analysis.Pass, *ast.File) {
+	t.Helper()
+
+	fset := token.NewFileSet()
+	file, err := parser.ParseFile(fset, "snippet.go", src, parser.SkipObjectResolution)
+	if err != nil {
+		t.Fatalf("ParseFile() error = %v", err)
+	}
+
+	info := &types.Info{
+		Types: make(map[ast.Expr]types.TypeAndValue),
+		Defs:  make(map[*ast.Ident]types.Object),
+		Uses:  make(map[*ast.Ident]types.Object),
+	}
+	cfg := types.Config{Importer: importer.Default()}
+	if _, err := cfg.Check("example.com/p", fset, []*ast.File{file}, info); err != nil {
+		t.Fatalf("type checking failed: %v", err)
+	}
+
+	return &analysis.Pass{TypesInfo: info}, file
+}
+
 func TestRhsExprForIndex(t *testing.T) {
 	t.Parallel()
 
@@ -424,3 +448,94 @@ func TestBuildContainsFix(t *testing.T) {
 		t.Fatalf("negated Message = %q, want %q", fixes[0].Message, "negated message")
 	}
 }
+
+func TestByteStringTypeHelpers(t *testing.T) {
+	t.Parallel()
+
+	const src = `package p
+
+func g(s string) []byte { return nil }
+
+func f(s string, b []byte) {
+	type myString string
+	var ms myString
+
+	_ = []byte(s)
+	_ = g(s)
+	_ = b
+	_ = s
+	_ = ms
+}
+`
+
+	pass, file := typecheckSnippet(t, src)
+
+	var fn *ast.FuncDecl
+	for _, decl := range file.Decls {
+		decl, ok := decl.(*ast.FuncDecl)
+		if ok && decl.Name.Name == "f" {
+			fn = decl
+			break
+		}
+	}
+	if fn == nil || fn.Body == nil {
+		t.Fatal("failed to find function f in test snippet")
+	}
+
+	var rhsExprs []ast.Expr
+	for _, stmt := range fn.Body.List {
+		assign, ok := stmt.(*ast.AssignStmt)
+		if !ok || len(assign.Rhs) != 1 {
+			continue
+		}
+		rhsExprs = append(rhsExprs, assign.Rhs[0])
+	}
+	if len(rhsExprs) != 5 {
+		t.Fatalf("found %d assignment expressions in f, want 5", len(rhsExprs))
+	}
+
+	byteConv, ok := rhsExprs[0].(*ast.CallExpr)
+	if !ok {
+		t.Fatalf("rhs[0] type = %T, want *ast.CallExpr", rhsExprs[0])
+	}
+	gCall, ok := rhsExprs[1].(*ast.CallExpr)
+	if !ok {
+		t.Fatalf("rhs[1] type = %T, want *ast.CallExpr", rhsExprs[1])
+	}
+	bIdent, ok := rhsExprs[2].(*ast.Ident)
+	if !ok {
+		t.Fatalf("rhs[2] type = %T, want *ast.Ident", rhsExprs[2])
+	}
+	sIdent, ok := rhsExprs[3].(*ast.Ident)
+	if !ok {
+		t.Fatalf("rhs[3] type = %T, want *ast.Ident", rhsExprs[3])
+	}
+	msIdent, ok := rhsExprs[4].(*ast.Ident)
+	if !ok {
+		t.Fatalf("rhs[4] type = %T, want *ast.Ident", rhsExprs[4])
+	}
+
+	if !IsByteSlice(pass, bIdent) {
+		t.Fatal("IsByteSlice(b) = false, want true")
+	}
+	if IsByteSlice(pass, sIdent) {
+		t.Fatal("IsByteSlice(s) = true, want false")
+	}
+
+	if !IsByteSliceConversion(pass, byteConv) {
+		t.Fatal("IsByteSliceConversion([]byte(s)) = false, want true")
+	}
+	if IsByteSliceConversion(pass, gCall) {
+		t.Fatal("IsByteSliceConversion(g(s)) = true, want false")
+	}
+
+	if !IsStringType(pass, sIdent) {
+		t.Fatal("IsStringType(s) = false, want true")
+	}
+	if !IsStringType(pass, msIdent) {
+		t.Fatal("IsStringType(ms) = false, want true for named string")
+	}
+	if IsStringType(pass, bIdent) {
+		t.Fatal("IsStringType(b) = true, want false")
+	}
+}
diff --git a/pkg/linters/writebytestring/writebytestring.go b/pkg/linters/writebytestring/writebytestring.go
index fcfcb9dc1a6..4a47f363279 100644
--- a/pkg/linters/writebytestring/writebytestring.go
+++ b/pkg/linters/writebytestring/writebytestring.go
@@ -98,11 +98,11 @@ func run(pass *analysis.Pass) (any, error) {
 		if !ok {
 			return
 		}
-		if !isByteSliceConversion(pass, conv) {
+		if !astutil.IsByteSliceConversion(pass, conv) {
 			return
 		}
 		strArg := conv.Args[0]
-		if !isStringType(pass, strArg) {
+		if !astutil.IsStringType(pass, strArg) {
 			return
 		}
 
@@ -146,39 +146,6 @@ func run(pass *analysis.Pass) (any, error) {
 	return nil, nil
 }
 
-// isByteSliceConversion reports whether conv is a []byte or []uint8 conversion expression.
-func isByteSliceConversion(pass *analysis.Pass, conv *ast.CallExpr) bool {
-	funTypeInfo, ok := pass.TypesInfo.Types[conv.Fun]
-	if !ok || !funTypeInfo.IsType() {
-		return false
-	}
-	return isByteSlice(pass, conv)
-}
-
-// isByteSlice reports whether expr has type []byte ([]uint8).
-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
-}
-
-// isStringType reports whether expr has type string (or named string type).
-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
-}
-
 // isExactString reports whether t is the predeclared string type, not a named
 // type whose underlying type is string. io.WriteString(w Writer, s string)
 // requires a predeclared string; named string types need an explicit string(...)
