FiloSottile/age · error
invalid first line: %q
Error message
invalid first line: %q
What it means
The age ASCII armor reader requires the very first non-whitespace line of the armored input to be exactly the Header constant ("-----BEGIN AGE ENCRYPTED FILE-----"). If the first line differs in any way, Read returns this error, indicating the input is not age-armored data or is corrupt.
Source
Thrown at armor/armor.go:146
return io.EOF
}
var removedWhitespace int
for !r.started {
line, err := getLine()
if err != nil {
return 0, r.setErr(err)
}
// Ignore leading whitespace.
if len(bytes.TrimSpace(line)) == 0 {
removedWhitespace += len(line) + 1
if removedWhitespace > maxWhitespace {
return 0, r.setErr(errors.New("too much leading whitespace"))
}
continue
}
if string(line) != Header {
return 0, r.setErr(fmt.Errorf("invalid first line: %q", line))
}
r.started = true
}
line, err := getLine()
if err != nil {
return 0, r.setErr(err)
}
if string(line) == Footer {
return 0, r.setErr(drainTrailing())
}
if len(line) == 0 {
return 0, r.setErr(errors.New("empty line in armored data"))
}
if len(line) > format.ColumnsPerLine {
return 0, r.setErr(errors.New("column limit exceeded"))
}
// Reject newline characters ignored by base64.Decode.
if bytes.ContainsAny(line, "\n\r") {View on GitHub (pinned to b74dce4cdb)
Solutions
- Ensure the input was produced with age's armor option (age -a / armor.NewWriter); plain age output has no armor header
- Check the first bytes of the input for a UTF-8 BOM or whitespace/garbage before the header and strip them
- Confirm the header reads exactly "-----BEGIN AGE ENCRYPTED FILE-----" with no edits, wrapping, or CRLF-only corruption from Windows transfer
- Verify you are not feeding a PGP-armored file ("-----BEGIN PGP MESSAGE-----"); decrypt it with the proper tool instead
- If you must accept both armored and raw input, detect the header before wrapping in armor.NewReader
Example fix
// before
f, _ := os.Open("file.age")
r, _ := armor.NewReader(f) // fails: "invalid first line" because file was not armored
// after
f, _ := os.Open("file.age")
head := make([]byte, 6)
n, _ := io.ReadFull(f, head)
f.Seek(0, 0)
var r io.Reader = f
if string(head[:n]) == "-----BEGIN AGE ENCRYPTED FILE-----" {
r = armor.NewReader(f)
} Defensive patterns
Strategy: validation
Validate before calling
func isAgeArmored(data []byte) bool {
s := strings.TrimLeft(string(data), " \t\r\n")
s = strings.TrimPrefix(s, "\ufeff") // BOM
return strings.HasPrefix(s, "-----BEGIN AGE ENCRYPTED FILE-----")
} Try / catch
r, err := armor.NewReader(f), then on err containing "invalid first line" fall back to treating input as raw age ciphertext:
if !isAgeArmored(raw) {
out, err := age.Decrypt(bytes.NewReader(raw), identities)
// handle raw path
} Prevention
- Always produce armored files with age -a or armor.NewWriter; never hand-edit the header
- Strip UTF-8 BOMs when reading files on Windows
- Use binary-safe transfer (scp/rsync with correct flags) instead of copy/paste through editors or email
- Detect armor presence before wrapping input in armor.NewReader
When it happens
Trigger: Calling Read on an armor.Reader whose underlying stream does not start with the exact header line — e.g. the file is raw (non-armored) age ciphertext, a different armor format (PGP blocks), the header was truncated, or the line has typos/extra characters like CRLF mangling or a UTF-8 BOM before the header.
Common situations: Piping output of `age` (unarmored) into an armor reader; opening a file that was edited by a tool adding BOM or changing line endings; confusing armored age files with OpenPGP armored files; copying armor text through systems that rewrap lines or strip characters; wrong file passed to a decryption script.
Related errors
- invalid closing line: %q
- ArmoredWriter already closed
- trailing data after armored file
- too much trailing whitespace
- too much leading whitespace
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/2ddec723aa235ab0.
Report an issue: GitHub.