diff --git a/distgit/tasks.py b/distgit/tasks.py
index e81b030..7cd5512 100644
--- a/distgit/tasks.py
+++ b/distgit/tasks.py
@@ -50,7 +50,7 @@ def _tagger_str(tagger: str | Signature | None) -> str:
 
 
 def _build_tag_raw(
-	target: Oid,
+	target: Oid | str,
 	name: str,
 	tagger: str,
 	message: str,
@@ -60,7 +60,8 @@ def _build_tag_raw(
 
 	libgit2 prepends 'tag <len>\\0'. object_type must match the target (e.g. 'commit' or 'tree').
 	"""
-	content = f"object {str(target)}\ntype {object_type}\ntag {name}\ntagger {_tagger_str(tagger)}\n\n{message}"
+	hex_str = str(target) if isinstance(target, Oid) else target
+	content = f"object {hex_str}\ntype {object_type}\ntag {name}\ntagger {_tagger_str(tagger)}\n\n{message}"
 	return content.encode("utf-8")
 
 
@@ -99,7 +100,7 @@ class Task:
 
 	def __init__(
 		self,
-		target: Oid,
+		target: Oid | str,
 		name: str,
 		tagger: str,
 		title: str,
@@ -125,7 +126,7 @@ class Task:
 		self.assignee = assignee
 		self.due_date = due_date
 		self.created_at = datetime.now()
-		self.comments = []  # Comment OIDs
+		self.comments: list[Oid | str] = []  # Comment OIDs
 		self.update_message()
 
 	def update_message(self) -> None:
@@ -174,7 +175,7 @@ def get_task(repo: Repository, ref: str) -> Task | None:
 	t = Task(
 		tag.target,
 		tag.name,
-		tag.tagger,
+		str(tag.tagger),
 		j.get("title", "Untitled"),
 		description=j.get("description", ""),
 		status=status,
@@ -199,7 +200,7 @@ def get_task_by_oid(repo: Repository, oid: Oid) -> Task | None:
 	t = Task(
 		obj.target,
 		obj.name,
-		obj.tagger,
+		str(obj.tagger),
 		j.get("title", "Untitled"),
 		description=j.get("description", ""),
 		status=status,
@@ -239,7 +240,7 @@ class Comment:
 		self.name = name
 		self.content = content
 		self.created_at = datetime.now()
-		self.edited_at = None
+		self.edited_at = self.created_at
 		self.update_message()
 
 	def update_message(self) -> None:
@@ -275,7 +276,7 @@ def get_comment(repo: Repository, oid: Oid) -> Comment | None:
 	if not isinstance(obj, Tag):
 		raise ValueError(f"Requested comment is not a tag: {oid}")
 	j = json.loads(obj.message)
-	c = Comment(obj.target, obj.tagger, j["content"], obj.name)
+	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"))
 	return c
@@ -291,7 +292,7 @@ The "tasks" field is an array of task OIDs.
 class Board:
 	def __init__(
 		self,
-		target: Oid,
+		target: Oid | str,
 		name: str,
 		tagger: str,
 		description: str = "",
@@ -302,7 +303,7 @@ class Board:
 		self.description = description
 		self.created_at = datetime.now()
 		self.updated_at = datetime.now()
-		self.tasks = []  # Task OIDs
+		self.tasks: list[Oid | str] = []  # Task OIDs
 		self.update_message()
 
 	def update_message(self) -> None:
@@ -330,7 +331,7 @@ def get_board(repo: Repository, ref: str) -> Board | None:
 	if not isinstance(tag, Tag):
 		return None
 	j = json.loads(tag.message)
-	b = Board(tag.target, tag.name, tag.tagger, j.get("description", ""))
+	b = Board(tag.target, tag.name, str(tag.tagger), j.get("description", ""))
 	b.tasks = j.get("tasks", [])
 	b.created_at = _parse_dt(j.get("created_at")) or b.created_at
 	b.updated_at = _parse_dt(j.get("updated_at")) or b.updated_at
diff --git a/pygitweb/actions.py b/pygitweb/actions.py
index 913dd3d..185a647 100644
--- a/pygitweb/actions.py
+++ b/pygitweb/actions.py
@@ -201,8 +201,8 @@ def _render_readme_card(
 		return card
 	return (
 		'<div class="card mt-3"><div class="card-header"><h2 class="card-title">README</h2></div>'
-		'<div class="card-body"><pre class="readme-plain"><code>'
-		+ esc_html(sanitize(readme_content) or "")
+		+ '<div class="card-body"><pre class="readme-plain"><code>'
+		+ (esc_html(sanitize(readme_content)) or "")
 		+ "</code></pre></div></div>"
 	)
 
@@ -391,11 +391,11 @@ def git_tree(project: str, h: str | None, f: str | None) -> HTMLResponse:
 	dirs = sorted((n, t, o) for n, t, o in entries if t == "tree")
 	blobs = sorted((n, t, o) for n, t, o in entries if t == "blob")
 	rows = []
-	for name, _typ, _ in dirs:
+	for name, _, _ in dirs:
 		sub_path = f"{f.strip('/')}/{name}".strip("/") if f else name
 		link = _tree_url(project, ref_oid, sub_path)
 		rows.append([f'<a href="{link}">{esc_html(name)}/</a>', "tree"])
-	for name, _typ, _ in blobs:
+	for name, _, _ in blobs:
 		sub_path = f"{f.strip('/')}/{name}".strip("/") if f else name
 		link = _tree_url(project, ref_oid, sub_path, a="blob")
 		rows.append([f'<a href="{link}">{esc_html(name)}</a>', "blob"])
@@ -612,7 +612,7 @@ def git_tag(project: str, h: str | None) -> HTMLResponse:
 	elif target_type == "tree":
 		object_link = f'<a href="/project/{project}?a=tree&h={quote(target_oid, safe="")}">{esc_html(target_short)}</a>'
 	else:
-		object_link = esc_html(target_short)
+		object_link = esc_html(target_short) or ""
 	table_rows = [
 		["Tag", esc_html(tag_name)],
 		["Object", f"{object_link} ({esc_html(target_type)})"],
diff --git a/pygitweb/config.py b/pygitweb/config.py
index 4f30c11..758ef9e 100644
--- a/pygitweb/config.py
+++ b/pygitweb/config.py
@@ -110,15 +110,15 @@ OIDCProvider: TODO - Will authenticate against an OIDC server.
 MatrixProvider: TODO - Will authenticate against the Matrix network.
 LDAPProvider: TODO - Will authenticate against a local LDAP server.
 """
-DISTGIT_AUTH: str = os.getenv("DISTGIT_AUTH", None)
-DISTGIT_ADMIN_USER: str = os.getenv("DISTGIT_ADMIN_USER", None)
-DISTGIT_ADMIN_PASSWORD: str = os.getenv("DISTGIT_ADMIN_PASSWORD", None)
-DISTGIT_SESSION_TIMEOUT: str = os.getenv("DISTGIT_SESSION_TIMEOUT", 3600 * 24 * 7)
+DISTGIT_AUTH: str | None = os.getenv("DISTGIT_AUTH", None)
+DISTGIT_ADMIN_USER: str | None = os.getenv("DISTGIT_ADMIN_USER", None)
+DISTGIT_ADMIN_PASSWORD: str | None = os.getenv("DISTGIT_ADMIN_PASSWORD", None)
+DISTGIT_SESSION_TIMEOUT: str | int | None = os.getenv("DISTGIT_SESSION_TIMEOUT", 3600 * 24 * 7)
 
 # Config file paths (can be overridden by env)
-GITWEB_CONFIG = os.getenv("GITWEB_CONFIG", "")
-GITWEB_CONFIG_SYSTEM = os.getenv("GITWEB_CONFIG_SYSTEM", "")
-GITWEB_CONFIG_COMMON = os.getenv("GITWEB_CONFIG_COMMON", "")
+GITWEB_CONFIG: str | None = os.getenv("GITWEB_CONFIG", None)
+GITWEB_CONFIG_SYSTEM: str | None = os.getenv("GITWEB_CONFIG_SYSTEM", None)
+GITWEB_CONFIG_COMMON: str | None = os.getenv("GITWEB_CONFIG_COMMON", None)
 
 # Snapshot formats (from %known_snapshot_formats)
 KNOWN_SNAPSHOT_FORMATS: dict[str, dict[str, Any]] = {
diff --git a/pygitweb/git_helpers.py b/pygitweb/git_helpers.py
index 859423f..c215cf2 100644
--- a/pygitweb/git_helpers.py
+++ b/pygitweb/git_helpers.py
@@ -139,7 +139,7 @@ def git_get_project_config(
 	project: str,
 	key: str,
 	config_type: str | None = None,
-) -> str | list[str] | bool | None:
+) -> int | str | list[str] | bool | None:
 	"""Single config value; gitweb.* section. Port of git_get_project_config."""
 	key = key.lower().replace("_", "")
 	if key.startswith("gitweb."):
@@ -238,7 +238,8 @@ def git_get_path_by_hash(project: str, base: str, oid_str: str) -> str | None:
 
 		def find_in_tree(t: pygit2.Tree, prefix: str) -> str | None:
 			for e in t:
-				p = f"{prefix}{e.name}" if prefix else e.name
+				name = e.name or ""
+				p = f"{prefix}{name}" if prefix else name
 				if str(e.id) == oid_str:
 					return p
 				if e.type_str == "tree":
@@ -424,7 +425,7 @@ def get_commit_history(
 		if not start_oid:
 			return []
 
-		commits = []
+		commits: list[dict[str, Any]] = []
 		skipped = 0
 		walker = repo.walk(start_oid, pygit2.GIT_SORT_TIME | pygit2.GIT_SORT_TOPOLOGICAL)
 
diff --git a/pygitweb/settings.py b/pygitweb/settings.py
index 1485e2f..f9536e4 100644
--- a/pygitweb/settings.py
+++ b/pygitweb/settings.py
@@ -337,7 +337,7 @@ async def settings_pygitweb_submit(request: Request):
 	form = await request.form()
 
 	def _get(k: str) -> str:
-		return (form.get(k) or "").strip()
+		return str(form.get(k) or "").strip() or ""
 
 	config.PROJECTROOT = _get("PROJECTROOT") or config.PROJECTROOT
 	config.PROJECTS_LIST = _get("PROJECTS_LIST") or config.PROJECTS_LIST
@@ -386,14 +386,14 @@ async def settings_pygit2_submit(request: Request):
 			continue
 		try:
 			if kind == "bool":
-				val = (form_value or "").strip().lower() in ("on", "1", "true", "yes")
-				setattr(st, attr, val)
+				_ = str(form_value or "").strip().lower() in ("on", "1", "true", "yes")
+				setattr(st, attr, _)
 			elif kind == "int":
-				s = (form_value or "").strip()
-				val = int(s) if s else 0
-				setattr(st, attr, val)
+				s = str(form_value or "").strip()
+				_ = int(s) if s else 0
+				setattr(st, attr, _)
 			else:
-				setattr(st, attr, (form_value or "").strip() or None)
+				setattr(st, attr, str(form_value).strip() if form_value else None)
 		except (TypeError, AttributeError, ValueError):
 			continue
 	return RedirectResponse(url="/settings/pygit2", status_code=303)
@@ -462,8 +462,8 @@ async def settings_project_submit(request: Request, name: str):
 				indices.add(int(form_key.split("_")[-1]))
 	pairs = []
 	for i in sorted(indices):
-		k = (form.get(f"config_key_{i}") or "").strip()
-		v = (form.get(f"config_value_{i}") or "").strip()
+		k = str(form.get(f"config_key_{i}") or "").strip() or ""
+		v = str(form.get(f"config_value_{i}") or "").strip() or ""
 		if k:
 			pairs.append((k, v))
 	repo = open_repo(project)
diff --git a/pygitweb/tasks.py b/pygitweb/tasks.py
index 77d31d7..c3cdecf 100644
--- a/pygitweb/tasks.py
+++ b/pygitweb/tasks.py
@@ -69,7 +69,7 @@ def _task_ref(task_id: str) -> str:
 	return f"{TASK_REF_PREFIX}{task_id}"
 
 
-def _repo_head_or_empty(repo: pygit2.Repository) -> pygit2.Oid:
+def _repo_head_or_empty(repo: pygit2.Repository) -> pygit2.Oid | str:
 	if repo.head:
 		return repo.head.target
 	return EMPTY_TREE_OID
