Umbraco-CMS (Agent Skills)

GitHub

Umbraco is a free and open source .NET content management system helping you deliver delightful digital experiences.

CLAUDE.md

# Umbraco CMS - Multi-Project Repository

Enterprise-grade CMS built on .NET 10.0. This repository contains 21 production projects organized in a layered architecture with clear separation of concerns.

**Repository**: https://github.com/umbraco/Umbraco-CMS
**License**: MIT
**Main Branch**: `main`

---

## 1. Overview

### What This Repository Contains

**21 Production Projects** organized in 3 main categories:

1. **Core Architecture** (Domain & Infrastructure)
   - `Umbraco.Core` - Interface contracts, domain models, notifications
   - `Umbraco.Infrastructure` - Service implementations, data access, caching

2. **Web & APIs** (Presentation Layer)
   - `Umbraco.Web.UI` - Main ASP.NET Core web application
   - `Umbraco.Web.Common` - Shared web functionality, controllers, middleware
   - `Umbraco.Cms.Api.Management` - Backoffice Management API (REST)
   - `Umbraco.Cms.Api.Delivery` - Content Delivery API (headless)
   - `Umbraco.Cms.Api.Common` - Shared API infrastructure

3. **Specialized Features** (Pluggable Modules)
   - Persistence: EF Core (modern), NPoco (legacy) for SQL Server & SQLite
   - Caching: `PublishedCache.HybridCache` (in-memory + distributed)
   - Search: `Examine.Lucene` (full-text search)
   - Imaging: `Imaging.ImageSharp` v1 & v2 (image processing)
   - Other: Static assets, targets, development tools

**6 Test Projects**:
- `Umbraco.Tests.Common` - Shared test utilities
- `Umbraco.Tests.UnitTests` - Unit tests
- `Umbraco.Tests.Integration` - Integration tests
- `Umbraco.Tests.Benchmarks` - Performance benchmarks
- `Umbraco.Tests.AcceptanceTest` - E2E tests
- `Umbraco.Tests.AcceptanceTest.UmbracoProject` - Test instance

### Key Technologies

- **.NET 10.0** - Target framework for all projects
- **ASP.NET Core** - Web framework
- **Entity Framework Core** - Modern ORM
- **OpenIddict** - OAuth 2.0/OpenID Connect authentication
- **Microsoft.AspNetCore.OpenApi** - OpenAPI document generation
- **Swashbuckle.AspNetCore.SwaggerUI** - Swagger UI for API documentation
- **Lucene.NET** - Full-text search via Examine
- **ImageSharp** - Image processing

---

## 2. General-Purpose by Default

This repository is a product platform, not an application. Every layer is consumed by code that does not live here โ€” implementors, package developers, and other parts of the CMS. A change is finished when it serves the use case that prompted it *and* the use cases nobody has described yet.

**Design to the contract.** When you change shared code, work out the rule the layer must uphold for *any* implementation of it, and make that rule hold there. Name a concrete implementation freely in the code that owns it โ€” a specific service, package, or project; a shared or generic layer implements only the contract. A specific editor alias, content type alias, or class name appearing in generic code is the signal that a fix has been fitted to one caller โ€” express it instead as a capability the contract exposes.

**Describe the contract.** Comments and public docs use the vocabulary of the layer they sit in. In public XML doc / JSDoc a concrete illustration is welcome where it reads as one possible implementation ("for example, an editor that emits several groups mayโ€ฆ"), never as the definition of the behaviour.

---

## 3. Repository Structure

```
Umbraco-CMS/
โ”œโ”€โ”€ src/                                    # 21 production projects
โ”‚   โ”œโ”€โ”€ Umbraco.Core/                      # Domain contracts (interfaces only)
โ”‚   โ”‚   โ””โ”€โ”€ CLAUDE.md                      # โญ Core architecture guide
โ”‚   โ”œโ”€โ”€ Umbraco.Infrastructure/            # Service implementations
โ”‚   โ”œโ”€โ”€ Umbraco.Web.Common/                # Web utilities
โ”‚   โ”œโ”€โ”€ Umbraco.Web.UI/                    # Main web application
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.Api.Management/        # Management API
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.Api.Delivery/          # Delivery API (headless)
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.Api.Common/            # Shared API infrastructure
โ”‚   โ”‚   โ””โ”€โ”€ CLAUDE.md                      # โญ API patterns guide
โ”‚   โ”œโ”€โ”€ Umbraco.PublishedCache.HybridCache/ # Content caching
โ”‚   โ”œโ”€โ”€ Umbraco.Examine.Lucene/            # Search indexing
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.Persistence.EFCore/    # EF Core data access
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.Persistence.EFCore.Sqlite/
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.Persistence.EFCore.SqlServer/
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.Persistence.Sqlite/    # Legacy SQLite
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.Persistence.SqlServer/ # Legacy SQL Server
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.Imaging.ImageSharp/    # Image processing v1
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.Imaging.ImageSharp2/   # Image processing v2
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.StaticAssets/          # Embedded assets
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.DevelopmentMode.Backoffice/
โ”‚   โ”œโ”€โ”€ Umbraco.Cms.Targets/               # NuGet targets
โ”‚   โ””โ”€โ”€ Umbraco.Cms/                       # Meta-package
โ”‚
โ”œโ”€โ”€ tests/                                  # 6 test projects
โ”‚   โ”œโ”€โ”€ Umbraco.Tests.Common/
โ”‚   โ”œโ”€โ”€ Umbraco.Tests.UnitTests/
โ”‚   โ”œโ”€โ”€ Umbraco.Tests.Integration/
โ”‚   โ”œโ”€โ”€ Umbraco.Tests.Benchmarks/
โ”‚   โ”œโ”€โ”€ Umbraco.Tests.AcceptanceTest/
โ”‚   โ””โ”€โ”€ Umbraco.Tests.AcceptanceTest.UmbracoProject/
โ”‚
โ”œโ”€โ”€ templates/                              # Project templates
โ”‚   โ””โ”€โ”€ Umbraco.Templates/
โ”‚
โ”œโ”€โ”€ tools/                                  # Build tools
โ”‚   โ””โ”€โ”€ Umbraco.JsonSchema/
โ”‚
โ”œโ”€โ”€ umbraco.sln                            # Main solution file
โ”œโ”€โ”€ Directory.Build.props                  # Shared build configuration
โ”œโ”€โ”€ Directory.Packages.props               # Centralized package versions
โ”œโ”€โ”€ .editorconfig                          # Code style
โ””โ”€โ”€ .globalconfig                          # Roslyn analyzers
```

### Architecture Layers

**Dependency Flow** (unidirectional, always flows inward):

```
Web.UI โ†’ Web.Common โ†’ Infrastructure โ†’ Core
                โ†“
          Api.Management โ†’ Api.Common โ†’ Infrastructure โ†’ Core
                โ†“
          Api.Delivery โ†’ Api.Common โ†’ Infrastructure โ†’ Core
```

**Key Principle**: Core has NO dependencies (pure contracts). Infrastructure implements Core. Web/APIs depend on Infrastructure.

### Project Dependencies

**Core Layer**:
- `Umbraco.Core` โ†’ No dependencies (only Microsoft.Extensions.*)

**Infrastructure Layer**:
- `Umbraco.Infrastructure` โ†’ `Umbraco.Core`
- `Umbraco.PublishedCache.*` โ†’ `Umbraco.Infrastructure`
- `Umbraco.Examine.Lucene` โ†’ `Umbraco.Infrastructure`
- `Umbraco.Cms.Persistence.*` โ†’ `Umbraco.Infrastructure`

**Web Layer**:
- `Umbraco.Web.Common` โ†’ `Umbraco.Infrastructure` + caching + search
- `Umbraco.Web.UI` โ†’ `Umbraco.Web.Common` + all features

**API Layer**:
- `Umbraco.Cms.Api.Common` โ†’ `Umbraco.Web.Common`
- `Umbraco.Cms.Api.Management` โ†’ `Umbraco.Cms.Api.Common`
- `Umbraco.Cms.Api.Delivery` โ†’ `Umbraco.Cms.Api.Common`

---

## 4. Teamwork & Collaboration

### Branching Strategy

- **Main branch**: `main` (protected)
- **Branch naming convention**: `v<version>/<type>/<description>`

**Format**: `v{major-version}/{type}/{kebab-case-description}`

**Version**: Read from `version.json` in the repository root. Use the major version number (e.g., `v17` for version 17.x.x).

**Types**:
| Type | Use Case |
|------|----------|
| `feature` | New feature being introduced to the product |
| `bugfix` | Fix to an existing issue with the product |
| `qa` | Adding or updating unit, integration, or end-to-end tests |
| `improvement` | Update to something that already exists but isn't broken (UI finessing, refactoring) |
| `task` | Update that doesn't directly impact product behavior (dependency updates, build pipeline) |

**Description**: A short, kebab-case description (a few words). This should be prefixed with the GitHub issue number if the update is related to resolving a tracked issue.

**Examples**:
```
v17/bugfix/12345-correct-display-of-pending-migrations
v17/feature/add-webhook-support
v17/improvement/optimize-content-cache
v17/qa/add-media-service-tests
v17/task/update-ef-core-dependency
```

See `.github/CONTRIBUTING.md` for full guidelines.

### Pull Request Process

- **PR Template**: `.github/pull_request_template.md`
- **Required CI Checks**:
  - All tests pass
  - Code formatting (dotnet format)
  - No build warnings
- **Merge Strategy**: Squash and merge (via GitHub UI)
- **Reviews**: Required from code owners

#### PR Naming Convention

Use the format: `Area: Description (closes #IssueID)`

**Examples**:
| Area | Description | Issue |
|------|-------------|-------|
| Relations: | Move persistence of relations from repository into notification handlers | (closes #00000) |
| Management API: | Correct the population of the parent for sibling items when retrieved under a folder | |
| Docs: | Updated contributing guidelines to welcome contributions on bugfixes | |

**Area**: The feature or aspect affected (e.g., UFM, TipTap, Docs, Segmentation, Migrations). Helps readers quickly understand what is being changed.

**Description Best Practices**:
- Include the area of change (Relations, Management API, etc.)
- Describe the change and its impact
- Be specific, not vague (describe "a golden retriever" not just "a dog")

**Issue Linking**: Add `(closes #IssueID)` to the title for readability, AND include a closing keyword on its own line in the PR body (e.g., `Fixes #IssueID`) so GitHub actually auto-links and auto-closes the issue on merge. GitHub only parses closing keywords (`closes`, `fixes`, `resolves`) from the PR body or commit messages โ€” the title suffix is cosmetic and does **not** trigger auto-close on its own.

### Commit Messages

Follow Conventional Commits format:
```
<type>(<scope>): <description>

Types: feat, fix, docs, style, refactor, test, chore
Scope: project name (core, web, api, etc.)

Examples:
feat(core): add IContentService.GetByIds method
fix(api): resolve null reference in schema handler
docs(web): update routing documentation
```

### Code Owners

Project ownership is distributed across teams. Check individual project directories for ownership.

---

## 5. Architecture Patterns

### Core Architectural Decisions

1. **Layered Architecture with Dependency Inversion**
   - Core defines contracts (interfaces)
   - Infrastructure implements contracts that need Infrastructure-owned machinery
   - Web/APIs consume implementations via DI

   **Where service implementations live**: Services whose dependencies are satisfiable from Core interfaces alone (repositories, scope, config, other Core services) live in `Umbraco.Core/Services/` โ€” this covers the majority of domain services (`MemberService`, `ContentService`, `MediaService`, `ContentTypeService`, `EntityService`, `AuditService`, `ExternalMemberService`, etc.). Service implementations only live in `Umbraco.Infrastructure/Services/Implement/` when they genuinely need Infrastructure concerns โ€” Examine indexes (`ContentSearchService`, `MediaSearchService`, `IndexedEntitySearchService`), log files (`LogViewerRepository`), packaging internals (`PackagingService`), webhook firing (`WebhookFiringService`), distributed-job coordination (`DistributedJobService`). When adding a new service, default to Core and only move to Infrastructure if a concrete dependency forces it.

2. **Interface-First Design**
   - All services defined as interfaces in Core
   - Enables testing, polymorphism, extensibility

3. **Notification Pattern** (not C# events)
   - See `/src/Umbraco.Core/CLAUDE.md` โ†’ "2. Notification System (Event Handling)"

4. **Composer Pattern** (DI registration)
   - See `/src/Umbraco.Core/CLAUDE.md` โ†’ "3. Composer Pattern (DI Registration)"

5. **Scoping Pattern** (Unit of Work)
   - See `/src/Umbraco.Core/CLAUDE.md` โ†’ "5. Scoping Pattern (Unit of Work)"

6. **Attempt Pattern** (operation results)
   - `Attempt<TResult, TStatus>` instead of exceptions
   - Strongly-typed operation status enums

### Key Design Patterns Used

- **Repository Pattern** - Data access abstraction
- **Unit of Work** - Scoping for transactions
- **Builder Pattern** - `ProblemDetailsBuilder` for API errors
- **Strategy Pattern** - OpenAPI handlers (schema ID, operation ID)
- **Options Pattern** - All configuration via `IOptions<T>`
- **Factory Pattern** - Content type factories
- **Mediator Pattern** - Notification aggregator

---

## 6. Avoiding Breaking Changes

No binary breaking changes are allowed within a major version. Three patterns are used:

### 6.1 Obsolete Constructor + StaticServiceProvider

When a public class needs new dependencies, obsolete the existing constructor and add a new one. The old constructor delegates to the new one, resolving missing deps via `StaticServiceProvider`.

```csharp
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public MyService(IDependencyA depA)
    : this(
        depA,
        StaticServiceProvider.Instance.GetRequiredService<IDependencyB>())
{
}

public MyService(IDependencyA depA, IDependencyB depB)
{
    _depA = depA;
    _depB = depB;
}
```

**Examples**:
- `ContentCollectionPresentationFactory` - added `FlagProviderCollection`
- `CacheInstructionService` - added `ILastSyncedManager`, `IRepositoryCacheVersionService`
- `DocumentPresentationFactory` - added `FlagProviderCollection`

**Rules**:
- Old constructor marked `[Obsolete("... Scheduled for removal in Umbraco {current-major+2}.")]`
- Old constructor calls new constructor via `: this(...)`
- Uses `StaticServiceProvider.Instance.GetRequiredService<T>()` for new params only
- DI registration must use the NEW constructor (old is for external consumers only)

### 6.2 Obsolete Method + New Overload

When a public method signature needs to change, add the new method/overload and obsolete the old. The obsolete method should call the new one with suitable defaults.

```csharp
[Obsolete("Use the overload taking all parameters. Scheduled for removal in Umbraco 19.")]
public void DoThing(string name)
    => DoThing(name, extraParam: null);

public void DoThing(string name, string? extraParam)
{
    // Real implementation here
}
```

**Rules**:
- Old method marked `[Obsolete]` with removal schedule
- DRY: old method calls new method, providing defaults for new parameters
- All internal callers must be updated to use the new method
- No callers should remain on the obsolete method within the codebase

### 6.3 Default Interface Implementation

When adding methods to a public interface, provide a default implementation so existing external implementations don't break.

```csharp
public interface IMyService
{
    // Existing method
    void ExistingMethod();

    // New method with default implementation
    void NewMethod(string param)
        => ExistingMethod(); // delegate to existing if possible
}
```

**Strategies for the default** (in order of preference):
1. **Use existing interface methods** to satisfy the contract (even if not optimal)
2. **Return a sensible default** like empty collection, null, etc.
3. **Throw `NotImplementedException`** if no reasonable default exists

**Example**: `IContentService.SaveBlueprint` - new overload with `IContent? createdFromContent` has a default impl that calls the old method (ignoring the new param).

**Example**: `IDocumentPresentationFactory.CreateCulturePublishScheduleModels` - full default implementation with logic, uses `StaticServiceProvider` for dependency resolution within the interface.

**Rules**:
- Add `// TODO (V{next-major}): Remove the default implementation when {obsolete method} is removed.` comment
- Default impl should be functionally correct even if not optimal
- If using `StaticServiceProvider` in a default impl, note this is temporary

### 6.4 General Rules

- **Removal policy**: Obsoleted members must remain for at least one full major version before removal. If obsoleted in version N, the earliest removal is version N+2. For example, something obsoleted in v17 is scheduled for removal in v19 (giving the whole of v18 as a deprecation period).
- All `[Obsolete]` attributes must include **"Scheduled for removal in Umbraco {current+2}"**
- Read `version.json` to determine the current major version
- Suppress `CS0618` warnings where obsolete members must call each other:
  ```csharp
  #pragma warning disable CS0618 // Type or member is obsolete
      => OldMethod(param);
  #pragma warning restore CS0618 // Type or member is obsolete
  ```
- Update ALL internal callers to use the new API - no internal code should use obsolete members

---

## 7. Project-Specific Notes

### Centralized Package Management

**NuGet package versions** are centralized in `Directory.Packages.props`. There are two `Directory.Packages.props` files in the source tree, with multi-level merging enabled so the test file inherits from the root:

| File | Scope |
|------|-------|
| `Directory.Packages.props` (root) | Production source code packages โ€” referenced by all `src/**` projects |
| `tests/Directory.Packages.props` | Test-only packages (NUnit, Moq, Bogus, BenchmarkDotNet, etc.) โ€” adds entries on top of the inherited root file |

When updating dependencies, decide which file the package belongs in:
- A package used only by test projects โ†’ `tests/Directory.Packages.props`
- A package used by any production project (or by both production and tests) โ†’ root `Directory.Packages.props`

```xml
<!-- Individual projects reference WITHOUT version -->
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />

<!-- Versions defined in Directory.Packages.props -->
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
```

**Opt-out**: `src/Umbraco.Web.UI/Umbraco.Web.UI.csproj` sets `<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>` and specifies versions inline (for `Microsoft.EntityFrameworkCore.Design`, `Microsoft.Build.Tasks.Core`, `Microsoft.ICU.ICU4C.Runtime`, etc.). Update those versions directly in that csproj when bumping. Two further `Directory.Packages.props` files exist under `templates/` for the project/extension templates and have their own version sets โ€” keep `Microsoft.AspNetCore.OpenApi` aligned between the root file and `templates/UmbracoExtension/`.

### Build Configuration

- `Directory.Build.props` - Shared properties (target framework, company, copyright)
- `.editorconfig` - Code style rules
- `.globalconfig` - Roslyn analyzer rules

### Persistence Layer - NPoco and EF Core

The repository contains BOTH (actively supported):
- **Current**: NPoco-based persistence (`Umbraco.Cms.Persistence.Sqlite`, `Umbraco.Cms.Persistence.SqlServer`) - widely used and fully supported
- **Future**: EF Core-based persistence (`Umbraco.Cms.Persistence.EFCore.*`) - migration in progress

**Note**: The codebase is actively migrating to EF Core, but NPoco remains the primary persistence layer and is not deprecated. Both are fully supported.

### Authentication: OpenIddict

All APIs use **OpenIddict** (OAuth 2.0/OpenID Connect):
- Reference tokens (not JWT) for better security
- **Secure cookie-based token storage** (v17+) - tokens stored in HTTP-only cookies with `__Host-` prefix
- Tokens are redacted from client-side responses and passed via secure cookies only (`[redacted]` placeholder)
- ASP.NET Core Data Protection for token encryption
- Configured in `Umbraco.Cms.Api.Common`
- API requests must include credentials (`credentials: include` for fetch)

**Load Balancing Requirement**: All servers must share the same Data Protection key ring.

**Frontend auth pitfalls** โ€” see `src/Umbraco.Web.UI.Client/docs/edge-cases.md` (Auth & Cross-tab section) and `docs/security.md`. Key points:
- Never call `validateToken()` per API request โ€” it revokes the previous reference token (ID2019 errors)
- `window.opener` is set for ANY `window.open()` target, not only OAuth popups โ€” scope guards to the pathname too
- BroadcastChannel does not deliver messages to the sender's own tab

### Content Caching Strategy

**HybridCache** (`Umbraco.PublishedCache.HybridCache`):
- In-memory cache + distributed cache support
- Published content only (not draft)
- Invalidated via notifications and cache refreshers

### API Versioning

APIs use `Asp.Versioning.Mvc`:
- Management API: `/umbraco/management/api/v{version}/*`
- Delivery API: `/umbraco/delivery/api/v{version}/*`
- OpenAPI docs: `/umbraco/openapi/management.json`, `/umbraco/openapi/delivery.json`
- Swagger UI: `/umbraco/openapi/`

### Updating `OpenApi.json` (Management API)

When a PR changes Management API controllers or models, the `OpenApi.json` file in the Management API project must be updated, along with the generated backoffice client that is derived from it.

With the Umbraco instance running locally (in a non-Production environment โ€” Swagger isn't mapped in Production):

```bash
npm --prefix src/Umbraco.Web.UI.Client run generate:openapi
npm --prefix src/Umbraco.Web.UI.Client run generate:server-api
```

The first fetches the document from `/umbraco/swagger/management/swagger.json` byte-for-byte into `src/Umbraco.Cms.Api.Management/OpenApi.json`; the second regenerates the hey-api client from that committed file. Both results must be committed together.

They are two separate commands on purpose. The client generator only ever reads the committed schema โ€” never a running site โ€” so the schema and the client it produced always land in the same commit.

The `/umb-update-openapi` skill wraps this: it starts and stops a backend for you if one isn't already running, and explains the diff. Reach for it when you want the whole round trip handled.

**Important**: The fetch is byte-for-byte on purpose, so the endpoint stays the source of truth. Don't reformat the result โ€” commit only the substantive changes, not IDE-applied formatting (whitespace, reordering, etc.). Extraneous formatting diffs make PRs harder to review and merge-ups more error-prone.

### Backoffice npm Package

The backoffice is published to npm as `@umbraco-cms/backoffice`. Runtime dependencies are provided via importmap; npm peerDependencies provide types only. For full details on dependency hoisting, version range logic, and plugin development, see `/src/Umbraco.Web.UI.Client/CLAUDE.md` โ†’ "npm Package Publishing".

### SQL Server 2100-parameter limit

Any `WHERE IN (@0, @1, ...)` built from a runtime-sized collection risks hitting SQL Server's 2100-parameter ceiling and throwing `SqlException` 8003 in production.

Batch with `IEnumerable<T>.InGroupsOf(Constants.Sql.MaxParameterCount)` or `Database.FetchByGroups(...)` whenever the collection size is driven by user data โ€” not just when it currently fits. Watch for products of two scaling dimensions (documents ร— languages, properties ร— versions) and config-tunable batch sizes whose defaults are safe but ceilings aren't.

Full guidance, safe patterns and decision rule: see `/src/Umbraco.Infrastructure/CLAUDE.md` โ†’ "Avoiding the SQL Server 2100-parameter limit".

### Known Limitations

1. **Circular Dependencies**: Avoided via `Lazy<T>` or event notifications
2. **Multi-Server**: Requires shared Data Protection key ring and synchronized clocks (NTP)
3. **Database Support**: SQL Server, SQLite

---

## 8. CI/CD โ€” Claude AI Assistant

Two GitHub Actions workflows powered by `anthropics/claude-code-action@v1`. Advisory only โ€” does not block merging.

### Workflows

| File | Trigger | Purpose |
|------|---------|---------|
| `claude-review.yml` | `pull_request: [opened, ready_for_review]` | Auto-review every non-draft PR using the `umb-review` skill |
| `claude.yml` | `@claude` comments, issue assign/label | Interactive assistant for PRs and issues |

### Auto-Review (`claude-review.yml`)

Runs the full `.claude/skills/umb-review/SKILL.md` procedure on every newly opened or un-drafted PR. Produces inline comments per finding and one summary comment with a verdict. Skips draft PRs. No turn limit.

### Interactive (`claude.yml`)

Responds to `@claude` mentions on PRs and issues. The trigger phrase is stripped before Claude sees the message, so:

- `@claude review` โ†’ light review using `gh pr diff` (not the umb-review skill)
- `@claude fix ...` โ†’ implements a fix on a new branch
- `@claude help` โ†’ answers questions about the codebase
- `@claude label` โ†’ applies labels
- `@claude` (empty) โ†’ defaults to `review` on PRs, `help` on issues

Also triggers on issue assignment to `claude` or adding the `claude` label. Gated: only runs when `@claude` appears in the comment/issue body. Max 25 turns.

**Allowed Bash tools**: `gh`, `git`, `npm`, `dotnet` (interactive only; auto-review allows `gh` and `git`).

### Labels

Both workflows apply labels based on content:

**On PRs** (based on changed files):

| Label | Condition |
|-------|-----------|
| `area/frontend` | Files under `src/Umbraco.Web.UI.Client/` |
| `area/backend` | `.cs` files outside the frontend client |
| `area/test` | Only test files changed |
| `category/api` | Management or Delivery API files |
| `category/breaking` | Breaking changes detected |
| `category/localization` | Localization/language files |
| `category/test-automation` | Only test files changed |
| `category/refactor` | Pure refactoring, no new features |
| `category/performance` | Performance-related changes |
| `category/ux` | User-facing changes |
| `category/ui` | UI layer changes |

**On Issues** (based on content): same `area/*` and `category/*` labels, plus `affected/v14` through `affected/v17` and `affected/backoffice`.

Labels are only added, never removed. Claude applies only labels it is confident about.

### Key Implementation Notes

- **Checkout required** โ€” the action internally runs `git fetch origin main` for trusted file restoration. Without `actions/checkout`, it fails with `fatal: not a git repository`.
- **`id-token: write` permission** โ€” required for OIDC token exchange with the Claude GitHub App.
- **Trigger phrase stripping** โ€” the action strips `@claude` from comments before passing to Claude. Prompts must reference commands without the prefix (e.g., `review` not `@claude review`).
- **PR number injection** โ€” the interactive workflow injects the PR/issue number into the prompt via `${{ github.event.issue.number }}` since Claude can't discover it from `gh pr view` when checked out on `main`.

---

## 9. Code Comment Policy

**Default to no comment.** Applies to all code in this repository โ€” C#, TypeScript, Razor, build scripts. Well-named identifiers and small functions carry the meaning; a comment is a fallback for what the code genuinely cannot say โ€” a non-obvious *why*, a subtle invariant the types don't enforce, or a surprising edge case the code handles deliberately. Add XML doc / JSDoc on public members, but keep it concise.

**Write the rule, at the altitude of the code it sits in.** A comment states what must hold going forward, in the vocabulary of the layer it lives in โ€” see ยง2. Where a comment exists because something once went wrong, the rule is what survives; the incident and the reported scenario belong in the commit message and PR body:

```typescript
// A resolver may emit several groups of inner values.
// Pair each draft group with its persisted group by the
// identifier the resolver supplies, not by call order.
```

**Keep issue references where they stay actionable.** A tracked issue link (`(#21996)`, `https://...`) is welcome wherever it explains a non-obvious *why* โ€” a guard whose reason isn't clear from the code, a workaround for a defect this code cannot fix (so it can be deleted when the fix lands), or a regression test recording why it exists. Elsewhere the comment stands on its own in general terms.

**Let commit messages and PR descriptions carry provenance.** Which task, PR, or issue produced a change (`Fix for X`, `Used by Y`, `Added for the Z flow`, `See PR #1234`) is recorded in git history, where it stays accurate. Source describes the code as it is now.

### TODOs

Allowed, and can name the specific issue, implementation, or use case it concerns โ€” the one exception to ยง2, since the comment is deleted once the TODO is done. Keep them short and trackable: `// TODO (V19): remove once obsolete overload is gone` or `// TODO: pagination [NL]`. A TODO should have an author or a version trigger.

---

## 10. Testing Practices

A test for shared code asserts the general rule from ยง2, not the scenario that reported it, and is named for the rule.

### Tests for a bug fix must fail before the fix

Verify any test you add for a bug fix actually catches the bug: either write the failing test first (TDD), or temporarily revert the production change and confirm the test fails before re-applying. A test that passes both ways proves nothing. Watch for coincidental passes โ€” default seed/sort orders can make a buggy path produce the right answer for the test's specific inputs; construct inputs so the broken and fixed behaviours give visibly different results.

For integration tests that exercise caching or cache refreshers, see `tests/Umbraco.Tests.Integration/CLAUDE.md` โ€” the harness disables caching by default, which can produce false greens.

---

## 11. Verification Discipline

- **Fresh build before trusting a green.** Never treat `--no-build` or cached/incremental output as proof a change compiles or passes โ€” a stale run can mask a compile error. Rebuild before reporting build or test state. (Integration tests have a related false-green trap โ€” see `tests/Umbraco.Tests.Integration/CLAUDE.md`.)
- **Grep the branch you think you're on.** A search only supports a claim against the branch actually checked out, so confirm HEAD is where you expect before drawing a conclusion from a grep. Easy to get wrong whenever the tree moves under you โ€” reviewing a PR head, switching worktrees, or mid merge-up/rebase.

---

## Quick Reference

### Essential Commands

```bash
# Build solution
dotnet build

# Run all tests
dotnet test

# Run specific test category
dotnet test --filter "Category=Integration"

# Format code
dotnet format

# Pack all projects
dotnet pack -c Release
```

### Integration Test Database Configuration

Integration tests are configured in `tests/Umbraco.Tests.Integration/appsettings.Tests.json`.

The `Tests:Database:DatabaseType` setting controls which database is used:
- `"SQLite"` (default) - No external dependencies
- `"LocalDb"` - Uses SQL Server LocalDB, required for SQL Server-specific tests (e.g., page-level locking, `sys.dm_tran_locks`)

SQL Server-specific tests use `BaseTestDatabase.IsSqlite()` to skip when running on SQLite.

### Key Projects

| Project | Type | Description |
|---------|------|-------------|
| **Umbraco.Core** | Library | Interface contracts and domain models |
| **Umbraco.Infrastructure** | Library | Service implementations and data access |
| **Umbraco.Web.UI** | Application | Main web application (Razor/MVC) |
| **Umbraco.Cms.Api.Management** | Library | Management API (backoffice) |
| **Umbraco.Cms.Api.Delivery** | Library | Delivery API (headless CMS) |
| **Umbraco.Cms.Api.Common** | Library | Shared API infrastructure |
| **Umbraco.PublishedCache.HybridCache** | Library | Published content caching |
| **Umbraco.Examine.Lucene** | Library | Full-text search indexing |

### Important Files

- **Solution**: `umbraco.sln`
- **Build Config**: `Directory.Build.props`, `Directory.Packages.props`
- **Code Style**: `.editorconfig`, `.globalconfig`
- **Documentation**: `/CLAUDE.md`, `/src/Umbraco.Core/CLAUDE.md`, `/src/Umbraco.Cms.Api.Common/CLAUDE.md`

### Project-Specific Documentation

For detailed information about individual projects, see their CLAUDE.md files:
- **Core Architecture**: `/src/Umbraco.Core/CLAUDE.md` - Service contracts, notification patterns
- **API Infrastructure**: `/src/Umbraco.Cms.Api.Common/CLAUDE.md` - OpenAPI, authentication, serialization
- **Backoffice Frontend**: `/src/Umbraco.Web.UI.Client/CLAUDE.md` - Lit web components, extension system, auth client

**Important**: When working on backoffice client code (anything under `src/Umbraco.Web.UI.Client/`), read `/src/Umbraco.Web.UI.Client/CLAUDE.md` first. It contains action-specific checklists (deprecation, testing, security, etc.) that are not duplicated here.

### Getting Help

- **Official Docs**: https://docs.umbraco.com/
- **Contributing Guide**: `.github/CONTRIBUTING.md`
- **Issues**: https://github.com/umbraco/Umbraco-CMS/issues
- **Community**: https://forum.umbraco.com/
- **Releases**: https://releases.umbraco.com/

---

**This repository follows a layered architecture with strict dependency rules. The Core defines contracts, Infrastructure implements them, and Web/APIs consume them. Each layer can be understood independently, but dependencies always flow inward toward Core.**