diff --git a/go.mod b/go.mod
index 8e0ad30ca43..9851a0b0262 100644
--- a/go.mod
+++ b/go.mod
@@ -21,6 +21,7 @@ require (
 	github.com/coreos/go-semver v0.3.1
 	github.com/fsnotify/fsnotify v1.8.0
 	github.com/go-git/go-git/v5 v5.13.2
+	github.com/goccy/go-yaml v1.15.15
 	github.com/google/go-github/v68 v68.0.0
 	github.com/google/renameio/v2 v2.0.0
 	github.com/gopasspw/gopass v1.15.15
@@ -56,7 +57,6 @@ require (
 	golang.org/x/sys v0.29.0
 	golang.org/x/term v0.28.0
 	gopkg.in/ini.v1 v1.67.0
-	gopkg.in/yaml.v3 v3.0.1
 	howett.net/plist v1.0.1
 	mvdan.cc/sh/v3 v3.10.0
 )
@@ -180,4 +180,5 @@ require (
 	golang.org/x/tools v0.29.0 // indirect
 	google.golang.org/protobuf v1.36.4 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 72222e0814e..0381aee4be1 100644
--- a/go.sum
+++ b/go.sum
@@ -242,6 +242,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7
 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/goccy/go-yaml v1.15.15 h1:5turdzAlutS2Q7/QR/9R99Z1K0J00qDb4T0pHJcZ5ew=
+github.com/goccy/go-yaml v1.15.15/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
 github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
diff --git a/internal/chezmoi/entrytypeset.go b/internal/chezmoi/entrytypeset.go
index 9923df4d3fc..9838bef7f1e 100644
--- a/internal/chezmoi/entrytypeset.go
+++ b/internal/chezmoi/entrytypeset.go
@@ -268,7 +268,7 @@ func (s *EntryTypeSet) MarshalJSON() ([]byte, error) {
 	}
 }
 
-// MarshalYAML implements gopkg.in/yaml.v3.Marshaler.
+// MarshalYAML implements github.com/goccy/go-yaml.Marshaler.
 func (s *EntryTypeSet) MarshalYAML() (any, error) {
 	if s.bits == EntryTypesAll {
 		return []string{"all"}, nil
diff --git a/internal/chezmoi/format.go b/internal/chezmoi/format.go
index 0baa4768826..d713e0630ff 100644
--- a/internal/chezmoi/format.go
+++ b/internal/chezmoi/format.go
@@ -10,9 +10,9 @@ import (
 	"slices"
 	"strings"
 
+	"github.com/goccy/go-yaml"
 	"github.com/pelletier/go-toml/v2"
 	"github.com/tailscale/hujson"
-	"gopkg.in/yaml.v3"
 )
 
 // Formats.
diff --git a/internal/chezmoi/hexbytes.go b/internal/chezmoi/hexbytes.go
index 67dd115bdea..83ad8d27606 100644
--- a/internal/chezmoi/hexbytes.go
+++ b/internal/chezmoi/hexbytes.go
@@ -2,6 +2,7 @@ package chezmoi
 
 import (
 	"encoding/hex"
+	"strconv"
 )
 
 // A HexBytes is a []byte which is marshaled as a hex string.
@@ -22,6 +23,15 @@ func (h HexBytes) MarshalText() ([]byte, error) {
 	return result, nil
 }
 
+// MarshalYAML implements github.com/goccy/go-yaml.BytesMarshaler.MarshalYAML.
+func (h HexBytes) MarshalYAML() ([]byte, error) {
+	data := make([]byte, 2+2*len(h))
+	data[0] = '"'
+	hex.Encode(data[1:len(data)-1], []byte(h))
+	data[len(data)-1] = '"'
+	return data, nil
+}
+
 // UnmarshalText implements encoding.TextUnmarshaler.UnmarshalText.
 func (h *HexBytes) UnmarshalText(text []byte) error {
 	if len(text) == 0 {
@@ -36,6 +46,24 @@ func (h *HexBytes) UnmarshalText(text []byte) error {
 	return nil
 }
 
+// UnmarshalYAML implements github.com/goccy/go-yaml.BytesUnmarshaler.UnmarshalYAML.
+func (h *HexBytes) UnmarshalYAML(data []byte) error {
+	s, err := strconv.Unquote(string(data))
+	if err != nil {
+		return err
+	}
+	if s == "" {
+		*h = nil
+		return nil
+	}
+	hexBytes, err := hex.DecodeString(s)
+	if err != nil {
+		return err
+	}
+	*h = hexBytes
+	return nil
+}
+
 func (h HexBytes) String() string {
 	return hex.EncodeToString(h)
 }
diff --git a/internal/chezmoi/template.go b/internal/chezmoi/template.go
index a08a5373303..f5f633d11e0 100644
--- a/internal/chezmoi/template.go
+++ b/internal/chezmoi/template.go
@@ -8,10 +8,10 @@ import (
 	"strings"
 	"text/template"
 
+	"github.com/goccy/go-yaml"
 	"github.com/mattn/go-runewidth"
 	"github.com/mitchellh/copystructure"
 	"github.com/pelletier/go-toml/v2"
-	"gopkg.in/yaml.v3"
 )
 
 // A Template extends text/template.Template with support for directives.
@@ -61,8 +61,9 @@ func ParseTemplate(name string, data []byte, options TemplateOptions) (*Template
 		}
 		funcs["toYaml"] = func(data any) string {
 			var builder strings.Builder
-			encoder := yaml.NewEncoder(&builder)
-			encoder.SetIndent(runewidth.StringWidth(options.FormatIndent))
+			encoder := yaml.NewEncoder(&builder,
+				yaml.Indent(runewidth.StringWidth(options.FormatIndent)),
+			)
 			if err := encoder.Encode(data); err != nil {
 				panic(err)
 			}
diff --git a/internal/cmd/autobool.go b/internal/cmd/autobool.go
index 66ea9dfa9f0..5271c9bac10 100644
--- a/internal/cmd/autobool.go
+++ b/internal/cmd/autobool.go
@@ -1,14 +1,13 @@
 package cmd
 
 import (
-	"errors"
+	"bytes"
 	"fmt"
 	"reflect"
 	"strconv"
 	"strings"
 
 	"github.com/mitchellh/mapstructure"
-	"gopkg.in/yaml.v3"
 
 	"github.com/twpayne/chezmoi/v2/internal/chezmoi"
 )
@@ -38,7 +37,7 @@ func (b autoBool) MarshalJSON() ([]byte, error) {
 	}
 }
 
-// MarshalYAML implements gopkg.in/yaml.v3.Marshaler.
+// MarshalYAML implements github.com/goccy/go-yaml.Marshaler.
 func (b autoBool) MarshalYAML() (any, error) {
 	if b.auto {
 		return "auto", nil
@@ -89,15 +88,12 @@ func (b *autoBool) UnmarshalJSON(data []byte) error {
 }
 
 // UnmarshalYAML implements gopkg.in/yaml.Unmarshaler.UnmarshalYAML.
-func (b *autoBool) UnmarshalYAML(value *yaml.Node) error {
-	if value.Kind != yaml.ScalarNode {
-		return errors.New("expected scalar node")
-	}
-	if value.Value == "auto" {
+func (b *autoBool) UnmarshalYAML(data []byte) error {
+	if bytes.Equal(data, []byte("auto")) {
 		b.auto = true
 		return nil
 	}
-	boolValue, err := chezmoi.ParseBool(value.Value)
+	boolValue, err := chezmoi.ParseBool(string(data))
 	if err != nil {
 		return err
 	}
diff --git a/internal/cmd/testdata/scripts/configstate.txtar b/internal/cmd/testdata/scripts/configstate.txtar
index 77b43adf708..c927c0727ea 100644
--- a/internal/cmd/testdata/scripts/configstate.txtar
+++ b/internal/cmd/testdata/scripts/configstate.txtar
@@ -64,8 +64,8 @@ exec chezmoi diff
     email = "me@home.org"
 -- golden/state-dump.yaml --
 configState:
-    configState:
-        configTemplateContentsSHA256: af43121a524340707b84e390f510c949731177e6f2a25b3b6b11b2fc656cf8f2
+  configState:
+    configTemplateContentsSHA256: af43121a524340707b84e390f510c949731177e6f2a25b3b6b11b2fc656cf8f2
 entryState: {}
 gitHubKeysState: {}
 gitHubLatestReleaseState: {}
diff --git a/internal/cmd/testdata/scripts/dumpyaml.txtar b/internal/cmd/testdata/scripts/dumpyaml.txtar
index 8c1e5b33414..1fde31aad50 100644
--- a/internal/cmd/testdata/scripts/dumpyaml.txtar
+++ b/internal/cmd/testdata/scripts/dumpyaml.txtar
@@ -10,125 +10,111 @@ cmp stdout golden/dump-except-dirs.yaml
 
 -- golden/dump-except-dirs.yaml --
 .create:
-    type: file
-    name: .create
-    contents: |
-        # contents of .create
-    perm: 420
+  type: file
+  name: .create
+  contents: "# contents of .create\n"
+  perm: 420
 .dir/file:
-    type: file
-    name: .dir/file
-    contents: |
-        # contents of .dir/file
-    perm: 420
+  type: file
+  name: .dir/file
+  contents: "# contents of .dir/file\n"
+  perm: 420
 .dir/subdir/file:
-    type: file
-    name: .dir/subdir/file
-    contents: |
-        # contents of .dir/subdir/file
-    perm: 420
+  type: file
+  name: .dir/subdir/file
+  contents: "# contents of .dir/subdir/file\n"
+  perm: 420
 .empty:
-    type: file
-    name: .empty
-    contents: ""
-    perm: 420
+  type: file
+  name: .empty
+  contents: ""
+  perm: 420
 .executable:
-    type: file
-    name: .executable
-    contents: |
-        # contents of .executable
-    perm: 493
+  type: file
+  name: .executable
+  contents: "# contents of .executable\n"
+  perm: 493
 .file:
-    type: file
-    name: .file
-    contents: |
-        # contents of .file
-    perm: 420
+  type: file
+  name: .file
+  contents: "# contents of .file\n"
+  perm: 420
 .private:
-    type: file
-    name: .private
-    contents: |
-        # contents of .private
-    perm: 384
+  type: file
+  name: .private
+  contents: "# contents of .private\n"
+  perm: 384
 .readonly:
-    type: file
-    name: .readonly
-    contents: |
-        # contents of .readonly
-    perm: 292
+  type: file
+  name: .readonly
+  contents: "# contents of .readonly\n"
+  perm: 292
 .symlink:
-    type: symlink
-    name: .symlink
-    linkname: .dir/subdir/file
+  type: symlink
+  name: .symlink
+  linkname: .dir/subdir/file
 .template:
-    type: file
-    name: .template
-    contents: |
-        key = value
-    perm: 420
+  type: file
+  name: .template
+  contents: |
+    key = value
+  perm: 420
 -- golden/dump.yaml --
 .create:
-    type: file
-    name: .create
-    contents: |
-        # contents of .create
-    perm: 420
+  type: file
+  name: .create
+  contents: "# contents of .create\n"
+  perm: 420
 .dir:
-    type: dir
-    name: .dir
-    perm: 493
+  type: dir
+  name: .dir
+  perm: 493
 .dir/file:
-    type: file
-    name: .dir/file
-    contents: |
-        # contents of .dir/file
-    perm: 420
+  type: file
+  name: .dir/file
+  contents: "# contents of .dir/file\n"
+  perm: 420
 .dir/subdir:
-    type: dir
-    name: .dir/subdir
-    perm: 493
+  type: dir
+  name: .dir/subdir
+  perm: 493
 .dir/subdir/file:
-    type: file
-    name: .dir/subdir/file
-    contents: |
-        # contents of .dir/subdir/file
-    perm: 420
+  type: file
+  name: .dir/subdir/file
+  contents: "# contents of .dir/subdir/file\n"
+  perm: 420
 .empty:
-    type: file
-    name: .empty
-    contents: ""
-    perm: 420
+  type: file
+  name: .empty
+  contents: ""
+  perm: 420
 .executable:
-    type: file
-    name: .executable
-    contents: |
-        # contents of .executable
-    perm: 493
+  type: file
+  name: .executable
+  contents: "# contents of .executable\n"
+  perm: 493
 .file:
-    type: file
-    name: .file
-    contents: |
-        # contents of .file
-    perm: 420
+  type: file
+  name: .file
+  contents: "# contents of .file\n"
+  perm: 420
 .private:
-    type: file
-    name: .private
-    contents: |
-        # contents of .private
-    perm: 384
+  type: file
+  name: .private
+  contents: "# contents of .private\n"
+  perm: 384
 .readonly:
-    type: file
-    name: .readonly
-    contents: |
-        # contents of .readonly
-    perm: 292
+  type: file
+  name: .readonly
+  contents: "# contents of .readonly\n"
+  perm: 292
 .symlink:
-    type: symlink
-    name: .symlink
-    linkname: .dir/subdir/file
+  type: symlink
+  name: .symlink
+  linkname: .dir/subdir/file
 .template:
-    type: file
-    name: .template
-    contents: |
-        key = value
-    perm: 420
+  type: file
+  name: .template
+  contents: |
+    key = value
+  perm: 420
diff --git a/internal/cmd/testdata/scripts/external.txtar b/internal/cmd/testdata/scripts/external.txtar
index a6b8bdd8000..c39b292f394 100644
--- a/internal/cmd/testdata/scripts/external.txtar
+++ b/internal/cmd/testdata/scripts/external.txtar
@@ -163,12 +163,12 @@ chhome home16/user
     url: {{ env "HTTPD_URL" }}/.file
     checksum:
         size: 20
-        md5: 49fe9018f97349cdd0a0ac7b7f668b05
-        ripemd160: 2320636f6e74656e7473206f66202e66696c650a9c1185a5c5e9fc54612808977ee8f548b2258d31
-        sha1: cb91d72dc73f6d984b33ac5745f1cf6f76745bd2
-        sha256: 634a4dd193c7b3b926d2e08026aa81a416fd41cec52854863b974af422495663
-        sha384: f8545bb66433eb514727bbc61c4e4939c436d38079767f39f12b8803d6472ca1dfcd101675b20cd525f7e3d02c368b61
-        sha512: a68814ec3d16e8bd28c9291bbc596f0282687c5ba5d1f4c26c4e427166666a03c11df1dab3577b4483142764c37d4887def77244c4a52cb9852a234fa8cb15ba
+        md5: "49fe9018f97349cdd0a0ac7b7f668b05"
+        ripemd160: "2320636f6e74656e7473206f66202e66696c650a9c1185a5c5e9fc54612808977ee8f548b2258d31"
+        sha1: "cb91d72dc73f6d984b33ac5745f1cf6f76745bd2"
+        sha256: "634a4dd193c7b3b926d2e08026aa81a416fd41cec52854863b974af422495663"
+        sha384: "f8545bb66433eb514727bbc61c4e4939c436d38079767f39f12b8803d6472ca1dfcd101675b20cd525f7e3d02c368b61"
+        sha512: "a68814ec3d16e8bd28c9291bbc596f0282687c5ba5d1f4c26c4e427166666a03c11df1dab3577b4483142764c37d4887def77244c4a52cb9852a234fa8cb15ba"
 -- home11/user/.local/share/chezmoi/.chezmoiexternal.toml --
 [".file"]
     type = "file"
diff --git a/internal/cmd/testdata/scripts/initconfig.txtar b/internal/cmd/testdata/scripts/initconfig.txtar
index bf8319c210a..64ddafa2ccc 100644
--- a/internal/cmd/testdata/scripts/initconfig.txtar
+++ b/internal/cmd/testdata/scripts/initconfig.txtar
@@ -110,16 +110,24 @@ exists $HOME/.chezmoi/chezmoistate.boltdb
 
 -- golden/chezmoi1.yaml --
 data:
-    email: "mail1@example.com"
+  email: "mail1@example.com"
 -- golden/chezmoi2.yaml --
 data:
-    email: "mail2@example.com"
+  email: "mail2@example.com"
 -- golden/chezmoi3.toml --
 [data]
-    email = "mail3@example.com"
+  email = "mail3@example.com"
 -- golden/error1.log --
 chezmoi: multiple config files: $CHEZMOICONFIGDIR/chezmoi.toml and $CHEZMOICONFIGDIR/chezmoi.yaml
 -- golden/error2.log --
-chezmoi: invalid config: $HOME/.chezmoi/athome.yaml: yaml: unmarshal errors: line 1: cannot unmarshal !!seq into map[string]interface {}
+chezmoi: invalid config: $WORK/home2/user/.chezmoi/athome.yaml: [2:3] value is not allowed in this context
+   1 | [data]
+>  2 |   email = "mail3@example.com"
+         ^
+
 -- golden/error3.log --
-chezmoi: invalid config: $HOME/.chezmoi/athome.txt: yaml: unmarshal errors: line 1: cannot unmarshal !!seq into map[string]interface {}
+chezmoi: invalid config: $WORK/home3/user/.chezmoi/athome.txt: [2:3] value is not allowed in this context
+   1 | [data]
+>  2 |   email = "mail3@example.com"
+         ^
+
diff --git a/internal/cmd/testdata/scripts/issue2695.txtar b/internal/cmd/testdata/scripts/issue2695.txtar
index 2c5f18692dd..0d5302bf0d3 100644
--- a/internal/cmd/testdata/scripts/issue2695.txtar
+++ b/internal/cmd/testdata/scripts/issue2695.txtar
@@ -27,7 +27,7 @@ stderr 'invalid config'
 
 # test that chezmoi doctor warns about invalid YAML config files
 ! exec chezmoi doctor
-stdout 'error\s+config-file\s+.*unmarshal errors'
+stdout 'error\s+config-file\s+.*string was used where mapping is expected'
 
 -- home/user/.config/chezmoi/chezmoi.json --
 {
diff --git a/internal/cmd/testdata/scripts/managed.txtar b/internal/cmd/testdata/scripts/managed.txtar
index d6c2790bde1..14e9e785986 100644
--- a/internal/cmd/testdata/scripts/managed.txtar
+++ b/internal/cmd/testdata/scripts/managed.txtar
@@ -146,25 +146,25 @@ $WORK/home/user/.template
 }
 -- golden/managed-all.yaml --
 .create:
-    absolute: $WORK/home2/user/.create
-    sourceAbsolute: $WORK/home2/user/.local/share/chezmoi/create_dot_create.tmpl
-    sourceRelative: create_dot_create.tmpl
+  absolute: $WORK/home2/user/.create
+  sourceAbsolute: $WORK/home2/user/.local/share/chezmoi/create_dot_create.tmpl
+  sourceRelative: create_dot_create.tmpl
 .file:
-    absolute: $WORK/home2/user/.file
-    sourceAbsolute: $WORK/home2/user/.local/share/chezmoi/modify_dot_file.tmpl
-    sourceRelative: modify_dot_file.tmpl
+  absolute: $WORK/home2/user/.file
+  sourceAbsolute: $WORK/home2/user/.local/share/chezmoi/modify_dot_file.tmpl
+  sourceRelative: modify_dot_file.tmpl
 .symlink:
-    absolute: $WORK/home2/user/.symlink
-    sourceAbsolute: $WORK/home2/user/.local/share/chezmoi/symlink_dot_symlink.tmpl
-    sourceRelative: symlink_dot_symlink.tmpl
+  absolute: $WORK/home2/user/.symlink
+  sourceAbsolute: $WORK/home2/user/.local/share/chezmoi/symlink_dot_symlink.tmpl
+  sourceRelative: symlink_dot_symlink.tmpl
 .template:
-    absolute: $WORK/home2/user/.template
-    sourceAbsolute: $WORK/home2/user/.local/share/chezmoi/dot_template.tmpl
-    sourceRelative: dot_template.tmpl
+  absolute: $WORK/home2/user/.template
+  sourceAbsolute: $WORK/home2/user/.local/share/chezmoi/dot_template.tmpl
+  sourceRelative: dot_template.tmpl
 script:
-    absolute: $WORK/home2/user/script
-    sourceAbsolute: $WORK/home2/user/.local/share/chezmoi/run_script.tmpl
-    sourceRelative: run_script.tmpl
+  absolute: $WORK/home2/user/script
+  sourceAbsolute: $WORK/home2/user/.local/share/chezmoi/run_script.tmpl
+  sourceRelative: run_script.tmpl
 -- golden/managed-exclude-encrypted --
 .create
 .dir
diff --git a/internal/cmd/testdata/scripts/state.txtar b/internal/cmd/testdata/scripts/state.txtar
index d757f9ee3c9..35fd30849aa 100644
--- a/internal/cmd/testdata/scripts/state.txtar
+++ b/internal/cmd/testdata/scripts/state.txtar
@@ -21,4 +21,4 @@ cmp stdout golden/data-after-delete.yaml
 bucket: {}
 -- golden/data.yaml --
 bucket:
-    key: value
+  key: value
diff --git a/internal/cmds/execute-template/main.go b/internal/cmds/execute-template/main.go
index c38b332a648..4375c22fb0b 100644
--- a/internal/cmds/execute-template/main.go
+++ b/internal/cmds/execute-template/main.go
@@ -16,9 +16,9 @@ import (
 	"text/template"
 
 	"github.com/Masterminds/sprig/v3"
+	"github.com/goccy/go-yaml"
 	"github.com/google/go-github/v68/github"
 	"github.com/google/renameio/v2/maybe"
-	"gopkg.in/yaml.v3"
 
 	"github.com/twpayne/chezmoi/v2/internal/chezmoi"
 )
diff --git a/internal/cmds/generate-install.sh/main.go b/internal/cmds/generate-install.sh/main.go
index 4411f616abf..a7b77eba8e3 100644
--- a/internal/cmds/generate-install.sh/main.go
+++ b/internal/cmds/generate-install.sh/main.go
@@ -10,7 +10,7 @@ import (
 	"slices"
 	"text/template"
 
-	"gopkg.in/yaml.v3"
+	"github.com/goccy/go-yaml"
 
 	"github.com/twpayne/chezmoi/v2/internal/chezmoiset"
 )