Skip to main content
Glama
README.md
# olcli — Overleaf CLI

**Command-line interface for Overleaf** — Sync, manage, and compile LaTeX projects from your terminal.

[![CI](https://github.com/aloth/olcli/actions/workflows/ci.yml/badge.svg)](https://github.com/aloth/olcli/actions/workflows/ci.yml)
[![npm version](https://img.shields.io/npm/v/@aloth/olcli.svg)](https://www.npmjs.com/package/@aloth/olcli)
[![npm downloads](https://img.shields.io/npm/dm/@aloth/olcli.svg)](https://www.npmjs.com/package/@aloth/olcli)
[![GitHub stars](https://img.shields.io/github/stars/aloth/olcli)](https://github.com/aloth/olcli)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![AgentSkills](https://img.shields.io/badge/AgentSkills-compatible-blue)](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.

<p align="center">
  <img src="screenshots/demo.gif" alt="olcli demo" width="600">
</p>

## 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/<id>
cd <project>
# 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 <file> [project]` | Upload a single file (`--to <path>` sets the remote destination) |
| `olcli download <file> [project]` | Download a single file |
| `olcli delete <file> [project]` | Delete a remote file or folder (alias: `rm`) |
| `olcli rename <old> <new> [project]` | Rename a remote file or folder (alias: `mv`) |
| `olcli project create <name>` | Create a blank or example project (`--template blank\|example`) |
| `olcli project rename <new> [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 <ids...>` | Accept tracked changes by id (`--project`, `--json`) |
| `olcli changes reject <ids...>` | Reject tracked changes by id; preview only unless `--yes` (`--project`, `--json`) |
| `olcli comments add <file> <msg>` | Add a comment to selected text |
| `olcli comments reply <id> <body>` | Reply to a comment thread |
| `olcli comments resolve <id>` | Resolve a comment thread |
| `olcli comments reopen <id>` | Reopen a resolved thread |
| `olcli comments delete <id>` | Delete a comment thread |
| `olcli ignored [dir]` | List ignore patterns in effect |
| `olcli config set-url <url>` | Set self-hosted base URL |
| `olcli config get-url` | Show the configured base URL |
| `olcli config set-cookie-name <name>` | Set session cookie name |
| `olcli config get-cookie-name` | Show the configured session cookie name |
| `olcli config set-timeout <ms>` | 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 <path>` | 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 <url>` | Override Overleaf instance URL |
| `--cookie-name <name>` | Override session cookie name |
| `--timeout <ms>` | 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 <path>` puts it elsewhere. The root document is the `.tex` file
declaring `\documentclass`; if several do, `--main <path>` 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)

TDQS

A3.7/5.0

Scored across 21 tools

Disambiguation4/5

Most tools have distinct resource+action targets, and rename_project explicitly disambiguates itself from rename_entity. However, the compile trio (compile, download_pdf, compile_with_outputs) all compile and differ mainly in output destination, and get_project_info (file tree) vs get_entities (flat list) vs pull_project (full zip) overlap enough to require reading descriptions carefully.

Naming Consistency4/5

Almost everything follows a snake_case verb_noun pattern (list_projects, push_file, resolve_comment, rename_entity, plan_project_renames). Minor deviations are the bare verb 'compile' and verb-only 'download_pdf', which are still readable and unambiguous.

Tool Count4/5

21 tools is on the heavy side but the domain genuinely spans projects, files, comments, tracked changes, compilation, and bulk rename planning, and each tool maps to a real operation. A few could be consolidated (compile variants), keeping it just under 'well-scoped'.

Completeness3/5

Core lifecycle is covered (list/create/rename projects, pull/push/download/delete/rename files, comment lifecycle, compile, accept/reject tracked changes). But accept_tracked_changes and reject_tracked_changes require change IDs with no corresponding list_tracked_changes tool, and there is no move-entity or comment delete/reopen operation, leaving notable gaps.

Maintenance

ActivityActive
ResponsivenessWithin a week