diff --git a/pygittools/hooks_push.py b/pygittools/hooks_push.py
index 23e91c7..3fcb092 100644
--- a/pygittools/hooks_push.py
+++ b/pygittools/hooks_push.py
@@ -62,7 +62,7 @@ class PostCommitPushRemotes(PostCommit):
 		if self.repo.workdir is None:
 			stderr.write("PostCommitPushRemotes: bare repository; skipping push.\n")
 			return HookResult.SUCCESS
-		remote_names: list[str] = list(self.repo.remotes.names())
+		remote_names: list[str] = [name for name in self.repo.remotes.names() if name]
 		if not remote_names:
 			return HookResult.SUCCESS
 		failures: list[str] = []
diff --git a/pygittools/merge.py b/pygittools/merge.py
index c0da589..1552e7c 100644
--- a/pygittools/merge.py
+++ b/pygittools/merge.py
@@ -145,7 +145,7 @@ class MergeRequest:
 	def write(self, repo: Repository) -> Oid:
 		object_type = assert_merge_head_object(repo, self.target)
 		tagger_sig = tagger_signature_at(self.opener, self.updated_at)
-		raw = _build_tag_raw(self.target, self.name, tagger_sig, self.message, object_type=object_type)
+		raw = _build_tag_raw(self.target, self.name, str(tagger_sig), self.message, object_type=object_type)
 		oid = repo.odb.write(GIT_OBJECT_TAG, raw)
 		repo.references.create(self.name, oid, force=True)
 		return oid
diff --git a/pygittools/tags_sync.py b/pygittools/tags_sync.py
index ada3ea6..19a39e2 100644
--- a/pygittools/tags_sync.py
+++ b/pygittools/tags_sync.py
@@ -211,8 +211,8 @@ def _merge_board_task_list(
 	local_order: list[str],
 	remote_order: list[str],
 	oids_by_ref: dict[str, str],
-) -> list[str]:
-	merged: list[str] = []
+) -> list[Oid | str]:
+	merged: list[Oid | str] = []
 	seen: set[str] = set()
 	for task_name in remote_order:
 		if task_name in oids_by_ref and task_name not in seen:
diff --git a/pygittools/tasks.py b/pygittools/tasks.py
index 330a596..ecd7a63 100644
--- a/pygittools/tasks.py
+++ b/pygittools/tasks.py
@@ -158,12 +158,12 @@ class Task:
 		return oid
 
 
-def _parse_dt(s):
+def _parse_dt(s: str | datetime | None) -> datetime | None:
 	if not s:
 		return None
-	if hasattr(s, "isoformat"):
+	if isinstance(s, datetime):
 		return s
-	return datetime.fromisoformat(str(s).replace("Z", "+00:00")) if s else None
+	return datetime.fromisoformat(s.replace("Z", "+00:00"))
 
 
 def get_task(repo: Repository, ref: str) -> Task | None:
@@ -226,7 +226,7 @@ class Comment:
 
 	def __init__(
 		self,
-		target: Oid,  # Task or parent comment OID
+		target: Oid | str,  # Task or parent comment OID
 		tagger: str,  # Author of the comment
 		content: str,
 		name: str | None = None,  # Fully-qualified ref, e.g. "refs/tags/comments/<comment-id>"
@@ -278,8 +278,14 @@ def get_comment(repo: Repository, oid: Oid) -> Comment:
 		raise ValueError(f"Requested comment is not a tag: {oid}")
 	j = json.loads(obj.message)
 	c = Comment(obj.target, str(obj.tagger), j["content"], obj.name)
-	c.created_at = _parse_dt(j.get("created_at")) or c.created_at
-	c.edited_at = _parse_dt(j.get("edited_at"))
+	created = _parse_dt(j.get("created_at"))
+	if not created:
+		raise ValueError(f"Invalid creation time for oid: {oid}")
+	c.created_at = created
+	edited = _parse_dt(j.get("edited_at"))
+	if not edited:
+		raise ValueError(f"Invalid edit time for oid: {oid}")
+	c.edited_at = edited
 	return c
 
 
diff --git a/pygittools/tui/git_colors.py b/pygittools/tui/git_colors.py
index aff3cef..2416f43 100644
--- a/pygittools/tui/git_colors.py
+++ b/pygittools/tui/git_colors.py
@@ -336,7 +336,7 @@ def _load_config(repo: Repository | None) -> Config:
 
 def _config_value(config: Config, key: str) -> str | None:
 	try:
-		values = list(config.get_multivar(key))
+		values = [value for value in config.get_multivar(key) if isinstance(value, str)]
 	except KeyError:
 		return None
 	if not values:
@@ -467,7 +467,7 @@ def _build_pair_attr(
 	bg_index = index_for(bg_rgb, fallback_bg)
 
 	init_pair(pair_id, fg_index, bg_index)
-	attr = COLOR_PAIR(pair_id)
+	attr = int(COLOR_PAIR(pair_id))
 	if parsed.bold:
 		attr |= A_BOLD
 	if parsed.dim:
diff --git a/pygittools/tui/input_context.py b/pygittools/tui/input_context.py
index 59d9c99..78753cc 100644
--- a/pygittools/tui/input_context.py
+++ b/pygittools/tui/input_context.py
@@ -11,7 +11,7 @@ from pygittools.tui.pages.branches import branch_picker_page
 from pygittools.tui.pages.help import help_page
 from pygittools.tui.pages.projects import projects_page
 from pygittools.tui.types import PageAction, PageContext, PageResult
-from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
+from pygittools.tui.ucurses import (
 	KEY_DOWN,
 	KEY_ENTER,
 	KEY_LEFT,
diff --git a/pygittools/tui/log_list.py b/pygittools/tui/log_list.py
index acd3dfc..21944b6 100644
--- a/pygittools/tui/log_list.py
+++ b/pygittools/tui/log_list.py
@@ -5,7 +5,8 @@ from __future__ import annotations
 from dataclasses import dataclass
 from datetime import UTC, datetime
 
-from pygit2 import GIT_SORT_TIME, Commit, GitError, Repository
+from pygit2 import Commit, GitError, Repository
+from pygit2.enums import SortMode
 
 from pygittools.tui.draw import clear_row, draw_text
 from pygittools.tui.git_colors import current_theme, log_entry_attrs
@@ -103,7 +104,7 @@ def _load_entries(repo: Repository) -> list[_LogEntry]:
 	except GitError:
 		return []
 	entries: list[_LogEntry] = []
-	for commit in repo.walk(head, GIT_SORT_TIME):
+	for commit in repo.walk(head, SortMode.TIME):
 		if not isinstance(commit, Commit):
 			continue
 		subject = commit.message.splitlines()[0] if commit.message else "(no message)"
diff --git a/pygittools/tui/pages/no_repo.py b/pygittools/tui/pages/no_repo.py
index 84ee7c5..75af1d5 100644
--- a/pygittools/tui/pages/no_repo.py
+++ b/pygittools/tui/pages/no_repo.py
@@ -7,7 +7,7 @@ from pathlib import Path
 from pygittools.tui.draw import draw_line
 from pygittools.tui.pages.help import help_page
 from pygittools.tui.types import PageAction, PageContext, PageResult
-from pygittools.tui.ucurses import A_BOLD, KEY_RESIZE  # type: ignore[import-untyped]
+from pygittools.tui.ucurses import A_BOLD, KEY_RESIZE
 
 
 class NoRepoPage:
diff --git a/pygittools/tui/pages/projects.py b/pygittools/tui/pages/projects.py
index d21ded9..fce09e2 100644
--- a/pygittools/tui/pages/projects.py
+++ b/pygittools/tui/pages/projects.py
@@ -6,7 +6,7 @@ from pygittools.tui.draw import draw_line
 from pygittools.tui.projects import ProjectEntry, list_projects
 from pygittools.tui.pygitweb_layout import load_pygitweb_layout
 from pygittools.tui.types import PageAction, PageContext, PageResult
-from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
+from pygittools.tui.ucurses import (
 	A_BOLD,
 	A_REVERSE,
 	KEY_DOWN,
diff --git a/pygittools/tui/tasks_list.py b/pygittools/tui/tasks_list.py
index 4facd8a..4030802 100644
--- a/pygittools/tui/tasks_list.py
+++ b/pygittools/tui/tasks_list.py
@@ -137,10 +137,11 @@ class TasksBoardList:
 			draw_line(screen_row, 0, row.label, width, attr)
 
 	def sync_with_default_remote(self, repo: Repository) -> None:
-		if not repo.remotes:
+		remote_names = [name for name in repo.remotes.names() if name]
+		if not remote_names:
 			self._error = "No remotes configured for task sync"
 			return
-		remote_name = "origin" if "origin" in repo.remotes.names() else next(iter(repo.remotes.names()))
+		remote_name = "origin" if "origin" in remote_names else remote_names[0]
 		board_name = self._board_name or "Tasks"
 		try:
 			result = sync_board_tags(repo, remote_name, board_name)
diff --git a/pygittools/tui/ucurses.py b/pygittools/tui/ucurses.py
index b1eb5a0..1fb588f 100644
--- a/pygittools/tui/ucurses.py
+++ b/pygittools/tui/ucurses.py
@@ -15,7 +15,7 @@ with warnings.catch_warnings():
 		category=SyntaxWarning,
 		message=r".*invalid escape sequence.*\\s",
 	)
-	import unicurses as _unicurses
+	import unicurses as _unicurses  # type: ignore[import-untyped]
 
 
 def __getattr__(name: str) -> Any:
diff --git a/pygitweb/actions.py b/pygitweb/actions.py
index dc30d99..b5683dc 100644
--- a/pygitweb/actions.py
+++ b/pygitweb/actions.py
@@ -16,7 +16,7 @@ from urllib.parse import parse_qsl, quote, urlencode
 
 import pygit2
 from fastapi import HTTPException, Request
-from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, Response
+from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse, Response
 from python_ripgrep import PySortMode, PySortModeKind
 from python_ripgrep import search as rg_search
 
@@ -370,6 +370,10 @@ class SummaryRefOption(TypedDict):
 	kind: str
 
 
+class SummaryRefsPayload(TypedDict):
+	options: list[SummaryRefOption]
+
+
 class SummaryRefState(TypedDict):
 	ref: str
 	display: str
@@ -1062,7 +1066,7 @@ def git_tags(project: str, request: Request, page: int, pagecount: int) -> HTMLR
 	)
 
 
-def git_tag(project: str, h: str | None, notice: str | None = None) -> HTMLResponse:
+def git_tag(project: str, h: str | None, notice: str | None = None) -> HTMLResponse | RedirectResponse:
 	"""Single tag view. Port of git_tag."""
 	if not h:
 		raise HTTPException(status_code=400, detail="Tag ref or hash (h) required")
diff --git a/pygitweb/auth.py b/pygitweb/auth.py
index 6da259e..a52747d 100644
--- a/pygitweb/auth.py
+++ b/pygitweb/auth.py
@@ -15,6 +15,7 @@ FastAPI security integration
 from __future__ import annotations
 
 import hmac
+from collections.abc import Callable
 from typing import Annotated
 
 from fastapi import APIRouter, Depends, Form, HTTPException, Query, status
@@ -194,7 +195,7 @@ def require_permission(
 	*,
 	project_from_query: bool = False,
 	project_from_form: bool = False,
-) -> object:
+) -> Callable[..., None]:
 	"""Return a FastAPI dependency that enforces ``permission`` when auth is enabled."""
 
 	if project_from_query:
diff --git a/pygitweb/change_queue.py b/pygitweb/change_queue.py
index 9594516..9eac96d 100644
--- a/pygitweb/change_queue.py
+++ b/pygitweb/change_queue.py
@@ -10,6 +10,7 @@ is notified or the timeout elapses.
 from __future__ import annotations
 
 import asyncio
+from typing import Literal
 
 
 class ChangeQueue:
@@ -36,8 +37,8 @@ class ChangeQueue:
 
 		if not projects:
 			if shutdown_event:
-				t_sleep: asyncio.Task[None] = asyncio.create_task(asyncio.sleep(timeout))
-				t_sd: asyncio.Task[None] = asyncio.create_task(shutdown_event.wait())
+				t_sleep: asyncio.Task[object] = asyncio.create_task(asyncio.sleep(timeout))
+				t_sd: asyncio.Task[object] = asyncio.create_task(shutdown_event.wait())
 				try:
 					done, pending = await asyncio.wait(
 						{t_sleep, t_sd},
@@ -58,8 +59,8 @@ class ChangeQueue:
 				self._waiters.setdefault(project, []).append(event)
 		try:
 			if shutdown_event:
-				t_notify: asyncio.Task[None] = asyncio.create_task(event.wait())
-				t_shutdown: asyncio.Task[None] = asyncio.create_task(shutdown_event.wait())
+				t_notify: asyncio.Task[Literal[True]] = asyncio.create_task(event.wait())
+				t_shutdown: asyncio.Task[Literal[True]] = asyncio.create_task(shutdown_event.wait())
 				try:
 					done, pending = await asyncio.wait(
 						{t_notify, t_shutdown},
diff --git a/pygitweb/conftest.py b/pygitweb/conftest.py
index 7151358..069f0f3 100644
--- a/pygitweb/conftest.py
+++ b/pygitweb/conftest.py
@@ -31,7 +31,7 @@ def client_as_user(client: TestClient, username: str) -> Generator[None, None, N
 
 
 @pytest.fixture(autouse=True)
-def _reset_pygitweb_app_lifecycle_state() -> None:
+def _reset_pygitweb_app_lifecycle_state() -> Generator[None]:
 	"""Starlette TestClient lifespan sets shutting_down on exit; httpx ASGITransport never runs lifespan.
 
 	Stale state made long-poll tests see ``shutting_down`` and skip ``wait_for_changes``, so notify woke 0 waiters.
diff --git a/pygitweb/main.py b/pygitweb/main.py
index 16f1cb6..aa892ea 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -24,6 +24,8 @@ from pygitweb import __meta__
 from pygitweb.actions import (
 	SearchFlagQuery,
 	SearchSortQuery,
+	SummaryRefsPayload,
+	SummaryRefState,
 	git_blame,
 	git_blame_raw,
 	git_blob,
@@ -253,7 +255,7 @@ def git_project_list(
 	_action: Annotated[ProjectListAction | None, Query(alias="a")] = None,
 	pf: Annotated[str | None, Query(alias="pf")] = None,
 	_order: Annotated[ProjectListOrder | None, Query(alias="o")] = None,
-):
+) -> HTMLResponse:
 	"""Project list page. Port of git_project_list."""
 	project_filter = pf or ""
 	all_projects = git_get_projects_list(
@@ -306,7 +308,7 @@ def git_project_list(
 def git_project_index(
 	token: Annotated[str | None, Depends(access_token_from_request)],
 	pf: Annotated[str | None, Query(alias="pf")] = None,
-):
+) -> PlainTextResponse:
 	"""Plain text project index (path owner). Port of git_project_index."""
 	from urllib.parse import quote_plus
 
@@ -398,7 +400,7 @@ def activity_page(
 
 
 @app.get("/opml", response_class=PlainTextResponse)
-def git_opml(token: Annotated[str | None, Depends(access_token_from_request)]):
+def git_opml(token: Annotated[str | None, Depends(access_token_from_request)]) -> PlainTextResponse:
 	"""OPML feed list. Port of git_opml (stub)."""
 	projects = filter_projects_by_read_access(
 		git_get_projects_list(export_ok=settings.EXPORT_OK),
@@ -453,7 +455,7 @@ def addproject_namevalid(
 @app.get("/addproject", response_class=HTMLResponse)
 def addproject_page(
 	token: Annotated[str | None, Depends(access_token_from_request)],
-):
+) -> HTMLResponse:
 	"""Add project form page."""
 	show_sign_in_notice = (
 		settings.AUTH and is_auth_configured(auth_config) and (not token or decode_access_token(token) is None)
@@ -481,7 +483,7 @@ async def addproject_submit(
 	perform_maintenance: Annotated[str, Form()] = "off",
 	create_task_board: Annotated[str, Form()] = "off",
 	repo_zip: UploadFile | None = _OPTIONAL_REPO_ZIP,
-):
+) -> RedirectResponse:
 	"""Create a new project (requires pgw.addprojects when auth is enabled)."""
 	project_name = (project_name or "").strip()
 	if not project_name:
@@ -567,7 +569,7 @@ def project_board_ref_manifest(
 
 
 @app.get("/project/{project:path}/summary-refs")
-def project_summary_refs(project: ValidatedReadableProject) -> dict[str, list[dict[str, str]]]:
+def project_summary_refs(project: ValidatedReadableProject) -> SummaryRefsPayload:
 	return {"options": summary_ref_options(project)}
 
 
@@ -575,7 +577,7 @@ def project_summary_refs(project: ValidatedReadableProject) -> dict[str, list[di
 def project_summary_ref_state(
 	project: ValidatedReadableProject,
 	ref: Annotated[str | None, Query(alias="ref")] = None,
-) -> dict[str, str]:
+) -> SummaryRefState:
 	return summary_ref_state(project, ref)
 
 
diff --git a/pygitweb/merge_requests.py b/pygitweb/merge_requests.py
index 47fcc4d..b8861f3 100644
--- a/pygitweb/merge_requests.py
+++ b/pygitweb/merge_requests.py
@@ -8,7 +8,7 @@ from urllib.parse import quote, urlencode
 
 import pygit2
 from fastapi import APIRouter, Depends, Form, HTTPException, Query
-from fastapi.responses import HTMLResponse, RedirectResponse, Response
+from fastapi.responses import HTMLResponse, RedirectResponse
 
 from pygittools.merge import MergeRequest, MergeRequestStatus, get_merge_request_by_oid
 from pygitweb.auth import access_token_from_request, principal_from_session, require_permission
@@ -106,7 +106,7 @@ def merge_request_tag_response(
 	tagger: str,
 	tagger_date: str,
 	notice: str | None = None,
-) -> Response:
+) -> HTMLResponse | RedirectResponse:
 	new_oid = mr.refresh_from_theirs(repo)
 	if new_oid is not None:
 		return RedirectResponse(
diff --git a/pygitweb/settings_config.py b/pygitweb/settings_config.py
index 13d8450..d4c0714 100644
--- a/pygitweb/settings_config.py
+++ b/pygitweb/settings_config.py
@@ -72,11 +72,11 @@ def _patch_settings_setattr() -> None:
 
 	original = Settings.__setattr__
 
-	def _setattr(self: Settings, name: str, value: object) -> None:
+	def _setattr(self: object, name: str, value: object) -> None:
 		original(self, name, value)
 		if not _persist_enabled or _batch_update:
 			return
-		if name not in Settings.model_fields:
+		if name not in Settings.model_fields or not isinstance(self, Settings):
 			return
 		write_settings_file(self)
 
diff --git a/pygitweb/shutdown.py b/pygitweb/shutdown.py
index 257c0fa..5b63fab 100644
--- a/pygitweb/shutdown.py
+++ b/pygitweb/shutdown.py
@@ -7,6 +7,8 @@ import signal
 import sys
 import threading
 from collections.abc import Callable
+from types import FrameType
+from typing import Any
 
 from fastapi import FastAPI
 
@@ -42,9 +44,9 @@ def install_graceful_shutdown_wakeup(app: FastAPI) -> bool:
 	for sig in signals:
 
 		def make_chain(
-			previous: Callable[[int, object | None], object] | int | None,
-		) -> Callable[[int, object | None], None]:
-			def handler(signum: int, frame: object | None) -> None:
+			previous: Callable[[int, FrameType | None], Any] | int | signal.Handlers | None,
+		) -> Callable[[int, FrameType | None], Any]:
+			def handler(signum: int, frame: FrameType | None) -> None:
 				begin_shutdown(app)
 				if callable(previous) and previous not in (signal.SIG_DFL, signal.SIG_IGN):
 					previous(signum, frame)
diff --git a/pygitweb/tasks.py b/pygitweb/tasks.py
index 7da4150..d411b84 100644
--- a/pygitweb/tasks.py
+++ b/pygitweb/tasks.py
@@ -484,7 +484,7 @@ def _principal_matches_comment_tagger(tagger: str, principal: PermissionPrincipa
 	name, email = _parse_tagger_display(raw)
 	valid_email = email and email.lower() == identity.lower()
 	valid_name = name and (name == identity or name.lower() == identity.lower())
-	return valid_email or valid_name
+	return bool(valid_email or valid_name)
 
 
 def _ensure_comment_modify_allowed(project: str, token: str | None, tagger: str) -> None:
