diff --git a/pygittools/tui/app.py b/pygittools/tui/app.py
index f8c3308..976dd10 100644
--- a/pygittools/tui/app.py
+++ b/pygittools/tui/app.py
@@ -18,7 +18,7 @@ from unicurses import (  # type: ignore[import-untyped]
 	refresh,
 )
 
-from pygittools.tui.pages.home import home_page
+from pygittools.tui.pages.home import HomePage, home_page
 from pygittools.tui.pages.no_repo import NoRepoPage
 from pygittools.tui.repo import try_open_page_context
 from pygittools.tui.status_bar import content_height, draw_status_bar, init_status_bar
@@ -29,6 +29,7 @@ def run_tui(repo_path: Path | None = None) -> int:
 	start = (repo_path or Path.cwd()).resolve()
 	ctx = try_open_page_context(start)
 	initial: Page = home_page() if ctx is not None else NoRepoPage(start)
+	stack: list[Page] = [initial]
 
 	stdscr = initscr()
 	try:
@@ -38,7 +39,6 @@ def run_tui(repo_path: Path | None = None) -> int:
 		keypad(stdscr, True)
 		status_attr = init_status_bar()
 
-		stack: list[Page] = [initial]
 		if ctx is not None:
 			stack[0].on_enter(ctx)
 
@@ -65,17 +65,28 @@ def run_tui(repo_path: Path | None = None) -> int:
 	except KeyboardInterrupt:
 		pass
 	finally:
+		_persist_commit_draft(stack, ctx)
 		endwin()
 
 	return 0
 
 
+def _persist_commit_draft(stack: list[Page], ctx: PageContext | None) -> None:
+	if ctx is None:
+		return
+	for page in reversed(stack):
+		if isinstance(page, HomePage):
+			page.changes.persist_commit_draft(ctx.repo)
+			return
+
+
 def _apply_result(
 	stack: list[Page],
 	ctx: PageContext | None,
 	result: PageResult,
 ) -> tuple[PageContext | None, list[Page]]:
 	if result.switch_repo is not None:
+		_persist_commit_draft(stack, ctx)
 		new_ctx = try_open_page_context(result.switch_repo)
 		if new_ctx is not None:
 			ctx = new_ctx
diff --git a/pygittools/tui/changes_list.py b/pygittools/tui/changes_list.py
index c212088..69ea9e3 100644
--- a/pygittools/tui/changes_list.py
+++ b/pygittools/tui/changes_list.py
@@ -4,6 +4,7 @@ from __future__ import annotations
 
 from dataclasses import dataclass
 from enum import Enum
+from pathlib import Path
 from typing import Literal
 
 from pygit2 import Repository
@@ -18,6 +19,7 @@ from unicurses import (  # type: ignore[import-untyped]
 	KEY_UP,
 )
 
+from pygittools.tui.commit_draft import save_commit_draft
 from pygittools.tui.draw import draw_line
 from pygittools.tui.worktree import (
 	categorize_status,
@@ -74,6 +76,16 @@ class ChangesList:
 		self._error: str | None = None
 		self._staged_count = 0
 
+	@property
+	def commit_message(self) -> str:
+		return self._commit_message
+
+	def set_commit_message(self, message: str) -> None:
+		self._commit_message = message
+
+	def persist_commit_draft(self, repo: Repository) -> None:
+		save_commit_draft(Path(repo.path), self._commit_message)
+
 	def is_at_top(self) -> bool:
 		return self._cursor == 0
 
@@ -171,6 +183,7 @@ class ChangesList:
 			ok, error = commit_staged(repo, self._commit_message)
 			if ok:
 				self._commit_message = ""
+				self.persist_commit_draft(repo)
 				self._error = None
 				self.refresh(repo)
 				return InputKeyResult(layout_changed=True, relinquish=True)
@@ -219,6 +232,7 @@ class ChangesList:
 			ok, error = commit_staged(repo, self._commit_message)
 			if ok:
 				self._commit_message = ""
+				self.persist_commit_draft(repo)
 				self._error = None
 				self.refresh(repo)
 				return True
diff --git a/pygittools/tui/commit_draft.py b/pygittools/tui/commit_draft.py
new file mode 100644
index 0000000..15e2bcf
--- /dev/null
+++ b/pygittools/tui/commit_draft.py
@@ -0,0 +1,50 @@
+"""Persist in-progress commit messages in the repository COMMIT_EDITMSG file."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+from pygit2 import Commit, GitError, Repository
+
+
+def commit_editmsg_path(repo_path: Path) -> Path:
+	return repo_path / "COMMIT_EDITMSG"
+
+
+def _read_draft_text(repo_path: Path) -> str:
+	path = commit_editmsg_path(repo_path)
+	if not path.is_file():
+		return ""
+	try:
+		text = path.read_text(encoding="utf-8")
+	except OSError:
+		return ""
+	for line in text.splitlines():
+		stripped = line.strip()
+		if stripped and not stripped.startswith("#"):
+			return stripped
+	return ""
+
+
+def load_commit_draft(repo_path: Path, repo: Repository | None = None) -> str:
+	draft = _read_draft_text(repo_path)
+	if not draft or repo is None:
+		return draft
+	try:
+		head_message = repo.head.peel(Commit).message.strip()
+	except GitError:
+		return draft
+	if draft.strip() == head_message:
+		return ""
+	return draft
+
+
+def save_commit_draft(repo_path: Path, message: str) -> None:
+	path = commit_editmsg_path(repo_path)
+	try:
+		if message:
+			path.write_text(f"{message}\n", encoding="utf-8")
+		elif path.is_file():
+			path.unlink()
+	except OSError:
+		return
diff --git a/pygittools/tui/commit_draft_test.py b/pygittools/tui/commit_draft_test.py
new file mode 100644
index 0000000..cc28e75
--- /dev/null
+++ b/pygittools/tui/commit_draft_test.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+from pygit2 import Repository, Signature, init_repository
+
+from pygittools.tui.commit_draft import (
+	commit_editmsg_path,
+	load_commit_draft,
+	save_commit_draft,
+)
+
+SIG = Signature("alice", "alice@example.com")
+
+
+@pytest.fixture
+def repo(tmp_path: Path) -> Repository:
+	repo_path = tmp_path / "repo"
+	repo_path.mkdir()
+	return init_repository(str(repo_path), bare=False)
+
+
+def test_load_returns_empty_when_missing(tmp_path: Path) -> None:
+	assert load_commit_draft(tmp_path) == ""
+
+
+def test_roundtrip_single_line_message(tmp_path: Path) -> None:
+	save_commit_draft(tmp_path, "feat: add draft persistence")
+	assert load_commit_draft(tmp_path) == "feat: add draft persistence"
+	assert commit_editmsg_path(tmp_path).read_text(encoding="utf-8") == "feat: add draft persistence\n"
+
+
+def test_load_ignores_git_comment_lines(tmp_path: Path) -> None:
+	commit_editmsg_path(tmp_path).write_text(
+		"feat: keep me\n\n# Please enter the commit message\n",
+		encoding="utf-8",
+	)
+	assert load_commit_draft(tmp_path) == "feat: keep me"
+
+
+def test_save_empty_removes_existing_file(tmp_path: Path) -> None:
+	save_commit_draft(tmp_path, "wip")
+	path = commit_editmsg_path(tmp_path)
+	assert path.is_file()
+
+	save_commit_draft(tmp_path, "")
+	assert not path.exists()
+
+
+def test_load_returns_empty_when_draft_matches_head(repo: Repository) -> None:
+	tree = repo.index.write_tree()
+	repo.create_commit("HEAD", SIG, SIG, "feat: shipped", tree, [])
+	save_commit_draft(Path(repo.path), "feat: shipped")
+
+	assert load_commit_draft(Path(repo.path), repo) == ""
+
+
+def test_load_returns_draft_when_it_differs_from_head(repo: Repository) -> None:
+	tree = repo.index.write_tree()
+	repo.create_commit("HEAD", SIG, SIG, "feat: shipped", tree, [])
+	save_commit_draft(Path(repo.path), "feat: next change")
+
+	assert load_commit_draft(Path(repo.path), repo) == "feat: next change"
diff --git a/pygittools/tui/pages/home.py b/pygittools/tui/pages/home.py
index 5db222d..522cdb9 100644
--- a/pygittools/tui/pages/home.py
+++ b/pygittools/tui/pages/home.py
@@ -12,6 +12,7 @@ from unicurses import (  # type: ignore[import-untyped]
 
 from pygittools.tui.branches import current_branch_name
 from pygittools.tui.changes_list import ChangesList
+from pygittools.tui.commit_draft import load_commit_draft
 from pygittools.tui.draw import draw_line
 from pygittools.tui.input_context import InputMode, input_context, nav_context
 from pygittools.tui.types import Page, PageAction, PageContext, PageResult
@@ -50,6 +51,7 @@ class HomePage:
 		self._ctx = ctx
 		self._header_focus = "changes"
 		self._input_mode = nav_context()
+		self._changes.set_commit_message(load_commit_draft(ctx.repo_path, ctx.repo))
 		self._changes.refresh(ctx.repo)
 
 	def status_text(self) -> str:
