diff --git a/src/Build.UnitTests/TaskHostFactoryLifecycle_E2E_Tests.cs b/src/Build.UnitTests/TaskHostFactoryLifecycle_E2E_Tests.cs
new file mode 100644
index 00000000000..0d542bbca4c
--- /dev/null
+++ b/src/Build.UnitTests/TaskHostFactoryLifecycle_E2E_Tests.cs
@@ -0,0 +1,92 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO;
+using Microsoft.Build.UnitTests;
+using Microsoft.Build.UnitTests.Shared;
+using Shouldly;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.Build.Engine.UnitTests
+{
+ ///
+ /// End-to-end tests for task host factory lifecycle behavior.
+ ///
+ /// Tests validate the behavior based on whether the TaskHost runtime matches
+ /// the executing MSBuild runtime and whether TaskHostFactory is explicitly requested.
+ ///
+ /// This is a regression test for https://github.com/dotnet/msbuild/issues/13013
+ ///
+ public class TaskHostFactoryLifecycle_E2E_Tests
+ {
+ private static string AssemblyLocation { get; } = Path.Combine(Path.GetDirectoryName(typeof(TaskHostFactoryLifecycle_E2E_Tests).Assembly.Location) ?? System.AppContext.BaseDirectory);
+
+ private static string TestAssetsRootPath { get; } = Path.Combine(AssemblyLocation, "TestAssets", "TaskHostLifecycle");
+
+ private readonly ITestOutputHelper _output;
+
+ public TaskHostFactoryLifecycle_E2E_Tests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ ///
+ /// Validates task host lifecycle behavior for all scenarios.
+ ///
+ /// Test scenarios:
+ /// 1. Runtime matches + TaskHostFactory requested → short-lived out of proc (nodereuse:False)
+ /// 2. Runtime matches + TaskHostFactory NOT requested → in-proc execution
+ /// 3. Runtime doesn't match + TaskHostFactory requested → short-lived out of proc (nodereuse:False)
+ /// 4. Runtime doesn't match + TaskHostFactory NOT requested → long-lived sidecar out of proc (nodereuse:True)
+ ///
+ /// The runtime to use for the task (CurrentRuntime or NET)
+ /// The task factory to use (TaskHostFactory or AssemblyTaskFactory)
+ /// Expected node reuse value (true for long-lived, false for short-lived, null for in-proc)
+ [Theory]
+ [InlineData("CurrentRuntime", "TaskHostFactory", false)] // Match + Explicit → short-lived out-of-proc
+ [InlineData("CurrentRuntime", "AssemblyTaskFactory", null)] // Match + No Explicit → in-proc
+
+ // Not test-able on .NET msbuild as it can't run a CLR2/CLR4 task host (out-of-proc)
+#if !NET
+ [InlineData("NET", "TaskHostFactory", false)] // No Match + Explicit → short-lived out-of-proc
+ [InlineData("NET", "AssemblyTaskFactory", true)] // No Match + No Explicit → long-lived sidecar out-of-proc
+#endif
+ public void TaskHostLifecycle_ValidatesAllScenarios(
+ string runtimeToUse,
+ string taskFactoryToUse,
+ bool? expectedNodeReuse)
+ {
+ using TestEnvironment env = TestEnvironment.Create(_output);
+ string testProjectPath = Path.Combine(TestAssetsRootPath, "TaskHostLifecycleTestApp.csproj");
+
+ string testTaskOutput = RunnerUtilities.ExecBootstrapedMSBuild(
+ $"{testProjectPath} -v:n /p:RuntimeToUse={runtimeToUse} /p:TaskFactoryToUse={taskFactoryToUse}",
+ out bool successTestTask,
+ outputHelper: _output);
+
+ successTestTask.ShouldBeTrue();
+
+ // Verify execution mode (out-of-proc vs in-proc) and node reuse behavior
+ if (expectedNodeReuse.HasValue)
+ {
+ // For out-of-proc scenarios, validate the task runs in a separate process
+ // by checking for the presence of command-line arguments that indicate task host execution
+ testTaskOutput.ShouldContain("/nodemode:",
+ customMessage: "Task should run out-of-proc and have /nodemode: in its command-line arguments");
+
+ // Validate the nodereuse flag in the task's command-line arguments
+ string expectedFlag = expectedNodeReuse.Value ? "/nodereuse:True" : "/nodereuse:False";
+ testTaskOutput.ShouldContain(expectedFlag,
+ customMessage: $"Task should have {expectedFlag} in its command-line arguments");
+ }
+ else
+ {
+ // For in-proc scenarios, validate the task does NOT run in a task host
+ // by ensuring task host specific command-line flags are not present
+ testTaskOutput.ShouldNotContain("/nodemode:",
+ customMessage: "Task should run in-proc and not have task host command-line arguments like /nodemode:");
+ }
+ }
+ }
+}
diff --git a/src/Build.UnitTests/TestAssets/TaskHostLifecycle/TaskHostLifecycleTestApp.csproj b/src/Build.UnitTests/TestAssets/TaskHostLifecycle/TaskHostLifecycleTestApp.csproj
new file mode 100644
index 00000000000..70cb2ac1a5b
--- /dev/null
+++ b/src/Build.UnitTests/TestAssets/TaskHostLifecycle/TaskHostLifecycleTestApp.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net10.0
+
+
+
+ $([System.IO.Path]::GetFullPath('$([System.IO.Path]::Combine('$(AssemblyLocation)', '..'))'))
+ $([System.IO.Path]::Combine('$(TestProjectFolder)', '$(TargetFramework)', 'ExampleTask.dll'))
+
+
+
+
+
+
+
+
+
diff --git a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs
index 9da4014e849..6e25f6c0879 100644
--- a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs
+++ b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs
@@ -363,8 +363,12 @@ internal ITask CreateTaskInstance(
mergedParameters = UpdateTaskHostParameters(mergedParameters);
(mergedParameters, bool isNetRuntime) = AddNetHostParamsIfNeeded(mergedParameters, getProperty);
- bool useSidecarTaskHost = !(_factoryIdentityParameters.TaskHostFactoryExplicitlyRequested ?? false)
- || isNetRuntime;
+ // Sidecar here means that the task host is launched with /nodeReuse:true and doesn't terminate
+ // after the task execution. This improves performance for tasks that run multiple times in a build.
+ // If the task host factory is explicitly requested, do not act as a sidecar task host.
+ // This is important as customers use task host factories for short lived tasks to release
+ // potential locks.
+ bool useSidecarTaskHost = !(_factoryIdentityParameters.TaskHostFactoryExplicitlyRequested ?? false);
TaskHostTask task = new(
taskLocation,