overleaf-ce-sync
Two-way "git-like" sync for a self-hosted Overleaf Community Edition.
The native Git integration (git-bridge) is a Server Pro / paid feature and is
not shippable with CE — the Snapshot API and OAuth2 server it needs live in
closed-source modules. This tool gives you the next best thing: pull a project
to disk, version it with your own git, and push changes back.
It talks to the same HTTP endpoints the web UI uses. The plain-HTTP login that is broken on overleaf.com (CAPTCHA) works fine on CE because CE has no CAPTCHA.
Install
With pipx (isolated venv, overleaf-ce-sync on PATH):
git clone https://git.all.ar/pub/overleaf-ce-sync.git
cd overleaf-ce-sync
pipx install -e . # -e = editable: source edits apply with no reinstall
The only dependency is requests; Python 3.8 or newer.
If pipx warns that ~/.local/bin isn't on PATH, run pipx ensurepath and open
a new shell.
Usage
# 1. log in once per host (cookie saved to ~/.config/overleaf-ce, 0600)
overleaf-ce-sync login --host overleaf.example.com --email you@example.com
# 2. find your project id
overleaf-ce-sync list
# 3. clone it into a git repo
overleaf-ce-sync clone <project-id> mybook
cd mybook
# 4. normal loop
overleaf-ce-sync pull # bring down remote changes (overwrites local)
# ...edit, git commit...
overleaf-ce-sync push # upload locally-changed files
After the first pull/clone, the host + project id are remembered in
.overleaf-ce.json, so inside the project dir you can just run
overleaf-ce-sync pull / overleaf-ce-sync push.
How it works
The sync is anchored to git commits, so push publishes only what you have
committed — never your working-tree scratch edits.
- pull →
GET /Project/<id>/download/zip, extract over the working tree, then make a "sync commit" and remember it aslast_sync_commitin.overleaf-ce.json. - push → upload the committed state (HEAD). It diffs
last_sync_commit..HEAD, uploads each changed file's committed bytes (git show HEAD:<path>) viaPOST /Project/<id>/upload(overwrites by name), then advanceslast_sync_committo HEAD. Uncommitted edits are ignored. - The upload endpoint needs the project's root folder id. It's discovered
once over socket.io xhr-polling (plain HTTP — works behind any reverse
proxy; no websocket upgrade needed) via the
joinProjectResponseevent, and cached in.overleaf-ce.json.
Limitations / notes
- push only sends committed changes. Edit,
git commit, thenpush. A file with uncommitted changes is skipped (you'll get a heads-up note). - Deletions are not propagated. Files removed in your commits are reported but
NOT deleted on Overleaf (safer default). Delete them in the web UI; use
pull --pruneto mirror remote deletions into your working tree. - pull overwrites the working tree and refuses if you have uncommitted
changes (pass
--forceto override). It auto-commits the pulled state, so your history interleaves "overleaf pull" sync commits with your own. - Binary vs doc: Overleaf decides per file; re-uploading creates new history versions, so push only sends files whose committed content changed.
- Don't log out in the browser session you used — Overleaf may revoke the cookie.
Fallback: root folder id without socket.io
If the xhr-polling discovery fails against your setup, fetch the id once from
Mongo on the server and pass it with --root-folder-id (it gets cached
afterwards):
docker compose exec mongo mongosh sharelatex --quiet --eval \
'printjson(db.projects.findOne({_id:ObjectId("<project-id>")},{ "rootFolder._id":1}))'
overleaf-ce-sync push --root-folder-id <root-folder-id>
License
MIT