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.mdrecords 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.
- 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_KEYescape hatch.
pipx install git+https://codeberg.org/scarrow/claude-projects
# or, into an existing virtualenv:
pip install git+https://codeberg.org/scarrow/claude-projectsThis 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/ -qclaude-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 itRegister the MCP server with Claude Code:
claude mcp add claude-projects -- claude-projects-mcp| 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.
Run with stdio transport:
claude-projects-mcp # or, from a checkout: python server.pyOr register with the Claude CLI:
claude mcp add claude-projects -- claude-projects-mcpExposed 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
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-statsChats 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.
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'stypewhere 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(defaultFalse, since they're usually noise) and, when included, are rendered as fenced```thinkingblocks. - 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.
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.
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 directorydocs 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.
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=Trueto 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 —
pushuploads local content,pullwrites remote content locally — matching the originalsync-docsclobber-on-first-run behavior.
--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 onsync-docs,pull-docs, andsync-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 askeep ... (untracked remote doc, not pruned). Review a--dry-run --prunepass 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 --forceAuthentication 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 whosecapabilitiesinclude"chat"— an account can have multiple orgs, and anapi-only org fails on every project/conversation endpoint)CLAUDE_SESSION_KEY- Session key (extracted from Firefox cookies if not set)
- 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_KEYinstead, treat it like a password — prefer a secrets manager over a plaintext shell profile. - The local chat cache is unencrypted.
sync-cachewrites 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 withrm ~/.cache/claude-projects/chats.db. - Never commit
chats.db,.claude-project.state.json, or exported chat transcripts. The.gitignorecovers the first two and*.sqlite3*. - Destructive operations (
delete-doc,sync-project --prune) support--dry-run.--pruneonly ever deletes docs this tool previously synced — never docs you uploaded through the claude.ai web UI.
- 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_conversationsandprojectshonorlimit/offset; thedocsendpoint 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}/memoryreturns 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-cacheon a large account downloads every conversation once. Subsequent syncs only re-fetch chats whoseupdated_atadvanced.
pip install -e . && python -m pytest test/ -qThe 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.
MIT — see LICENSE.