diff --git a/CHANGES.md b/CHANGES.md
index f6096ea..7524770 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,29 @@
 # Changelog
 
+## 0.2.3 (2026-08-12)
+
+### pygittools
+
+- Fix an issue where under some conditions, the default auth config would always load instead of the on-disk config.
+
+## 0.2.2 (2026-08-09)
+
+### pygittools
+
+- Add tag support to the official task schema.
+- Fix `pgt` worktree removals by using `index.remove` instead of `index.add`.
+- Split MCP-related dependencies into a separate `pygittools-mcp` package.
+
+### pygitweb
+
+- Add per-project and per-branch read/write permissions for repository access.
+- Display a user’s preferred username (or configured email) instead of a raw user ID.
+- Move the `pygitweb_pytesthtml` plugin into a separate project to decouple testing utilities.
+
+### Tests and docs
+
+- Improve OAuth2 coverage in `pygitweb` by mocking an identity provider.
+
 ## 0.2.1 (2026-07-26)
 
 ### pygittools
diff --git a/pygittools-mcp/pyproject.toml b/pygittools-mcp/pyproject.toml
index f9b421c..7360c9b 100644
--- a/pygittools-mcp/pyproject.toml
+++ b/pygittools-mcp/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "pygittools-mcp"
-version = "0.1.0"
+version = "0.2.1"
 description = "MCP server exposing pygittools task boards to AI clients."
 requires-python = ">=3.11"
 dependencies = [
diff --git a/pygittools/pyproject.toml b/pygittools/pyproject.toml
index 23ef0e5..0ee04d0 100644
--- a/pygittools/pyproject.toml
+++ b/pygittools/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "pygittools"
-version = "0.2.1"
+version = "0.2.2"
 description = "Hooks and task storage in Git"
 readme = "README.md"
 license = "MIT"
diff --git a/pygittools/tasks.py b/pygittools/tasks.py
index ebbef5f..dc39a83 100644
--- a/pygittools/tasks.py
+++ b/pygittools/tasks.py
@@ -108,6 +108,7 @@ class Task:
 		priority: Priority | None = None,
 		assignee: str | None = None,
 		due_date: datetime | None = None,
+		tags: str = "",
 	):
 		if name is None:
 			name = f"{TASK_REF_PREFIX}{title.lower().replace(' ', '_')}"
@@ -124,6 +125,7 @@ class Task:
 		self.priority = priority
 		self.assignee = assignee
 		self.due_date = due_date
+		self.tags = tags
 		self.created_at = datetime.now()
 		self.comments: list[Oid | str] = []  # Comment OIDs
 		self.update_message()
@@ -180,6 +182,7 @@ def get_task(repo: Repository, ref: str) -> Task | None:
 		priority=priority,
 		assignee=j.get("assignee"),
 		due_date=_parse_dt(j.get("due_date")),
+		tags=j.get("tags", "") or "",
 	)
 	t.comments = j.get("comments", [])
 	t.created_at = _parse_dt(j.get("created_at")) or t.created_at
@@ -205,6 +208,7 @@ def get_task_by_oid(repo: Repository, oid: Oid) -> Task | None:
 		priority=priority,
 		assignee=j.get("assignee"),
 		due_date=_parse_dt(j.get("due_date")),
+		tags=j.get("tags", "") or "",
 	)
 	t.comments = j.get("comments", [])
 	t.created_at = _parse_dt(j.get("created_at")) or t.created_at
diff --git a/pygitweb/__meta__.py b/pygitweb/__meta__.py
index 00e334d..83f5cf4 100644
--- a/pygitweb/__meta__.py
+++ b/pygitweb/__meta__.py
@@ -2,7 +2,7 @@
 Metadata for PyGitWeb - this is the canonical source of all information below.
 """
 
-__version__ = "0.2.0"
+__version__ = "0.2.3"
 __author__ = "Will Bowers"
 __license__ = "MIT"
 __description__ = "FastAPI + Pygit2 Repo Browser"
diff --git a/pygitweb/auth.py b/pygitweb/auth.py
index ded4bf3..6da259e 100644
--- a/pygitweb/auth.py
+++ b/pygitweb/auth.py
@@ -23,13 +23,12 @@ from fastapi.security import APIKeyCookie, OAuth2PasswordBearer, OAuth2PasswordR
 from pydantic import BaseModel
 
 from pygitweb.auth_config import (
-	auth_config,
 	is_auth_configured,
 	is_local_auth_available,
 	is_oauth_auth_available,
 )
 from pygitweb.auth_oauth import OAUTH_STATE_COOKIE_NAME, oauth_router
-from pygitweb.config import settings
+from pygitweb.config import auth_config, settings
 from pygitweb.gravatar import gravatar_url
 from pygitweb.password_hash import verify_password
 from pygitweb.permissions import Permission, PermissionPrincipal, has_branch_read_permission
diff --git a/pygitweb/auth_config.py b/pygitweb/auth_config.py
index e550037..97398aa 100644
--- a/pygitweb/auth_config.py
+++ b/pygitweb/auth_config.py
@@ -94,7 +94,7 @@ class AuthConfig(BaseModel):
 	oauth_client_id: str = ""
 	oauth_client_secret: str = ""
 	oauth_redirect_uri: str = ""
-	oauth_username_from: OAuthUsernameFrom = "email"
+	oauth_username_from: OAuthUsernameFrom = "preffered_username"
 	oauth_allowed_emails: list[str] = Field(default_factory=list)
 	oauth_permissions: PermissionsMap = Field(default_factory=PermissionsMap.empty)
 	local_users: list[LocalUser] = Field(default_factory=list)
@@ -135,7 +135,7 @@ def default_auth_config(*, admin_password: str, salt: str) -> AuthConfig:
 		oauth_client_id="",
 		oauth_client_secret="",
 		oauth_redirect_uri="",
-		oauth_username_from="email",
+		oauth_username_from="preffered_username",
 		oauth_allowed_emails=[],
 		oauth_permissions=PermissionsMap.default_access(),
 		local_users=[LocalUser(user="admin", pass_hash=hash_password(admin_password, config_salt=salt))],
diff --git a/pygitweb/auth_oauth.py b/pygitweb/auth_oauth.py
index 5c283f9..4d732ef 100644
--- a/pygitweb/auth_oauth.py
+++ b/pygitweb/auth_oauth.py
@@ -23,8 +23,8 @@ import httpx
 from fastapi import APIRouter, HTTPException, Query, Request, status
 from fastapi.responses import RedirectResponse
 
-from pygitweb.auth_config import AuthConfig, OAuthUsernameFrom, auth_config, is_oauth_auth_available
-from pygitweb.config import settings
+from pygitweb.auth_config import AuthConfig, OAuthUsernameFrom, is_oauth_auth_available
+from pygitweb.config import auth_config, settings
 from pygitweb.sessions import create_session
 
 OAUTH_STATE_COOKIE_NAME = "pygitweb_oauth_state"
@@ -323,10 +323,9 @@ async def oauth_callback(
 	if not is_oauth_email_allowed(auth_config, email):
 		raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Email is not allowed to sign in")
 	username = oauth_username_from_profile(auth_config, profile)
-	subject = _profile_subject(profile)
 	sid = create_session(
 		username,
-		subject=subject,
+		subject=username,
 		email=email,
 		auth_method="oauth",
 	)
diff --git a/pygitweb/auth_test.py b/pygitweb/auth_test.py
index ab91ef4..1b996c3 100644
--- a/pygitweb/auth_test.py
+++ b/pygitweb/auth_test.py
@@ -5,10 +5,13 @@ import sys
 from collections.abc import Generator
 from pathlib import Path
 from unittest.mock import AsyncMock, patch
+from urllib.parse import parse_qs, urlparse
 
+import httpx
 import pytest
-from fastapi import HTTPException
+from fastapi import FastAPI, HTTPException, Request
 from fastapi.testclient import TestClient
+from httpx import ASGITransport
 
 from pygitweb.auth import ACCESS_TOKEN_COOKIE_NAME
 from pygitweb.auth_config import AuthConfig, LocalUser, init_auth_config, write_auth_config
@@ -29,6 +32,8 @@ from pygitweb.password_hash import hash_password, verify_password
 from pygitweb.permissions import PERMISSION_ADD_PROJECTS, PermissionsMap
 from pygitweb.sessions import get_session
 
+_REAL_ASYNC_CLIENT = httpx.AsyncClient
+
 
 class _DummyResponse:
 	def __init__(self, payload: object, is_success: bool = True) -> None:
@@ -48,6 +53,47 @@ _LOCAL_ADMIN = AuthConfig(
 )
 
 
+def _build_test_idp_app(
+	email: str = "user@example.com",
+	preferred_username: str = "oauthuser",
+) -> FastAPI:
+	idp = FastAPI()
+
+	@idp.post("/token")
+	async def google_token(request: Request) -> dict[str, str]:
+		form = await request.form()
+		grant_type = str(form.get("grant_type") or "")
+		code = str(form.get("code") or "")
+		if grant_type != "authorization_code" or code != "auth-code":
+			return {"error": "invalid_grant"}
+		return {"access_token": "idp-access-token"}
+
+	@idp.post("/login/oauth/access_token")
+	async def github_token(request: Request) -> dict[str, str]:
+		form = await request.form()
+		code = str(form.get("code") or "")
+		if code != "auth-code":
+			return {"error": "bad_code"}
+		return {"access_token": "idp-access-token"}
+
+	@idp.get("/v1/userinfo")
+	async def google_userinfo() -> dict[str, str]:
+		return {"sub": "oauth-sub-1", "email": email, "preferred_username": preferred_username}
+
+	@idp.get("/user")
+	async def github_user() -> dict[str, object]:
+		return {"id": "123", "login": preferred_username, "email": None}
+
+	@idp.get("/user/emails")
+	async def github_emails() -> list[dict[str, object]]:
+		return [
+			{"email": email, "primary": True, "verified": True},
+			{"email": "other@example.com", "primary": False, "verified": True},
+		]
+
+	return idp
+
+
 @pytest.fixture
 def client() -> Generator[TestClient, None, None]:
 	with TestClient(app) as c:
@@ -371,15 +417,50 @@ def test_oauth_callback_creates_session(client: TestClient, oauth_auth_config: A
 		assert r.status_code == 303
 		rec = get_session(r.cookies["pygitweb_access_token"])
 		assert rec is not None
-		assert rec.username == "user@example.com"
-		assert rec.subject == "oauth-sub-1"
+		assert rec.username == "oauthuser"
+		assert rec.subject == "oauthuser"
 		assert rec.email == "user@example.com"
 		assert rec.auth_method == "oauth"
-		assert client.get("/users/me").json()["username"] == "user@example.com"
+		assert client.get("/users/me").json()["username"] == "oauthuser"
 		status = client.get("/auth/status").json()
-		assert status["username"] == "user@example.com"
+		assert status["username"] == "oauthuser"
 		assert status["gravatar_url"] == gravatar_url("user@example.com")
-	clear_client_cookies(client)
+		clear_client_cookies(client)
+
+
+def test_oauth_flow_with_asgi_idp(client: TestClient, oauth_auth_config: AuthConfig) -> None:
+	idp_app = _build_test_idp_app()
+
+	async def runner() -> None:
+		transport = ASGITransport(app=idp_app)
+
+		def _idp_client_factory(*_: object, **__: object) -> httpx.AsyncClient:
+			return _REAL_ASYNC_CLIENT(transport=transport, base_url="https://idp.test")
+
+		with (
+			patch.object(settings, "AUTH", True),
+			patch("pygitweb.auth_oauth.httpx.AsyncClient", side_effect=_idp_client_factory),
+		):
+			start = client.get("/auth/oauth/start", params={"next": "/boards"}, follow_redirects=False)
+			assert start.status_code == 303
+			location = start.headers["location"]
+			state_values = parse_qs(urlparse(location).query).get("state", [])
+			assert state_values
+			state = state_values[0]
+			callback = client.get(
+				"/auth/oauth/callback",
+				params={"code": "auth-code", "state": state},
+				follow_redirects=False,
+			)
+			assert callback.status_code == 303
+			rec = get_session(callback.cookies[ACCESS_TOKEN_COOKIE_NAME])
+			assert rec is not None
+			assert rec.username == "oauthuser"
+			assert rec.subject == "oauthuser"
+			assert rec.email == "user@example.com"
+			assert rec.auth_method == "oauth"
+
+	asyncio.run(runner())
 
 
 def test_auth_status_no_gravatar_for_local_login(client: TestClient, local_auth_config: AuthConfig) -> None:
diff --git a/pygitweb/main.py b/pygitweb/main.py
index 7e8b14e..cf8acdb 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -60,10 +60,10 @@ from pygitweb.auth import (
 	ensure_permission,
 	require_permission,
 )
-from pygitweb.auth_config import auth_config, is_auth_configured
+from pygitweb.auth_config import is_auth_configured
 from pygitweb.board_refs import BoardRefManifest, board_ref_manifest
 from pygitweb.change_queue import CHANGE_QUEUE
-from pygitweb.config import ACTIONS, get_loadavg, settings
+from pygitweb.config import ACTIONS, auth_config, get_loadavg, settings
 from pygitweb.dependencies import (
 	ValidatedBoardCreateProject,
 	ValidatedNotifyProject,
diff --git a/pygitweb/pyproject.toml b/pygitweb/pyproject.toml
index 41273fb..dc480b2 100644
--- a/pygitweb/pyproject.toml
+++ b/pygitweb/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "pygitweb"
-version = "0.2.1"
+version = "0.2.3"
 description = "Gitweb reimplementation with FastAPI and Pygit2"
 readme = "README.md"
 license = "MIT"
diff --git a/pygitweb/static/board.js b/pygitweb/static/board.js
index 8f83ae2..42db49e 100644
--- a/pygitweb/static/board.js
+++ b/pygitweb/static/board.js
@@ -225,11 +225,13 @@
       var statusEl = details.querySelector('.board-card-status');
       var priorityEl = details.querySelector('.board-card-priority');
       var assigneeEl = details.querySelector('.board-card-assignee');
+      var tagsEl = details.querySelector('.board-card-tags');
       if (dueDate) dueDate.textContent = task.due_date || 'None';
       if (updatedAt) updatedAt.textContent = task.updated_at || 'None';
       if (statusEl) statusEl.textContent = status;
       if (priorityEl) priorityEl.textContent = task.priority || 'LOW';
       if (assigneeEl) assigneeEl.textContent = task.assignee || 'None';
+      if (tagsEl) tagsEl.textContent = task.tags || '';
       updateCardCommentCount(card, task.comments_count || 0);
     }
     return clone;
@@ -617,6 +619,7 @@
     var dueEl = popoutCard.querySelector('.board-card-due-date');
     var statusEl = popoutCard.querySelector('.board-card-status');
     var priorityEl = popoutCard.querySelector('.board-card-priority');
+    var tagsEl = popoutCard.querySelector('.board-card-tags');
 
     function pushTaskUpdate() {
       var taskRef = popoutCard.dataset.taskRef;
@@ -629,16 +632,19 @@
       var sEl = popoutCard.querySelector('.board-card-status');
       var pEl = popoutCard.querySelector('.board-card-priority');
       var aEl = popoutCard.querySelector('.board-card-assignee');
+      var tagsEl_ = popoutCard.querySelector('.board-card-tags');
       var dueVal = (dueEl_ && dueEl_.textContent) ? dueEl_.textContent.trim() : '';
       var statusDisplay = (sEl && sEl.textContent) ? sEl.textContent.trim() : 'TODO';
       var priorityDisplay = (pEl && pEl.textContent) ? pEl.textContent.trim() : 'LOW';
+      var tagsVal = (tagsEl_ && tagsEl_.textContent) ? tagsEl_.textContent.trim() : '';
       var body = {
         title: (tEl && tEl.textContent) ? tEl.textContent.trim() || 'Untitled' : 'Untitled',
         description: getMarkdownFromElement(dEl),
         status: statusDisplay ? statusDisplay.replace(/\s+/g, '_').toUpperCase() : 'TODO',
         priority: priorityDisplay ? priorityDisplay.replace(/\s+/g, '_').toUpperCase() : 'LOW',
         assignee: (aEl && aEl.textContent && aEl.textContent.trim() !== 'None') ? aEl.textContent.trim() : null,
-        due_date: (dueVal && dueVal !== 'None') ? dueVal : null
+        due_date: (dueVal && dueVal !== 'None') ? dueVal : null,
+        tags: tagsVal || ''
       };
       fetch('/tasks/update?' + new URLSearchParams({ project: ctx.project, board: ctx.board, task: taskRef }).toString(), {
         method: 'POST',
@@ -783,6 +789,12 @@
         sel.addEventListener('change', commitPriority);
       });
     }
+
+    if (tagsEl) {
+      tagsEl.addEventListener('blur', function () {
+        pushTaskUpdate();
+      });
+    }
   }
 
   function showCardPopout(card) {
diff --git a/pygitweb/static/main.css b/pygitweb/static/main.css
index aef0f90..f995e32 100644
--- a/pygitweb/static/main.css
+++ b/pygitweb/static/main.css
@@ -1126,6 +1126,12 @@ pre {
 		text-transform: uppercase;
 	}
 
+	.board-card-tags[contenteditable="true"]:empty::before {
+		content: "(click to add tags)";
+		color: var(--pgw-text-muted);
+		opacity: 0.8;
+	}
+
 	.board-card-comment-body {
 		min-width: 0;
 		flex: 1;
diff --git a/pygitweb/tasks.py b/pygitweb/tasks.py
index 20342ea..9ce10cd 100644
--- a/pygitweb/tasks.py
+++ b/pygitweb/tasks.py
@@ -220,6 +220,7 @@ def _task_to_json(t: Task) -> dict[str, Any]:
 		"priority": t.priority.value if t.priority else None,
 		"assignee": t.assignee,
 		"due_date": t.due_date.isoformat() if t.due_date else None,
+		"tags": getattr(t, "tags", "") or "",
 		"created_at": t.created_at.isoformat() if t.created_at else None,
 		"updated_at": t.updated_at.isoformat() if t.updated_at else None,
 		"comments_count": len(getattr(t, "comments", [])),
@@ -305,6 +306,7 @@ def task_create(
 	priority: str = Query("LOW", description="Task priority"),
 	assignee: str = Query("", description="Assignee"),
 	due_date: str = Query("", description="Due date ISO"),
+	tags: str = Query("", description="Task tags (KEY=VALUE list)"),
 ) -> JSONResponse:
 	"""Create a new task on a board."""
 	repo = pygit2.Repository(os.path.join(settings.PROJECTROOT, project))
@@ -335,6 +337,7 @@ def task_create(
 		priority=priority_enum,
 		assignee=assignee or None,
 		due_date=due,
+		tags=tags or "",
 	)
 	oid = task.write(repo)
 	b.tasks = getattr(b, "tasks", []) or []
@@ -355,7 +358,7 @@ async def task_update(
 	board: str = Query(..., description="Board name"),
 	task_ref: str = Query(..., alias="task", description="Task ref (e.g. refs/tags/tasks/task_123)"),
 ) -> JSONResponse:
-	"""Update a task (body: optional title, description, status, priority, assignee, due_date)."""
+	"""Update a task (body: optional title, description, status, priority, assignee, due_date, tags)."""
 	try:
 		body = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {}
 	except Exception:
@@ -385,6 +388,8 @@ async def task_update(
 			)
 		except ValueError:
 			pass
+	if "tags" in body:
+		t.tags = str(body.get("tags") or "")
 	old_oid = str(repo.references[ref].resolve().target)
 	t.update_message()
 	new_oid = t.write(repo)
diff --git a/pygitweb/templates/board_card.html b/pygitweb/templates/board_card.html
index c705fe4..29d5201 100644
--- a/pygitweb/templates/board_card.html
+++ b/pygitweb/templates/board_card.html
@@ -24,13 +24,46 @@
       {% endif %}
     </div>
     <div class="board-card-details d-none mt-2 pt-2 border-top small">
-      <div class="mb-2"><strong class="text-muted">Ref</strong><div class="board-card-task-ref font-monospace text-muted mt-1 text-break">{% if task and task.ref %}{{ task.ref }}{% else %}{% endif %}</div></div>
-      <div class="mb-2"><strong class="text-muted">Description</strong><div class="board-card-description-full board-card-editable board-card-markdown mt-1">{% if task and task.description %}{{ task.description }}{% else %}{% endif %}</div></div>
-      <div class="mb-2"><strong class="text-muted">Due date</strong><div class="board-card-due-date board-card-editable text-muted mt-1">{% if task and task.due_date %}{{ task.due_date }}{% else %}None{% endif %}</div></div>
-      <div class="mb-2"><strong class="text-muted">Last modified</strong><div class="board-card-updated-at text-muted mt-1">{% if task and task.updated_at %}{{ task.updated_at }}{% else %}None{% endif %}</div></div>
-      <div class="mb-2"><strong class="text-muted">Status</strong><div class="board-card-status board-card-editable text-muted mt-1">{% if task %}{{ task.status or 'TODO' }}{% else %}TODO{% endif %}</div></div>
-      <div class="mb-2"><strong class="text-muted">Priority</strong><div class="board-card-priority board-card-editable text-muted mt-1">{% if task and task.priority %}{{ task.priority }}{% else %}LOW{% endif %}</div></div>
-      <div class="mb-2"><strong class="text-muted">Assignee</strong><div class="board-card-assignee text-muted mt-1">{% if task and task.assignee %}{{ task.assignee }}{% else %}None{% endif %}</div></div>
+      <div class="mb-2">
+        <strong class="text-muted">Ref</strong>
+        <div class="board-card-task-ref font-monospace text-muted mt-1 text-break">{% if task and task.ref %}{{ task.ref }}{% else %}{% endif %}</div>
+      </div>
+      <div class="mb-2">
+        <strong class="text-muted">Description</strong>
+        <div class="board-card-description-full board-card-editable board-card-markdown mt-1">{% if task and task.description %}{{ task.description }}{% else %}{% endif %}</div>
+      </div>
+      <div class="mb-2">
+        <strong class="text-muted">Due date</strong>
+        <div class="board-card-due-date board-card-editable text-muted mt-1">{% if task and task.due_date %}{{ task.due_date }}{% else %}None{% endif %}</div>
+      </div>
+      <div class="mb-2">
+        <strong class="text-muted">Last modified</strong>
+        <div class="board-card-updated-at text-muted mt-1">{% if task and task.updated_at %}{{ task.updated_at }}{% else %}None{% endif %}</div>
+      </div>
+      <div class="mb-2">
+        <strong class="text-muted">Status</strong>
+        <div class="board-card-status board-card-editable text-muted mt-1">{% if task %}{{ task.status or 'TODO' }}{% else %}TODO{% endif %}</div>
+      </div>
+      <div class="mb-2">
+        <strong class="text-muted">Priority</strong>
+        <div class="board-card-priority board-card-editable text-muted mt-1">{% if task and task.priority %}{{ task.priority }}{% else %}LOW{% endif %}</div>
+      </div>
+      <div class="mb-2">
+        <strong class="text-muted">Assignee</strong>
+        <div class="board-card-assignee text-muted mt-1">{% if task and task.assignee %}{{ task.assignee }}{% else %}None{% endif %}</div>
+      </div>
+      <div class="mb-2">
+        <details class="board-card-tags-details" open>
+          <summary class="text-muted">Tags</summary>
+          <div
+            class="board-card-tags font-monospace text-muted mt-1 small"
+            contenteditable="true"
+            spellcheck="false"
+          >
+            {% if task and task.tags %}{{ task.tags }}{% endif %}
+          </div>
+        </details>
+      </div>
       <div class="mb-0 board-card-comments-section">
         <strong class="text-muted">Comments</strong>
         <div class="board-card-comments-list mt-2"></div>
diff --git a/uv.lock b/uv.lock
index 2da9452..9a708d2 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1152,7 +1152,7 @@ wheels = [
 
 [[package]]
 name = "pygittools"
-version = "0.2.1"
+version = "0.2.2"
 source = { editable = "pygittools" }
 dependencies = [
     { name = "pygit2" },
@@ -1167,7 +1167,7 @@ requires-dist = [
 
 [[package]]
 name = "pygittools-mcp"
-version = "0.1.0"
+version = "0.2.1"
 source = { editable = "pygittools-mcp" }
 dependencies = [
     { name = "mcp" },
@@ -1182,7 +1182,7 @@ requires-dist = [
 
 [[package]]
 name = "pygitweb"
-version = "0.2.1"
+version = "0.2.3"
 source = { editable = "pygitweb" }
 dependencies = [
     { name = "fastapi", extra = ["standard-no-fastapi-cloud-cli"] },
