Local LaTeX workflow
2026-08-11 → 2026-09-01
This is a practical companion to Leaving Overleaf.
There are two levels of automation. Latexmk enables compilation of the paper upon edits, giving me a fast edit-to-PDF loop while I write. Snakemake is a higher-level automation that rebuilds the full chain from data and analysis to figures, tables, and the paper by invoking the paper folder’s Makefile. The Makefile then runs Latexmk.
You can avoid worrying about all these details if you use Visual Studio Code-like editors that handle autocompilation. I just prefer to have a light-weight, explicit, and terminal-based (and neovim-based) workflow.
Monitor, compile, and update#
I put a small Makefile in the paper folder. Having one for each folder (e.g., current) is simpler. A minimal makefile would look like:
MAIN := main
.PHONY: all monitor clean wordcount
all:
latexmk -pdf $(MAIN).tex
monitor:
latexmk -pvc -pdf $(MAIN).tex
clean:
latexmk -CA $(MAIN).tex
wordcount:
detex -l -n -w $(MAIN).tex sections/*.tex | wc -w
My project template already includes a fuller version of this local Makefile under paper/current/, with targets for the main paper, supplement, cover, continuous preview, word count, and cleanup. Its example Snakefile currently stops at an analysis summary; the paper rule below is the additional piece that makes the paper the final pipeline target.
make monitor with -pvc is the key command. This option invokes the Latexmk’s continuous-preview mode, where it watches the manuscript’s dependencies and recompiles after any change in the source files.
I keep the PDF open in Skim app (Mac), which refreshes it after each successful build. Having this line in ~/.latexmkrc tells Latexmk to use Skim:
$pdf_previewer = 'open -a Skim';
make builds once, make clean removes generated temporary files, and make wordcount gives a rough text count. You can add other various automated “tasks” in the Makefile.
For a main paper plus supplement, give each document its own build and monitor target, then keep make monitor as an alias for the document you edit most often. Or simply include the supplementary material into the end of the main paper while working.
Snakemake pipeline that includes the paper#
The full research pipeline should culminate in the paper. A minimal Snakefile can make the manuscript depend on its source files and generated results:
from glob import glob
PAPER_SOURCES = [
"paper/main.tex",
"paper/references.bib",
*glob("paper/sections/*.tex"),
]
rule all:
input:
"paper/main.pdf",
rule paper:
input:
makefile="paper/Makefile",
sources=PAPER_SOURCES,
figures=[
"results/figure-1.pdf",
"results/figure-2.pdf",
],
output:
"paper/main.pdf",
shell:
"make -C paper"
make -C paper tells Make to change into paper/ before running its Makefile. This keeps the Latexmk command in one place and lets relative paths in main.tex resolve from paper/. Latexmk’s -cd option similarly changes into the source file’s directory when Latexmk is called directly from the repository root, but it is unnecessary here.
The figure paths become dependencies once other rules declare how to produce them. Snakemake rules connect named inputs and outputs; snakemake -n previews the jobs that would run, and snakemake --cores all rebuilds every stale dependency through paper/main.pdf. In a real project, the same graph can include data cleaning, model fitting, tables, the supplement, and presubmission checks.
I still use make monitor for ordinary paper edits. Running the full Snakemake graph on every save is usually wasteful; run it when an analysis input or figure-producing script changes, and let Latexmk handle the rapid LaTeX-only cycle.
Collaborate through branches and pull requests#
Multiple people can work on the same paper the same way they work on shared code: each person works on a short-lived branch and proposes the changes through a pull request. I treat each branch and pull request as a work session rather than a major release: open a draft pull request early so everyone can see what is changing, commit and push small edits as you work, merge when the session is done, then pull the updated main branch before starting again. My broader GitHub workflow describes this in more detail.
A pull request makes the proposed text and its discussion visible without requiring heavy review for every edit. It does not eliminate conflicts: keep branches short-lived, sync often, and tell collaborators what part of the paper you are editing. When people truly need to write simultaneously, splitting the manuscript into section files and using semantic line breaks makes those branches easier to merge.
Split the manuscript for simultaneous work#
Splitting a manuscript can itself add unnecessary complexity. If collaborators work asynchronously, keeping everything in a single main.tex and using pull requests may be simpler. Split the manuscript when people need to edit different parts at the same time; keep main.tex as a stable wrapper and move substantive sections into named files:
paper/
├── main.tex
├── preamble.tex
├── macros.tex
├── references.bib
└── sections/
├── introduction.tex
├── results.tex
├── discussion.tex
└── methods.tex
Then assemble them explicitly:
\input{preamble}
\input{macros}
\begin{document}
\input{sections/introduction}
\input{sections/results}
\input{sections/discussion}
\input{sections/methods}
\end{document}
This reduces merge conflicts and keeps generated results out of hand-maintained manuscript files.
The same logic applies inside each file. LaTeX renders a single newline as a space, so you can start every sentence on its own line—semantic line breaks, which I argue for at more length in my pet peeves. Git’s merge is line-based, so this pushes its granularity down to the sentence: two people editing different sentences of the same paragraph will often merge cleanly, while a hard-wrapped or single-line paragraph turns every edit into a collision on the same line.
File boundaries help, but they do not replace coordination. Sync frequently, and use Slack or another messaging channel to tell collaborators what you are editing. A quick pull and message before starting, followed by small pushes, can prevent many avoidable merge conflicts.
Sync automatically during a synchronous session#
You can manually edit, commit, pull, and push, but you can also automate the process. For instance, you can use a simple script like:
#!/bin/bash
set -euo pipefail
branch=$(git symbolic-ref --quiet --short HEAD)
git add -A
git diff --cached --quiet || git commit -m "${1:?commit message required}"
git fetch origin "$branch"
git rebase "origin/$branch"
git push origin "$branch"
Then let a “watcher” call it. For instance, monex.py (monitor and execute) is a super simple script that I have been using for a long time. This script watches specified files and runs a command whenever any of them changes:
monex.py -c 'git-autosync "wip"' *.tex sections/*.tex references.bib
There are other similar utilities: entr takes its file list from a pipe, watchexec filters by extension, fswatch uses the native file-event APIs, and watchfiles is the Python equivalent. For syncing you can also skip change detection entirely, since the script is safe to run when nothing has changed: while sleep 30; do git-autosync wip; done.
Note that the history would fill with small wip commits, but you can squash them if you care about a clean log. Also, git add -A stages everything, so make sure build artifacts and large data files are actually gitignored before you point this at a repository.
This will be most similar to using overleaf, but I’d use this “mode” only when multiple people intensively work on the same paper (e.g., near the deadline).
Reload external edits in Neovim#
An agent, formatter, or git operation may change a file while it is already open in your editor. Each editor may handle this slightly differently (I use Neovim) and for many modern editors, you may not need to do anything.
For Neovim, I use the following Lua configuration so returning to Neovim checks & load the files on disk:
vim.o.autoread = true
vim.api.nvim_create_autocmd({ "FocusGained", "BufEnter" }, {
command = "checktime",
})
FocusGained runs the check when Neovim regains focus; BufEnter also runs it when I switch buffers. With autoread enabled, an unchanged buffer reloads from disk. If I have unsaved edits, Neovim warns instead of silently replacing them.
Default workflow#
- Run
make monitorand leave the PDF open in Skim (you can let the agent handle it) - Edit in Neovim or agent changes the file; the PDF rebuilds on save and externally edited files refresh.
- After changing analysis code or inputs, inspect
snakemake -n, then runsnakemake --cores all. - Share the source, workflow, environment files, and any built artifacts required by collaborators who cannot run the pipeline. Use a pull request for asynchronous work; commit directly during a coordinated synchronous editing session, with
monex.pydrivinggit-autosyncso every save is pushed.