# olcli — Overleaf CLI
**Command-line interface for Overleaf** — Sync, manage, and compile LaTeX projects from your terminal.
[](https://github.com/aloth/olcli/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/@aloth/olcli)
[](https://www.npmjs.com/package/@aloth/olcli)
[](https://github.com/aloth/olcli)
[](https://opensource.org/licenses/MIT)
[](https://agentskills.io)
Work with Overleaf projects directly from your command line. Edit locally with your favorite editor, version control with Git, and sync seamlessly with Overleaf's cloud compilation.
## Features
- 📋 **List** all your Overleaf projects
- ✨ **Create** blank or example projects
- ⬇️ **Pull** project files to local directory for offline editing
- ⬆️ **Push** local changes back to Overleaf
- 🔄 **Sync** bidirectionally with smart conflict detection
- 🔍 **Diff** local files against the live remote before pushing
- 📝 **Marked-up revisions** — `diff --latexdiff` produces the struck-through/underlined PDF advisors and journals ask for
- 🔀 **Git remote** — use Overleaf as a native git remote ([docs](docs/GIT-REMOTE.md))
- ✌️ **Two-way deletions** — files removed locally are deleted on Overleaf on next sync
- 🗑️ **Delete** and ✏️ **rename** remote files by path
- 🚫 **Smart ignore** — LaTeX build artifacts and OS noise filtered automatically; extend with `.olignore`
- 📄 **Compile** PDFs using Overleaf's remote compiler
- 📦 **Download** individual files or full project archives
- 📤 **Upload** files to projects
- 💬 **Review comments** — list, add, resolve, reopen, delete, and reply to threads
- 🗂️ **Preserve folder structure** when pushing nested files
- ⏱️ **Configurable timeout** for slow connections
- 🔑 **Password login** for self-hosted instances (no browser required)
- ⚙️ **Self-hosted Overleaf/ShareLaTeX** support
- 📊 **Output** compile artifacts (`.bbl`, `.log`, `.aux` for arXiv submissions)
- 🤖 **MCP server** for AI assistants ([docs](docs/MCP.md))
**Perfect for:**
- Editing LaTeX in your preferred text editor (Vim, VS Code, Emacs, etc.)
- Version control with Git while using Overleaf's compiler
- Automating workflows and CI/CD pipelines
- Offline work with periodic sync
## Installation
### Homebrew (macOS/Linux)
```bash
brew tap aloth/tap
brew install olcli
```
### npm (all platforms)
```bash
npm install -g @aloth/olcli
```
Or use with `npx` without installation:
```bash
npx @aloth/olcli list
```
### For AI agents (via AgentSkills)
```bash
npx skills add aloth/olcli
```
## Quick Start
### 1. Authenticate
**Session cookie** (overleaf.com and self-hosted):
```bash
olcli auth --cookie "your_session_cookie_value"
```
**Email/password** (self-hosted without reCAPTCHA):
```bash
olcli auth --email "you@example.com"
# prompts for the password, so it stays out of your shell history
```
The password is **not stored** unless you pass `--save-password`. A session
cookie is saved either way and is what later commands use; the password only
buys an automatic re-login once that cookie expires. For scripts, set
`OVERLEAF_EMAIL` and `OVERLEAF_PASSWORD` — every command reads them, so a
scripted run never needs `olcli auth` at all.
### 2. List Projects
```bash
olcli list
```
### 3. Pull, Edit, Push
```bash
olcli pull "My Thesis"
cd My_Thesis/
vim main.tex
olcli push
```
### 4. Compile PDF
```bash
olcli pdf
# Compile a specific .tex file (for multi-doc projects):
olcli pdf -r appendix.tex
```
### 5. Or use native git commands
```bash
git clone overleaf::https://www.overleaf.com/project/
cd
# edit, commit, push — standard git workflow
git push
```
See [Git Remote Helper docs](docs/GIT-REMOTE.md) for details.
## Commands
All commands auto-detect the project when run from a synced directory (contains `.olcli.json`).
| Command | Description |
|---------|-------------|
| `olcli auth` | Set session cookie or login with email/password |
| `olcli whoami` | Check authentication status |
| `olcli logout` | Clear the global config and the local `.olauth`, reporting each |
| `olcli list` | List all projects |
| `olcli info [project]` | Show project details and file list |
| `olcli pull [project] [dir]` | Download project files to local directory |
| `olcli push [dir]` | Upload local changes to Overleaf (`--delete` also removes files deleted locally) |
| `olcli sync [dir]` | Bidirectional sync (pull + push) |
| `olcli diff [project] [dir]` | Show content-level changes between local files and the remote |
| `olcli upload [project]` | Upload a single file (`--to ` sets the remote destination) |
| `olcli download [project]` | Download a single file |
| `olcli delete [project]` | Delete a remote file or folder (alias: `rm`) |
| `olcli rename [project]` | Rename a remote file or folder (alias: `mv`) |
| `olcli project create ` | Create a blank or example project (`--template blank\|example`) |
| `olcli project rename [project]` | Rename the project itself (`--dry-run`) |
| `olcli project rename-bulk` | Rename many projects by pattern (dry-run unless `--apply`) |
| `olcli compile [project]` | Trigger PDF compilation |
| `olcli pdf [project]` | Compile and download PDF |
| `olcli output [type]` | Download compile output files |
| `olcli zip [project]` | Download project as zip archive |
| `olcli comments list [project]` | List comments (`--status`, `--context`, `--concurrency`); `--changes` also lists tracked changes (with `--json` the output becomes `{ comments, changes }`) |
| `olcli changes accept ` | Accept tracked changes by id (`--project`, `--json`) |
| `olcli changes reject ` | Reject tracked changes by id; preview only unless `--yes` (`--project`, `--json`) |
| `olcli comments add ` | Add a comment to selected text |
| `olcli comments reply ` | Reply to a comment thread |
| `olcli comments resolve ` | Resolve a comment thread |
| `olcli comments reopen ` | Reopen a resolved thread |
| `olcli comments delete ` | Delete a comment thread |
| `olcli ignored [dir]` | List ignore patterns in effect |
| `olcli config set-url ` | Set self-hosted base URL |
| `olcli config get-url` | Show the configured base URL |
| `olcli config set-cookie-name ` | Set session cookie name |
| `olcli config get-cookie-name` | Show the configured session cookie name |
| `olcli config set-timeout ` | Set default HTTP timeout |
| `olcli config get-timeout` | Show the configured HTTP timeout |
| `olcli check` | Show config paths and credential sources |
> **Tracked changes are an Overleaf premium feature.** They are only available in projects whose owner has a premium plan; invited collaborators can then use them regardless of their own plan. In other projects there are no tracked changes, so `--changes` returns an empty list and `changes accept`/`changes reject` report the ids as not found. See the [Overleaf documentation](https://docs.overleaf.com/collaborating/track-changes) for details.
### Compile Options
The compile-related commands (`compile`, `pdf`, `output`) accept:
| Flag | Description |
|------|-------------|
| `-r, --resource ` | Compile a specific `.tex` file as the root document (e.g. `appendix.tex`, `folder/test.tex`) |
Useful in multi-doc projects: each `-r` run compiles the file as if it were the main document.
### Global Options
| Flag | Description |
|------|-------------|
| `--verbose` | Print HTTP requests and responses to stderr |
| `--base-url ` | Override Overleaf instance URL |
| `--cookie-name ` | Override session cookie name |
| `--timeout ` | Override HTTP timeout (default: 10000) |
## Sync Behavior
### Pull
- Downloads all files from Overleaf
- Skips local files modified after last pull (won't overwrite your changes)
- Use `--force` to overwrite local changes
### Push
- Uploads files modified after last pull
- Preserves nested folder structure
- Filters out LaTeX build artifacts and OS noise
- Use `--all` to upload all files, `--dry-run` to preview
### Sync
- Pulls remote changes, then pushes local changes
- Local modifications win if newer
- **Propagates local deletions** — use `--no-delete` to opt out
- Use `--dry-run` to preview without applying
### Diff
`olcli diff` compares the bytes of your local files against the project's
current contents and prints a unified diff.
```bash
olcli diff # every changed file, as patches
olcli diff --name-only # just the changed paths
olcli diff --file main.tex # one file
olcli diff -U 8 # wider context
olcli diff --exit-code # exit 1 if anything differs, for CI
```
**The remote side is fetched fresh on every run.** The diff describes the
project as it is at that moment — which is what a subsequent `push` would
overwrite — not a comparison against your last `pull`. `.olcli.json` records
remote *paths*, never remote *contents*, so there is no stored snapshot to
compare against; and the whole project arrives in a single request, the same
one `pull` makes, so fetching fresh costs one round trip rather than one per
file. A collaborator editing between `diff` and `push` can still change the
outcome, which is why the fetch time is printed.
In the output, `a/` is the remote and `b/` is local: a `+` line is content
`push` would upload, a `-` line is content it would overwrite. Files that
differ only in bytes that are not text (PDFs, images) are reported as
`Binary files ... differ`. Both sides pass through the same ignore layers, so
build artifacts sitting on Overleaf are not reported as locally deleted.
`diff --name-only` and `push --dry-run` answer different questions and will
disagree. `push --dry-run` lists files whose **modification time** is newer
than the last pull, because that is what `push` uploads; `diff` lists files
whose **contents** actually differ. A file you touched without editing appears
in the first and not the second.
#### Marked-up revisions with latexdiff
A unified diff is the right artifact for a developer and the wrong one for a
thesis advisor. `--latexdiff` marks the same revision up inside the document
instead — deletions struck through, additions underlined — which is what
advisors and journals ask for.
```bash
olcli diff --latexdiff # write .olcli-diff/main-diff.tex
olcli diff --latexdiff --pdf # ...and compile it, download .olcli-diff/main-diff.pdf
```
Requires `latexdiff` on your PATH. It ships with TeX Live and MacTeX; nothing
else in `olcli diff` needs an external tool.
**`--pdf` compiles on Overleaf, so you do not need a local TeX installation.**
The compile endpoint can only build a file that is in the project, so the
marked-up document is uploaded as `olcli-latexdiff.tex` next to your root
document, compiled, downloaded, and then removed. The command says so before it
does it, refuses to overwrite a file of that name if one already exists, and
prints the exact `olcli rm` command if the cleanup itself fails.
`\input` and `\include` are inlined before comparing (`--no-flatten` to opt
out). Without that, a remote compile would resolve those against the files
sitting in the project — the old content — and quietly produce a PDF showing
every input file as unchanged.
Output goes to `.olcli-diff/`, which is dotted so that `push` and `sync` never
pick it up; `-o ` puts it elsewhere. The root document is the `.tex` file
declaring `\documentclass`; if several do, `--main ` picks one rather
than the command guessing. `--latexdiff-opt` passes anything else straight
through, e.g. `--latexdiff-opt --math-markup=0`.
One limit worth knowing: `--pdf` compiles against the project, not against your
working directory, so anything the markup needs must already be on Overleaf. A
`.sty` or `.cls` you added locally fails the compile — the compiler log is
written next to the marked-up source when that happens. A *figure* you added
locally does not fail it; Overleaf draws a placeholder box naming the missing
file and the rest of the PDF is fine.
#### Using `diff` as a CI gate
`--exit-code` turns the command into a check, with the statuses `diff(1)` uses:
| Status | Meaning |
|--------|---------|
| `0` | Nothing differs |
| `1` | Something differs |
| `2` | The run failed — bad flags, no session, project unreachable |
```yaml
- name: Fail if the paper on Overleaf has drifted from the repo
run: olcli diff --exit-code --name-only
env:
OVERLEAF_SESSION: ${{ secrets.OVERLEAF_SESSION }}
```
**The `1` versus `2` split is the point of the flag.** Without it a pipeline
cannot tell a changed file from an expired session cookie, and a job that goes
red for the second reason sends whoever reads the log hunting for a diff that
was never computed. Every other olcli command reports failure as `1`, and
`olcli diff` still does when `--exit-code` is absent, so adding the flag does
not change what existing scripts see.
The gate covers whatever was compared: `--file main.tex` narrows it to one
file, the way a `git diff --exit-code` pathspec does, and a `--file` that
matches nothing is `0` rather than an error. Under `--latexdiff` it still
reports on the project as a whole — a changed figure is a real difference even
though a marked-up root document cannot show it.
Note that this compares against Overleaf **now**, not against your last pull,
so the check is "has anyone drifted from what is committed here", which is what
makes it worth running on a schedule as well as on a push.
#### How deletion propagation works
`olcli` records a manifest of remote files in `.olcli.json`. On next sync:
- File missing locally + still on remote → deleted on Overleaf
- File new locally → uploaded
- File modified locally → uploaded (local wins)
- File only on remote → downloaded
First-time syncs skip the deletion phase (no prior manifest to compare).
## Ignoring Files
### Three layers
| Layer | Source | Purpose |
|---|---|---|
| 1 | Built-in | LaTeX intermediates, OS noise, build dirs. Always on. |
| 2 | `.olignore` | Project-level patterns (gitignore syntax). |
| 3 | `.olignore.local` | Machine-specific patterns. |
Later layers override earlier ones. Negation (`!important.aux`) is supported.
### Special PDF rule
`X.pdf` is ignored only if `X.tex` (or `.ltx`) exists in the same folder.
### Inspecting and overriding
```bash
olcli ignored # list patterns in effect
olcli push --show-ignored # see what was skipped
olcli sync --no-default-ignore # only .olignore applies
olcli sync --no-ignore # upload everything
```
## Configuration
Credentials are checked in order:
1. `OVERLEAF_SESSION` environment variable
2. `.olauth` file in current directory
3. Global config — run `olcli check` to see the exact path
The global config path is platform-dependent (`conf` resolves it), so it is not
hardcoded here: on macOS it lands under `~/Library/Preferences/`, on Linux under
`~/.config/`. `olcli auth` prints the path it wrote to.
⚠️ `olcli auth --save-local` writes `.olauth` into the **current directory**,
which is usually your LaTeX project. Add it to that project's `.gitignore`
before committing.
### What is stored, and how to clear it
Everything is stored in plaintext, so it is worth knowing what is on disk:
| Credential | Stored by default | Where |
|---|---|---|
| Session cookie | yes | global config, or `.olauth` with `--save-local` |
| Email + password | **no** — only with `--save-password` | global config |
`olcli check` reports what exists without printing any secret.
`olcli logout` clears the global config **and** the `.olauth` file in the
current directory, then lists what it removed. It cannot unset environment
variables, so if `OVERLEAF_SESSION` or `OVERLEAF_EMAIL`/`OVERLEAF_PASSWORD` are
set, it says so instead of implying you are logged out — those take precedence
over anything on disk.
### Self-hosted Overleaf
```bash
olcli config set-url https://latex.example.org
olcli config set-cookie-name overleaf.sid
```
Or pass per-command: `olcli --base-url https://latex.example.org list`
### Timeout
```bash
olcli config set-timeout 60000 # persist
olcli --timeout 60000 pull "Big Thesis" # one-off
export OVERLEAF_TIMEOUT=60000 # env var
```
Precedence: `--timeout` > `OVERLEAF_TIMEOUT` > config > default (10000ms).
### Environment variables
olcli reads these. The MCP server and the git remote helper read the same session, password and cookie name variables. Where a flag exists, the flag takes precedence over the variable, and the variable over the stored config.
| Variable | Purpose |
|---|---|
| `OVERLEAF_SESSION` | Session cookie value |
| `OVERLEAF_EMAIL`, `OVERLEAF_PASSWORD` | Password login, used when no session cookie is set or it is rejected |
| `OVERLEAF_BASE_URL` | Instance URL, like `--base-url` (default: `https://www.overleaf.com`) |
| `OVERLEAF_COOKIE_NAME` | Session cookie name, like `--cookie-name` (default: `overleaf_session2`; self-hosted instances often use `overleaf.sid`) |
| `OVERLEAF_TIMEOUT` | HTTP timeout in milliseconds, like `--timeout` |
| `OLCLI_CONCURRENCY` | Documents fetched in parallel by `comments list`, 1 to 8 (default: 4); `--concurrency` takes precedence |
| `OLCLI_MCP_UNCONFIRMED_WRITES` | MCP server only: `1` turns off write confirmation, see [docs/MCP.md](docs/MCP.md#write-confirmation) |
| `GIT_REMOTE_OVERLEAF_DEBUG` | Git remote helper only: `1` prints protocol debug output, see [docs/GIT-REMOTE.md](docs/GIT-REMOTE.md#debugging) |
## Examples
```bash
# Daily thesis workflow
olcli pull "PhD Thesis" thesis && cd thesis
vim chapters/methods.tex
olcli sync && olcli pdf -o draft.pdf
# Quick PDF download
olcli pdf "Conference Paper" -o paper.pdf
# Compile a specific root document
olcli pdf "Conference Paper" -r appendix.tex -o appendix.pdf
olcli compile "Conference Paper" -r folder/test.tex
# Upload figures
olcli upload figures/diagram.png # relative path is preserved
olcli upload /tmp/build/diagram.png # absolute path lands in the project root
olcli upload /tmp/build/diagram.png --to figures/diagram.png # explicit destination
# arXiv submission prep
olcli output bbl -o main.bbl
olcli output bbl -r folder/test.tex -o main.bbl # compile artifacts from a specific root doc
olcli zip -o arxiv-submission.zip
# Backup all projects
for proj in $(olcli list --json | jq -r '.[].name'); do
olcli zip "$proj" -o "backups/${proj}.zip"
done
```
## Programmatic Usage (Library API)
`@aloth/olcli` exposes `OverleafClient` and all public interfaces as a library.
### Install
```bash
npm install @aloth/olcli
```
### Basic example
```ts
import { OverleafClient } from '@aloth/olcli';
const client = await OverleafClient.fromSessionCookie(cookie);
const created = await client.createProject('My Paper');
const projects = await client.listProjects();
const info = await client.getProjectInfo(projectId);
const zipBuf = await client.downloadProject(projectId);
const pdfBuf = await client.downloadPdf(projectId);
await client.uploadFile(projectId, null, 'main.tex', readFileSync('main.tex'));
const comments = await client.listComments(projectId, { status: 'open' });
```
### Available exports
```ts
import {
OverleafClient,
// Types
Project, ProjectInfo, ProjectTemplate, CreateProjectOptions, CreatedProject,
FolderEntry, DocEntry, FileEntry,
CommentMessage, ProjectComment, CommentContext, CommentStatus,
ListCommentsOptions, AddCommentOptions, Credentials, SessionCookiePair,
// Config utilities
getBaseUrl, setBaseUrl, getSessionCookie, setSessionCookie,
getSessionCookieName, setSessionCookieName, getCsrf, setCsrf,
getLastProject, setLastProject, clearConfig, getConfigPath, saveOlAuth,
getTimeout, setTimeout, getPasswordCredentials, setPasswordCredentials,
clearPasswordCredentials, type PasswordCredentials,
// Ignore utilities
DEFAULT_IGNORE_PATTERNS, loadIgnore, shouldIgnore, buildTexSiblingSet,
IgnoreContext, LoadIgnoreOptions,
} from '@aloth/olcli';
```
## Further Documentation
- [MCP Server](docs/MCP.md) — AI assistant integration (Claude, Cursor, Windsurf)
- Tools that change a project ask you to confirm through MCP elicitation and write nothing without it; clients without elicitation get a preview and the `olcli` command to run instead. `OLCLI_MCP_UNCONFIRMED_WRITES=1` turns this off. See [Write confirmation](docs/MCP.md#write-confirmation)
- [Git Remote Helper](docs/GIT-REMOTE.md) — use Overleaf as a native git remote
## Troubleshooting
**Session expired** — Get a fresh cookie from the browser and run `olcli auth` again.
**Compilation fails** — Check the Overleaf web editor for detailed error logs (missing packages, syntax errors, missing bibliography files).
## Contributing
Contributions are welcome! Please open an issue or submit a pull request.
See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, which tests need a real
Overleaf account, and what to expect from CI on a pull request.
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) explains how the client works —
there is no public Overleaf API, so it authenticates as a browser session.
## License
MIT © [Alexander Loth](https://alexloth.com)