diff --git a/pygitweb/actions.py b/pygitweb/actions.py
index bf4c74b..d4f659c 100644
--- a/pygitweb/actions.py
+++ b/pygitweb/actions.py
@@ -1057,7 +1057,7 @@ def git_tags(project: str, request: Request, page: int, pagecount: int) -> HTMLR
 	)
 
 
-def git_tag(project: str, h: str | None) -> HTMLResponse:
+def git_tag(project: str, h: str | None, notice: str | None = None) -> HTMLResponse:
 	"""Single tag view. Port of git_tag."""
 	if not h:
 		raise HTTPException(status_code=400, detail="Tag ref or hash (h) required")
@@ -1101,6 +1101,7 @@ def git_tag(project: str, h: str | None) -> HTMLResponse:
 			tag_oid=tag_oid,
 			tagger=tagger,
 			tagger_date=_format_date(tagger_epoch, tagger_tz),
+			notice=notice,
 		)
 	target_short = target_oid[:7] if target_oid else ""
 	if target_type == "commit":
diff --git a/pygitweb/git_helpers.py b/pygitweb/git_helpers.py
index e24663f..fc32bde 100644
--- a/pygitweb/git_helpers.py
+++ b/pygitweb/git_helpers.py
@@ -17,6 +17,10 @@ import pygit2
 
 from pygitweb.config import settings
 from pygitweb.formatting import to_utf8
+from pygitweb.permissions import PermissionPrincipal
+
+_DEFAULT_MR_OPENER = pygit2.Signature("PyGitWeb", "noreply@pygitweb.local")
+_REPO_CONFIG_LEVELS = frozenset({5, 6})  # git_config_level_t local, worktree
 
 # Common README filenames to look for (order matters: prefer README.md)
 README_CANDIDATES = ["README.md", "README", "readme.md", "readme", "Readme.md"]
@@ -32,6 +36,61 @@ def open_repo(project: str):
 	return pygit2.Repository(path)
 
 
+def signature_from_principal_email(principal: PermissionPrincipal | None) -> pygit2.Signature | None:
+	if principal is None:
+		return None
+	email = (principal.email or "").strip()
+	if not email:
+		return None
+	name = principal.username.strip() or email
+	return pygit2.Signature(name, email)
+
+
+def repo_git_signature(repo: pygit2.Repository) -> pygit2.Signature | None:
+	by_name: dict[str, str] = {}
+	for entry in repo.config:
+		if entry.level not in _REPO_CONFIG_LEVELS:
+			continue
+		by_name[entry.name] = entry.value or ""
+	name = by_name.get("user.name", "").strip()
+	email = by_name.get("user.email", "").strip()
+	if name and email:
+		return pygit2.Signature(name, email)
+	return None
+
+
+def signature_from_user_credentials(principal: PermissionPrincipal | None) -> pygit2.Signature | None:
+	if principal is None:
+		return None
+	username = principal.username.strip()
+	if not username:
+		return None
+	return pygit2.Signature(username, f"{username}@local")
+
+
+def resolve_mr_opener(
+	repo: pygit2.Repository,
+	principal: PermissionPrincipal | None,
+) -> tuple[pygit2.Signature, str | None]:
+	"""Resolve MR opener: IdP/session email, then repo git user, then logged-in user, then default."""
+	sig = signature_from_principal_email(principal)
+	if sig is not None:
+		return sig, None
+	sig = repo_git_signature(repo)
+	if sig is not None:
+		return sig, None
+	sig = signature_from_user_credentials(principal)
+	if sig is not None:
+		return sig, None
+	default = _DEFAULT_MR_OPENER
+	warning = (
+		"No opener identity was available (signed-in email, repository "
+		"user.name/user.email, or user credentials). Used default: "
+		f"{default.name} <{default.email}>."
+	)
+	return default, warning
+
+
 def git_get_type(project: str, ref: str) -> str | None:
 	"""Object type: commit, tree, blob, tag. Port of git_get_type (pygit2 obj.type)."""
 	try:
diff --git a/pygitweb/git_helpers_test.py b/pygitweb/git_helpers_test.py
index 6756096..5b568b2 100644
--- a/pygitweb/git_helpers_test.py
+++ b/pygitweb/git_helpers_test.py
@@ -32,7 +32,10 @@ from pygitweb.git_helpers import (
 	hash_set_multi,
 	parse_commit,
 	parse_tag,
+	resolve_mr_opener,
+	signature_from_principal_email,
 )
+from pygitweb.permissions import PermissionPrincipal
 
 
 def _run_git(repo_dir: Path, *args: str) -> str:
@@ -221,3 +224,44 @@ class TestGitHelpers:
 			assert "docs/guide.txt" in commit_diff
 			assert "src/app.py" in commit_diff
 			assert oid_short == repo_ctx["second_commit"][:7]
+
+
+class TestResolveMrOpener:
+	def test_prefers_principal_email(self, tmp_path: Path) -> None:
+		repo = pygit2.init_repository(str(tmp_path / "bare"), bare=True)
+		principal = PermissionPrincipal(username="alice", email="alice@idp.example")
+		opener, warning = resolve_mr_opener(repo, principal)
+		assert warning is None
+		assert opener.email == "alice@idp.example"
+		assert opener.name == "alice"
+
+	def test_falls_back_to_repo_git_user(self, tmp_path: Path) -> None:
+		repo_dir = tmp_path / "configured"
+		pygit2.init_repository(str(repo_dir), bare=True)
+		config_path = repo_dir / "config"
+		config_path.write_text(
+			f"{config_path.read_text(encoding='utf-8')}\n[user]\n\tname = Repo Bot\n\temail = bot@example.com\n",
+			encoding="utf-8",
+		)
+		repo = pygit2.Repository(str(repo_dir))
+		opener, warning = resolve_mr_opener(repo, None)
+		assert warning is None
+		assert opener.email == "bot@example.com"
+
+	def test_falls_back_to_user_credentials(self, tmp_path: Path) -> None:
+		repo = pygit2.init_repository(str(tmp_path / "bare"), bare=True)
+		principal = PermissionPrincipal(username="bob", email=None)
+		opener, warning = resolve_mr_opener(repo, principal)
+		assert warning is None
+		assert opener.name == "bob"
+		assert opener.email == "bob@local"
+
+	def test_uses_default_with_warning(self, tmp_path: Path) -> None:
+		repo = pygit2.init_repository(str(tmp_path / "bare"), bare=True)
+		opener, warning = resolve_mr_opener(repo, None)
+		assert warning is not None
+		assert opener.email == "noreply@pygitweb.local"
+
+	def test_signature_from_principal_email_requires_email(self) -> None:
+		assert signature_from_principal_email(PermissionPrincipal(username="x", email=None)) is None
+		assert signature_from_principal_email(PermissionPrincipal(username="x", email="  ")) is None
diff --git a/pygitweb/main.py b/pygitweb/main.py
index 16f15a5..4fce3a0 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -770,6 +770,7 @@ async def dispatch(
 	search_multiline: Annotated[SearchFlagQuery | None, Query(alias="multiline")] = None,
 	search_sort: Annotated[SearchSortQuery | None, Query(alias="sort")] = None,
 	search_max_count: Annotated[str | None, Query(alias="max_count")] = None,
+	notice: Annotated[str | None, Query(alias="notice")] = None,
 ):
 	"""
 	Dispatch by path: /project/{project} -> summary; /project/{project}/action/... -> action.
@@ -852,7 +853,7 @@ async def dispatch(
 		p, pc = parse_pagination(page, pagecount)
 		return git_tags(project, request, p, pc)
 	if action == "tag":
-		return git_tag(project, hash_param)
+		return git_tag(project, hash_param, notice=notice)
 	if action == "commit":
 		return git_commit(project, hash_param)
 	if action == "patch":
diff --git a/pygitweb/merge_requests.py b/pygitweb/merge_requests.py
index 77c6863..b3c77aa 100644
--- a/pygitweb/merge_requests.py
+++ b/pygitweb/merge_requests.py
@@ -3,18 +3,18 @@
 from __future__ import annotations
 
 import json
-from typing import Any
-from urllib.parse import quote
+from typing import Annotated, Any
+from urllib.parse import quote, urlencode
 
 import pygit2
 from fastapi import APIRouter, Depends, Form, HTTPException, Query
 from fastapi.responses import HTMLResponse, RedirectResponse, Response
 
 from pygittools.merge import MergeRequest, MergeRequestStatus, get_merge_request_by_oid
-from pygitweb.auth import require_permission
+from pygitweb.auth import access_token_from_request, principal_from_session, require_permission
 from pygitweb.config import settings
 from pygitweb.dependencies import ValidatedFormProject, ValidatedQueryProject
-from pygitweb.git_helpers import open_repo
+from pygitweb.git_helpers import open_repo, resolve_mr_opener
 from pygitweb.permissions import Permission
 from pygitweb.templates_env import POSTAMBLE, PREAMBLE, env, jinja_escape
 from pygitweb.validation import is_valid_ref_format
@@ -105,6 +105,7 @@ def merge_request_tag_response(
 	tag_oid: str,
 	tagger: str,
 	tagger_date: str,
+	notice: str | None = None,
 ) -> Response:
 	new_oid = mr.refresh_from_theirs(repo)
 	if new_oid is not None:
@@ -149,6 +150,7 @@ def merge_request_tag_response(
 		"updated_at": mr.updated_at.isoformat() if mr.updated_at else "",
 		"comments": comments_rows,
 		"merge_action_url": f"/mr/ff?project={quote(project, safe='')}&h={quote(current_tag_oid, safe='')}",
+		"notice": notice or "",
 		**status_fields,
 	}
 	title = f"Merge request: {jinja_escape(mr.title) or ''} — {jinja_escape(project) or ''}"
@@ -166,6 +168,7 @@ def merge_request_create(
 	theirs: str = Form(...),
 	ours: str = Form(...),
 	title: str = Form(""),
+	token: Annotated[str | None, Depends(access_token_from_request)] = None,
 ) -> RedirectResponse:
 	theirs_ref = theirs.strip()
 	ours_ref = ours.strip()
@@ -182,7 +185,7 @@ def merge_request_create(
 		repo.revparse_single(ours_ref)
 	except (KeyError, ValueError, pygit2.GitError) as exc:
 		raise HTTPException(status_code=404, detail=f"Cannot resolve ours ref {ours_ref!r}") from exc
-	opener = repo.default_signature
+	opener, opener_warning = resolve_mr_opener(repo, principal_from_session(token))
 	t = title.strip()
 	if not t:
 		t = f"Merge {_branch_short_name(theirs_ref)} into {_branch_short_name(ours_ref)}"
@@ -195,7 +198,10 @@ def merge_request_create(
 		description="",
 	)
 	oid = mr.write(repo)
-	dest = f"/project/{project}?a=tag&h={quote(str(oid), safe='')}"
+	params: dict[str, str] = {"a": "tag", "h": str(oid)}
+	if opener_warning:
+		params["notice"] = opener_warning
+	dest = f"/project/{project}?{urlencode(params)}"
 	return RedirectResponse(url=dest, status_code=303)
 
 
diff --git a/pygitweb/mr_permissions_test.py b/pygitweb/mr_permissions_test.py
index 62f726d..4292f2a 100644
--- a/pygitweb/mr_permissions_test.py
+++ b/pygitweb/mr_permissions_test.py
@@ -10,11 +10,13 @@ from fastapi import FastAPI
 from fastapi.testclient import TestClient
 
 from pygittools.merge import MR_REF_PREFIX, MergeRequest
+from pygitweb.auth import ACCESS_TOKEN_COOKIE_NAME
 from pygitweb.auth_config import AuthConfig, LocalUser
 from pygitweb.config import settings
 from pygitweb.conftest import client_as_user
 from pygitweb.merge_requests import merge_router
 from pygitweb.permissions import Permission, PermissionsMap, permission_key
+from pygitweb.sessions import create_session
 
 
 def _commit_chain(repo: pygit2.Repository) -> tuple[pygit2.Oid, pygit2.Oid]:
@@ -32,7 +34,7 @@ _MR_AUTH = AuthConfig(
 	local_users=[LocalUser(user="creator", password="secret"), LocalUser(user="merger", password="secret")],
 	oauth_permissions=PermissionsMap.model_validate({
 		"*": [],
-		permission_key(Permission.MR_CREATE, "demo"): ["creator"],
+		permission_key(Permission.MR_CREATE, "demo"): ["creator", "creator@idp.example"],
 		permission_key(Permission.MR_MERGE, "demo"): ["merger"],
 	}),
 )
@@ -146,3 +148,81 @@ def test_mr_ff_allowed_with_merge_grant(mr_env: dict[str, str]) -> None:
 			follow_redirects=False,
 		)
 	assert r.status_code == 303
+
+
+def _bare_repo_with_branch(tmp_path: Path) -> tuple[Path, pygit2.Oid, pygit2.Oid]:
+	root = tmp_path / "bare-mr-create"
+	root.mkdir()
+	repo_dir = root / "demo"
+	repo = pygit2.init_repository(str(repo_dir), bare=True)
+	a, b = _commit_chain(repo)
+	repo.create_reference("refs/heads/feature", b)
+	return root, a, b
+
+
+def test_mr_create_bare_repo_uses_principal_email(tmp_path: Path) -> None:
+	root, _a, _b = _bare_repo_with_branch(tmp_path)
+	projects_list = root / "projects.list"
+	projects_list.write_text("demo tester\n", encoding="utf-8")
+	client = _build_client()
+	with (
+		patch.object(settings, "PROJECTROOT", str(root)),
+		patch.object(settings, "PROJECTS_LIST", str(projects_list)),
+		patch.object(settings, "STRICT_EXPORT", False),
+		patch.object(settings, "EXPORT_OK", ""),
+		patch("pygitweb.auth_config.auth_config", _MR_AUTH),
+		patch("pygitweb.auth.auth_config", _MR_AUTH),
+		patch.object(settings, "AUTH", True),
+	):
+		client.cookies.set(
+			ACCESS_TOKEN_COOKIE_NAME,
+			create_session("creator", email="creator@idp.example", auth_method="local"),
+		)
+		r = client.post(
+			"/mr/create",
+			data={
+				"project": "demo",
+				"theirs": "refs/heads/feature",
+				"ours": "refs/heads/main",
+				"title": "Bare MR",
+			},
+			follow_redirects=False,
+		)
+	assert r.status_code == 303
+	location = r.headers["location"]
+	assert "notice=" not in location
+	repo = pygit2.Repository(str(root / "demo"))
+	for ref_name in repo.references:
+		if ref_name.startswith("refs/tags/mr/"):
+			tag = repo[repo.references[ref_name].resolve().target]
+			assert isinstance(tag, pygit2.Tag)
+			assert tag.tagger is not None
+			assert tag.tagger.email == "creator@idp.example"
+			return
+	raise AssertionError("merge request tag not created")
+
+
+def test_mr_create_bare_repo_without_identity_shows_notice(tmp_path: Path) -> None:
+	root, _a, _b = _bare_repo_with_branch(tmp_path)
+	projects_list = root / "projects.list"
+	projects_list.write_text("demo tester\n", encoding="utf-8")
+	client = _build_client()
+	with (
+		patch.object(settings, "PROJECTROOT", str(root)),
+		patch.object(settings, "PROJECTS_LIST", str(projects_list)),
+		patch.object(settings, "STRICT_EXPORT", False),
+		patch.object(settings, "EXPORT_OK", ""),
+		patch.object(settings, "AUTH", False),
+	):
+		r = client.post(
+			"/mr/create",
+			data={
+				"project": "demo",
+				"theirs": "refs/heads/feature",
+				"ours": "refs/heads/main",
+				"title": "Bare MR",
+			},
+			follow_redirects=False,
+		)
+	assert r.status_code == 303
+	assert "notice=" in r.headers["location"]
diff --git a/pygitweb/templates/merge_request_tag.html b/pygitweb/templates/merge_request_tag.html
index ee7ea44..32726f3 100644
--- a/pygitweb/templates/merge_request_tag.html
+++ b/pygitweb/templates/merge_request_tag.html
@@ -2,6 +2,10 @@
 
 <h1 class="page-title mb-3">Merge request</h1>
 
+{% if notice %}
+<div class="alert board-task-error-hint py-2 px-3 mb-3" role="alert">{{ notice }}</div>
+{% endif %}
+
 <p class="text-muted mb-4">
   Project: <a href="{{ project_url }}" class="text-reset">{{ project }}</a>
   · Tag: <a href="{{ tag_view_url }}" class="text-reset">{{ tag_name }}</a>
