Skip to content

git-scarrow/claude-projects

Repository files navigation

claude-projects

Manage your claude.ai Projects from the command line, or from Claude Code via MCP. Sync a repo's docs into a Project's knowledge base, export chats to markdown, and grep your entire chat history offline.

Unofficial and unsupported. This is not an Anthropic product. It drives claude.ai's internal web API — the same endpoints the website calls — by reusing your browser session cookie. That API is undocumented and can change or break without warning; docs/api-endpoints.md records what was observed and when. Use it on your own account, at your own risk, and read Security before you start.

If you want a stable, supported, documented interface, use the Anthropic API instead. This tool exists because the API has no concept of claude.ai Projects or chat history.

Requirements

  • Python 3.11+ (needs tomllib)
  • Firefox, logged into claude.ai — the session cookie is read from its cookie store. Chrome and Safari are not supported; see Authentication for the CLAUDE_SESSION_KEY escape hatch.

Install

pipx install git+https://codeberg.org/scarrow/claude-projects
# or, into an existing virtualenv:
pip install git+https://codeberg.org/scarrow/claude-projects

This installs two commands: claude-projects (CLI) and claude-projects-mcp (MCP server). To hack on it instead:

git clone https://codeberg.org/scarrow/claude-projects && cd claude-projects
python3 -m venv .venv && . .venv/bin/activate
pip install -e . && python -m pytest test/ -q

Quickstart

claude-projects list-projects                  # find your project's UUID
claude-projects list-docs PROJECT_ID           # what's in its knowledge base

claude-projects sync-cache                     # pull chat history into a local cache
claude-projects search-chats "that thing I said about retries"

To keep a repo's docs in sync with a Project, drop a .claude-project.toml in the repo root (see Bidirectional sync) and run:

claude-projects sync-project --direction push --dry-run   # preview
claude-projects sync-project --direction push             # do it

Register the MCP server with Claude Code:

claude mcp add claude-projects -- claude-projects-mcp

CLI usage

Command Description
list-projects List all projects
list-docs PROJECT_ID List knowledge files in a project
get-instructions PROJECT_ID Print project instructions
set-instructions PROJECT_ID FILE Update project instructions from a file
diff-instructions PROJECT_ID FILE Diff local file against remote instructions
upload-doc PROJECT_ID FILE Upload a knowledge file
sync-docs PROJECT_ID FILE [FILE ...] [--dry-run] Sync local files to project knowledge (replace if changed, add if new)
pull-docs PROJECT_ID FILE [FILE ...] [--dry-run] Pull remote docs down to local files (matched by basename)
sync-project [--config PATH] [--direction push|pull|both] [--dry-run] [--prune] [--force] Mapping-driven bidirectional sync — see Bidirectional sync
delete-doc PROJECT_ID DOC_UUID Delete a knowledge file by UUID
sync-cache [--full] Sync all account chats into the local search cache
search-chats QUERY [--regex] [--project UUID | --no-project] [--since ISO] [--until ISO] Search cached chats account-wide
cache-stats Show local chat cache statistics

All commands are also available as python cli.py <command> from a source checkout.

MCP server

Run with stdio transport:

claude-projects-mcp        # or, from a checkout: python server.py

Or register with the Claude CLI:

claude mcp add claude-projects -- claude-projects-mcp

Exposed tools:

  • claude_list_projects
  • claude_list_docs
  • claude_get_instructions
  • claude_set_instructions
  • claude_upload_doc
  • claude_delete_doc
  • claude_delete_doc_by_name
  • claude_read_doc
  • claude_search_docs
  • claude_list_chats
  • claude_read_chat
  • claude_list_turns
  • claude_read_chat_turns
  • claude_extract_chats
  • claude_search_chats
  • claude_sync_docs
  • claude_pull_docs
  • claude_sync_project
  • claude_clone_project
  • claude_sync_cache
  • claude_search_all_chats
  • claude_cache_stats

Account-wide chat search

claude_search_chats searches one project by re-downloading every conversation on each call. The cache is the fast path and the only way to reach chats that aren't in any project.

claude-projects sync-cache                      # incremental; only re-fetches changed chats
claude-projects search-chats "retry backoff"   # substring, case-insensitive
claude-projects search-chats "def \w+_sync" --regex --no-project --since 2026-01-01
claude-projects cache-stats

Chats are cached in SQLite at ~/.cache/claude-projects/chats.db (override with --db / db_path). Sync compares each conversation's updated_at against the cached value and downloads message bodies only for new or changed chats, so repeat syncs cost one listing call per 100 conversations. The first sync of a large account downloads everything once.

Search uses SQLite FTS5 with the trigram tokenizer for true substring matching where available, falling back to a plain scan otherwise; both paths return identical results. Thinking blocks are deliberately not indexed; attachment text (extracted_content) is.

Chat export

claude_read_chat, claude_read_chat_turns, claude_extract_chats, and claude_clone_project render conversations with rich content preserved, not just flat text:

  • Artifacts — inline <antArtifact> markup inside a message is rendered as a titled fenced code block (language hinted from the artifact's type where sensible), with the surrounding prose left in place.
  • Attachments (user-uploaded, text-extracted files) are listed per message with their extracted text inlined — no extra fetch needed.
  • Files (images/blobs/documents without inlined content) are listed as reference lines (name + kind); their content is not downloaded.
  • Thinking blocks are opt-in via include_thinking=True (default False, since they're usually noise) and, when included, are rendered as fenced ```thinking blocks.
  • Any content block type not listed above (the API can add new ones) is rendered as a visible [unrendered block: <type>] placeholder rather than silently dropped.

Bidirectional sync

sync-project (CLI) / claude_sync_project (MCP) is a mapping-driven, conflict-aware sync between a local directory and a Claude.ai Project. It's the richer counterpart to sync-docs/pull-docs: instead of passing an explicit file list on every call, it reads a small config file that declares the project and which local files map to instructions/docs, and it tracks sync history so it can tell a genuine edit-on-both-sides conflict apart from a normal one-directional change.

.claude-project.toml

Place this in the directory you want to sync (read from the current working directory by default, or pass --config PATH / config_path=...):

project_id = "uuid"
instructions = "CLAUDE.md"                # optional; synced to/from project instructions
docs = ["docs/*.md", "notes/**/*.txt"]    # globs, relative to this file's directory

docs globs are resolved with recursive ** support. On push, every file matched by a glob is synced. On pull, remote docs are matched to local files by basename first; a remote doc with no local counterpart yet is placed using the directory of whichever glob pattern's basename pattern it matches (e.g. a new remote doc foo.md lands in docs/ given the pattern docs/*.md) — remote docs don't carry directory info, so this is a best-effort placement, not a full path reconstruction.

State file and conflict detection

Alongside the config, a .claude-project.state.json file (gitignored) records the content hash last successfully synced for each doc and for instructions. On each sync:

  • If only the local side changed since that baseline → push proceeds (or pull skips, reporting "try push").
  • If only the remote side changed → pull proceeds (or push skips, reporting "try pull").
  • If both sides changed since the baseline → it's a conflict: the file is skipped and reported (conflict <name> ...) rather than silently clobbered. Pass --force / force=True to overwrite anyway, in the direction of the sync you're running.
  • If there's no baseline yet (first sync of this mapping), the direction you invoked wins — push uploads local content, pull writes remote content locally — matching the original sync-docs clobber-on-first-run behavior.

Flags

  • --direction push|pull|both — which way to sync (default: push).
  • --dry-run — report add/update/skip/conflict/prune per file without writing anything, locally or remotely, and without touching the state file. Available on sync-docs, pull-docs, and sync-project.
  • --prune — (push only) delete remote docs that this tool previously synced but that are no longer matched by any local mapping glob — i.e. docs you deleted locally. Remote docs with no entry in the state file (for example, uploaded through the claude.ai web UI) are never deleted; they are reported as keep ... (untracked remote doc, not pruned). Review a --dry-run --prune pass before running it for real.
  • --force — overwrite a detected conflict in the direction of the current sync.

Examples:

claude-projects sync-project --direction both --dry-run
claude-projects sync-project --direction push --prune
claude-projects sync-project --direction pull --force

Authentication

Authentication uses Firefox session cookies via claude_cookie_extract.py. Ensure you are logged into claude.ai in Firefox. The extractor searches all standard Firefox profile locations for the current platform:

  • Linux - ~/.mozilla/firefox/*/cookies.sqlite, plus Snap (~/snap/firefox/common/.mozilla/firefox/*/cookies.sqlite) and Flatpak (~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/cookies.sqlite) install locations
  • macOS - ~/Library/Application Support/Firefox/Profiles/*/cookies.sqlite
  • Windows - %APPDATA%/Mozilla/Firefox/Profiles/*/cookies.sqlite (best-effort, untested)

If multiple profiles are found, the one with a live claude.ai sessionKey cookie is used, falling back to the most recently modified profile. Firefox's cookie DB is copied to a temp file before reading; if the copy fails (e.g. the file is locked while Firefox is running), it falls back to a read-only, immutable SQLite connection against the live file.

Environment variables:

  • CLAUDE_ORG_ID - Organization UUID (auto-detected if not set, by picking the org whose capabilities include "chat" — an account can have multiple orgs, and an api-only org fails on every project/conversation endpoint)
  • CLAUDE_SESSION_KEY - Session key (extracted from Firefox cookies if not set)

Security

  • Your session cookie is a bearer credential. Anyone holding it has full access to your claude.ai account. This tool reads it from Firefox's cookie store at runtime and never writes it to disk or logs it. If you set CLAUDE_SESSION_KEY instead, treat it like a password — prefer a secrets manager over a plaintext shell profile.
  • The local chat cache is unencrypted. sync-cache writes the full text of your conversations to ~/.cache/claude-projects/chats.db. Anything sensitive you ever typed into claude.ai ends up there in plaintext. Delete it with rm ~/.cache/claude-projects/chats.db.
  • Never commit chats.db, .claude-project.state.json, or exported chat transcripts. The .gitignore covers the first two and *.sqlite3*.
  • Destructive operations (delete-doc, sync-project --prune) support --dry-run. --prune only ever deletes docs this tool previously synced — never docs you uploaded through the claude.ai web UI.

Limitations and caveats

  • The API is undocumented and unversioned. It has already changed once in ways this repo had to chase (see docs/api-endpoints.md). Expect breakage.
  • Pagination is not uniform. chat_conversations and projects honor limit/offset; the docs endpoint silently ignores both.
  • Project create / rename / archive / delete are not implemented. Only PUT (used for instructions, name, description) is verified. The create and delete endpoints were never probed, because doing so mutates a real account.
  • Project memory is not supported. GET /projects/{id}/memory returns 404 on every project tested; there is no known working endpoint.
  • Tool-use blocks are not rendered. None were observed in any conversation sampled, so no extraction logic was written for them. Unknown block types render as a visible [unrendered block: <type>] placeholder rather than being dropped.
  • The first sync-cache on a large account downloads every conversation once. Subsequent syncs only re-fetch chats whose updated_at advanced.

Contributing

pip install -e . && python -m pytest test/ -q

The test suite is fully mocked and makes no network calls; please keep it that way. If you discover something new about the internal API, record it in docs/api-endpoints.md, and mark clearly whether you observed it or inferred it — that distinction is the whole value of the file.

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors