Projects API
The Projects API is the read-only part of ccsinfo that groups Claude Code sessions by project. It lets you discover all available projects, inspect one project, list its sessions, see which of those sessions are currently active, and fetch per-project totals without loading every session into the client yourself.
All routes in this page live under /projects. The usual workflow is:
- Call
GET /projects. - Keep the
idfrom the project you want. - Reuse that same
idin the detail, sessions, active sessions, and stats endpoints.
Tip: If you are running the built-in API server, it binds to
127.0.0.1:8080by default. The CLI can also target a remote server with--server-urlorCCSINFO_SERVER_URL.
@app.command()
def serve(
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to (use 0.0.0.0 for network access)"),
port: int = typer.Option(8080, "--port", "-p", help="Port to bind"),
) -> None:
"""Start the API server."""
uvicorn.run(fastapi_app, host=host, port=port)
At a Glance
| Endpoint | Use it for | Query parameters | Returns |
|---|---|---|---|
GET /projects |
List every known project | None | Project[] |
GET /projects/{project_id} |
Fetch one project | None | Project |
GET /projects/{project_id}/sessions |
List sessions for one project | limit (1 to 500, default 50) |
SessionSummary[] |
GET /projects/{project_id}/sessions/active |
List currently active sessions for one project | None | SessionSummary[] |
GET /projects/{project_id}/stats |
Get per-project totals | None | ProjectStats |
The server defines those routes exactly like this:
@router.get("", response_model=list[Project])
async def list_projects() -> list[Project]:
"""List all projects."""
return project_service.list_projects()
@router.get("/{project_id}", response_model=Project)
async def get_project(project_id: str) -> Project:
"""Get project details."""
project = project_service.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
return project
@router.get("/{project_id}/sessions", response_model=list[SessionSummary])
async def get_project_sessions(
project_id: str,
limit: int = Query(50, ge=1, le=500),
) -> list[SessionSummary]:
"""Get sessions for a project."""
return session_service.list_sessions(project_id=project_id, limit=limit)
@router.get("/{project_id}/sessions/active", response_model=list[SessionSummary])
async def get_project_active_sessions(project_id: str) -> list[SessionSummary]:
"""Get active sessions for a project."""
return session_service.list_sessions(project_id=project_id, active_only=True)
@router.get("/{project_id}/stats", response_model=ProjectStats)
async def get_project_stats(project_id: str) -> ProjectStats | dict[str, Any]:
"""Get project statistics."""
stats = project_service.get_project_stats(project_id)
if not stats:
raise HTTPException(status_code=404, detail="Project not found")
return stats
Understanding project_id
project_id is not a human slug. It is the encoded directory name Claude Code uses under ~/.claude/projects. The safest pattern is to read it from GET /projects and reuse it as-is.
Warning: The decode step is explicitly lossy. Use
pathandproject_pathfor display, not as authoritative identifiers.
def encode_project_path(project_path: str) -> str:
"""Encode a project path to Claude Code's directory name format.
Claude Code replaces:
- '/' with '-'
- '.' with '-'
Example: '/home/user/project' -> '-home-user-project'
"""
return project_path.replace("/", "-").replace(".", "-")
def decode_project_path(encoded_path: str) -> str:
"""Decode a Claude Code directory name back to the original path.
Note: This is lossy - we cannot distinguish between original '-' and encoded '/' or '.'.
The path returned should be treated as approximate.
"""
# Handle the pattern where /. becomes --
result = encoded_path.replace("--", "/.")
result = result.replace("-", "/")
return result
In practice, /home/user/project becomes -home-user-project. A dotted path such as /home/user/.config/project becomes -home-user--config-project.
How the Data Is Assembled
The Projects API is file-backed. It reads Claude Code’s project directories and session JSONL files on demand, then builds project and session summaries from that data. Active session views add a live process check on top of the file data.
flowchart TD
A[API client] --> B[/projects endpoints]
B --> C[project_service]
B --> D[session_service]
C --> E[~/.claude/projects/<encoded_project_id>/]
D --> E
E --> F[session JSONL files]
B --> G[/projects/{project_id}/sessions/active]
G --> H[active session detector]
H --> I[pgrep -f claude and /proc inspection]
Note: There is no separate database behind these endpoints. Project and session views come from Claude Code files; active-session status is calculated live.
Response Objects
All JSON keys use snake_case. Timestamp fields serialize as ISO 8601 strings or null.
Project
| Field | Type | Description |
|---|---|---|
id |
string | Encoded project identifier used in project routes |
name |
string | Display name derived from the last segment of the decoded path |
path |
string | Decoded project path for display; treat it as approximate |
session_count |
integer | Number of session *.jsonl files in the project directory |
last_activity |
string or null |
Most recent parsed session timestamp for the project |
SessionSummary
| Field | Type | Description |
|---|---|---|
id |
string | Session UUID, taken from the session filename |
project_path |
string | Decoded project path |
project_name |
string | Display name derived from project_path |
created_at |
string or null |
First timestamp found in the session |
updated_at |
string or null |
Last timestamp found in the session |
message_count |
integer | Count of parsed user and assistant messages |
is_active |
boolean | Whether the session is currently considered active |
ProjectStats
| Field | Type | Description |
|---|---|---|
project_id |
string | Encoded project identifier |
project_name |
string | Display name for the project |
session_count |
integer | Number of sessions in the project |
message_count |
integer | Sum of message counts across the project’s sessions |
last_activity |
string or null |
Latest timestamp found across the project’s sessions |
Endpoint Reference
GET /projects
Use this to discover valid project_id values.
Behavior:
- Returns every project as
Project[]. - Results are sorted by
last_activitydescending, so the most recently active projects come first. - There are no query parameters.
- This is the best starting point for clients that need to drill into a specific project.
GET /projects/{project_id}
Use this when you already know the encoded project ID and want one object back.
Behavior:
- Returns a single
Project. - The response shape is the same as each item from
GET /projects. - It does not include nested sessions or stats.
- If the project does not exist, the server returns
404withProject not found.
GET /projects/{project_id}/sessions
Use this to list a project’s sessions without loading full session detail.
Behavior:
- Returns
SessionSummary[]. - Supports a
limitquery parameter with a default of50, a minimum of1, and a maximum of500. - Results are sorted by
updated_atdescending, so the newest sessions appear first. - The URL uses the encoded
project_id, but the response uses decoded display fields such asproject_pathandproject_name. - If nothing matches, the endpoint returns an empty list.
Note:
message_countis based on parseduserandassistantentries in the session JSONL file, not every raw JSONL record.
GET /projects/{project_id}/sessions/active
Use this when you only want sessions that are currently running.
Behavior:
- Returns
SessionSummary[]with the same shape as the regular project sessions endpoint. - There are no query parameters.
- There is no built-in
limiton this route. - If no active sessions match the project, the endpoint returns an empty list.
Note: Active status is calculated live, not stored in the session file.
Note: The active-session lookup is cached for 5 seconds, so sessions may take a moment to appear or disappear.
Warning: Active-session detection is best-effort. The implementation looks for running
claudeprocesses and inspects/proc, so results depend on the host environment and permissions.
try:
# Use pgrep to find claude processes
result = subprocess.run(
["pgrep", "-f", "claude"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode != 0:
_active_sessions_cache = active_ids
_active_sessions_cache_time = current_time
return active_ids
pids = result.stdout.strip().split("\n")
# UUID pattern for session IDs
uuid_pattern = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.IGNORECASE)
for pid in pids:
if not pid:
continue
try:
cmdline_path = Path(f"/proc/{pid}/cmdline")
if cmdline_path.exists():
cmdline = cmdline_path.read_text()
# Extract all UUIDs from cmdline
active_ids.update(uuid_pattern.findall(cmdline))
# Also check environment variables
environ_path = Path(f"/proc/{pid}/environ")
if environ_path.exists():
environ = environ_path.read_text()
active_ids.update(uuid_pattern.findall(environ))
# Check file descriptors for session UUIDs
fd_dir = Path(f"/proc/{pid}/fd")
if fd_dir.exists():
for fd_link in fd_dir.iterdir():
try:
target = fd_link.resolve()
target_str = str(target)
# Look for ~/.claude/tasks/{UUID} or ~/.claude/projects/*/{UUID}.jsonl
if ".claude/tasks/" in target_str or ".claude/projects/" in target_str:
active_ids.update(uuid_pattern.findall(target_str))
GET /projects/{project_id}/stats
Use this when you want per-project totals and recency without aggregating session data in the client.
Behavior:
- Returns a single
ProjectStatsobject. session_countcomes from the number of session files in the project.message_countis the sum of parsed session message counts across that project.last_activityis the newest timestamp found across the project’s sessions.- If the project does not exist, the server returns
404withProject not found.
Tip: This is the best project endpoint for dashboard cards, summary panels, and overview pages.
Missing-Project Behavior
These endpoints do not all behave the same way when a project_id does not match anything.
| Endpoint | Missing project_id behavior |
|---|---|
GET /projects/{project_id} |
404 Project not found |
GET /projects/{project_id}/sessions |
200 OK with [] |
GET /projects/{project_id}/sessions/active |
200 OK with [] |
GET /projects/{project_id}/stats |
404 Project not found |
That difference matters if you are building a client. Use the detail or stats endpoints when you want a hard existence check, and use the sessions endpoints when an empty collection is a normal outcome.