diff --git a/src/options.go b/src/options.go
index d8b2bd874515245585b5dc557180da85fa1fcedc..17df2210b2ecb2bb5f908ef9d4d59d086708dcf4 100644
--- a/src/options.go
+++ b/src/options.go
@@ -784,6 +784,10 @@ func parseOptions(opts *Options, allArgs []string) {
 		}
 	}
 
+	if opts.HeaderLines < 0 {
+		errorExit("header lines must be a non-negative integer")
+	}
+
 	// Change default actions for CTRL-N / CTRL-P when --history is used
 	if opts.History != nil {
 		if _, prs := keymap[curses.CtrlP]; !prs {
diff --git a/src/terminal.go b/src/terminal.go
index 844574a1457d6e78fc02e9bc766070e2cad2bf97..52c36cf92921cceba40dff0cbaa51f2dac44a2d0 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -377,10 +377,18 @@ func (t *Terminal) printHeader() {
 	if len(t.header) == 0 {
 		return
 	}
+	max := C.MaxY()
 	for idx, lineStr := range t.header {
 		if !t.reverse {
 			idx = len(t.header) - idx - 1
 		}
+		line := idx + 2
+		if t.inlineInfo {
+			line -= 1
+		}
+		if line >= max {
+			break
+		}
 		trimmed, colors := extractColor(&lineStr)
 		item := &Item{
 			text:   trimmed,
@@ -388,10 +396,6 @@ func (t *Terminal) printHeader() {
 			colors: colors,
 			rank:   Rank{0, 0, 0}}
 
-		line := idx + 2
-		if t.inlineInfo {
-			line -= 1
-		}
 		t.move(line, 2, true)
 		t.printHighlighted(item, false, C.ColHeader, 0, false)
 	}
@@ -993,6 +997,7 @@ func (t *Terminal) constrain() {
 		t.offset = util.Max(0, count-height)
 		t.cy = util.Constrain(t.offset+diffpos, 0, count-1)
 	}
+	t.offset = util.Max(0, t.offset)
 }
 
 func (t *Terminal) vmove(o int) {
@@ -1021,8 +1026,9 @@ func (t *Terminal) vset(o int) bool {
 }
 
 func (t *Terminal) maxItems() int {
+	max := C.MaxY() - 2 - len(t.header)
 	if t.inlineInfo {
-		return C.MaxY() - 1 - len(t.header)
+		max += 1
 	}
-	return C.MaxY() - 2 - len(t.header)
+	return util.Max(max, 0)
 }
diff --git a/test/test_go.rb b/test/test_go.rb
index 88dff86cd1a420cb968c769dff162e912e7a15c2..7effb5fbd340bfcaed850e065292df8b44d00386 100644
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -648,6 +648,61 @@ class TestGoFZF < TestBase
     tmux.until { |lines| lines[-10].start_with? '>' }
   end
 
+  def test_header_lines
+    tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5'}", :Enter
+    2.times do
+      tmux.until do |lines|
+        lines[-2].include?('/90') &&
+        lines[-3]  == '  1' &&
+        lines[-4]  == '  2' &&
+        lines[-13] == '> 15'
+      end
+      tmux.send_keys :Down
+    end
+    tmux.send_keys :Enter
+    assert_equal '15', readonce.chomp
+  end
+
+  def test_header_lines_reverse
+    tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5 --reverse'}", :Enter
+    2.times do
+      tmux.until do |lines|
+        lines[1].include?('/90') &&
+        lines[2]  == '  1' &&
+        lines[3]  == '  2' &&
+        lines[12] == '> 15'
+      end
+      tmux.send_keys :Up
+    end
+    tmux.send_keys :Enter
+    assert_equal '15', readonce.chomp
+  end
+
+  def test_header_lines_overflow
+    tmux.send_keys "seq 100 | #{fzf '--header-lines=200'}", :Enter
+    tmux.until { |lines| lines[-2].include?('0/0') }
+    tmux.send_keys :Enter
+    assert_equal '', readonce.chomp
+  end
+
+  def test_header_file
+    tmux.send_keys "seq 100 | #{fzf "--header-file <(head -5 #{__FILE__})"}", :Enter
+    header = File.readlines(__FILE__).take(5).map(&:strip)
+    tmux.until do |lines|
+      lines[-2].include?('100/100') &&
+      lines[-7..-3].map(&:strip) == header
+    end
+  end
+
+  def test_header_file_reverse
+    tmux.send_keys "seq 100 | #{fzf "--header-file <(head -5 #{__FILE__}) --reverse"}", :Enter
+    header = File.readlines(__FILE__).take(5).map(&:strip)
+    tmux.until do |lines|
+      lines[1].include?('100/100') &&
+      lines[2..6].map(&:strip) == header
+    end
+  end
+
 private
   def writelines path, lines
     File.unlink path while File.exists? path