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

  1. Ensure the input was produced with age's armor option (age -a / armor.NewWriter); plain age output has no armor header
  2. Check the first bytes of the input for a UTF-8 BOM or whitespace/garbage before the header and strip them
  3. Confirm the header reads exactly "-----BEGIN AGE ENCRYPTED FILE-----" with no edits, wrapping, or CRLF-only corruption from Windows transfer
  4. Verify you are not feeding a PGP-armored file ("-----BEGIN PGP MESSAGE-----"); decrypt it with the proper tool instead
  5. 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

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


AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31). Data as JSON: /api/errors/2ddec723aa235ab0. Report an issue: GitHub.