NAME
File::SOPS::Format::YAML - YAML format handler for SOPS
VERSION
version 0.003
SYNOPSIS
use File::SOPS::Format::YAML;
# Parse YAML with SOPS metadata
my ($data, $metadata) = File::SOPS::Format::YAML->parse($yaml_content);
# Serialize data with SOPS metadata
my $yaml = File::SOPS::Format::YAML->serialize(
data => $encrypted_data,
metadata => $metadata_obj,
);
# Check if filename is YAML
if (File::SOPS::Format::YAML->detect('secrets.yaml')) {
# It's a YAML file
}
DESCRIPTION
YAML format handler for File::SOPS. Handles parsing and serialization of SOPS-encrypted YAML files.
Uses YAML::XS for fast, spec-compliant YAML processing.
Booleans are round-tripped as JSON::PP::Boolean objects, by setting YAML::XS's $YAML::XS::Boolean mode to 'JSON::PP'. That is the class JSON::MaybeXS blesses booleans into on every one of its backends, so a true loaded from YAML and a true decoded from JSON are the same kind of object throughout this distribution, and both are emitted as bare true / false rather than degrading to 1 / 0 on the next write.
The mode is set with local around this module's own Load and Dump calls. $YAML::XS::Boolean is a process global that changes what YAML::XS does for every other user of it in the same interpreter, so before 0.003 merely loading File::SOPS changed how unrelated code parsed YAML.
Multi-document YAML
A YAML stream with more than one document is refused. "parse" dies rather than returning part of it.
This is a restriction, not a preference: it replaces silent data loss. Until 0.003 the stream was loaded in scalar context, which yields only the last document, so a: 1\n---\nb: 2 parsed to {b => 2} and "encrypt_file" in File::SOPS wrote that back as the entire file. Every document but the last disappeared with no error.
sops itself does support multi-document YAML, so this is a gap to close rather than a rule to keep. Its model, measured against sops 3.13.3, is not "several independent files in one":
One metadata section for the whole stream, written into every document -- the same age blob,
lastmodifiedandmacbyte for byte. On read it is taken from the first document; a stream carrying it only in a later document is rejected withsops metadata not found.One MAC spanning all documents, in order. Removing a document or swapping two of them fails verification.
The AAD carries no document index. A given key path has the same AAD in every document, so a value encrypted in one document decrypts in another document's slot at that path.
Documents are joined by
---; a leading separator is dropped, while a trailing one is preserved as a real (empty) document. An empty document anywhere is a document: it gets its own metadata block and reads back as{}.Every document must be a mapping. sops rejects a top-level sequence or scalar itself (
YAML documents that are sequences are not supported), which is the same rule as the HashRef check in "parse".
Supporting that means one tree of N branches with a shared metadata and a digest spanning all of them, which reaches into encryption, MAC computation and the shape of the public API -- not this parser alone.
parse
my ($data, $metadata) = File::SOPS::Format::YAML->parse($yaml_string);
Class method to parse a YAML string.
Returns a two-element list:
- 1.
$data- HashRef of the data (without thesopssection) - 2.
$metadata- File::SOPS::Metadata object, orundefif nosopssection
Dies if the YAML is invalid, doesn't parse to a HashRef, or contains more than one document. See "Multi-document YAML".
Dies too if the document has a top-level sops entry that is not a mapping -- sops: mine, a list, or an explicit null. Until 0.003 that entry was deleted from the tree and reported as no metadata at all, so "encrypt_file" in File::SOPS wrote the document back without it. See "from_hash" in File::SOPS::Metadata, which is where the refusal lives.
A literal that overflows a double comes back as a string
1e400, a 401-digit integer, and the bare spellings Inf, inf, INF, Infinity, NaN, nan, NAN, -Inf and +Inf are all resolved to a number by libyaml, and the number each of them lands on is +Inf, -Inf or NaN. go-yaml -- the parser sops reads a document with -- resolves none of them: strconv.ParseFloat answers ErrRange and it keeps a string. So sops writes type:str for such a leaf and digests the literal's own text.
Since 0.003 this method hands back the string go-yaml sees, so that "detect_type" in File::SOPS::Encrypted and the MAC agree with sops. Before it, such a document could be neither read (17 of 20 measured documents that sops writes failed MAC verification here) nor written (all 20 hit the non-finite refusal in "assert_representable" in File::SOPS::Encrypted).
Three things this is not. It does not touch the twelve spellings go-yaml really does resolve to a non-finite float -- .inf .Inf .INF +.inf +.Inf +.INF -.inf -.Inf -.INF .nan .NaN .NAN -- because YAML::XS returns every one of those as a plain string with no numeric half at all; those have a repair of their own since 0.003, and a different one, described in "A plain infinity comes back as the float go-yaml reads". It does not loosen "assert_representable" in File::SOPS::Encrypted: a caller who passes "encrypt" in File::SOPS a real 9**9**9 still gets the refusal, because what is repaired here is a parse result and not a rule about values. And it does not apply to File::SOPS::Format::JSON, where sops refuses the equivalent document itself, at unmarshal time.
The cost is that the leaf is written back quoted -- v: '1e400' where sops writes v: 1e400. Both are a string to both parsers, so the digest is the same text either way. See docs/adr/0023.
A plain infinity comes back as the float go-yaml reads
The mirror image of the section above, and it needs a different authority. YAML::XS leaves .inf a string; go-yaml resolves it to the float +Inf, and sops digests +Inf. So a document sops writes and sops -d verifies used to fail MAC verification here, with an error that named nothing.
Since 0.003 a leaf the document wrote as a plain scalar, and whose token is one of the twelve above, comes back as a "dualvar" in Scalar::Util: the float go-yaml resolved, carrying the document's own token as its text. So "value_to_bytes" in File::SOPS::Encrypted derives +Inf / -Inf / NaN -- the bytes sops put in the MAC -- while every emitter writes the file back exactly as it read it, and "decrypt_file" in File::SOPS reproduces sops's own plaintext byte for byte.
The word "plain" is the whole of it, and it is why this repair is not the one above. Measured against sops 3.13.3, in one document:
list_unencrypted:
- .inf # go-yaml: a float, digested +Inf
- ".inf" # go-yaml: a string, digested .inf
Both spellings arrive here as the same YAML::XS string, and the two have different digests. Anything that decided from the leaf's text would fix the first element and silently break the second, which reads correctly today. So the document is asked instead: YAML::PP, already this distribution's second parser, resolves such a scalar to a non-finite number exactly when it was written plain, and only a token that is already in this module's model of go-yaml is repaired at all. If YAML::PP refuses the document, or the two parse trees disagree about its shape, nothing is repaired.
This runs on every document, plaintext included, and the plaintext half is newer than the rest. Until 0.003 it was gated on the sops: section, on the argument that a plaintext has no MAC for anyone to disagree with; that gate is gone, because the document such a plaintext turns into can now be written (see below), so the gate was refusing a document this module produces. sops resolves a plain scalar the same way on every parse it makes, and so does this.
Such a document is written back since 0.003: an unencrypted YAML slot holding one of the twelve tokens reaches the file as that token, with the digest covering +Inf / -Inf / NaN, which is what sops writes and what sops digests for the same document. An encrypted slot is still refused -- the wire form there is type:float with the plaintext +Inf, which "encrypt_value" in File::SOPS::Encrypted refuses because it cannot see which format is being written (k122).
See docs/adr/0026, docs/adr/0031 and docs/adr/0034.
A plain lastmodified in the sops section is warned about
The read-side mirror of what "serialize" does on the way out. sops writes lastmodified: "2026-08-21T09:05:08Z" quoted, and so does this distribution, because go-yaml resolves a bare RFC3339 scalar to a Go time.Time where sops's own decoder wants a string. Measured against sops 3.13.3:
lastmodified: "2026-08-21T09:05:08Z" sops -d exit 0
lastmodified: 2026-08-21T09:05:08Z sops -d exit 1
'lastmodified' expected type 'string', got unconvertible type
'time.Time'
This method reads both, and since 0.003 it carps on the second: the values and the MAC are unaffected, but the file is one no sops can open. It is not refused, because every write path here re-stamps and quotes the timestamp -- so rotate, edit, encrypt_in_place and decrypt_file plus encrypt_file all turn such a document into one sops -d reads at exit 0, and a refusal would take that away and leave the file unopenable by every tool.
The warning cannot fire on a document sops reads. All 15 RFC3339 spellings sops accepts quoted were measured refused bare, and a tagged plain scalar -- !!str 2026-08-21T09:05:08Z, where the explicit tag stops go-yaml's implicit resolver -- is read by sops at exit 0 and is not warned about here.
The question is plain or quoted, which is a fact about the source bytes and not about the value: both spellings arrive at "from_hash" in File::SOPS::Metadata as the same Perl string, which is why this lives here and not there. Unlike the infinity above, the answer does not come from a resolver -- YAML 1.2's Core schema has no timestamp type, so YAML::PP reads both spellings as the same string too -- but from YAML::PP's parser, asked for one scalar's style. A document that parser will not read is not warned about. See docs/adr/0050 and docs/adr/0044.
A merge key keeps its <<, and loses its !!merge tag
sops does not expand a YAML merge key. It unmarshals into a yaml.Node tree, where go-yaml resolves no merges, so << survives as an ordinary mapping key -- and go-yaml's emitter writes the tag its resolver assigned back out explicitly:
derived:
!!merge <<:
x: ENC[AES256_GCM,...,type:int]
"y": ENC[AES256_GCM,...,type:int]
YAML::XS accepts !!str, !!int and !!float on a scalar and dies on every other tag, so before 0.003 such a document could not be opened here at all: bad tag found for scalar: 'tag:yaml.org,2002:merge', a parse error rather than a MAC error, on a file sops -d reads at exit 0. Nor was it only sops's own documents -- measured, one sops rotate -i on a document File::SOPS had written with a << key added the tag, and File::SOPS could no longer read its own output.
Since 0.003 the tag is dropped from the text and the parse retried. Only that tag, only after YAML::XS has already refused the document, and only when YAML::PP's parser confirms the substitution removed exactly as many merge-tagged scalars as the document really contains; otherwise nothing is retried and libyaml's own error stands. A document that parses today therefore takes the identical path it always did.
Nothing about the value layer changes, which is why this is a parser repair and not a wire one. Measured against sops 3.13.3 across five documents, one per position sops writes the tag in: << is a path component in the AAD (a leaf under it authenticates as derived:<<:x: and under nothing else) and a member of the MAC digest with its whole subtree, in the document's own order. No AAD path, no digest byte and no emitted byte moves.
Two things this is not. It does not make YAML::XS resolve the merge: <<: *b still gives a literal << key holding the aliased mapping, with nothing folded into the parent -- which is what makes the round trip correct, because sops does not fold it either. And it does not put the tag back on the way out; this module writes the plain <<: spelling, which go-yaml re-tags for itself (measured, sops -d on a document we wrote as <<: prints !!merge <<:).
A merge tag this cannot see -- flow style, or a %TAG-directive spelling -- is refused as it is today. The other tags YAML::XS rejects are covered by the section below; sops was not measured to write any of them into an encrypted document. See docs/adr/0028.
An explicit type tag on a scalar
YAML::XS accepts exactly three tags on a scalar -- !!str, !!int and !!float -- and dies on every other one. sops accepts them all, resolves them, and writes the resolved value with the tag gone, so a hand-written plaintext that sops -e encrypts at exit 0 could not be opened here.
Since 0.003, !!bool on a plain true or false is dropped from the text and the parse retried, by the same count-reconciled mechanism as !!merge above and under the same conditions: only after YAML::XS has already refused the document, and only when YAML::PP's parser confirms the substitution removed exactly as many such scalars as the document really contains. That tag is the only one here that carries nothing. Measured against sops 3.13.3 with the stored mac: decrypted out of each document, !!bool true and a bare true produce byte-identical output -- type:bool, plaintext True, and the same MAC digest.
Every other tag here carries a type, and in this distribution the type comes from the scalar rather than from the document's text, so removing one would retype the leaf. Those are refused, naming the tag, the leaf's key path, and what sops resolves the tag to -- in place of libyaml's bad tag found for scalar, which reads like a defect in a foreign library. The scalar's own text is never quoted back; it is plaintext. Measured:
!!binary aGVsbG8= sops base64-DECODES it -> the value `hello`
!!timestamp 2026-08-21 sops re-renders it -> `2026-08-21T00:00:00Z`, type:time
!!bool True sops resolves it bool -> a string to libyaml
!!null Null sops resolves it null -> a string to libyaml
!!value 1 sops keeps the text -> the integer 1 to libyaml
!!null on ~, null or an empty scalar has always been read and still is. A tag YAML::XS accepts but cannot resolve -- !!int 0x10, !!int 1_000, !!float .inf, all of which sops resolves -- keeps libyaml's own message, because that is a resolver disagreement rather than a tag this module refuses. See docs/adr/0032.
A comment inside a list is kept
sops attaches a YAML comment to the node that follows it. Above a mapping key the comment stays a comment -- #ENC[...,type:comment] on a line of its own, which YAML::XS discards and sops does not hash, so such a document reads correctly here and always has. Above an entry of a sequence there is no comment line to write, so sops emits the comment as a real element:
list:
- ENC[AES256_GCM,...,type:comment]
- ENC[AES256_GCM,...,type:str]
This method hands that element on like any other. "decrypt" in File::SOPS turns it into a File::SOPS::Comment (see File::SOPS::Comment), keeps it at its index, leaves it out of the MAC digest -- which is what sops does with it -- and writes it back as a type:comment element. Before 0.003 the comment was read as an ordinary list element: strict mode failed MAC verification, and ignore_mac => 1 returned a list with the comment's text in it as a silent extra string, which a decrypt plus encrypt cycle then made a permanent value. See docs/adr/0041, which supersedes docs/adr/0024.
What is still refused is a comment this emitter would have to write as plain text: "emit" croaks on a File::SOPS::Comment leaf, naming its path, because YAML::XS has no way to write a comment at all and would not read one back either. That is what makes decrypt_file and edit refuse a document with a comment in it, rather than dropping the comment on the way through. A comment in a mapping value slot is refused too, by File::SOPS and in both formats, because no SOPS store writes that shape.
A comment above a mapping key -- and one on the file's first line, which sops writes the same way -- is still lost on a read here, as it always has been: YAML::XS discards it before this method sees a tree, and nothing here can write one back. That is the open half of k76, filed as k148, and it is not a lane handoff but a wall: YAML::XS is libyaml, whose emitter cannot write a comment at all, and YAML::PP's emitter has no comment event either -- its own documentation lists comment-preserving round trips as a TODO. The read half is nearer than that reads: YAML::PP's parser keeps the comment text in its raw token stream, so the line can be seen. But there is nowhere to put it. A sequence comment is preserved because a sequence element is a slot the tree model already has; a mapping-position comment sits before a key, and a Perl hash has no slot before a key.
The two positions therefore behave differently, and the difference is measured: a document whose comment is a sequence element is refused by "emit" -- which is what makes "decrypt_file" in File::SOPS and "edit" in File::SOPS refuse it rather than drop it -- while a document whose only comment is above a mapping key is written back silently without it. sops -d returns both comments intact for both documents.
parse_in_document_order
my $ordered = File::SOPS::Format::YAML->parse_in_document_order($content);
Reparses $content for its key order only and returns its mappings iterating in the order the file writes them, with the sops section removed. A single document comes back as a HashRef; a multi-document stream comes back as an ArrayRef of such HashRefs, one per document in document order (docs/adr/0033). Returns nothing when the text cannot be read that way.
This is the format half of the MAC's order recovery (docs/adr/0001), and File::SOPS asks the handler that parsed the document rather than reaching for a parser of its own (docs/adr/0036). What comes back supplies order and shape; the values are never looked at, so the loader used here does not have to agree with YAML::XS about how a scalar resolves -- only about where the mappings, sequences and leaves are.
Declining is safe and losing the order is not an error: the caller then hashes in sorted key order, which can make verification fail but can never make it wrongly succeed. The stream is read in list context, matching "parse", so document i's order is paired with document i's values -- the scalar-context trap docs/adr/0033 names (YAML::PP yielding the first document where YAML::XS yields the last) cannot arise.
serialize
my $yaml = File::SOPS::Format::YAML->serialize(
data => \%data, # one document
metadata => $metadata_obj,
);
my $stream = File::SOPS::Format::YAML->serialize(
data => [ \%doc1, \%doc2 ], # a multi-document stream
metadata => $metadata_obj,
);
Class method to serialize data and metadata to YAML.
The data parameter is a HashRef for a single document, or an ArrayRef of HashRefs for a multi-document stream (docs/adr/0033, k31); a one-element ArrayRef is byte-identical to the bare HashRef. The metadata parameter must be a File::SOPS::Metadata object.
For a stream, the same metadata section is written into every document, byte-identical (same age blob, lastmodified and mac) -- the exact inverse of the read-side detach in "parse", which takes the metadata from the first document only. The documents are joined by ---; an empty document in the list is a real document and comes out as {} carrying only its own sops: section.
Dies if any document has a top-level sops key: that is where the metadata section is written, so the value would be overwritten. Until 0.003 it was, silently, and the resulting document failed its own MAC because the digest had already covered the discarded value.
Returns a YAML string with the sops section appended to each document.
A leaf whose YAML spelling Go's parser resolves differently is refused here, and only here. The document this method writes carries a MAC, and sops recomputes that MAC from the values gopkg.in/yaml.v3 resolves out of these bytes -- so a leaf that libyaml and Go read differently makes the file disagree with its own MAC. mode: 0755 is the realistic case: this module reads 755, Go reads 493, and the file was written silently and rejected later with MAC mismatch. Refused as well: 0o10, 0x1f, 0b101, 1_000, Null, TRUE and a date that is not already exactly RFC3339 -- all spellings libyaml leaves a string and Go resolves to something else. 007, 08, 1e3, True, null, yes, 1:30 and 2015-01-01T12:00:00Z are not refused: measured, the two resolvers derive the same digest bytes from each of them. .inf and .nan are no longer refused either -- since docs/adr/0070 they belong to the nine-leaf safe set this emitter force-quotes instead (see below).
Where the refused leaf is already a string, fifteen of these spellings stay refused as a limitation this distribution states; the seven parse-unambiguous non-finite ones are the exception and are written double-quoted. sops does not resolve a string away: given the string ".inf", "1_000" or "2015-01-01" it writes it double-quoted and reads it back -- measured against sops 3.13.3, 22 such spellings, sops -d exit 0 for every one, and this module reads all 22 of those documents correctly, in both slots, with the MAC verified. For fifteen of them it cannot write the leaf: a bare 2015-01-01 and a quoted "2015-01-01" arrive as the same Perl string, while sops writes 2015-01-01T00:00:00Z for the first and "2015-01-01" for the second, so quoting a leaf whose source may have been bare would turn a loud refusal into a silent value divergence. Those fifteen stay refused, and the message names the two remedies measured to work for them -- encrypt the leaf, or write the document as JSON. The other seven -- the non-finite str spellings .inf, .Inf, .INF, +.inf, -.inf, .nan and .NaN -- parse unambiguously: a bare one is resolved to a float at parse (docs/adr/0026), so a leaf still holding the string can only have come from a quoted source or a caller's own Perl string, and double-quoting it states the type it already has. Since docs/adr/0070 this emitter writes those seven double-quoted through a fail-closed sentinel substitution -- the same nine-leaf safe set as True and False below -- byte-identical to what sops writes. See docs/adr/0039, k135, docs/adr/0070 and k99.
A True or False string is written double-quoted where the MAC covers it, and warned about where it does not. The digest bytes agree -- sops renders a boolean Title-cased, which is the same text this module derives from the string -- so the MAC holds and sops -d exits 0. What differs is the type: YAML::XS writes the string as a bare True because libyaml's resolver knows only true and false, and Go's yaml.v3 reads a boolean out of it. Measured, sops 3.13.3: from a bare True sops -d hands the value on as true, and sops rotate, sops set and sops edit each rewrite the leaf to a bare true, after which this module reads a JSON::PP::Boolean where the caller put a string. Until docs/adr/0019 this was a carp in both MAC modes; since docs/adr/0070 the MAC-covered path double-quotes the leaf instead -- through the same fail-closed sentinel substitution as the seven non-finite spellings above -- so it stays a string on both sides and a sops write-back keeps it (sops -e "True" writes "True"). In a mac_only_encrypted document the leaf is not MAC-covered and the document already works, so the safe-set force-quoting is deliberately not run there and the carp remains: its message names the two remedies -- encrypt the leaf, or write the document as JSON, where every string is quoted. Neighbours that look like this one do not warn or quote, because measured they do not diverge: Yes, No, on, off, y, n and the rest of YAML 1.1's boolean family are strings to yaml.v3 and to libyaml alike, ~ and null are written quoted, and an RFC3339 timestamp -- a string here and a time.Time to Go -- comes back from sops rotate as the identical token. See docs/adr/0019, k92, docs/adr/0070 and k99.
The refuse-or-warn rule does not apply to an encrypted slot (an ENC[...] string carries any spelling verbatim), to "emit" on its own (a plaintext document has no MAC for a reader to disagree with -- though the nine-leaf safe-set force-quoting still runs there, so that decrypt_file and edit write what sops writes, docs/adr/0071; see "emit"), or to the sops metadata section (the digest does not cover it). See docs/adr/0013 and k86.
In a mac_only_encrypted document the same leaf is warned about rather than refused. There the digest covers encrypted values only, so an unencrypted leaf cannot make the document disagree with its own MAC -- measured, the same mode_unencrypted: 0755 is sops -d exit 0 with the flag set. What remains is that sops reads 493 out of it where this module reads 755, in a file neither of them complains about, so the check runs and carps instead of refusing: the document is written exactly as before. The warning names the leaf's key path and never the value. Silence it with a local $SIG{__WARN__} if the divergence is known and accepted. Measured over 217 such documents: 66 warn, all 66 really do diverge, none is refused, and 0 warn about a leaf the two implementations agree on. See docs/adr/0018 and k87.
emit
my $yaml = File::SOPS::Format::YAML->emit(\%data);
Class method to emit a data structure as YAML, with no sops section and no metadata of any kind -- a plain document. This is what "decrypt_file" in File::SOPS writes and what "edit" in File::SOPS hands to the editor.
Returns UTF-8 encoded bytes, unconditionally: YAML::XS encodes regardless of whether the strings it is given carry Perl's UTF-8 flag. See "Character encoding" in File::SOPS.
The output begins with the document-start marker ---, because YAML::XS always emits one. sops writes none, in either direction. The line is cosmetic -- YAML resolves a document with or without it identically, sops -d accepts these files, and the MAC covers values rather than serialized text -- and it is kept rather than stripped, since the MAC's encrypt side rides on this emitter (docs/adr/0001). See "Every YAML file starts with ---, where sops writes none" in File::SOPS and k83.
Called on its own -- which is what the plaintext emitters do -- it writes most YAML spellings unchanged, 0755 and 2015-01-01 included, and a bare type:float .inf stays bare. What it does not pass through untouched is the safe set "serialize" force-quotes: a True or False string and the seven parse-unambiguous non-finite str spellings (.inf, .Inf, .INF, +.inf, -.inf, .nan, .NaN) are double-quoted here too. That is what sops itself writes for those leaves, so decrypt_file and edit are faithful inverses of it: a decrypt then re-encrypt round trip no longer flips such a leaf from string to float or bool (a bare .inf would resolve to +Inf at the next parse). It reuses ADR 0070's fail-closed sentinel mechanism unchanged. See docs/adr/0071 and k186.
The refuse-or-warn guard "serialize" installs against the rest of the divergent class is deliberately still not here: a plaintext document carries no MAC for a reader to disagree with, and refusing them would refuse to write out documents this module reads correctly. "serialize" turns it on with one of the two arguments this method takes beyond the tree -- mac_covered => 1 to refuse such a leaf, or warn_foreign_resolution => 1 to warn about it, which is what a mac_only_encrypted document gets. Force-quoting the safe set runs on every path except the warn one, so a True/False leaf is quoted under mac_covered and on this plaintext path but still reaches the guard, and is warned about, under warn_foreign_resolution. See "serialize".
"serialize" is this method plus the metadata section, so both go through the same emitter options rather than two copies of them. Those options are not cosmetic -- sorted key emission is what the MAC's encrypt side relies on -- so a change here moves the encrypted document as well as the plaintext one.
Floats are written in a form that parses back to the same double. YAML::XS renders a float by Perl stringification, roughly 15 significant digits, while the MAC digest covers the shortest decimal that round-trips -- up to 17. For a value needing 16 or 17 the document stated one number and the digest another, and the file failed its own verification. This method now reparses its own output and substitutes the canonical decimal from "value_to_bytes" in File::SOPS::Encrypted only where the value does not survive, so a float that already emitted faithfully keeps exactly the bytes it had. In practice that is most of them: YAML::XS retains the text of every float it parsed, so only bare NVs -- computed by the caller, or parsed out of JSON -- are ever rewritten. NaN and Inf are unchanged -- they have no YAML form Go reads back, and "assert_representable" in File::SOPS::Encrypted refuses them on the encrypt path. A negative zero is rewritten, and it is the one value whose written decimal is not "value_to_bytes" in File::SOPS::Encrypted's output verbatim: that is -0, which YAML resolves as an integer and every reader digests as 0, so this emitter writes -0.0 instead -- the spelling measured to read back as the same double in sops 3.13.3 and here. See docs/adr/0006.
An integer leaf whose string form contradicts its number is refused. YAML::XS writes the string half bare -- five for a Scalar::Util::dualvar of 5 -- while "detect_type" in File::SOPS::Encrypted calls the leaf an int, so the MAC digest covers the number. The document and its own MAC then state different things: measured against sops 3.13.3, sops -d exit 51. A source spelling is not refused here, and that is measured too -- a 007, +7, -0 or 1e3 this emitter received from a YAML parse is written back exactly as it came, and Go reads the same number the digest covers (exit 0), where File::SOPS::Format::JSON has to refuse them because it quotes them. The refusal names the leaf's key path and neither half of the value; an encrypted slot is unaffected. See k84 and docs/adr/0012.
A reference as a leaf value is refused, with one exception. YAML::XS writes a blessed reference as a Perl-specific !!perl/ tagged structure -- !!perl/hash:Math::BigFloat, !!perl/scalar:Foo, !!perl/regexp -- and an unblessed one as !!perl/ref or !!perl/code, while "detect_type" in File::SOPS::Encrypted calls the leaf str so the MAC digest covers its stringification. The document and its own MAC then state different things, and the file is unreadable to sops and to this module alike. Until 0.003 it was written anyway, without a word; now it dies naming the class (never the value).
The exception is an exact JSON::PP::Boolean, which this emitter writes as bare true / false -- the one reference whose document form and digest agree. A subclass of it is not covered: detect_type calls it bool but YAML::XS writes it as a tag, so it is refused like any other object.
Only leaves that reach the document verbatim can trigger this -- values excluded by the encryption rules, everything in a plaintext document, and the sops section. A value that gets encrypted is an ENC[...] string by the time this method sees it, so an object in an encrypted slot is unaffected and still stores its stringification as type:str. File::SOPS::Format::JSON has refused the same leaves all along; see docs/adr/0008.
format_name
Returns 'yaml'.
file_extensions
Returns a list of file extensions: ('yaml', 'yml').
detect
if (File::SOPS::Format::YAML->detect($filename)) {
# File is YAML based on extension
}
Class method to detect if a filename is YAML based on extension.
Returns true if filename ends with .yaml or .yml (case-insensitive).
SEE ALSO
File::SOPS - Main SOPS interface
YAML::XS - YAML parser/serializer
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-file-sops/issues.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.