claude --resume <sid> worked when I ran it in my terminal and returned No conversation found when my web app ran the exact same command, on the same machine, against a session file that demonstrably existed. The difference came down to one environment variable I never thought to set, and the reason it bit me is a trap every Windows service that shells out to a per-user CLI will eventually hit.

The setup

I run a personal agent cockpit. One of its jobs is to stream a Claude Code session into the browser: you click a session in the dashboard, and a bridge endpoint on the Node server spawns claude.exe --resume <sid> and pipes the output back to the page. The bridge itself was already built. Spawning, sending a message, fetching history, all wired. So when I hooked up the resume path and it came back with No conversation found, I expected a five-minute fix.

The server is a Node process registered as a Windows service through NSSM. That detail is the whole story, but I did not know that yet.

Confirming the obvious thing first

The first thing I did was confirm the session file actually existed, because No conversation found is exactly what you get when it does not. Claude Code stores each session as a JSONL transcript under your user home. The file was right there:

C:/Users/<username>/.claude/projects/<project>/1b169ea4-....jsonl

A 7 MB file, last modified at 11:59 PM that same night. Not missing, not empty, not zero bytes. And the smoking gun: if I dropped into a terminal as myself and ran claude --resume 1b169ea4-..., it resumed fine. The CLI was not broken. The session was not corrupt. The only variable that changed between “works” and “fails” was who, and what, was launching the process.

That reframes the question entirely. It is not “why can’t Claude find the file.” It is “why does Claude look somewhere else when my service launches it.”

How a CLI finds your home directory

Claude Code resolves its project store from the OS home directory. In Node terms that is os.homedir(), which on Windows reads the USERPROFILE environment variable. When I run the CLI in my own terminal, USERPROFILE is C:\Users\<username>, so the CLI looks in C:\Users\<username>\.claude\projects\ and finds everything.

When a process spawns a child, the child inherits the parent’s environment. So whatever USERPROFILE the cockpit server has, the spawned claude.exe inherits. And the cockpit server does not run as me.

LocalSystem has a home, and it is not yours

NSSM-registered services, by default, run as the LocalSystem account. LocalSystem is a service account, not a human one, and it has its own profile. Its USERPROFILE is:

C:\Windows\system32\config\systemprofile

So the spawned CLI was doing exactly what it was told. It called os.homedir(), got C:\Windows\system32\config\systemprofile, and looked for the session in:

C:\Windows\system32\config\systemprofile\.claude\projects\

That directory is empty. Of course it is. I have never run Claude Code as LocalSystem. The file existed under my profile and the CLI was looking under the service account’s profile, and both of those statements are simultaneously true, which is the part that makes this kind of bug hard to see. Nothing is missing. The lookup is just rooted in the wrong place.

The path variable that did not save me

Here is the part worth internalizing, because it is where I wasted time. The cockpit already had a CLAUDE_PROJECTS_PATH style variable in its own environment, pointing at my real .claude/projects. So my first instinct was: the variable is set, the path is correct, why is the CLI ignoring it.

It was ignoring it because it never reads it. That variable is cockpit-internal. The cockpit server reads it to find transcripts for its own dashboard rendering. claude.exe has no idea that variable exists. The CLI resolves its home through os.homedir() and nothing else. An app-level path variable does not help you when the thing you are trying to redirect is a separate binary that resolves its own paths from the OS home.

This is the general trap. You can have every app-specific path pointing at the right place and still feed a child process the wrong home, because the child does not consult your app’s config. It consults USERPROFILE. If you do not override that on the spawn, you have fixed nothing.

The fix

Override USERPROFILE and HOME on the spawn environment, so the child CLI inherits a home that points at the real user profile instead of systemprofile:

spawn(CLAUDE_BIN, args, {
cwd,
env: { ...process.env, USERPROFILE: "C:\\Users\\<username>", HOME: "C:\\Users\\<username>" },
windowsHide: true,
})

I set both USERPROFILE and HOME. USERPROFILE is what Windows and os.homedir() use, but setting HOME too costs nothing and covers any tool with POSIX-flavored home resolution. Spread process.env first so you keep everything else the service already has, then stomp the two home vars on top.

That made resume work immediately. But a per-spawn override only fixes the running process. To make it survive a service restart, I baked a CLAUDE_USER_HOME=C:/Users/<username> style value into the NSSM service config through AppEnvironmentExtra, so the override is part of the service environment and not just a code-path that has to remember to set it. Belt and suspenders: the spawn sets the home explicitly, and the service-level env carries the canonical user-home value so nothing downstream has to guess.

How to confirm you are actually in this hole

If you suspect this, do not theorize, check. From inside the service process, print process.env.USERPROFILE (or os.homedir()). If it comes back C:\Windows\system32\config\systemprofile, you are running as LocalSystem and every per-user path your child processes resolve is pointed at the service profile.

The other tell is the asymmetry that started this whole thing: it works in your terminal and fails from the service. Any time a command behaves differently launched by a service than launched by you at a prompt, environment inheritance is the first suspect, and on Windows the home directory is the most common thing that differs. NSSM writes the service stderr to whatever path you set in AppStderr (nssm get <service> AppStderr tells you where). When the spawn itself is silent, go read that log to confirm the child even launched before you chase the path.

The takeaway

Any Windows service that spawns a per-user CLI is, by default, handing that CLI the home directory C:\Windows\system32\config\systemprofile, not yours. Tools that resolve their config through os.homedir() will silently read the wrong location and tell you nothing is there, because from where they are standing, nothing is.

Two rules fall out of this:

  1. On the spawn env, override USERPROFILE and HOME to the real user profile. Do not assume the child inherits a useful home from a service account.
  2. App-specific path variables will not save you. The child binary reads the OS home, not your app’s config. If you only set the variable your own code reads, you have fixed your own code and nothing else.

It worked in my terminal and failed in my web app, and both were telling the truth. The session was always there. The service was just looking under a different account’s roof.