VCF Header Lines Explained: How to Read and Validate Metadata

By Dr. Zubair Khalid, DVM, MS, PhD ·

VCF Header Lines Explained: How to Read and Validate Metadata

A variant file that loads in one tool and fails in another almost always has a header problem. The most common cause is a missing or mismatched ##contig line, and the first thing to check is the output of bcftools view -h yourfile.vcf. That command prints the header without touching the data, so you can compare what the file claims about itself against what the body actually contains. Most repairs take one command once you know which claim is wrong.

This guide covers VCF header lines explained from the ground up: what each line means, which fields parsers actually enforce, the causes behind the usual error messages, and the solutions that work without corrupting the file. It also places VCF next to the other bioinformatics file formats you handle daily, including the FASTQ vs FASTA distinction, because metadata handling is the thread that connects them.

What a VCF header actually is

The Variant Call Format (VCF) is a tab-delimited text format created for the 1000 Genomes Project and now maintained within the hts-specs specification family alongside SAM, BAM, CRAM, and BCF. A VCF file has exactly two regions. The header occupies every line from the top of the file down to the column header line. The body holds one record per line after that.

A meta-information line is any header line beginning with two hash characters (##). A column header line begins with a single hash (#) and names the eight fixed columns plus any genotype columns. Everything before the #CHROM line is metadata. Everything after it is data.

The specification sets a small number of hard rules that determine whether a parser can open the file at all:

  1. The first line must be ##fileformat=VCFv4.2 (or another supported version such as VCFv4.3), with no leading whitespace and no byte-order mark.
  2. Every metadata line starts with ## and has the form key=value.
  3. Structured values are wrapped in angle brackets and use comma-separated key=value pairs inside, for example ##INFO=<ID=DP,Number=1,Type=Integer,Description="Total depth">.
  4. The #CHROM line is the last header line. No ## line may appear after it.
  5. If genotype data is present, the word FORMAT appears after INFO, and each sample gets one column after that. Sample names must be unique.
  6. Metadata IDs (the value of ID=) use letters, digits, underscores, and dots, and start with a letter or underscore. Spaces and angle brackets are not allowed.

A realistic, valid header looks like this:

##fileformat=VCFv4.2
##FILTER=<ID=PASS,Description="All filters passed">
##fileDate=20260101
##source=bcftools-1.19
##reference=file:///refs/GRCh38.fa
##contig=<ID=chr1,length=248956422>
##contig=<ID=chr2,length=242193529>
##INFO=<ID=DP,Number=1,Type=Integer,Description="Approximate read depth">
##INFO=<ID=AF,Number=A,Type=Float,Description="Allele frequency">
##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">
##FORMAT=<ID=AD,Number=R,Type=Integer,Description="Allelic depths">
#CHROM	POS	ID	REF	ALT	QUAL	FILTER	INFO	FORMAT	NA12878

The values 248956422 and 242193529 are the GRCh38 lengths of chromosome 1 and chromosome 2. Those numbers matter. When a downstream tool compares them to a reference dictionary and finds a different value, it will either stop or silently reorder records.

The lines you will meet most often

##fileformat declares the spec version and must come first. ##fileDate records when the file was written. ##source names the software that produced it. ##reference points to the reference sequence, ideally as a URI such as file:///refs/GRCh38.fa. ##contig describes one sequence, usually with ID and length. ##INFO defines a body tag used in the INFO column. ##FILTER defines a named filter such as LowQual. ##FORMAT defines a per-sample field such as GT or DP. ##ALT defines symbolic alleles such as <DEL> or <DUP>.

Three lines carry provenance rather than biology. ##bcftools_viewCommand and its sibling ##bcftools_viewVersion record the exact bcftools invocation and version. GATK writes ##GATKCommandLine records with the full argument string. These lines make a pipeline auditable months later. Deleting them to shrink a file is a common and costly mistake.

How to read a header in under a minute

The fastest inspection is a short sequence of commands. Run them in this order on any unfamiliar VCF.

head -1 sample.vcf
bcftools view -h sample.vcf
grep -c '^##' sample.vcf
bcftools query -l sample.vcf
bcftools view -h sample.vcf | grep '^##contig' | wc -l
awk -F'\t' '/^#CHROM/{print NF}' sample.vcf
awk -F'\t' '!/^#/{print NF}' sample.vcf | sort -u

The first command confirms the format declaration. The second prints the entire header, which is usually a few hundred lines. The third counts metadata lines, a quick sanity number that should be stable across files from the same pipeline. The fourth lists sample names in column order. The fifth counts declared contigs. The last two compare the column count in the header line against the column count in every body line. If those two numbers differ, the file is broken regardless of what the header says.

For compressed files the same commands work because bcftools reads gzip and BGZF transparently. If you prefer plain shell tools, zcat sample.vcf.gz | head -40 works on any gzip stream, and tabix -l sample.vcf.gz lists the sequence names present in the tabix index. Compare that index list to the ##contig list. A contig in the data but missing from the index usually means the index was built before the file was updated.

Reading the column header line

The #CHROM line is where most structural errors become visible. It must contain exactly these eight names in this order: #CHROM, POS, ID, REF, ALT, QUAL, FILTER, INFO. If genotypes are present, FORMAT follows INFO and then one column per sample follows FORMAT. The number of tab-separated fields in that line must equal the number of fields in every data line. A header that lists three samples against a body that carries four is the single most common cause of "wrong number of columns" errors after merging.

Number, Type, and ID: the fields parsers enforce

Most header content is descriptive and ignored by software. Three attributes are not descriptive. They are contracts between the header and the body, and a violation produces either a hard error or silent corruption.

ID is the tag name used in the body. It must match the string in the INFO or FORMAT column exactly, including case.

Type declares the datatype. The allowed values are Integer, Float, Flag, Character, and String. A Flag is a boolean with no value in the body, so its Number must be 0. Declaring a tag as Integer when the body contains decimals is a frequent cause of parse failures.

Number declares how many values the tag may carry per record. The codes are fixed by the specification.

NumberMeaningTypical example
1Exactly one valueDP
AOne value per ALT alleleAF, AC
ROne value per allele including REFAD
GOne value per genotypePL, GL
.Variable, unknown in advanceANN
A positive integerThat exact countCustom tags

The A, R, and G codes are where multiallelic records break. A record with two ALT alleles needs two values for a Number=A tag, three for Number=R, and three genotype combinations for a diploid Number=G tag. A file called with one ALT and later split or merged without updating the header will violate this contract.

Reserved tags add another layer. The specification reserves names such as AA, AC, AF, AN, DP, NS, and DB for INFO, and GT, GQ, DP, AD, PL, and GL for FORMAT. Defining a reserved name with a different Type or Number than the specification allows creates a conflict that strict validators reject. Local tags should avoid reserved names entirely.

VCF header line causes: a troubleshooting table

Work down this table from the most common cause to the rarest. Each row gives the confirmation command and the repair.

SymptomLikely causeHow to confirmFix
unknown file type or Failed to read from inputMissing or malformed ##fileformat, byte-order mark, or Windows line endingshead -1 file.vcf and file file.vcfRewrite line 1 as ##fileformat=VCFv4.2, then run dos2unix file.vcf
Contig chr1 is not defined in the headerBody uses contigs but no ##contig lines exist`bcftools view -h f.vcf \grep -c '^##contig'` returns 0Append contig lines built from the reference .fai
Reference allele mismatchREF column disagrees with the FASTA, often from a different assembly or chr namingbcftools norm -f ref.fa -c eRename contigs with the rename-chrs option of bcftools annotate, or recall against the correct reference
Unknown INFO field or no such tag definedBody tag used but never declared`bcftools view -h f.vcf \grep '##INFO=<ID=TAG,'` returns nothingAdd a definition line with bcftools annotate -h
Duplicate header line for IDTwo declarations for one tag, usually after merging two filesgrep '##INFO=<ID=DP' f.vcf returns multiple hitsDeduplicate, keeping the declaration that matches the body
Header names 3 samples but body has 4 columnsColumn count drift after a merge or a manual editawk -F'\t' '/^#CHROM/{print NF}' differs from body field countRebuild the #CHROM line or restore the missing sample column
Unsorted positions when indexingBody not in coordinate orderIndexing fails while bcftools view still runsbcftools sort -o sorted.vcf then recompress and index
not a BGZF file or not compressed with bgzipPlain gzip or uncompressed inputfile f.vcf.gz reports gzip rather than BGZFbgzip -c f.vcf > f.vcf.gz then tabix -p vcf f.vcf.gz
Could not parse header with an angle bracket complaintUnquoted comma inside DescriptionInspect the offending line for a comma before the closing bracketQuote the description and escape internal quotes
Number of values in INFO/AD is inconsistentWrong Number for a multiallelic siteCount comma-separated values in one recordCorrect the Number or fix the caller output
GT allele index out of rangeBody genotype refers to an ALT index that does not existInspect the record with the largest ALT countRecall the site or filter malformed genotypes
FORMAT column present but no FORMAT definitionsHeader truncated during a custom scriptgrep -c '##FORMAT' returns 0Add GT and other required FORMAT lines
Sequence dictionary mismatch in GATK##contig order or length differs from the .dictCompare ##contig lines to the dictionary fileRewrite contigs from the dictionary
Sample not found in header when using -sSample name typo or a renamed filebcftools query -l f.vcf shows the real namesUse exact names or bcftools reheader -s

Missing contig lines

This is the most frequent failure by a wide margin. Many tools build an internal sequence dictionary from ##contig and use it for region queries, index checks, and reference validation. GATK refuses to process a VCF whose contigs do not match its dictionary. bcftools is more permissive for plain reading but stricter when a reference is supplied. A VCF exported by a custom script that writes only #CHROM and data lines will fail at the first serious consumer.

The fix is to derive contigs from the reference itself, not from memory. samtools faidx ref.fa produces ref.fa.fai with sequence name and length in the first two columns. Convert that into header lines and append them.

cut -f1,2 ref.fa.fai | awk '{print "##contig=<ID="$1",length="$2">"}' > contigs.txt
bcftools annotate -h contigs.txt -o fixed.vcf in.vcf

If you need to replace the whole header rather than append, extract it, edit it, and use bcftools reheader -h header.txt -o fixed.vcf in.vcf. Reheader swaps the header and streams the body unchanged, which makes it the safest tool for this job.

Undefined INFO, FORMAT, and FILTER tags

Strict parsers reject any body tag that lacks a header declaration. This is by design. An undefined tag has unknown Type, so the parser cannot tell whether "1,2" is two integers or one string. Adding the declaration restores that contract.

printf '##INFO=<ID=SEGMENT,Number=1,Type=String,Description="Segment label">\n' > defs.txt
bcftools annotate -h defs.txt -o fixed.vcf in.vcf

Removing a tag is equally straightforward. bcftools annotate -x INFO,SEGMENT strips both the data and the definition, which is cleaner than editing the header alone.

Sample name and column count problems

The #CHROM line is a table header, and tables do not tolerate drift. A merge that adds a sample without updating the header, or a cut command that drops a column, produces a file that looks fine in a text editor and fails in every parser. Confirm the counts first, then decide whether the header or the body is wrong.

Renaming samples is a header-only operation. Write the new names one per line in the same order as the current columns, verify the count matches bcftools query -l, then run bcftools reheader -s names.txt -o renamed.vcf in.vcf. A mismatch in line count produces a fatal error, which is the desired behavior.

fileformat, encoding, and quoting

The ##fileformat line must be the first bytes of the file. A UTF-8 byte-order mark inserted by a Windows editor is invisible in most viewers and fatal to parsers. Carriage returns at line ends produce similar symptoms, especially on the first line. Convert with dos2unix and confirm the first line with head -1 file.vcf | od -c.

Descriptions are free text for humans, and parsers ignore their content. The syntax still matters. An unquoted comma inside a description splits the structured field into pieces, and an unmatched double quote breaks the bracket parser. Keep descriptions inside double quotes and avoid internal double quotes, or escape them.

A diagnostic sequence to run in order

When a VCF fails and the error message is vague, this sequence isolates the layer of the problem in about two minutes.

  1. Confirm the file type with file sample.vcf or file sample.vcf.gz. Plain gzip is not BGZF.
  2. Print the first line with head -1. It must be ##fileformat=.
  3. Print the header with bcftools view -h. If this fails, the problem is in the header or the compression wrapper.
  4. Check that ##contig lines exist. Count them and compare to the number of distinct sequences in the body, which you can get with grep -v '^#' sample.vcf | cut -f1 | sort -u.
  5. Extract every tag used in the body INFO column with bcftools query -f '%INFO\n' sample.vcf | tr ';' '\n' | cut -d= -f1 | sort -u and compare to the declared INFO IDs.
  6. Do the same for FORMAT tags with bcftools query -f '[%FORMAT\n]' sample.vcf on the first few records.
  7. Compare column counts in the #CHROM line and the body.
  8. Check sample names against your metadata sheet with bcftools query -l.
  9. Run a formal validator, described below.
  10. Only after steps 1 through 9 should you edit anything.

The order matters. Editing a header before locating the actual defect often hides the original cause and adds a second one.

Validation: syntax, semantics, and external consistency

Validation has three levels, and most pipelines check only the first.

Syntactic validation asks whether the file obeys the specification grammar. The vcf-validator tool distributed with VCFtools checks the fileformat line, structured field syntax, ID character rules, duplicate declarations, and the #CHROM column layout. Running vcf-validator sample.vcf is a cheap first gate.

Semantic validation asks whether the body honors the header contracts. Counts for Number=A, Number=R, and Number=G tags must match the ALT column. Genotype indices must reference existing alleles. The file must be sorted if it will be indexed. bcftools norm -f ref.fa -c e checks REF alleles against the reference and errors on mismatch, which catches build and naming errors that syntax checks cannot see.

External consistency asks whether the header agrees with the world outside the file. The ##reference URI should resolve to the FASTA actually used for calling. Contig lengths should match the .fai and the sequence dictionary. Sample IDs should match the pedigree or phenotype table. No validator can check this automatically because it requires your project context.

A sample command set for a release gate looks like this:

vcf-validator cohort.vcf
bcftools norm -f ref.fa -c e cohort.vcf -o /dev/null
bcftools sort cohort.vcf -o cohort.sorted.vcf
bgzip -c cohort.sorted.vcf > cohort.sorted.vcf.gz
tabix -p vcf cohort.sorted.vcf.gz

Validation cannot catch a swapped sample, a mislabeled population, or a call set generated against the wrong reference when someone also edited the contig lines to match. The header is a claim about the file, not proof of its provenance.

Fixing headers without breaking the file

Four operations cover nearly every repair.

Append lines. Use bcftools annotate -h extra.txt. It reads the existing header, adds the lines from the file, and writes a new file. This is the correct tool for adding INFO, FORMAT, FILTER, and ALT definitions.

Replace the whole header. Use bcftools reheader -h header.txt. This is correct when the existing header contains wrong contig lengths or stale provenance that you want gone.

Rename sequences. Use the sequence renaming option of bcftools annotate, which takes a two-column mapping file with the old name and new name on each line separated by whitespace. This converts between chr1 and 1 conventions without touching coordinates.

Rename samples. Use bcftools reheader -s names.txt. The name file must contain exactly one line per sample column in the current order.

Two cautions apply to all four. First, never redirect output onto the input path. bcftools view -h in.vcf > in.vcf truncates the input before the tool reads it, and the shell does this silently. Write to a new file and move it afterward. Second, recompression and reindexing are required after any header change that affects contigs, because the tabix index stores contig names from the old header.

A safety note

Two hazards deserve attention. The shell truncation problem above destroys data with no warning and no recovery path, so always write to a temporary name. The second is data governance. Human VCFs can carry identifiable genotypes and are often subject to controlled-access agreements. Keep files on access-controlled storage, record who ran which command, and strip sample identifiers before sharing a file outside the project. Animal and agricultural variant files are usually less regulated, but the same handling discipline prevents accidental release of commercial breeding data.

Where VCF sits among bioinformatics file formats

Header handling is the concept that unifies the formats you work with. Each one solves metadata differently, and knowing the pattern makes new formats easier to learn.

FormatStructureMetadata locationTypical use
FASTA> header then sequence linesOne description line per record, no formal schemaReference genomes, proteins, contigs
FASTQFour lines per read: @ header, sequence, +, qualityRead name encodes instrument, run, lane, tile, and coordinatesRaw sequencing reads
VCFHeader block then tab-delimited recordsFormal ## lines with typed declarationsVariant calls, genotypes
BCFBinary VCFSame header, stored in binaryFast random access to variants
SAM/BAM/CRAMHeader block then alignment records@HD, @SQ, @RG, @PG linesRead alignments
BEDTab-delimited intervalsOptional track and browser linesRegions, coverage, annotation
GFF3Tab-delimited features##gff-version 3 directive and commentsGene models, annotations

FASTQ vs FASTA

The FASTQ vs FASTA comparison is the clearest illustration of why VCF headers exist.

A FASTA file stores sequences with almost no metadata. Each record starts with a > character followed by a free-text description, and the sequence follows on subsequent lines. There is no formal place to declare the assembly, the source, or the checksum. If you need those facts, they live in a separate README, a filename convention, or a database record.

A FASTQ file stores raw reads in four lines. Line one starts with @ and carries the read identifier. Line two is the sequence. Line three starts with + and optionally repeats the identifier. Line four carries one quality character per base. The quality is encoded as a Phred score plus an offset of 33, so the ASCII value of each character minus 33 gives the score. A Phred score of 30 means a one in one thousand chance that the base is wrong, from the standard definition Q = -10 log10(P). FASTQ has no header block at all. Provenance is packed into the read name, which is why missing or renamed read groups cause so much trouble later.

A VCF solves the problem that FASTA and FASTQ leave open. It has a dedicated, typed metadata block that declares the reference, the contigs, every data tag, and the software that wrote the file. The ##contig line in VCF does the same job as the @SQ line in a SAM file. If you understand one, you understand the other.

Common Mistakes and Limitations

Deleting ##contig lines to silence an error is the most damaging habit in this list. It makes the error disappear while breaking indexing, region queries, and reference checks, often weeks later and in a different pipeline.

Editing a header with sed -i on a production file risks partial writes and leaves no audit trail. Extract, edit a copy, validate, then reheader.

Assuming the Description text controls behavior is another common error. Parsers ignore it completely. Only ID, Number, and Type change how the body is interpreted.

Treating the fileformat version as cosmetic causes subtle problems. Version 4.3 introduced features that 4.2 parsers do not understand, so declare the version you actually use.

Mixing reference builds is a silent killer. Chromosome 1 is 249,250,621 bases in GRCh37 and 248,956,422 bases in GRCh38. A header that claims one build while the body was called on the other will pass syntax checks and fail every downstream comparison.

Compressing with plain gzip and then wondering why tabix refuses to index is a weekly occurrence in most groups. Tabix requires BGZF, which is gzip-compatible but block-structured. Use bgzip, not gzip.

Assuming bcftools is strict enough to catch your problem is a mistake. Different tools enforce different subsets of the specification, so a file that passes one parser can fail another. Run more than one validator on release artifacts.

Finally, remember that a header is metadata, not evidence. Clinical interpretation of any variant requires review by a qualified professional, and animal data requires a veterinarian with genetics training. The header tells you what the file claims about itself.

Prevention practices

Generate headers programmatically instead of typing them. A two-line script that reads the reference .fai and emits ##contig lines eliminates the most common defect class permanently.

Keep the reference FASTA, its .fai index, its .dict dictionary, and a checksum in the same directory. Record the checksum in the ##reference line or in a sidecar file.

Add vcf-validator and bcftools norm -c e to continuous integration for any pipeline that emits VCFs. Both are fast and both catch regressions that unit tests on code will miss.

Pin tool versions and keep the provenance lines. ##bcftools_viewCommand and ##GATKCommandLine records answer the question "how was this made" without a meeting.

Adopt one contig naming convention for the whole project before the first alignment. Renaming later is possible but touches every file, index, and dictionary.

Store the header separately as header.txt and diff it between releases. A one-line change in a contig length is easy to miss in a full file diff and trivial to spot in a header diff.

Test the round trip. Convert VCF to BCF and back with bcftools view -O b and bcftools view -O v, then confirm the header survives. Files that fail this test usually have a quoting or encoding defect.

Frequently Asked Questions

What is a VCF header line?

A VCF header line is any line above the #CHROM column line, and each one begins with ## except the column line itself. These lines declare the format version, the reference, contigs, and every INFO, FORMAT, and FILTER tag used in the body.

Why does bcftools report an unknown INFO field?

The body uses a tag that has no ##INFO declaration. Add the declaration with bcftools annotate -h defs.txt, or remove the tag from the data with bcftools annotate -x.

How do I add contig lines to a VCF header?

Build them from the reference index with cut -f1,2 ref.fa.fai and an awk one-liner, then append with bcftools annotate -h contigs.txt. Deriving lengths from the FASTA avoids transcription errors.

Does a VCF header have to match the reference genome?

Yes, for any tool that validates against a reference. Contig names and lengths must match the FASTA index and the sequence dictionary, or GATK and bcftools will reject the file or misplace records.

What does Number=A mean in a VCF header line?

It means one value per ALT allele. A biallelic record carries one value, and a triallelic record carries two. The same rule applies to Number=R for all alleles and Number=G for genotypes.

What is the difference between FASTQ and FASTA?

FASTQ stores raw reads in four lines including per-base quality scores, while FASTA stores sequences in two or more lines with no quality data. FASTQ is for sequencing output, and FASTA is for references and assembled sequences.

Is VCFv4.2 different from VCFv4.3 in practice?

Yes. Version 4.3 adds features and clarifications that 4.2 parsers do not handle. Declare the version you actually produce, and check that every tool in the pipeline supports it.

Can I edit a VCF header in a text editor?

You can, and you should validate afterward. Extract the header, edit a copy, confirm the syntax, and apply it with bcftools reheader. Editing the only copy of a large file by hand risks corruption with no backup.