Built with Zig 0.17.0 for Apple Silicon Macs running macOS 15 or newer. Intel Macs, Linux and Windows are unsupported. The implementation uses the Zig standard library with no package dependencies; Git and the macOS download tools handle transport and installation.
Settings and plugin sync for JetBrains IDEs, across every IDE on a machine and across every machine you use.
jbsync stores only the settings you actually changed, merges them one setting
at a time so two IDEs editing the same file never collide, and owns the sync
repository itself so you never have to manage one.
$ jbsync sync
mac · git@github.com:you/jetbrains-settings.git on main
2 settings into IDEs · 1 setting into the store · 1 conflict
IntelliJIdea2026.2 (IntelliJ IDEA)
options/editor.xml
to IDE Editor/fontSize 13 -> 15
to store CodeInsightSettings/AUTO_POPUP false -> true
PyCharm2026.2 (PyCharm)
options/editor.xml
to IDE Editor/fontSize 13 -> 15
options/laf.xml
conflict LafManager/laf/themeId
this machine Islands Dark <- kept
other machine Light
CLion2026.2 (CLion)
no changes
1 conflict resolved in favour of this machine. Re-run with --prefer remote to flip the choice.
Committed: 3 file(s) at 8f21c0a4
| Getting started | Install, first machine, second machine, troubleshooting |
| Configuration | Every option, with complete annotated examples |
| How it works | What gets synced, and how merging works |
| Recipes | Short answers to common tasks |
Apple Silicon macOS:
curl -fsSL https://github.com/cdenihan/jbsync/releases/latest/download/install.sh | shThe installer accepts only Apple Silicon macOS, verifies the release SHA-256
file, and installs atomically. After that, jbsync update replaces the
same executable in place.
jbsync init # create ~/.jbsync and a local store
jbsync status # show what a sync would do, changing nothing
jbsync sync # reconcile every IDE, then publishThat is already useful with one machine: it keeps IntelliJ, PyCharm, CLion, WebStorm and RustRover in step with each other.
To share across machines, point it at an empty Git repository you own:
jbsync repo set git@github.com:you/jetbrains-settings.git
jbsync syncOn the next machine, install jbsync, run the same two commands, and it adopts
what is already there. You never clone, commit, pull or push — jbsync owns
that repository. It lives under ~/.jbsync/data and you can ignore it.
Finally, turn off JetBrains' own sync so the two do not overwrite each other:
jbsync disable-builtin-syncAny IDE that has run JetBrains' bundled Backup and Sync leaves a settingsSync/
directory, and that directory is the platform's own answer to what roams — it is
produced by the same RoamingType annotations the IDE uses internally.
jbsync pools those lists across every installed IDE and records the union in
the store, so a manifest learned once by any machine reaches all of them — even
one set up from scratch where Backup and Sync has never run. A curated built-in
list is unioned in as a floor. This matters: a plausible-looking options/*.xml rule
would sweep up other.xml, which is per-machine UI state, and the llm.*.xml
files, which are opaque JSON blobs that cannot be merged meaningfully. JetBrains
excludes both, so jbsync does too, without anyone maintaining a list.
Caches, credentials, telemetry, window geometry and per-host trust are excluded in every case. An IDE that has never been launched is skipped rather than harvested, so an installer's factory defaults never reach the store.
The IntelliJ platform already does most of this work: when a component's state
equals the state its default constructor produces, nothing is written to the XML
at all. A live options/*.xml is therefore close to a diff against defaults
before jbsync sees it.
What is left is residue of four kinds, and each is handled by a declarative rule rather than by code:
- components that serialize a whole map including untouched entries — tutorial
progress where every lesson is
NOT_PASSED, inlay-hint tables; - values the IDE set for itself rather than for you — registry keys carrying
source="SYSTEM"or"MANAGER", one-shot migration flags; - components that persist only a schema version and no settings;
- dialog state the platform files beside real settings — window geometry and
splitter positions in
project.default.xml.
Better still, jbsync learns the defaults. An IDE that has been installed but never opened contains nothing but its product's factory settings, so jbsync records them into the store before skipping it. After that, any setting still holding its shipped value is known not to be a choice, on every machine.
jbsync sync --verbose lists exactly what was dropped and why. To cover
something new, add an [[xml.omit]] block to sync.toml — no code changes.
Pruning decides what is shared, never what your IDE keeps. Registry keys and tutorial state stay in your IDE's own files untouched.
Every reconciliation is a three-way merge between the last state both sides agreed on, what this machine has now, and what the other side has now:
- both sides agree — nothing happens
- only the other side moved — take it, shown as
<incoming - only this side moved — keep it, shown as
>outgoing - both moved, differently — a conflict, resolved by policy and reported
The merge runs on individual settings, not on file text. Two IDEs changing
different settings in the same editor.xml is a non-event; Git's line merge
would call that a conflict and can produce XML that no longer parses.
Conflicts default to keeping the value on the machine you are sitting at.
--prefer remote flips that, and --prefer neither reports without changing
anything.
.vmoptions files are merged as a set of JVM flags: additions from both sides
are kept, and only two different values for the same flag conflict, so
-Xmx4g and -Xmx16g are caught while -Xmx4g and -XX:+UseZGC are not.
- Lossless storage. Settings are stored as canonical XML, not converted to another format. Canonicalization is verified against real XML fixtures: parsing, re-serializing and re-parsing must produce an identical document and an identical projection, and serialization must be idempotent.
- Backups. Every IDE file is copied to
~/.jbsync/backups/<timestamp>/before it is overwritten. - Surgical writes. Incoming settings are applied leaf by leaf to your IDE's
real file, so anything
jbsyncdoes not manage is left exactly as it was. - Atomic writes. Files are written to a temporary path and renamed, so an interrupted run cannot truncate a settings file.
- One run at a time. A sync takes an exclusive lock; a second run reports that one is already in progress rather than interleaving with it.
- Dry runs are honest.
--dry-runbuffers writes in memory and still runs the full reconciliation, so it reports what a real run would do — including changes that only appear once an earlier IDE has contributed.
jbsync records which third-party plugins you have in plugins.json, together
with the compatibility metadata from each descriptor, and other machines install
them from Marketplace through the IDE's own launcher.
Plugin directories are never copied. They contain compiled code and sometimes native libraries, so copying them between IDE versions and machines is unsound.
Compatibility is checked before anything is installed: build ranges, required
modules, and incompatible-with declarations. A Python-only plugin is not
offered to CLion. jbsync plugins shows the manifest and the verdict per IDE;
a sync installs what is missing, and --no-install-plugins reduces that to a
report.
To keep a plugin in one product, scope it with a single rule:
$ jbsync plugins only com.falsepattern.zigbrains --ide 'CLion*'Two files, deliberately separated by whether they should travel.
~/.jbsync/config.toml is local to this machine and never synced:
[repo]
backend = "git"
remote = "git@github.com:you/jetbrains-settings.git"
branch = "main"
# path = "/somewhere/else" # where the store's working copy lives
[jetbrains]
# root = "auto" # detected per OS; override if you must
# install_roots = ["/Applications"]
[machine]
# id = "work-laptop" # defaults to the hostnamesync.toml lives inside the store, so it reaches every machine:
[jetbrains]
ides = ["*20??.*"]
backups = true
explicit_include = ["*.vmoptions"]
exclude = ["options/databaseSettings.xml"]
[plugins]
enabled = true
# launchers = { IntelliJIdea = "idea" }
# Stop sharing one setting. It keeps whatever value it already has in each IDE.
[[xml.omit]]
file = "options/editor.xml"
component = "EditorSettings"
option = "SHOW_INTENTION_BULB"machines/<id>.toml inside the store holds overrides for a single machine.
Every key, with defaults and worked examples, is in the configuration reference.
| Command | Purpose |
|---|---|
jbsync init |
Create the local config and the store |
jbsync status |
Show what a sync would do, changing nothing |
jbsync sync |
Reconcile every IDE, then publish |
jbsync ides |
List the IDEs jbsync can see |
jbsync repo show|set|unset |
Inspect or change where the store lives |
jbsync plugins |
Show the plugin manifest and per-IDE verdicts |
jbsync plugins only <id> --ide <glob> |
Confine a plugin to matching IDEs |
jbsync plugins allow|deny <id> |
Force a verdict for a plugin |
jbsync disable-builtin-sync |
Turn off JetBrains' bundled Backup and Sync |
jbsync update |
Replace the running binary with the latest release |
jbsync completions <shell> |
Print a shell completion script |
Useful flags: --dry-run, --verbose, --prefer local|remote|neither,
--ide <selector> (repeatable), --collect-only, --no-install-plugins.
jbsync update prints a sentence; add --json for a machine-readable summary.
src/zig/xml.zig preserves XML order and projects settings into stable addresses.
merge.zig reconciles XML, text and JVM options. settings.zig applies roaming
policy and factory defaults; plugins.zig checks plugin compatibility.
engine.zig stages changes, checks conflicts and concurrent edits, creates
backups, and converges IDEs. git.zig transports the store through Git.
files.zig owns bounded reads and atomic writes; config.zig reads configuration.
mise install # Zig 0.17.0
mise run build # generic ARM, ReleaseSafe
mise run ci # full disposable validation
sh scripts/check.sh Debug
sh scripts/check.sh ReleaseFastPython 3 and Git are required for validation. See development and release validation for the frozen compatibility corpus, migration fixtures, read-only live XML checks, and release process.
MIT