Managing Claude Code sessions on Windows
Claude Code writes every session to a JSONL file in an encoded folder and gives you no way to browse them. What is on disk, and how to work with it.
7 min readBabar Muhammad Anas
claude-codewindowssessionsworkflow
On this page
The short answer
Claude Code stores every session as a JSONL transcript under
%USERPROFILE%\.claude\projects\<encoded-project-path>\<session-id>.jsonland gives you no interface onto them beyond/resume, which lists recent sessions in the current directory only. There is no search, no naming, no tagging, no archive, and no view across projects. Managing them means either reading the JSONL yourself or putting a workspace layer in front — a browsable list per project, full-text search across every project, and per-project launch control. The Windows-specific pain is concentrated in three places: the lossy folder encoding, console encoding, and shortcut/elevation plumbing.What is on disk
%USERPROFILE%\.claude\ ├── projects\ │ └── D--Projects-my-app\ │ ├── 6aff4b52-....jsonl # the transcript │ ├── 6aff4b52-....name # display name (archeus) │ ├── tags.json # per-session tags (archeus) │ ├── system-prompt.txt # injected on every launch (archeus) │ ├── add-dirs.txt # --add-dir roots (archeus) │ └── archived\ # restorable archive (archeus) ├── settings.json # hooks, permissions, statusLine, outputStyle ├── CLAUDE.md # loaded in every session on this account └── file-history\<session-id>\ # Claude Code's checkpoint storeThe
.jsonlfiles are the whole record: user turns, assistant turns, tool calls, tool results, one JSON object per line. They are append-only and nothing reads them back — Claude Code does not consult a prior transcript when you start a new session.They also get big. A 2,787-message session is a normal week. Any tool that touches these must stream rather than
readlines(); archeus funnels every transcript read through one module (transcripts.iter_json) that yields objects with alimit/offset, amax_bytescap, and aprefiltersubstring tested against the raw line, so a caller that only wants the"Bash"entries never pays for ajson.loadsit throws away.The folder name is lossy — do not decode it
D:\Projects\my-appencodes toD--Projects-my-app. Every non-alphanumeric character maps to a dash, which means the encoding is one-way. Splitting on--to recover a drive letter works right up until it does not:\\server\share\Project → --server-share-ProjectThe leading
--makes the drive-letter split yield an empty drive, and every UNC-hosted project was silently dropped from the project list, dashboard, usage and search — no error anywhere, just an absent row. This is a real bug that shipped, and the fix is instructive for anyone writing their own tooling: every transcript line already carries the realcwd. Reading it is exact for every encoding, and cheaper than the recursive directory walk it replaces. Walk the filesystem only as a fallback for a project folder with no transcripts to read.If you are scripting against
~/.claude/projectsyourself, readcwdfrom line one of any.jsonlin the folder. Do not parse the folder name.Console encoding: the Windows tax
Two independent bugs, same root cause, both worth knowing before you write a hook or a status line.
Writing. Claude Code captures a hook's or status line's stdout as a pipe, so CPython picks the locale codepage rather than UTF-8 — cp1252 on Windows. A middle dot goes out as a bare
0xB7and the terminal rendersOpus 5 � default �. A block glyph like▕is worse:UnicodeEncodeError, exit 1, and a Python traceback parked under your prompt for the rest of the session. The fix is one line at the top of anything that prints:sys.stdout.reconfigure(encoding='utf-8')This only looked fine in development because
PYTHONIOENCODING=utf-8was set in that environment. The regression test now strips that variable before running.Reading. The mirror image bites when you shell out to git.
subprocess.run(text=True)with noencoding=decodes with the same codepage, so one non-ASCII branch name or path raises insidesubprocessand the wrapper returnsNone— which surfaced to the user as "not a git repository" on a perfectly good repo. Pin it:subprocess.run(cmd, text=True, encoding='utf-8', errors='ignore')archeus routes every git call through a single function for exactly this reason; one door, one place to get the encoding right.
Repo discovery:
.gitis not always a directoryIf your workflow involves submodules or worktrees — common on Windows monorepos — this one matters. Testing
isdir('.git')misses both. A submodule and a linked worktree store.gitas a file containing onegitdir:line, and that line is the whole classifier:gitdir:containsIt is .git/modules/a submodule .git/worktrees/a linked worktree No subprocess needed to tell them apart. Getting this wrong in the obvious direction is also a trap: counting linked worktrees as repos adds every copy of a repo to its own repo list. And
git worktree listrun inside a submodule reports the gitdir (…/.git/modules/<name>), not the working directory, which quietly breaks any join from session to path.What a workspace layer adds
With that on-disk picture, the useful operations are the ones Claude Code does not expose. In archeus's session menu:
Key Action /Action palette — every action, type-to-filter rRename session (writes <session-id>.name)tTag session (tags show inline and are searchable) fFork session dArchive (moves to archived/, restorable)vView transcript in a pager, /searches inside iteExport the transcript to markdown iSession info — tokens, estimated cost, models, git branch, duration FChanged files, derived from the session's tool calls uProject usage stats ⇧KHand off: new session seeded with another session's context Two of those are worth calling out. Changed files (
F) is derived from tool calls in the transcript, so it answers "what did that session actually touch" without a git diff — useful when the session ran across a branch switch. And the transcript viewer's position counter readsmsg N/M, counting conversation messages rather than raw lines, because most lines in a.jsonlare tool traffic.Cross-project search is separate from per-project filtering: 🔍 Search all sessions indexes names, AI-generated titles and previews across every project, caches the index so later opens are instant, and resumes the match directly regardless of which project it lives in.
Usage, read from your own transcripts
Everything needed to cost a session is already in the file — tokens in, out, cache, per model. archeus's usage dashboard parses local transcripts (no API call) into a per-project and per-session table with estimated cost at published API rates, plus a per-day table of the last 14 days. On a subscription plan those numbers are not a bill, they are a consumption gauge — which is the right way to read them.
The plan-usage bars on the main screen are a different source: they come from the rate-limit windows Claude Code itself reports.
Windows shell plumbing
Pin to taskbar (Windows 11). Windows 11 will not pin a
.batshortcut, so the shortcut must targetcmd.exe:$shell = New-Object -ComObject WScript.Shell $lnk = $shell.CreateShortcut("$env:USERPROFILE\Desktop\Open Repo Claude.lnk") $lnk.TargetPath = "C:\Windows\System32\cmd.exe" $lnk.Arguments = "/c `"$PWD\Open Repo cmd.bat`"" $lnk.WorkingDirectory = "$PWD" $lnk.IconLocation = "$PWD\claude_sessions\archeus.ico, 0" $lnk.Save()Then right-click the desktop shortcut → Pin to taskbar.
Elevation without a UAC prompt every time. If
claude.exeor your project paths need admin rights, ticking "Run as administrator" on a shortcut gives you a UAC prompt on every launch. Register a scheduled task that already runs at highest privilege and point the shortcut atschtasks /run /tn "Archeus"instead; leave the shortcut's own elevation checkbox unticked, becauseschtasks.exedoes not need elevating, only the task it triggers. Launch the task throughwt.exerather thancmd.exedirectly — elevated console apps otherwise fall back to legacy conhost, which renders a TUI with broken colours and box-drawing.No console window.
pythonw.exeruns the GUI without one; the full shortcut recipe including the GUI icon is in the install guide.Killing a session tree. If you script session launches, note that
taskkill /Tcan report failure, and a fallback toPopen.killis not optional. The POSIX equivalent has a sharper edge:os.killpgmay only be used when the child actually leads its own process group — otherwise its group is yours, and the tree kill takes your own tool down with it.Terminal or window
The same operations exist in both interfaces over one engine.
archeusopens the TUI;archeus --guiopens a desktop window — PyQt6 native if installed, otherwise an Edge app-mode window, otherwise your browser, served on loopback only. Neither needs a third-party Python package; archeus is standard library only, and uses the Claude Code authentication you already have.Full key map and command line: docs.claudectl.space/usage.
Frequently asked
- Where does Claude Code store sessions on Windows?
- Under `%USERPROFILE%\.claude\projects\<encoded-project-path>\`, one `<session-id>.jsonl` file per session, with one JSON object per line. The folder name encodes the project's real path by replacing separators with dashes, so `D:\Projects\my-app` becomes `D--Projects-my-app`.
- How do I search across all my Claude Code sessions?
- Claude Code has no cross-session search — `/resume` only lists recent sessions in the current directory. archeus indexes session names, AI-generated titles and previews across every project and caches the result, so you can filter live and press ENTER to resume any match regardless of which project it belongs to.
- Can I rename, tag or archive a Claude Code session?
- Not with Claude Code alone; sessions are identified by UUID and there is no delete-safe archive. archeus adds a display name (`<session-id>.name`), per-session tags stored in `tags.json`, and an archive that moves a session into a restorable `archived/` folder rather than deleting the transcript.
- Why does my Claude Code status line show broken characters on Windows?
- Because Claude Code captures a status line's stdout as a pipe, so CPython picks the locale codepage — cp1252 on Windows — and a non-ASCII glyph goes out mis-encoded or raises UnicodeEncodeError outright. Any script writing a status line or hook output must call `sys.stdout.reconfigure(encoding='utf-8')` before printing.
- How do I pin archeus to the Windows 11 taskbar?
- Windows 11 cannot pin a `.bat` shortcut directly, so the shortcut has to point at `cmd.exe` with the batch file as an argument. Create it with WScript.Shell in PowerShell, set `IconLocation` to the bundled `.ico`, then right-click the desktop shortcut and choose Pin to taskbar.