diff --git a/src/CodeShellManager/Services/ClaudeSessionService.cs b/src/CodeShellManager/Services/ClaudeSessionService.cs
index dd7864e..62fb851 100644
--- a/src/CodeShellManager/Services/ClaudeSessionService.cs
+++ b/src/CodeShellManager/Services/ClaudeSessionService.cs
@@ -18,20 +18,37 @@ public static bool IsClaudeCommand(string command) =>
command.Equals("claude", StringComparison.OrdinalIgnoreCase) ||
command.StartsWith("claude ", StringComparison.OrdinalIgnoreCase);
+ ///
+ /// Resolves the directory Claude Code keeps its data in. CLAUDE_CONFIG_DIR wins when
+ /// set; otherwise the default ~/.claude.
+ ///
+ /// This matters because a machine that has ever run without the env var keeps a stale
+ /// ~/.claude/projects/ tree. Reading that one yields a session id from the wrong store
+ /// and `claude --resume <id>` fails with "No conversation found with session ID".
+ ///
+ internal static string ResolveClaudeHome(string? configDir, string userProfile) =>
+ string.IsNullOrWhiteSpace(configDir)
+ ? Path.Combine(userProfile, ".claude")
+ : configDir;
+
///
/// Finds the most recently modified session ID for the given working folder.
/// Returns null if no session exists (new project or claude not yet run there).
///
- public static string? GetLastSessionId(string workingFolder)
+ public static string? GetLastSessionId(string workingFolder) =>
+ GetLastSessionId(workingFolder, ResolveClaudeHome(
+ Environment.GetEnvironmentVariable("CLAUDE_CONFIG_DIR"),
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)));
+
+ /// Claude's data directory — see .
+ internal static string? GetLastSessionId(string workingFolder, string claudeHome)
{
if (string.IsNullOrWhiteSpace(workingFolder)) return null;
try
{
string projectDir = ToProjectDirName(workingFolder);
- string claudeProjectsPath = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
- ".claude", "projects", projectDir);
+ string claudeProjectsPath = Path.Combine(claudeHome, "projects", projectDir);
if (!Directory.Exists(claudeProjectsPath)) return null;
diff --git a/tests/CodeShellManager.Tests/ClaudeSessionServiceTests.cs b/tests/CodeShellManager.Tests/ClaudeSessionServiceTests.cs
new file mode 100644
index 0000000..561c6bc
--- /dev/null
+++ b/tests/CodeShellManager.Tests/ClaudeSessionServiceTests.cs
@@ -0,0 +1,145 @@
+using CodeShellManager.Services;
+using Xunit;
+
+namespace CodeShellManager.Tests;
+
+///
+/// Claude Code stores conversations under CLAUDE_CONFIG_DIR when that env var is set,
+/// falling back to ~/.claude. Reading the wrong root yields a session id from a stale
+/// store, and `claude --resume <id>` then fails with "No conversation found with
+/// session ID". Timestamps are set explicitly rather than via Task.Delay — Windows'
+/// ~15.6ms timer granularity makes wall-clock ordering flaky.
+///
+public class ClaudeSessionServiceTests : IDisposable
+{
+ private readonly string _root;
+
+ public ClaudeSessionServiceTests()
+ {
+ _root = Path.Combine(Path.GetTempPath(), "csm-claude-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(_root);
+ }
+
+ public void Dispose()
+ {
+ try { Directory.Delete(_root, recursive: true); } catch { }
+ }
+
+ /// Creates <claudeHome>/projects/<dirName> and returns it.
+ private string MakeProjectDir(string claudeHome, string dirName)
+ {
+ string p = Path.Combine(claudeHome, "projects", dirName);
+ Directory.CreateDirectory(p);
+ return p;
+ }
+
+ private static void WriteSession(string projectDir, string sessionId, DateTime lastWriteUtc)
+ {
+ string f = Path.Combine(projectDir, sessionId + ".jsonl");
+ File.WriteAllText(f, "{}\n");
+ File.SetLastWriteTimeUtc(f, lastWriteUtc);
+ }
+
+ // ── ResolveClaudeHome ────────────────────────────────────────────────────
+
+ [Fact]
+ public void ResolveClaudeHome_UsesConfigDir_WhenSet()
+ {
+ string result = ClaudeSessionService.ResolveClaudeHome(
+ @"C:\Users\someone\.claude-work", @"C:\Users\someone");
+
+ Assert.Equal(@"C:\Users\someone\.claude-work", result);
+ }
+
+ [Fact]
+ public void ResolveClaudeHome_FallsBackToDotClaude_WhenConfigDirNull()
+ {
+ string result = ClaudeSessionService.ResolveClaudeHome(null, @"C:\Users\someone");
+
+ Assert.Equal(Path.Combine(@"C:\Users\someone", ".claude"), result);
+ }
+
+ [Fact]
+ public void ResolveClaudeHome_FallsBackToDotClaude_WhenConfigDirBlank()
+ {
+ string result = ClaudeSessionService.ResolveClaudeHome(" ", @"C:\Users\someone");
+
+ Assert.Equal(Path.Combine(@"C:\Users\someone", ".claude"), result);
+ }
+
+ // ── GetLastSessionId ─────────────────────────────────────────────────────
+
+ [Fact]
+ public void GetLastSessionId_ReturnsNewestSessionByWriteTime()
+ {
+ string dir = MakeProjectDir(_root, "C--Github-Foo");
+ WriteSession(dir, "11111111-1111-1111-1111-111111111111", new DateTime(2026, 6, 10, 0, 0, 0, DateTimeKind.Utc));
+ WriteSession(dir, "22222222-2222-2222-2222-222222222222", new DateTime(2026, 8, 10, 0, 0, 0, DateTimeKind.Utc));
+
+ string? id = ClaudeSessionService.GetLastSessionId(@"C:\Github\Foo", _root);
+
+ Assert.Equal("22222222-2222-2222-2222-222222222222", id);
+ }
+
+ [Fact]
+ public void GetLastSessionId_ReadsFromTheGivenClaudeHome_NotAHardcodedOne()
+ {
+ // The regression: a stale ~/.claude alongside a live CLAUDE_CONFIG_DIR. Only the
+ // session in the home we were handed may be returned.
+ string stale = Path.Combine(_root, "stale");
+ string live = Path.Combine(_root, "live");
+ WriteSession(MakeProjectDir(stale, "C--Github-Foo"), "5ta1e000-0000-0000-0000-000000000000",
+ new DateTime(2026, 6, 10, 0, 0, 0, DateTimeKind.Utc));
+ WriteSession(MakeProjectDir(live, "C--Github-Foo"), "11ve0000-0000-0000-0000-000000000000",
+ new DateTime(2026, 8, 10, 0, 0, 0, DateTimeKind.Utc));
+
+ string? id = ClaudeSessionService.GetLastSessionId(@"C:\Github\Foo", live);
+
+ Assert.Equal("11ve0000-0000-0000-0000-000000000000", id);
+ }
+
+ [Fact]
+ public void GetLastSessionId_ReturnsNull_WhenProjectDirDoesNotExist()
+ {
+ string? id = ClaudeSessionService.GetLastSessionId(@"C:\Github\NeverUsed", _root);
+
+ Assert.Null(id);
+ }
+
+ [Fact]
+ public void GetLastSessionId_ReturnsNull_WhenProjectDirHasNoSessions()
+ {
+ MakeProjectDir(_root, "C--Github-Empty");
+
+ string? id = ClaudeSessionService.GetLastSessionId(@"C:\Github\Empty", _root);
+
+ Assert.Null(id);
+ }
+
+ [Fact]
+ public void GetLastSessionId_IgnoresSubdirectoriesAndNonSessionFiles()
+ {
+ // Real layout has a / directory (subagent transcripts) next to the
+ // .jsonl, plus a memory/ dir. Neither may be mistaken for a session.
+ string dir = MakeProjectDir(_root, "C--Github-Foo");
+ WriteSession(dir, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc));
+ Directory.CreateDirectory(Path.Combine(dir, "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb.jsonl"));
+ Directory.CreateDirectory(Path.Combine(dir, "memory"));
+ File.WriteAllText(Path.Combine(dir, "notes.txt"), "x");
+
+ string? id = ClaudeSessionService.GetLastSessionId(@"C:\Github\Foo", _root);
+
+ Assert.Equal("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", id);
+ }
+
+ [Fact]
+ public void GetLastSessionId_MapsDriveAndSeparatorsToProjectDirName()
+ {
+ string dir = MakeProjectDir(_root, "C--Github-umage-CodeShellManager");
+ WriteSession(dir, "cccccccc-cccc-cccc-cccc-cccccccccccc", new DateTime(2026, 8, 1, 0, 0, 0, DateTimeKind.Utc));
+
+ string? id = ClaudeSessionService.GetLastSessionId(@"C:\Github\umage\CodeShellManager", _root);
+
+ Assert.Equal("cccccccc-cccc-cccc-cccc-cccccccccccc", id);
+ }
+}