diff --git a/docs/docs/writing-tests/mocking/advanced.md b/docs/docs/writing-tests/mocking/advanced.md
index 964de2898e..df1c8e7e9a 100644
--- a/docs/docs/writing-tests/mocking/advanced.md
+++ b/docs/docs/writing-tests/mocking/advanced.md
@@ -377,6 +377,34 @@ ships and loads; an `IgnoresAccessChecksTo` attribute (honored by the .NET runti
compiled IL valid against it at execution time. This is the established "publicizer" pattern
used by several long-lived OSS tools, wired into the TUnit.Mocks package.
+### Editors and design-time builds
+
+The publicized copy reaches the compiler through the reference list, so editors and IDE tooling
+see the same internals the build does.
+
+One case needs a nudge: when a listed assembly comes from a `ProjectReference`, tooling that
+loads projects through Roslyn's MSBuild workspace (the C# language server, OmniSharp, anything on
+`MSBuildWorkspace`) binds to the referenced *project's* compilation, which has no publicized
+internals — the build succeeds while the editor underlines every internal type with `CS0122`
+([#6836](https://github.com/thomhurst/TUnit/issues/6836)). TUnit therefore
+detaches that project reference in **design-time builds only**, leaving the publicized copy as
+the reference for that assembly. Real builds are untouched: the project still builds, copies
+local, and lands in `deps.json`.
+
+The trade-off is that the referenced project shows up in the editor as a compiled assembly: "go
+to definition" lands on metadata rather than its source, and edits to it reach the test project
+after a rebuild. To keep the live project reference instead — and the false `CS0122` reports that
+come with it — set:
+
+```xml
+
+ false
+
+```
+
+For a project you own, `[assembly: InternalsVisibleTo]` remains the simpler answer; internals
+access exists for assemblies you cannot change.
+
### Caveats
- **Experimental.** `IgnoresAccessChecksToAttribute` is honored by the runtime but is not a
diff --git a/src/TUnit.Mocks/TUnit.Mocks.InternalsAccess.targets b/src/TUnit.Mocks/TUnit.Mocks.InternalsAccess.targets
index 0b16aa073a..4403c5d2a2 100644
--- a/src/TUnit.Mocks/TUnit.Mocks.InternalsAccess.targets
+++ b/src/TUnit.Mocks/TUnit.Mocks.InternalsAccess.targets
@@ -44,6 +44,11 @@
true
+
+ true
+
+
+
+ <_TUnitMocksPublicizedProjectFile Include="@(_TUnitMocksPublicizedReference->'%(MSBuildSourceProjectFile)')"
+ Condition="'%(_TUnitMocksPublicizedReference.MSBuildSourceProjectFile)' != ''" />
+
+
+
+
diff --git a/tests/TUnit.Mocks.InternalsAccess.Tests/DesignTimeProjectReferenceTests.cs b/tests/TUnit.Mocks.InternalsAccess.Tests/DesignTimeProjectReferenceTests.cs
new file mode 100644
index 0000000000..66c0efbc43
--- /dev/null
+++ b/tests/TUnit.Mocks.InternalsAccess.Tests/DesignTimeProjectReferenceTests.cs
@@ -0,0 +1,294 @@
+using System.Diagnostics;
+using System.Security;
+using System.Text;
+using System.Text.Json;
+using Task = System.Threading.Tasks.Task;
+
+namespace TUnit.Mocks.InternalsAccess.Tests;
+
+// #6836: a publicized reference that came from a ProjectReference stays a live project
+// reference for Roslyn-workspace tooling (MSBuildWorkspace, the C# language server,
+// OmniSharp). Those hosts reference the referenced project's own compilation, which has no
+// publicized internals, so the editor reports a false CS0122 on code the compiler accepts.
+// The targets detach that project reference in design-time builds only; a real build must keep
+// it, or copy-local and deps.json would lose the assembly.
+//
+// The scenario is generated outside the repository so the probe is a plain SDK project: no
+// repo-wide props, no shared obj directory, nothing to race with a parallel build.
+
+public class DesignTimeProjectReferenceTests
+{
+ [Test]
+ public async Task Design_Time_Build_Detaches_The_Publicized_Project_Reference()
+ {
+ await using var scenario = await Scenario.CreateAsync();
+
+ var reference = await scenario.QueryProjectReferenceAsync(designTimeBuild: true);
+
+ await Assert.That(reference).IsEqualTo("false");
+ }
+
+ [Test]
+ public async Task Detached_Project_Reference_Still_Compiles_Against_The_Publicized_Copy()
+ {
+ await using var scenario = await Scenario.CreateAsync();
+
+ // The detach must not cost the reference: what the editor reads back is the compiler
+ // command line, and the publicized copy has to be the assembly on it.
+ var references = await scenario.QueryCompilerReferencesAsync(designTimeBuild: true);
+
+ await Assert.That(references).HasSingleItem();
+ await Assert.That(references[0]).Contains(Path.Combine("tunit-mocks-internals"));
+ }
+
+ [Test]
+ public async Task Real_Build_Keeps_The_Publicized_Project_Reference()
+ {
+ await using var scenario = await Scenario.CreateAsync();
+
+ var reference = await scenario.QueryProjectReferenceAsync(designTimeBuild: false);
+
+ // Untouched: no ReferenceOutputAssembly metadata was written at all.
+ await Assert.That(reference).IsEqualTo("");
+ }
+
+ [Test]
+ public async Task Detaching_Can_Be_Opted_Out_Of()
+ {
+ await using var scenario = await Scenario.CreateAsync();
+
+ var reference = await scenario.QueryProjectReferenceAsync(
+ designTimeBuild: true,
+ "-p:TUnitMocksInternalsAccessDetachDesignTimeProjectReferences=false");
+
+ await Assert.That(reference).IsEqualTo("");
+ }
+
+ private sealed class Scenario : IAsyncDisposable
+ {
+ private const string LibraryAssemblyName = "DesignTimeSdkLib";
+
+ private Scenario(string root, string probeProject)
+ {
+ Root = root;
+ ProbeProject = probeProject;
+ }
+
+ private string Root { get; }
+
+ private string ProbeProject { get; }
+
+ public static async Task CreateAsync()
+ {
+ var root = Path.Combine(Path.GetTempPath(), "tunit-mocks-ia-designtime", Guid.NewGuid().ToString("N"));
+ var library = Path.Combine(root, "lib");
+ var probe = Path.Combine(root, "probe");
+ Directory.CreateDirectory(library);
+ Directory.CreateDirectory(probe);
+
+ // Stop MSBuild walking out of the temp directory for props/targets it should not find.
+ File.WriteAllText(Path.Combine(root, "Directory.Build.props"), "");
+ File.WriteAllText(Path.Combine(root, "Directory.Build.targets"), "");
+
+ File.WriteAllText(Path.Combine(library, "lib.csproj"),
+ $"""
+
+
+ {TargetFramework}
+ {LibraryAssemblyName}
+
+
+ """);
+
+ File.WriteAllText(Path.Combine(library, "Api.cs"),
+ $$"""
+ namespace {{LibraryAssemblyName}};
+
+ internal interface IQuotaPolicy
+ {
+ bool Allow(string clientId);
+ }
+ """);
+
+ File.WriteAllText(Path.Combine(probe, "probe.csproj"),
+ $"""
+
+
+ {TargetFramework}
+ true
+ {Xml(TasksAssembly)}
+
+
+
+
+
+
+
+ """);
+
+ // Naming the internal type is what the publicized reference buys; if the swap stops
+ // working this file no longer compiles.
+ File.WriteAllText(Path.Combine(probe, "Use.cs"),
+ $$"""
+ using {{LibraryAssemblyName}};
+
+ internal static class Use
+ {
+ internal static bool Allow(IQuotaPolicy policy) => policy.Allow("acme");
+ }
+ """);
+
+ var probeProject = Path.Combine(probe, "probe.csproj");
+
+ try
+ {
+ await RunAsync("build", probeProject);
+ }
+ catch
+ {
+ Directory.Delete(root, recursive: true);
+ throw;
+ }
+
+ return new Scenario(root, probeProject);
+ }
+
+ ///
+ /// A checkout path may legally contain characters that are markup in a project file.
+ ///
+ private static string Xml(string path) => SecurityElement.Escape(path);
+
+ public ValueTask DisposeAsync()
+ {
+ // -nr:false leaves no MSBuild node behind, so nothing still holds the outputs.
+ Directory.Delete(Root, recursive: true);
+ return ValueTask.CompletedTask;
+ }
+
+ ///
+ /// Runs the compile pipeline without invoking the compiler and reports the
+ /// ReferenceOutputAssembly metadata the project reference carries afterwards.
+ ///
+ public async Task QueryProjectReferenceAsync(bool designTimeBuild, params string[] extraArguments)
+ {
+ var arguments = ArgumentsFor(designTimeBuild);
+
+ arguments.AddRange(extraArguments);
+
+ var items = await QueryItemsAsync("ProjectReference", [.. arguments]);
+ var item = items.EnumerateArray().Single();
+ return item.TryGetProperty("ReferenceOutputAssembly", out var metadata) ? metadata.GetString() ?? "" : "";
+ }
+
+ ///
+ /// The /reference: arguments the compiler would be invoked with for the publicized
+ /// assembly — what a workspace host reads back as the project's metadata references.
+ ///
+ public async Task QueryCompilerReferencesAsync(bool designTimeBuild)
+ {
+ var items = await QueryItemsAsync("CscCommandLineArgs", [.. ArgumentsFor(designTimeBuild)]);
+
+ return items.EnumerateArray()
+ .Select(item => item.GetProperty("Identity").GetString() ?? "")
+ .Where(argument => argument.StartsWith("/reference:", StringComparison.Ordinal)
+ && argument.Contains(LibraryAssemblyName, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+ }
+
+ private async Task QueryItemsAsync(string itemName, string[] arguments)
+ {
+ var output = await RunAsync("msbuild", ProbeProject, [.. arguments, "-getItem:" + itemName]);
+
+ // -getItem prints JSON, but a warning can still precede it.
+ var document = JsonDocument.Parse(output[output.IndexOf('{')..]);
+ return document.RootElement.GetProperty("Items").GetProperty(itemName);
+ }
+
+ private static List ArgumentsFor(bool designTimeBuild)
+ {
+ List arguments =
+ [
+ "-t:Compile",
+ "-p:SkipCompilerExecution=true",
+ "-p:ProvideCommandLineArgs=true",
+ ];
+
+ if (designTimeBuild)
+ {
+ // What an IDE passes: nothing is compiled or copied, so the reference swap is
+ // free to reshape the reference set the workspace reads back. The last two also
+ // drive _ComputeNonExistentFileProperty, which is what makes CoreCompile run
+ // (and so report its command line) even when the outputs are up to date.
+ arguments.Add("-p:DesignTimeBuild=true");
+ arguments.Add("-p:BuildProjectReferences=false");
+ arguments.Add("-p:BuildingInsideVisualStudio=true");
+ arguments.Add("-p:BuildingProject=false");
+ }
+
+ return arguments;
+ }
+
+ private static async Task RunAsync(string verb, string project, params string[] arguments)
+ {
+ var startInfo = new ProcessStartInfo("dotnet")
+ {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ // A reused MSBuild node keeps the task assembly — this test project's own build
+ // output — loaded and locked for the next build in this repository. The MSBuild
+ // server outlives the process entirely and holds the same lock, so both are off.
+ Environment =
+ {
+ ["MSBUILDUSESERVER"] = "0",
+ ["MSBUILDDISABLENODEREUSE"] = "1",
+ },
+ ArgumentList = { verb, project, "-nologo", "-nr:false" },
+ };
+
+ foreach (var argument in arguments)
+ {
+ startInfo.ArgumentList.Add(argument);
+ }
+
+ using var process = Process.Start(startInfo)!;
+ var standardOutput = process.StandardOutput.ReadToEndAsync();
+ var standardError = process.StandardError.ReadToEndAsync();
+ await process.WaitForExitAsync();
+
+ var output = await standardOutput;
+ if (process.ExitCode != 0)
+ {
+ throw new InvalidOperationException(
+ new StringBuilder()
+ .AppendLine($"dotnet {verb} {project} {string.Join(' ', arguments)} exited with {process.ExitCode}.")
+ .AppendLine(output)
+ .AppendLine(await standardError)
+ .ToString());
+ }
+
+ return output;
+ }
+
+ private static string TargetFramework => "net10.0";
+
+ private static string TasksAssembly =>
+ Path.Combine(AppContext.BaseDirectory, "TUnit.Mocks.InternalsAccess.Tasks.dll");
+
+ private static string TargetsFile => FindRepositoryFile(
+ Path.Combine("src", "TUnit.Mocks", "TUnit.Mocks.InternalsAccess.targets"));
+
+ private static string FindRepositoryFile(string relativePath)
+ {
+ for (var directory = new DirectoryInfo(AppContext.BaseDirectory); directory is not null; directory = directory.Parent)
+ {
+ var candidate = Path.Combine(directory.FullName, relativePath);
+ if (File.Exists(candidate))
+ {
+ return candidate;
+ }
+ }
+
+ throw new FileNotFoundException($"'{relativePath}' was not found above '{AppContext.BaseDirectory}'.");
+ }
+ }
+}