cc-sessions-man
Lists every Claude Code session on this machine and says whether it is finished or waiting on you.
AI-written: almost all of this project's code, tests and documentation were written and reviewed by AI (Claude, through Claude Code). A human decided what to build, checked the results and approved each change.
- cc-sessions-man
What it reports
Each session lands in one group. Each group has a rank, and lower ranks sort first. The gaps in the numbering leave room for new statuses, and Done stays last at 99.
| Rank | Group | Meaning | How it's decided |
|---|---|---|---|
| 0 | Running | A Claude process is still open (busy or idle) | Live process in ~/.claude/sessions/ |
| 1 | Needs action | You must do something outside the session (edit a file, merge a PR, run sudo) | Haiku reads the final message |
| 2 | Needs decision | Claude asked a question or wants approval | Haiku |
| 3 | Interrupted | Ended on your prompt with no reply, or partway through a tool call | Transcript structure |
| 4 | Offered extras | Finished, with optional follow-ups you can ignore | Haiku |
| 5 | Ended (not classified) | Not labelled yet (--no-llm, or the model skipped it) |
— |
| 6 | Classification failed | The claude -p call for its batch failed (not logged in, rate limited, timed out). The error shows in the details panel and --json output. Never cached, so it's retried on refresh (r) or the next run |
— |
| 99 | Done | Nothing left to do | Haiku |
Ranks are defined in STATUS_RANK in src/cc_sessions_man/cli.py.
The --json output includes each session's status_rank, context_tokens and model.
Only sessions that ended on a normal reply go to the model. For each one, the model gets your last prompt and Claude's final message. It returns a label and a short note on what is still outstanding.
Requirements
- Python 3.14 or later. With uv, it's downloaded for you if it isn't installed.
- The
claudeCLI on yourPATH, logged in. Classification runsclaude -p --model haiku.
Installation
Install it as a tool with uv (recommended, as it fetches Python 3.14 if needed):
uv tool install cc-sessions-man
Or with pipx:
pipx install --python python3.14 cc-sessions-man
Or with pip, using Python 3.14's pip:
python3.14 -m pip install cc-sessions-man
On newer Linux systems, pip refuses to install into the system Python ("externally managed environment"). Install into your user site instead, at your own risk, with:
python3.14 -m pip install --user --break-system-packages cc-sessions-man
If pip3 already runs Python 3.14 (check with pip3 --version), pip3 install cc-sessions-man works too.
Each method installs two commands: cc-sessions (the CLI) and cc-sessions-ui (the terminal UI).
Upgrade
uv tool upgrade cc-sessions-man
pipx upgrade cc-sessions-man
python3.14 -m pip install --upgrade cc-sessions-man
Use the one that matches how you installed it, adding --user --break-system-packages to the pip command if you used them to install.
Run
cc-sessions # all sessions, grouped
cc-sessions --pending # only decision / action / interrupted
cc-sessions --days 7 # only the last week
cc-sessions -n 20 # only the 20 most recent
cc-sessions --json # machine-readable, includes a resume command
cc-sessions --search "first session where /advisor was used" # AI search of the transcripts
From a clone of the repository, prefix the commands with uv run (for example uv run cc-sessions), or install the clone as an editable tool: uv tool install --editable /path/to/cc-sessions-man.
To resume a session, use the directory and full session ID it prints:
cd ~/code/my-app && claude --resume 3f2a9c1e-7b4d-4e8a-9c6f-1d2e3f4a5b6c
Terminal UI
cc-sessions-ui # the terminal UI
cc-sessions-ui --no-llm # cached labels only, no model calls
cc-sessions-ui --refresh-interval 60 # auto-refresh every minute; 0 turns it off
The UI is built with Textual. It shows a table of sessions with status, start time, last activity, title, folder, context size and model.
-
Context is how full the session's context was at Claude's last reply: the input tokens it was given, including cached ones. It's shown as, for example,
134kor1.0M, and is blank for sessions with no recorded usage. Its colour shows the band: blue below 250k, green from 250k, yellow from 500k, and red from 800k. Click the heading to sort largest first. -
Model is the model that wrote Claude's latest reply in the main conversation, shortened to, for example,
opus-5-5orhaiku-4-5. Subagents' models aren't shown. After a/modelswitch, it changes once Claude next replies. The details panel shows the full model ID. It's blank for sessions with no reply yet. -
Folders longer than 40 characters keep their end, for example
…/my-app/.claude/skills/changelog-writer. The details panel shows the full path. -
The list appears as soon as the transcripts are scanned, using cached labels. Sessions with no cached label show as "Classifying…". They're sent to
claude -pin batches of 5, 16 batches at a time, and each row updates as soon as its batch returns. The progress (for exampleClassifying 15/35) shows in the header. Change these with--batch-size Nand--jobs N. -
Auto refresh: the list refreshes itself every 30 seconds, picking up new sessions, status changes and sessions that have finished.
- New or changed sessions are classified as part of the refresh.
- It runs quietly, without the "Scanning…" status, and keeps your selection, marks, filter, sort and scroll position. When nothing has changed, the table isn't redrawn at all.
- A tick is skipped while a scan or classification is still running, and while a dialog, the command palette or another screen is open.
- Deletes and renames you make while a refresh is scanning are kept; the refresh can't bring a deleted session back.
- After a classification error (for example, logged out or rate limited), auto refresh pauses classification.
It keeps rescanning but makes no model calls, and the title bar says "Classification paused after an error. Press r to retry."
The error is shown once, not on every tick. Press
rto retry; when a run completes without errors, auto refresh classifies again. - Set the interval with
--refresh-interval SECONDS: at least 1, or0to turn auto refresh off.
-
By default it sorts by status rank, with the newest last activity first within each status.
-
scycles through four sort orders (see the keys below). -
Click a column header to sort by that column alone. Click it again to reverse the order.
-
The headings show the current sort:
^for ascending (A to Z, lowest first) andvfor descending (newest or largest first). With several sort columns, the marks are numbered by precedence: inStatus ^1andLast activity v2, status is the primary sort and last activity the secondary. A single-column sort from a header click shows just^orv. The marks are shown in blue, so they stand apart from the column names. -
Column widths fit every session, not just the ones shown, and leave room for the sort mark. Sorting, searching and filtering never change them; they only change when a refresh adds, removes or updates sessions. Status sorts by rank (needs you first), last activity newest first, and folder A to Z.
-
The panel at the bottom shows the selected session's outstanding work, full ID, folder and full model ID.
| Key | Action |
|---|---|
↑ / ↓, or k / j |
Move between sessions |
← / →, or h / l |
Scroll the table sideways when it's wider than the window |
| Enter | Resume the session: the UI closes and runs claude --resume in the session's folder |
o |
Resume the session in a new terminal window and keep the UI open. When you quit Claude, the window stays open at a shell in the session's folder. Linux only; see below |
p |
Show only sessions that need you (decision, action, interrupted) |
a |
Show all sessions: resets the group drop-down to All, clears the search, and clears or stops an AI search |
/ |
Filter by title, folder or session ID. Enter or Esc returns to the table. Not in the footer: the search box shows it |
f |
AI search: Claude looks through every transcript for sessions matching what you describe. See AI search |
Space |
Mark or unmark the session for deletion, and move down a row |
u |
Unmark all. Does nothing when nothing is marked |
Shift+Space |
Unmark all; with nothing marked, it marks the current session, like Space. Only in terminals that report Shift with Space; many send a plain Space |
n |
Rename the selected session. Enter saves, Esc cancels |
m |
Move the selected session to another working folder. Tab completes folder names, Enter moves, Esc cancels |
d |
Delete the marked sessions, or the selected one if none are marked. Asks first |
s |
Cycle the sort order, one step per press, back to 1 after 4: 1. Status, then last activity 2. Last activity, then status 3. Folder, then status, then last activity 4. Last activity, then folder, then status From a column-header sort, s starts again at 1 |
r |
Rescan and classify any new, changed or failed sessions. If a scan or classification is already running, it runs again straight after. A run without errors lifts a classification pause |
| Ctrl+K | Show or hide the keys panel, which lists every key and what it does. Listed in the keys panel, not the footer; Ctrl+P > Keys opens it too. In the search box, Ctrl+K deletes to the end of the text instead |
| Ctrl+T / Ctrl+Y | Switch to the next or previous theme, in the order of the Ctrl+P theme list. Listed in the keys panel, not the footer |
| Ctrl+R | Go back to the default theme: textual-dark, or the one set by TEXTUAL_THEME. Listed in the keys panel, not the footer |
q |
Quit |
o finds a terminal in this order: the $TERMINAL environment variable (a command, which gets -e and the command to run), then xdg-terminal-exec, then x-terminal-emulator (Debian and Ubuntu's default terminal, set with update-alternatives).
If none is found, or the terminal fails to start, it says so and Enter still works.
A session opened with o can't be opened again for a minute, and about five seconds after opening, the list rescans quietly so it moves to Running.
Use the drop-down at the top left to show a single group. A session in the Running group is open in another terminal, so it can't be resumed, renamed, moved or deleted from here.
AI search
/ filters by matching text in the title, folder and session ID.
f asks Claude to search the transcripts themselves, for questions text matching can't answer:
- "first session where
/advisorwas used" - "session that created github repository MY-EXAMPLE-REPO"
How it works:
- It runs
claude -p(Sonnet by default) with read-only tools (Read, Grep, Glob) over~/.claude/projects, plus an index of the sessions' IDs, dates, folders and titles. Claude greps the transcripts, reads the relevant parts and returns the matching sessions, most relevant first, each with a reason. - A search usually takes 10–20 seconds and costs about $0.15. It stops if it would cost more than $1 (
--search-budget). The budget is checked between steps, so a search can go over by one step. - While it runs, a notification and the title bar show the query, until it finishes. You can keep using the list.
Only one search runs at a time:
fis blocked until it finishes (orastops it). - Results replace the list, in Claude's order; the details panel shows why each session matched. The title bar shows the query and the count.
Results stay through refreshes.
sor a header click re-sorts them, and/narrows them further. agoes back to all sessions, and stops a search that's still running (with the file searches it started). So does quitting.fwaits until the list has loaded, so the search knows every session.- Session IDs Claude makes up are ignored.
The search reads your transcripts, so excerpts of them are sent to the Anthropic API (see How the claude calls are isolated).
Anything you type into a session is in its transcript, so a session where you discussed a topic can match a later search for it.
Renaming sessions
Renaming saves the title the same way Claude Code's /rename does, so claude --resume shows the new name too:
- a
custom-titleline is appended to the transcript, and <session-id>/custom-title.jsonis written next to it.
The session's label is kept, so a rename doesn't trigger classification again.
A running session can't be renamed from here; use /rename inside it.
Moving sessions
m moves a session to another working folder, for example after you moved or renamed a repo, or started Claude in the wrong place.
Type the folder (it must exist; ~ works) and press Enter.
-
Tab fills in the next folder whose name starts with what you've typed after the last
/. Press it again to cycle through the other matches. Just after a/, it cycles through every folder there.- In an empty field, Tab starts at your home folder (
~/). - Hidden folders are offered only once you've typed a
., as in the shell. - Type
/after a folder to move into it; typing anything else starts a new round of matches.
- In an empty field, Tab starts at your home folder (
-
Red text shows what doesn't exist: the folder part if that folder is missing, otherwise the name if no folder starts with it.
-
A name with no
/(such asDocuments) means a folder in your home folder, for Tab, the red text and Enter alike. -
Every working folder recorded in the transcript (
cwd) is changed to the new folder. A subfolder the session changed into keeps its place if the same subfolder exists in the new folder; otherwise it becomes the new folder. -
The transcript and its
<session-id>/folder move to the new folder's project folder under~/.claude/projects/, named the way Claude Code names it. -
After the move:
- the session shows the new folder, and resuming it from here runs Claude there;
- it's listed in that folder's
claude --resumepicker, andclaude --continuethere picks it up.
-
The label is kept, so a move doesn't trigger classification again.
-
The move is all or nothing: the new copy is saved to disk before the original is removed, and if anything fails, or the session is resumed elsewhere mid-move, everything stays where it was.
Not changed: paths inside messages and tool calls, the subagent transcripts, and the /rewind file backups, which still restore files to their original paths.
Memory isn't moved or copied. Claude Code's auto memory (projects/<project>/memory/) belongs to a folder, not a session, and every session in that folder shares it.
So it stays with the old folder, and once moved, the session uses the new folder's memory, like any other session started there.
claude --resume <session-id> finds a session from any folder, so a move doesn't change whether it can be resumed, only which folder it belongs to.
Deleting sessions
Delete moves a session's files to the desktop Trash, so you can restore them from there. For each session it moves:
- the transcript,
~/.claude/projects/<project>/<session-id>.jsonl - its folder of subagent transcripts and tool output,
~/.claude/projects/<project>/<session-id>/ - its file backups for
/rewind,~/.claude/file-history/<session-id>/ ~/.claude/session-env/<session-id>/
Your prompt history in ~/.claude/history.jsonl is shared by all sessions, so it is left alone.
The confirmation dialog opens with Cancel selected, so pressing Enter by mistake deletes nothing.
CLI options
| Option | Effect |
|---|---|
-n, --limit N |
Only the N most recent sessions |
--days N |
Only sessions active in the last N days |
-s, --show GROUP |
Only this group; repeatable. Groups: running, decision, action, interrupted, optional, done, unclassified |
--pending |
Same as -s decision -s action -s interrupted |
--no-llm |
Don't call the model; use cached labels only. Uncached sessions show as "Ended (not classified)" |
--refresh |
Classify the listed sessions again, ignoring their cached labels. With -n or --days, other sessions keep their labels |
--model M |
Model alias or ID (default haiku) |
--batch-size N |
Sessions per claude call (default 5) |
-j, --jobs N |
Parallel claude calls (default 16). They share your account's rate limits with other Claude sessions |
--claude-home DIR |
Claude config directory (default ~/.claude, or $CLAUDE_CONFIG_DIR) |
--search QUERY |
AI search instead of listing: print the sessions matching QUERY, most relevant first, with a reason each (or JSON with --json). With -n or --days, only those sessions can match |
--search-model M |
Model for AI search (default sonnet). Also in the UI |
--search-budget USD |
Stop an AI search that costs more than USD (default 1). Also in the UI |
Using another Claude config directory
The tool reads the same directory Claude Code does:
$CLAUDE_CONFIG_DIRif it's set and not empty. A leading~is expanded.~/.claudeotherwise.
--claude-home DIR overrides both for one run.
When the directory isn't ~/.claude, the tool passes it on to claude:
- Resuming from the UI runs
claudewithCLAUDE_CONFIG_DIRset to that directory, so--resumefinds the session. - The resume commands the CLI prints start with
CLAUDE_CONFIG_DIR=....
CLAUDE_CONFIG_DIR=~/.claude-work cc-sessions-ui
cc-sessions --claude-home ~/.claude-work --pending
Cost and caching
Two caches live in ~/.cache/cc-sessions-man/ (or $XDG_CACHE_HOME/cc-sessions-man/):
scan-<hash>.jsonholds a summary of each transcript, one file per Claude config directory: title, times, state, context size, model, and the last prompt and reply. A transcript is only read again when its size or modification time changes. This keeps startup fast; reading all transcripts in full takes seconds.labels.jsonholds the model's label for each session, keyed by session ID and the transcript's modification time. A session is only classified again after its transcript changes.
Both are safe to delete. The next run rebuilds them.
To watch classification run from scratch, delete labels.json and start the UI.
The first classification of all sessions costs a few cents. Later runs only send new or changed sessions. An AI search costs about $0.15 each time; its results aren't cached.
How the claude calls are isolated
Both kinds of call run with:
--no-session-persistence, so the calls don't appear as sessions in the list (or in later searches).--strict-mcp-config, so no MCP servers load.--setting-sources ""and an empty temporary working directory, so no hooks, settings or projectCLAUDE.mdload.
They differ in what the model can see:
- Classification runs with
--tools "": no tools, so the model only reads the last prompt and reply it's given. - AI search runs with
--tools Read,Grep,Globand--add-dir ~/.claude/projects: it can read every transcript, but can't change anything or run commands. It can't read anything else either: with nobody to approve a permission prompt,claude -prefuses those tools outside the folders it's given (tested with files in/tmpand in the home folder), and the user's settings, which could widen that, aren't loaded.
Session text is sent to the Anthropic API, the same place the sessions themselves ran. For AI search that's whatever parts of the transcripts Claude reads while searching.
Limits
- Labels are a model's judgement of the final message. An occasional session will land in the wrong group.
- It can't tell whether you already dealt with a question elsewhere, for example by merging a PR by hand.
- It reads Claude Code's internal transcript format, which isn't a stable public API. A future Claude Code release could break parsing.
Development
uv run pytest
uv run black --check src tests benchmarks
uv run isort --check-only src tests benchmarks
uv run flake8 src tests benchmarks
uv run pytest --cov=cc_sessions_man --cov-report=html # coverage report in htmlcov/
GitHub Actions runs the same checks and the tests on every pull request (.github/workflows/pr.yml), and keeps the HTML coverage report as a cov-reports artifact for 7 days.
Publishing a GitHub release builds the package and uploads it to PyPI (.github/workflows/publish.yml), using the TWINE_USERNAME and TWINE_PASSWORD repository secrets.
The package version comes from the release's tag, so tag releases as v<version>, for example v0.2.0.
Build and publish
Build the distribution:
uv build
This creates dist/ with .tar.gz and .whl files.
The version comes from git tags (hatch-vcs): on a v0.2.0 tag it's 0.2.0, and between tags it's a development version such as 0.2.1.dev3+g1a2b3c4.
To release, tag the commit and push the tag, then publish a GitHub release for it:
git tag v0.2.0
git push origin v0.2.0
Configure Publishing to Pypi
Configure PyPI credentials in ~/.pypirc:
[distutils]
index-servers =
pypi
testpypi
[pypi]
username = __token__
password = pypi-<your-api-token>
[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-<your-test-api-token>
API tokens can be created at:
Alternatively, set credentials via environment variables instead of ~/.pypirc:
export TWINE_USERNAME=__token__
export TWINE_PASSWORD=pypi-<your-api-token>
To target TestPyPI, override the repository URL:
export TWINE_REPOSITORY_URL=https://test.pypi.org/legacy/
Publishing to Test Pypi
To publish to TestPyPI first:
# Using ini file:
uvx twine upload --repository testpypi dist/*
# Or, with environment variables:
export TWINE_REPOSITORY_URL=https://test.pypi.org/legacy/
uvx twine upload dist/*
Publishing to Pypi
Publish to PyPI:
uvx twine upload dist/*
Benchmarking classification
benchmarks/classify_batch_size.py measures how batch size and the number of parallel calls affect classification speed, cost and accuracy:
uv run python benchmarks/classify_batch_size.py # 60 sessions; batch 1, 5, 10, 20, 40; 4 and 8 parallel
uv run python benchmarks/classify_batch_size.py --sample 40 --batch-sizes 5 20 --jobs 8
- It takes a fixed random sample of finished sessions. Use
--seedto change the sample. - It classifies the sample once per configuration, using the same
claude -pcall as the app. - Accuracy is compared against a reference run with batch size 1. The reference is run twice, so the model's own run-to-run noise gives a baseline to compare against.
- "Agree (needs you)" checks only whether a session needs you (decision or action) or not. That is the distinction that matters most in the UI.
- It never writes to the app's caches.
It prints a Markdown table and saves the numbers to
benchmarks/results/<timestamp>.json, so you can compare runs over time. - A full run takes a few minutes and costs well under a dollar.
Metadata
Release files for cc-sessions-man 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| cc_sessions_man-0.0.1.tar.gz | 77.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cc_sessions_man-0.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 131.8 kB
Release files / cc_sessions_man-0.0.1.tar.gz
| Download URL | cc_sessions_man-0.0.1.tar.gz |
|---|---|
| Size | 77.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f3661191287b3251c60ee80dc60da2cfb6fd1bc4b744e8af1cdf251e8a15e71e
|
|
BLAKE2b-256 checksum How to use checksums |
eee28d03255b1ce00b302ded0265930a821932c7e476b7aa84709f2ebf48a809
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|
Release files / cc_sessions_man-0.0.1-py3-none-any.whl
| Download URL | cc_sessions_man-0.0.1-py3-none-any.whl |
|---|---|
| Size | 54.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
623537fb025425c592fb7759167112bd0c200c67ee1a2668ee106f3272d0a38d
|
|
BLAKE2b-256 checksum How to use checksums |
270966b0f276567f7e6498ec30f5ce8684cf098a6bdd61e4b57954e04f908144
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|
Security for cc-sessions-man 0.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.