diff --git a/.mcp/server.json b/.mcp/server.json
index 2c8721f..4095568 100644
--- a/.mcp/server.json
+++ b/.mcp/server.json
@@ -3,13 +3,13 @@
   "name": "io.github.chrispulman/codex-computer-run-mcp-server",
   "title": "Codex Computer Run MCP Server",
   "description": "Windows-only MCP server for Codex desktop control: screenshots, mouse, keyboard, clipboard paste, cursor position, visible window discovery, and a bundled Codex Skill.",
-  "version": "1.0.0",
+  "version": "1.0.1",
   "packages": [
     {
       "registryType": "nuget",
       "registryBaseUrl": "https://api.nuget.org/v3/index.json",
       "identifier": "CP.CodexComputerRun.Mcp.Server",
-      "version": "1.0.0",
+      "version": "1.0.1",
       "runtimeHint": "dnx",
       "transport": {
         "type": "stdio"
diff --git a/CodexComputerRunMCPServer.slnx b/CodexComputerRunMCPServer.slnx
index f4b7fb2..230e489 100644
--- a/CodexComputerRunMCPServer.slnx
+++ b/CodexComputerRunMCPServer.slnx
@@ -1,4 +1,8 @@
 <Solution>
   <Project Path="src/CodexComputerRunMCPServer/CodexComputerRunMCPServer.csproj" />
+  <Folder Name="/Config/">
+    <File Path="Directory.Build.props" />
+    <File Path="Directory.Packages.props" />
+  </Folder>
   <Project Path="src/CodexComputerRunMCPServer.Tests/CodexComputerRunMCPServer.Tests.csproj" />
 </Solution>
diff --git a/README.md b/README.md
index eb0a064..0161308 100644
--- a/README.md
+++ b/README.md
@@ -236,6 +236,27 @@ skills/
 
 ## Configuration
 
+### Lifecycle Safeguards
+
+The server enforces one active desktop-control instance per Windows user profile by taking an exclusive lock at:
+
+```text
+%LOCALAPPDATA%\CodexComputerRunMCPServer\server.lock
+```
+
+If another instance is already running, startup exits with code `2` before exposing MCP tools. This prevents multiple agents from sending mouse, keyboard, and clipboard input to the same desktop at the same time.
+
+The server also shuts itself down after `5` minutes without MCP tool activity. Every tool call updates activity state and active calls are never stopped mid-invocation.
+
+Optional environment overrides:
+
+| Variable | Default | Detail |
+|----------|---------|--------|
+| `CODEX_COMPUTER_RUN_SINGLE_INSTANCE` | `true` | Set `false` to disable the single-instance lock. |
+| `CODEX_COMPUTER_RUN_IDLE_SHUTDOWN` | `true` | Set `false` to disable idle shutdown. |
+| `CODEX_COMPUTER_RUN_IDLE_TIMEOUT_SECONDS` | `300` | Seconds without tool activity before shutdown. Values `0` or lower disable idle shutdown. |
+| `CODEX_COMPUTER_RUN_IDLE_CHECK_INTERVAL_SECONDS` | `10` | Seconds between idle checks. |
+
 ### Fast Codex Desktop Configuration
 
 After publishing, Codex can launch the optimized executable directly:
@@ -336,8 +357,8 @@ dotnet test .\src\CodexComputerRunMCPServer.Tests\CodexComputerRunMCPServer.Test
 ```
 
 Current verification:
-- 32 TUnit tests passed.
-- Coverage: 91.56% line coverage, 79.03% branch coverage for testable code.
+- 37 TUnit tests passed.
+- Coverage: 87.50% line coverage, 66.16% branch coverage for testable code.
 - NuGet package verification confirms `skills/codex-computer-run/SKILL.md` and `skills/codex-computer-run/agents/openai.yaml` are bundled.
 - Native Win32 P/Invoke shims are excluded from coverage and verified through the service boundary plus live MCP tool discovery.
 
diff --git a/src/CodexComputerRunMCPServer.Tests/ComputerRunLifecycleTests.cs b/src/CodexComputerRunMCPServer.Tests/ComputerRunLifecycleTests.cs
new file mode 100644
index 0000000..77c46f2
--- /dev/null
+++ b/src/CodexComputerRunMCPServer.Tests/ComputerRunLifecycleTests.cs
@@ -0,0 +1,129 @@
+using Microsoft.Extensions.Configuration;
+using ModelContextProtocol.Protocol;
+
+namespace CodexComputerRunMCPServer.Tests;
+
+public class ComputerRunLifecycleTests
+{
+    [Test]
+    public async Task ActivityTracker_TracksActiveInvocationAndCompletion()
+    {
+        var clock = new ManualTimeProvider(new DateTimeOffset(2026, 5, 12, 12, 0, 0, TimeSpan.Zero));
+        var tracker = new ComputerRunActivityTracker(clock);
+
+        clock.Advance(TimeSpan.FromSeconds(1));
+        var invocation = tracker.BeginInvocation();
+        var running = tracker.GetSnapshot();
+
+        clock.Advance(TimeSpan.FromSeconds(2));
+        invocation.Dispose();
+        var completed = tracker.GetSnapshot();
+
+        await Assert.That(running.ActiveInvocations).IsEqualTo(1);
+        await Assert.That(running.LastActivityUtc).IsEqualTo(new DateTimeOffset(2026, 5, 12, 12, 0, 1, TimeSpan.Zero));
+        await Assert.That(completed.ActiveInvocations).IsEqualTo(0);
+        await Assert.That(completed.LastActivityUtc).IsEqualTo(new DateTimeOffset(2026, 5, 12, 12, 0, 3, TimeSpan.Zero));
+    }
+
+    [Test]
+    public async Task StaticTools_TrackActivityDuringServiceInvocation()
+    {
+        var service = new ActivityInspectingService();
+        var tracker = new ComputerRunActivityTracker();
+        using var restoreService = ComputerRunToolRuntime.ReplaceServiceForTests(service);
+        using var restoreTracker = ComputerRunToolRuntime.ReplaceActivityTrackerForTests(tracker);
+
+        _ = ComputerRunTools.cursor_position();
+
+        await Assert.That(service.ActiveInvocationsDuringCall).IsEqualTo(1);
+        await Assert.That(tracker.GetSnapshot().ActiveInvocations).IsEqualTo(0);
+    }
+
+    [Test]
+    public async Task IdleShutdown_StopsOnlyWhenNoActiveInvocationsExceedTimeout()
+    {
+        var options = ComputerRunLifecycleOptions.Default with { IdleTimeout = TimeSpan.FromMinutes(5) };
+        var now = new DateTimeOffset(2026, 5, 12, 12, 10, 0, TimeSpan.Zero);
+        var idleSnapshot = new ComputerRunActivitySnapshot(now - TimeSpan.FromMinutes(5), ActiveInvocations: 0);
+        var activeSnapshot = new ComputerRunActivitySnapshot(now - TimeSpan.FromMinutes(30), ActiveInvocations: 1);
+        var recentSnapshot = new ComputerRunActivitySnapshot(now - TimeSpan.FromMinutes(4), ActiveInvocations: 0);
+
+        await Assert.That(IdleShutdownService.ShouldStopForIdle(options, idleSnapshot, now)).IsTrue();
+        await Assert.That(IdleShutdownService.ShouldStopForIdle(options, activeSnapshot, now)).IsFalse();
+        await Assert.That(IdleShutdownService.ShouldStopForIdle(options, recentSnapshot, now)).IsFalse();
+    }
+
+    [Test]
+    public async Task LifecycleOptions_ReadsConfigurationOverrides()
+    {
+        var configuration = new ConfigurationBuilder()
+            .AddInMemoryCollection(new Dictionary<string, string?>
+            {
+                ["ComputerRun:SingleInstanceEnabled"] = "false",
+                ["CODEX_COMPUTER_RUN_IDLE_SHUTDOWN"] = "false",
+                ["ComputerRun:IdleTimeoutSeconds"] = "12.5",
+                ["ComputerRun:IdleCheckIntervalSeconds"] = "1",
+            })
+            .Build();
+
+        var options = ComputerRunLifecycleOptions.FromConfiguration(configuration);
+
+        await Assert.That(options.SingleInstanceEnabled).IsFalse();
+        await Assert.That(options.IdleShutdownEnabled).IsFalse();
+        await Assert.That(options.IdleTimeout).IsEqualTo(TimeSpan.FromSeconds(12.5));
+        await Assert.That(options.IdleCheckInterval).IsEqualTo(TimeSpan.FromSeconds(1));
+    }
+
+    [Test]
+    public async Task SingleInstanceGuard_RejectsSecondConcurrentOwner()
+    {
+        var lockFilePath = Path.Combine(
+            Path.GetTempPath(),
+            "codex-computer-run-tests",
+            Guid.NewGuid().ToString("N"),
+            "server.lock");
+
+        using var first = SingleInstanceGuard.TryAcquire(enabled: true, lockFilePath: lockFilePath);
+        using var second = SingleInstanceGuard.TryAcquire(enabled: true, lockFilePath: lockFilePath);
+
+        await Assert.That(first.HasOwnership).IsTrue();
+        await Assert.That(second.HasOwnership).IsFalse();
+    }
+
+    private sealed class ManualTimeProvider(DateTimeOffset utcNow) : TimeProvider
+    {
+        private DateTimeOffset _utcNow = utcNow;
+
+        public override DateTimeOffset GetUtcNow() => _utcNow;
+
+        public void Advance(TimeSpan duration) => _utcNow += duration;
+    }
+
+    private sealed class ActivityInspectingService : IComputerRunService
+    {
+        public int ActiveInvocationsDuringCall { get; private set; }
+
+        public string CursorPosition()
+        {
+            ActiveInvocationsDuringCall = ComputerRunToolRuntime.ActivityTracker.GetSnapshot().ActiveInvocations;
+            return "{}";
+        }
+
+        public CallToolResult Screenshot(string? path, bool includeImage) => throw new NotSupportedException();
+
+        public string MoveMouse(int x, int y, double? delay) => throw new NotSupportedException();
+
+        public string Click(int? x, int? y, string button, int clicks, double interval, double? delay)
+            => throw new NotSupportedException();
+
+        public string Scroll(int amount, int? x, int? y, double? delay) => throw new NotSupportedException();
+
+        public string PressKey(string key, double duration, double? delay) => throw new NotSupportedException();
+
+        public string Hotkey(string keys, double? delay) => throw new NotSupportedException();
+
+        public string TypeText(string text, double? delay) => throw new NotSupportedException();
+
+        public string ListWindows(int limit) => throw new NotSupportedException();
+    }
+}
diff --git a/src/CodexComputerRunMCPServer.Tests/McpIntegrationTests.cs b/src/CodexComputerRunMCPServer.Tests/McpIntegrationTests.cs
index cb47279..06dba64 100644
--- a/src/CodexComputerRunMCPServer.Tests/McpIntegrationTests.cs
+++ b/src/CodexComputerRunMCPServer.Tests/McpIntegrationTests.cs
@@ -17,6 +17,7 @@ public async Task Host_CreatesMcpHostedService()
         var hostedServices = host.Services.GetServices<IHostedService>();
 
         await Assert.That(hostedServices.Any()).IsTrue();
+        await Assert.That(hostedServices.OfType<IdleShutdownService>().Any()).IsTrue();
     }
 
     [Test]
@@ -75,9 +76,9 @@ public async Task McpManifest_UsesNuGetPackageAndStdioTransport()
         var package = root.GetProperty("packages")[0];
 
         await Assert.That(root.GetProperty("name").GetString()).IsEqualTo("io.github.chrispulman/codex-computer-run-mcp-server");
-        await Assert.That(root.GetProperty("version").GetString()).IsEqualTo("1.0.0");
+        await Assert.That(root.GetProperty("version").GetString()).IsEqualTo("1.0.1");
         await Assert.That(package.GetProperty("identifier").GetString()).IsEqualTo("CP.CodexComputerRun.Mcp.Server");
-        await Assert.That(package.GetProperty("version").GetString()).IsEqualTo("1.0.0");
+        await Assert.That(package.GetProperty("version").GetString()).IsEqualTo("1.0.1");
         await Assert.That(package.GetProperty("transport").GetProperty("type").GetString()).IsEqualTo("stdio");
     }
 
diff --git a/src/CodexComputerRunMCPServer/CodexComputerRunMCPServer.csproj b/src/CodexComputerRunMCPServer/CodexComputerRunMCPServer.csproj
index c081613..de9574f 100644
--- a/src/CodexComputerRunMCPServer/CodexComputerRunMCPServer.csproj
+++ b/src/CodexComputerRunMCPServer/CodexComputerRunMCPServer.csproj
@@ -5,7 +5,7 @@
     <AssemblyName>CodexComputerRunMCPServer</AssemblyName>
     <RootNamespace>CodexComputerRunMCPServer</RootNamespace>
     <PackageId>CP.CodexComputerRun.Mcp.Server</PackageId>
-    <Version>1.0.0</Version>
+    <Version>1.0.1</Version>
     <Title>Codex Computer Run MCP Server</Title>
     <Description>Windows-only MCP server for Codex desktop control: screenshots, mouse, keyboard, clipboard paste, cursor position, visible window discovery, and a bundled Codex Skill.</Description>
     <Authors>Chris Pulman</Authors>
diff --git a/src/CodexComputerRunMCPServer/ComputerRunActivityTracker.cs b/src/CodexComputerRunMCPServer/ComputerRunActivityTracker.cs
new file mode 100644
index 0000000..75f14a0
--- /dev/null
+++ b/src/CodexComputerRunMCPServer/ComputerRunActivityTracker.cs
@@ -0,0 +1,83 @@
+namespace CodexComputerRunMCPServer;
+
+/// <summary>
+/// Tracks recent MCP tool activity so the server can shut itself down after it becomes idle.
+/// </summary>
+internal sealed class ComputerRunActivityTracker
+{
+    private readonly TimeProvider _timeProvider;
+    private long _lastActivityUtcTicks;
+    private int _activeInvocations;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="ComputerRunActivityTracker"/> class.
+    /// </summary>
+    /// <param name="timeProvider">Optional time provider used by tests.</param>
+    public ComputerRunActivityTracker(TimeProvider? timeProvider = null)
+    {
+        _timeProvider = timeProvider ?? TimeProvider.System;
+        Touch();
+    }
+
+    /// <summary>
+    /// Records the start of an MCP tool invocation.
+    /// </summary>
+    /// <returns>An invocation scope that records completion when disposed.</returns>
+    public IDisposable BeginInvocation()
+    {
+        _ = Interlocked.Increment(ref _activeInvocations);
+        Touch();
+        return new InvocationScope(this);
+    }
+
+    /// <summary>
+    /// Gets an immutable snapshot of current tool activity.
+    /// </summary>
+    /// <returns>The current activity state.</returns>
+    public ComputerRunActivitySnapshot GetSnapshot()
+    {
+        var ticks = Interlocked.Read(ref _lastActivityUtcTicks);
+        var activeInvocations = Volatile.Read(ref _activeInvocations);
+        return new ComputerRunActivitySnapshot(new DateTimeOffset(ticks, TimeSpan.Zero), activeInvocations);
+    }
+
+    private void EndInvocation()
+    {
+        Touch();
+        _ = Interlocked.Decrement(ref _activeInvocations);
+    }
+
+    private void Touch()
+    {
+        var timestamp = _timeProvider.GetUtcNow().UtcTicks;
+        Interlocked.Exchange(ref _lastActivityUtcTicks, timestamp);
+    }
+
+    private sealed class InvocationScope(ComputerRunActivityTracker owner) : IDisposable
+    {
+        private int _disposed;
+
+        public void Dispose()
+        {
+            if (Interlocked.Exchange(ref _disposed, 1) == 0)
+            {
+                owner.EndInvocation();
+            }
+        }
+    }
+}
+
+/// <summary>
+/// Represents an immutable snapshot of MCP tool activity.
+/// </summary>
+/// <param name="LastActivityUtc">The latest observed tool start or completion time.</param>
+/// <param name="ActiveInvocations">The number of tool calls currently running.</param>
+internal sealed record ComputerRunActivitySnapshot(DateTimeOffset LastActivityUtc, int ActiveInvocations)
+{
+    /// <summary>
+    /// Calculates how long the server has been idle at the specified time.
+    /// </summary>
+    /// <param name="nowUtc">The current UTC time.</param>
+    /// <returns>The elapsed time since the last tool activity.</returns>
+    public TimeSpan GetIdleDuration(DateTimeOffset nowUtc) => nowUtc - LastActivityUtc;
+}
diff --git a/src/CodexComputerRunMCPServer/ComputerRunLifecycleOptions.cs b/src/CodexComputerRunMCPServer/ComputerRunLifecycleOptions.cs
new file mode 100644
index 0000000..0618db8
--- /dev/null
+++ b/src/CodexComputerRunMCPServer/ComputerRunLifecycleOptions.cs
@@ -0,0 +1,143 @@
+using System.Globalization;
+using Microsoft.Extensions.Configuration;
+
+namespace CodexComputerRunMCPServer;
+
+/// <summary>
+/// Configures process lifecycle safeguards for the computer run MCP server.
+/// </summary>
+/// <param name="SingleInstanceEnabled">Whether startup should reject concurrent server instances.</param>
+/// <param name="IdleShutdownEnabled">Whether the host should stop after a period without tool activity.</param>
+/// <param name="IdleTimeout">How long the server may remain unused before it shuts down.</param>
+/// <param name="IdleCheckInterval">How often idle state is checked.</param>
+internal sealed record ComputerRunLifecycleOptions(
+    bool SingleInstanceEnabled,
+    bool IdleShutdownEnabled,
+    TimeSpan IdleTimeout,
+    TimeSpan IdleCheckInterval)
+{
+    /// <summary>
+    /// Default time without MCP tool use before the server stops itself.
+    /// </summary>
+    public static readonly TimeSpan DefaultIdleTimeout = TimeSpan.FromMinutes(5);
+
+    /// <summary>
+    /// Default cadence for checking idle state.
+    /// </summary>
+    public static readonly TimeSpan DefaultIdleCheckInterval = TimeSpan.FromSeconds(10);
+
+    /// <summary>
+    /// Gets the default lifecycle options.
+    /// </summary>
+    public static ComputerRunLifecycleOptions Default { get; } = new(
+        SingleInstanceEnabled: true,
+        IdleShutdownEnabled: true,
+        IdleTimeout: DefaultIdleTimeout,
+        IdleCheckInterval: DefaultIdleCheckInterval);
+
+    /// <summary>
+    /// Creates lifecycle options from application configuration and supported environment variables.
+    /// </summary>
+    /// <param name="configuration">Application configuration.</param>
+    /// <returns>The resolved lifecycle options.</returns>
+    public static ComputerRunLifecycleOptions FromConfiguration(IConfiguration configuration)
+    {
+        ArgumentNullException.ThrowIfNull(configuration);
+
+        var singleInstanceEnabled = ReadBoolean(
+            configuration,
+            "SingleInstanceEnabled",
+            "CODEX_COMPUTER_RUN_SINGLE_INSTANCE",
+            Default.SingleInstanceEnabled);
+
+        var idleShutdownEnabled = ReadBoolean(
+            configuration,
+            "IdleShutdownEnabled",
+            "CODEX_COMPUTER_RUN_IDLE_SHUTDOWN",
+            Default.IdleShutdownEnabled);
+
+        var idleTimeout = ReadSeconds(
+            configuration,
+            "IdleTimeoutSeconds",
+            "CODEX_COMPUTER_RUN_IDLE_TIMEOUT_SECONDS",
+            Default.IdleTimeout);
+
+        var idleCheckInterval = ReadSeconds(
+            configuration,
+            "IdleCheckIntervalSeconds",
+            "CODEX_COMPUTER_RUN_IDLE_CHECK_INTERVAL_SECONDS",
+            Default.IdleCheckInterval);
+
+        if (idleTimeout <= TimeSpan.Zero)
+        {
+            idleShutdownEnabled = false;
+        }
+
+        if (idleCheckInterval <= TimeSpan.Zero)
+        {
+            idleCheckInterval = Default.IdleCheckInterval;
+        }
+
+        return new ComputerRunLifecycleOptions(
+            singleInstanceEnabled,
+            idleShutdownEnabled,
+            idleTimeout,
+            idleCheckInterval);
+    }
+
+    /// <summary>
+    /// Creates lifecycle options from environment variables only.
+    /// </summary>
+    /// <returns>The resolved lifecycle options.</returns>
+    public static ComputerRunLifecycleOptions FromEnvironment()
+    {
+        var configuration = new ConfigurationBuilder()
+            .AddEnvironmentVariables()
+            .Build();
+
+        return FromConfiguration(configuration);
+    }
+
+    private static string? ReadValue(IConfiguration configuration, string key, string environmentVariable)
+        => configuration[$"ComputerRun:{key}"]
+        ?? configuration[key]
+        ?? configuration[environmentVariable]
+        ?? Environment.GetEnvironmentVariable(environmentVariable);
+
+    private static bool ReadBoolean(
+        IConfiguration configuration,
+        string key,
+        string environmentVariable,
+        bool defaultValue)
+    {
+        var value = ReadValue(configuration, key, environmentVariable);
+        if (string.IsNullOrWhiteSpace(value))
+        {
+            return defaultValue;
+        }
+
+        return value.Trim().ToLowerInvariant() switch
+        {
+            "1" or "true" or "yes" or "on" => true,
+            "0" or "false" or "no" or "off" => false,
+            _ => defaultValue,
+        };
+    }
+
+    private static TimeSpan ReadSeconds(
+        IConfiguration configuration,
+        string key,
+        string environmentVariable,
+        TimeSpan defaultValue)
+    {
+        var value = ReadValue(configuration, key, environmentVariable);
+        if (string.IsNullOrWhiteSpace(value))
+        {
+            return defaultValue;
+        }
+
+        return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var seconds)
+            ? TimeSpan.FromSeconds(seconds)
+            : defaultValue;
+    }
+}
diff --git a/src/CodexComputerRunMCPServer/ComputerRunToolRuntime.cs b/src/CodexComputerRunMCPServer/ComputerRunToolRuntime.cs
index 936904a..817f07c 100644
--- a/src/CodexComputerRunMCPServer/ComputerRunToolRuntime.cs
+++ b/src/CodexComputerRunMCPServer/ComputerRunToolRuntime.cs
@@ -7,6 +7,7 @@ namespace CodexComputerRunMCPServer;
 internal static class ComputerRunToolRuntime
 {
     private static IComputerRunService _service = ComputerRunService.CreateDefault();
+    private static ComputerRunActivityTracker _activityTracker = new();
 
     /// <summary>
     /// Gets the current <see cref="IComputerRunService"/> instance using a volatile read
@@ -14,6 +15,17 @@ internal static class ComputerRunToolRuntime
     /// </summary>
     public static IComputerRunService Service => Volatile.Read(ref _service);
 
+    /// <summary>
+    /// Gets the activity tracker used by MCP tool calls and lifecycle services.
+    /// </summary>
+    public static ComputerRunActivityTracker ActivityTracker => Volatile.Read(ref _activityTracker);
+
+    /// <summary>
+    /// Starts tracking a tool invocation until the returned scope is disposed.
+    /// </summary>
+    /// <returns>An invocation scope that must be disposed when the tool call completes.</returns>
+    public static IDisposable BeginToolInvocation() => ActivityTracker.BeginInvocation();
+
     /// <summary>
     /// Replaces the current <see cref="IComputerRunService"/> with the specified instance
     /// for the duration of a test, restoring the previous service when the returned
@@ -32,6 +44,23 @@ internal static IDisposable ReplaceServiceForTests(IComputerRunService service)
         return new RestoreService(previous);
     }
 
+    /// <summary>
+    /// Replaces the current activity tracker for the duration of a test.
+    /// </summary>
+    /// <param name="activityTracker">The test activity tracker to use.</param>
+    /// <returns>
+    /// An <see cref="IDisposable"/> that, when disposed, restores the previous tracker.
+    /// </returns>
+    /// <exception cref="ArgumentNullException">
+    /// Thrown when <paramref name="activityTracker"/> is <see langword="null"/>.
+    /// </exception>
+    internal static IDisposable ReplaceActivityTrackerForTests(ComputerRunActivityTracker activityTracker)
+    {
+        ArgumentNullException.ThrowIfNull(activityTracker);
+        var previous = Interlocked.Exchange(ref _activityTracker, activityTracker);
+        return new RestoreActivityTracker(activityTracker, previous);
+    }
+
     /// <summary>
     /// Restores a previously held <see cref="IComputerRunService"/> instance when disposed.
     /// </summary>
@@ -42,4 +71,20 @@ private sealed class RestoreService(IComputerRunService previous) : IDisposable
         /// </summary>
         public void Dispose() => Interlocked.Exchange(ref _service, previous);
     }
+
+    /// <summary>
+    /// Restores a previously held <see cref="ComputerRunActivityTracker"/> instance when disposed.
+    /// </summary>
+    private sealed class RestoreActivityTracker(
+        ComputerRunActivityTracker current,
+        ComputerRunActivityTracker previous) : IDisposable
+    {
+        /// <summary>
+        /// Restores the previous <see cref="ComputerRunActivityTracker"/> instance in a thread-safe manner.
+        /// </summary>
+        public void Dispose()
+        {
+            _ = Interlocked.CompareExchange(ref _activityTracker, previous, current);
+        }
+    }
 }
diff --git a/src/CodexComputerRunMCPServer/ComputerRunTools.cs b/src/CodexComputerRunMCPServer/ComputerRunTools.cs
index 8fd8eb5..026eeda 100644
--- a/src/CodexComputerRunMCPServer/ComputerRunTools.cs
+++ b/src/CodexComputerRunMCPServer/ComputerRunTools.cs
@@ -32,7 +32,7 @@ public static class ComputerRunTools
     public static CallToolResult screenshot(
         [Description("Optional output PNG path. If omitted, no temporary file is created.")] string? path = null,
         [Description("Include PNG image data in the MCP tool result.")] bool include_image = true)
-        => ComputerRunToolRuntime.Service.Screenshot(path, include_image);
+        => Invoke(service => service.Screenshot(path, include_image));
 
     /// <summary>
     /// Moves the mouse cursor to absolute virtual-screen coordinates.
@@ -47,7 +47,7 @@ public static string move_mouse(
         [Description("Absolute X coordinate.")] int x,
         [Description("Absolute Y coordinate.")] int y,
         [Description("Optional delay after the action, in seconds.")] double? delay = null)
-        => ComputerRunToolRuntime.Service.MoveMouse(x, y, delay);
+        => Invoke(service => service.MoveMouse(x, y, delay));
 
     /// <summary>
     /// Performs a mouse click at the current cursor position or at provided coordinates.
@@ -68,7 +68,7 @@ public static string click(
         [Description("Number of clicks.")] int clicks = 1,
         [Description("Delay between repeated clicks, in seconds.")] double interval = 0.08,
         [Description("Optional delay after the action, in seconds.")] double? delay = null)
-        => ComputerRunToolRuntime.Service.Click(x, y, button, clicks, interval, delay);
+        => Invoke(service => service.Click(x, y, button, clicks, interval, delay));
 
     /// <summary>
     /// Scrolls the mouse wheel, optionally after moving to specified coordinates.
@@ -85,7 +85,7 @@ public static string scroll(
         [Description("Optional absolute X coordinate to move to before scrolling.")] int? x = null,
         [Description("Optional absolute Y coordinate to move to before scrolling.")] int? y = null,
         [Description("Optional delay after the action, in seconds.")] double? delay = null)
-        => ComputerRunToolRuntime.Service.Scroll(amount, x, y, delay);
+        => Invoke(service => service.Scroll(amount, x, y, delay));
 
     /// <summary>
     /// Presses and releases a single keyboard key.
@@ -100,7 +100,7 @@ public static string press_key(
         [Description("Key name or single character.")] string key,
         [Description("How long to hold the key, in seconds.")] double duration = 0.03,
         [Description("Optional delay after the action, in seconds.")] double? delay = null)
-        => ComputerRunToolRuntime.Service.PressKey(key, duration, delay);
+        => Invoke(service => service.PressKey(key, duration, delay));
 
     /// <summary>
     /// Presses a keyboard shortcut chord such as <c>ctrl+l</c> or <c>ctrl+shift+escape</c>.
@@ -113,7 +113,7 @@ public static string press_key(
     public static string hotkey(
         [Description("Shortcut text. Use +, comma, or space separators, e.g. ctrl+shift+escape.")] string keys,
         [Description("Optional delay after the action, in seconds.")] double? delay = null)
-        => ComputerRunToolRuntime.Service.Hotkey(keys, delay);
+        => Invoke(service => service.Hotkey(keys, delay));
 
     /// <summary>
     /// Pastes Unicode text into the currently focused Windows application via clipboard and Ctrl+V.
@@ -126,7 +126,7 @@ public static string hotkey(
     public static string type_text(
         [Description("Text to paste into the focused application.")] string text,
         [Description("Optional delay after the action, in seconds.")] double? delay = null)
-        => ComputerRunToolRuntime.Service.TypeText(text, delay);
+        => Invoke(service => service.TypeText(text, delay));
 
     /// <summary>
     /// Gets the current cursor position.
@@ -135,7 +135,7 @@ public static string type_text(
     [McpServerTool]
     [Description("Return the current Windows cursor position as JSON.")]
     public static string cursor_position()
-        => ComputerRunToolRuntime.Service.CursorPosition();
+        => Invoke(service => service.CursorPosition());
 
     /// <summary>
     /// Lists visible top-level desktop windows.
@@ -145,5 +145,12 @@ public static string cursor_position()
     [McpServerTool]
     [Description("List visible top-level Windows desktop windows as JSON.")]
     public static string list_windows([Description("Maximum number of windows to return.")] int limit = 50)
-        => ComputerRunToolRuntime.Service.ListWindows(limit);
+        => Invoke(service => service.ListWindows(limit));
+
+    private static TResult Invoke<TResult>(Func<IComputerRunService, TResult> action)
+    {
+        ArgumentNullException.ThrowIfNull(action);
+        using var invocation = ComputerRunToolRuntime.BeginToolInvocation();
+        return action(ComputerRunToolRuntime.Service);
+    }
 }
diff --git a/src/CodexComputerRunMCPServer/IdleShutdownService.cs b/src/CodexComputerRunMCPServer/IdleShutdownService.cs
new file mode 100644
index 0000000..9844639
--- /dev/null
+++ b/src/CodexComputerRunMCPServer/IdleShutdownService.cs
@@ -0,0 +1,75 @@
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace CodexComputerRunMCPServer;
+
+/// <summary>
+/// Stops the MCP host after a period with no active or recent tool invocations.
+/// </summary>
+/// <param name="options">Lifecycle options.</param>
+/// <param name="applicationLifetime">Host lifetime controller.</param>
+/// <param name="logger">Logger used for lifecycle diagnostics.</param>
+internal sealed class IdleShutdownService(
+    ComputerRunLifecycleOptions options,
+    IHostApplicationLifetime applicationLifetime,
+    ILogger<IdleShutdownService> logger) : BackgroundService
+{
+    private readonly TimeProvider _timeProvider = TimeProvider.System;
+
+    /// <inheritdoc />
+    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+    {
+        if (!options.IdleShutdownEnabled)
+        {
+            logger.LogDebug("Computer run idle shutdown is disabled.");
+            return;
+        }
+
+        using var timer = new PeriodicTimer(options.IdleCheckInterval, _timeProvider);
+
+        try
+        {
+            while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false))
+            {
+                var snapshot = ComputerRunToolRuntime.ActivityTracker.GetSnapshot();
+                var nowUtc = _timeProvider.GetUtcNow();
+
+                if (!ShouldStopForIdle(options, snapshot, nowUtc))
+                {
+                    continue;
+                }
+
+                logger.LogInformation(
+                    "Stopping computer run MCP server after {IdleDuration:g} without MCP tool activity.",
+                    snapshot.GetIdleDuration(nowUtc));
+
+                applicationLifetime.StopApplication();
+                return;
+            }
+        }
+        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
+        {
+            // Normal host shutdown.
+        }
+    }
+
+    /// <summary>
+    /// Determines whether current activity satisfies the configured idle shutdown threshold.
+    /// </summary>
+    /// <param name="options">Lifecycle options.</param>
+    /// <param name="snapshot">Current activity snapshot.</param>
+    /// <param name="nowUtc">The current UTC time.</param>
+    /// <returns><see langword="true"/> when the server should stop for idleness.</returns>
+    internal static bool ShouldStopForIdle(
+        ComputerRunLifecycleOptions options,
+        ComputerRunActivitySnapshot snapshot,
+        DateTimeOffset nowUtc)
+    {
+        ArgumentNullException.ThrowIfNull(options);
+        ArgumentNullException.ThrowIfNull(snapshot);
+
+        return options.IdleShutdownEnabled
+            && snapshot.ActiveInvocations == 0
+            && snapshot.GetIdleDuration(nowUtc) >= options.IdleTimeout;
+    }
+}
diff --git a/src/CodexComputerRunMCPServer/Program.cs b/src/CodexComputerRunMCPServer/Program.cs
index 842664d..9fe00b3 100644
--- a/src/CodexComputerRunMCPServer/Program.cs
+++ b/src/CodexComputerRunMCPServer/Program.cs
@@ -1,81 +1,95 @@
-    using System.Diagnostics.CodeAnalysis;
-    using Microsoft.Extensions.DependencyInjection;
-    using Microsoft.Extensions.Hosting;
-    using Microsoft.Extensions.Logging;
-    using ModelContextProtocol.Server;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using ModelContextProtocol.Server;
 
-    namespace CodexComputerRunMCPServer;
+namespace CodexComputerRunMCPServer;
 
+/// <summary>
+/// Provides application startup and host configuration for the MCP server process.
+/// </summary>
+public static class Program
+{
     /// <summary>
-    /// Provides application startup and host configuration for the MCP server process.
+    /// Application entry point.
     /// </summary>
-    public static class Program
+    /// <param name="args">Command-line arguments passed to the process.</param>
+    /// <returns>
+    /// A task that resolves to an exit code:
+    /// <c>0</c> when the server exits normally; <c>1</c> when startup is rejected
+    /// because the current operating system is not Windows; <c>2</c> when another
+    /// server instance already owns the desktop-control lock.
+    /// </returns>
+    /// <remarks>
+    /// This server is supported only in a signed-in Windows desktop session.
+    /// The method also enables per-monitor DPI awareness before creating and running the host.
+    /// </remarks>
+    [ExcludeFromCodeCoverage]
+    public static async Task<int> Main(string[] args)
     {
-        /// <summary>
-        /// Application entry point.
-        /// </summary>
-        /// <param name="args">Command-line arguments passed to the process.</param>
-        /// <returns>
-        /// A task that resolves to an exit code:
-        /// <c>0</c> when the server exits normally; <c>1</c> when startup is rejected
-        /// because the current operating system is not Windows.
-        /// </returns>
-        /// <remarks>
-        /// This server is supported only in a signed-in Windows desktop session.
-        /// The method also enables per-monitor DPI awareness before creating and running the host.
-        /// </remarks>
-        [ExcludeFromCodeCoverage]
-        public static async Task<int> Main(string[] args)
+        if (!OperatingSystem.IsWindows())
         {
-            if (!OperatingSystem.IsWindows())
-            {
-                Console.Error.WriteLine("CodexComputerRunMCPServer is Windows-only. Run it from a signed-in Windows desktop session.");
-                return 1;
-            }
-
-            if (CodexSkillInstaller.IsInstallRequested(args))
-            {
-                var result = CodexSkillInstaller.InstallBundledSkill(
-                    createCodexHome: true,
-                    overwrite: CodexSkillInstaller.IsForceRequested(args));
+            Console.Error.WriteLine("CodexComputerRunMCPServer is Windows-only. Run it from a signed-in Windows desktop session.");
+            return 1;
+        }
 
-                Console.Error.WriteLine(result.Message);
-                return result.Success ? 0 : 1;
-            }
+        if (CodexSkillInstaller.IsInstallRequested(args))
+        {
+            var result = CodexSkillInstaller.InstallBundledSkill(
+                createCodexHome: true,
+                overwrite: CodexSkillInstaller.IsForceRequested(args));
 
-            NativeMethods.TryEnablePerMonitorDpiAwareness();
-            _ = CodexSkillInstaller.TryAutoInstall(Console.Error);
+            Console.Error.WriteLine(result.Message);
+            return result.Success ? 0 : 1;
+        }
 
-            using var host = CreateHost(args);
-            await host.RunAsync().ConfigureAwait(false);
-            return 0;
+        var lifecycleOptions = ComputerRunLifecycleOptions.FromEnvironment();
+        using var instanceGuard = SingleInstanceGuard.TryAcquire(lifecycleOptions.SingleInstanceEnabled);
+        if (!instanceGuard.HasOwnership)
+        {
+            Console.Error.WriteLine(
+                "Another CodexComputerRunMCPServer instance is already running. " +
+                "Only one computer-control MCP server can own the desktop at a time.");
+            return SingleInstanceGuard.ConcurrentInstanceExitCode;
         }
 
-        /// <summary>
-        /// Creates and configures the application host used by the MCP server.
-        /// </summary>
-        /// <param name="args">Command-line arguments used to initialize the host builder.</param>
-        /// <returns>A fully configured <see cref="IHost"/> instance.</returns>
-        /// <remarks>
-        /// Logging providers are reset and console logging is directed to standard error so that
-        /// standard output remains reserved for MCP JSON-RPC stdio transport traffic.
-        /// </remarks>
-        public static IHost CreateHost(string[] args)
+        NativeMethods.TryEnablePerMonitorDpiAwareness();
+        _ = CodexSkillInstaller.TryAutoInstall(Console.Error);
+
+        using var host = CreateHost(args);
+        await host.RunAsync().ConfigureAwait(false);
+        return 0;
+    }
+
+    /// <summary>
+    /// Creates and configures the application host used by the MCP server.
+    /// </summary>
+    /// <param name="args">Command-line arguments used to initialize the host builder.</param>
+    /// <returns>A fully configured <see cref="IHost"/> instance.</returns>
+    /// <remarks>
+    /// Logging providers are reset and console logging is directed to standard error so that
+    /// standard output remains reserved for MCP JSON-RPC stdio transport traffic.
+    /// </remarks>
+    public static IHost CreateHost(string[] args)
+    {
+        var builder = Host.CreateApplicationBuilder(args);
+
+        builder.Logging.ClearProviders();
+        builder.Logging.AddConsole(options =>
         {
-            var builder = Host.CreateApplicationBuilder(args);
+            // MCP stdio reserves stdout for JSON-RPC. Diagnostics must go to stderr.
+            options.LogToStandardErrorThreshold = LogLevel.Trace;
+        });
 
-            builder.Logging.ClearProviders();
-            builder.Logging.AddConsole(options =>
-            {
-                // MCP stdio reserves stdout for JSON-RPC. Diagnostics must go to stderr.
-                options.LogToStandardErrorThreshold = LogLevel.Trace;
-            });
+        builder.Services.AddSingleton(_ => ComputerRunLifecycleOptions.FromConfiguration(builder.Configuration));
+        builder.Services.AddHostedService<IdleShutdownService>();
 
-            builder.Services
-                .AddMcpServer()
-                .WithStdioServerTransport()
-                .WithToolsFromAssembly();
+        builder.Services
+            .AddMcpServer()
+            .WithStdioServerTransport()
+            .WithToolsFromAssembly();
 
-            return builder.Build();
-        }
+        return builder.Build();
     }
+}
diff --git a/src/CodexComputerRunMCPServer/SingleInstanceGuard.cs b/src/CodexComputerRunMCPServer/SingleInstanceGuard.cs
new file mode 100644
index 0000000..a6b8996
--- /dev/null
+++ b/src/CodexComputerRunMCPServer/SingleInstanceGuard.cs
@@ -0,0 +1,118 @@
+namespace CodexComputerRunMCPServer;
+
+/// <summary>
+/// Owns a process-wide lock file that prevents concurrent computer run MCP server instances.
+/// </summary>
+internal sealed class SingleInstanceGuard : IDisposable
+{
+    private readonly FileStream? _lockFile;
+
+    private SingleInstanceGuard(string lockFilePath, FileStream? lockFile, bool isEnabled)
+    {
+        LockFilePath = lockFilePath;
+        _lockFile = lockFile;
+        IsEnabled = isEnabled;
+    }
+
+    /// <summary>
+    /// Gets the exit code used when startup is rejected because another instance is running.
+    /// </summary>
+    public const int ConcurrentInstanceExitCode = 2;
+
+    /// <summary>
+    /// Gets the process-wide lock file path used by default.
+    /// </summary>
+    public static string DefaultLockFilePath { get; } = CreateDefaultLockFilePath();
+
+    /// <summary>
+    /// Gets a value indicating whether single-instance enforcement is enabled.
+    /// </summary>
+    public bool IsEnabled { get; }
+
+    /// <summary>
+    /// Gets a value indicating whether this process owns the single-instance lock.
+    /// </summary>
+    public bool HasOwnership => !IsEnabled || _lockFile is not null;
+
+    /// <summary>
+    /// Gets the lock file path used by this guard.
+    /// </summary>
+    public string LockFilePath { get; }
+
+    /// <summary>
+    /// Attempts to acquire the single-instance guard.
+    /// </summary>
+    /// <param name="enabled">Whether single-instance enforcement should be active.</param>
+    /// <param name="lockFilePath">Optional lock file path used by tests.</param>
+    /// <returns>A guard describing whether this process owns the lock file.</returns>
+    public static SingleInstanceGuard TryAcquire(bool enabled, string? lockFilePath = null)
+    {
+        var resolvedLockFilePath = Path.GetFullPath(
+            string.IsNullOrWhiteSpace(lockFilePath) ? DefaultLockFilePath : lockFilePath);
+
+        if (!enabled)
+        {
+            return new SingleInstanceGuard(resolvedLockFilePath, lockFile: null, isEnabled: false);
+        }
+
+        Directory.CreateDirectory(Path.GetDirectoryName(resolvedLockFilePath)!);
+
+        try
+        {
+            var lockFile = new FileStream(
+                resolvedLockFilePath,
+                FileMode.OpenOrCreate,
+                FileAccess.ReadWrite,
+                FileShare.None);
+
+            WriteOwnerMetadata(lockFile);
+            return new SingleInstanceGuard(resolvedLockFilePath, lockFile, isEnabled: true);
+        }
+        catch (IOException)
+        {
+            return new SingleInstanceGuard(resolvedLockFilePath, lockFile: null, isEnabled: true);
+        }
+    }
+
+    /// <inheritdoc />
+    public void Dispose()
+    {
+        _lockFile?.Dispose();
+
+        if (_lockFile is not null)
+        {
+            try
+            {
+                File.Delete(LockFilePath);
+            }
+            catch (IOException)
+            {
+                // The process is already shutting down; a stale unlocked file is harmless.
+            }
+            catch (UnauthorizedAccessException)
+            {
+                // The process is already shutting down; a stale unlocked file is harmless.
+            }
+        }
+    }
+
+    private static void WriteOwnerMetadata(FileStream lockFile)
+    {
+        var metadata = $"pid={Environment.ProcessId}{Environment.NewLine}startedUtc={DateTimeOffset.UtcNow:O}";
+        using var writer = new StreamWriter(lockFile, leaveOpen: true);
+        lockFile.SetLength(0);
+        writer.Write(metadata);
+        writer.Flush();
+        lockFile.Flush(flushToDisk: true);
+    }
+
+    private static string CreateDefaultLockFilePath()
+    {
+        var localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+        var root = string.IsNullOrWhiteSpace(localApplicationData)
+            ? Path.GetTempPath()
+            : localApplicationData;
+
+        return Path.Combine(root, "CodexComputerRunMCPServer", "server.lock");
+    }
+}
diff --git a/version.json b/version.json
index 12b8436..f1ea35e 100644
--- a/version.json
+++ b/version.json
@@ -1,6 +1,6 @@
 {
     "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
-    "version": "1.0.0",
+    "version": "1.0.1",
     "publicReleaseRefSpec": [
         "^refs/heads/main$",
         "^refs/tags/v\\d+\\.\\d+\\.\\d+$"
