Configuration file
How config.json works — location, opt-in, live two-way sync, schema and error handling.
BetterCmdTab can keep every setting in a plain JSON file that syncs both ways with the settings window. Edit the file and the change applies live; change something in the app and the file is updated. It is designed to sit in a dotfiles repo.
Location
~/.config/bettercmdtab/config.json$XDG_CONFIG_HOME is honoured when it is set to an absolute path, in which case the
file is $XDG_CONFIG_HOME/bettercmdtab/config.json.
The path may be a symlink — writes resolve it first, so the link's target is replaced rather than the link itself.
Opting in
The file's existence is the switch. BetterCmdTab never creates it on its own.
- File absent — the whole feature is dormant. Nothing is written, nothing watched.
- File present — edits apply live, and settings changed in the app are written back.
- Delete the file — the sync stops.
To create it, use Settings → General → Backup → Configuration file → Create…, which writes your current settings out and starts syncing. Creating it by hand works too, as long as the directory already existed when the app launched:
mkdir -p ~/.config/bettercmdtab
echo '{}' > ~/.config/bettercmdtab/config.jsonIf neither ~/.config nor ~/.config/bettercmdtab existed at launch, there is
nothing to watch and a file created afterwards is not picked up until you relaunch
the app (or press Create…). This is deliberate — watching $HOME for .config
to appear would wake the app on every dotfile write.
Format
A flat JSON object of key: value pairs. Keys are the Switcher.* preference names
with the prefix stripped, so Switcher.layoutMode is written as layoutMode.
{
"$schema": "schema.json",
"layoutMode": "windowPreview",
"sortOrder": "mruWindows",
"spaceScope": "visibleSpaces",
"panelScalePercent": 110,
"stayOpenOnRelease": true,
"pinnedBundleIDs": ["com.apple.Safari", "com.figma.Desktop"]
}Fully-qualified keys ("Switcher.layoutMode") are also accepted on read, so an old
hand-written file keeps working. They are never written.
The format is deliberately unversioned. Unknown keys are ignored and absent keys keep their current value, so a file can be shared between app versions in both directions.
Editor support
Next to config.json, the app writes a schema.json and points at it from the
config's $schema key. It is a JSON Schema (draft 2020-12) generated by the running
build, so it can never lag behind the app: every key is described, every enum lists
its allowed values with the same labels the settings window uses, and every numeric
key carries its accepted range.
That gives you completion, hover documentation and inline validation in VS Code, JetBrains IDEs, Zed, and any editor with a JSON language server.
$schema is a pointer, not a setting — it is ignored on import and never stored.
How the sync behaves
File → app. Saves are coalesced for 200 ms and read off the main thread, then applied on the main thread. Editors that save atomically (write a temp file, rename over the target) are handled — the watcher re-attaches to the new file.
App → file. Changes made in the settings window are debounced for 500 ms and written atomically. A pending change is flushed synchronously at quit, so the last thing you toggled is never lost.
The file is normalised on every write
Any applied change rewrites the file as the full canonical snapshot — every setting that is currently stored, pretty-printed, keys sorted alphabetically. A hand-written partial file gets expanded the first time anything changes, and custom formatting is not preserved.
That is the cost of the two-way sync — the file and the settings window can never disagree. If you want a minimal file in version control, keep your own copy and let the synced one be generated. (JSON has no comments, so there is nothing else to lose.)
Why your file has fewer keys than the reference
The file contains the settings that have actually been written, not all 85 documented keys. A preference is stored the first time you change it (or when a migration writes it), so a fresh install produces a nearly empty file and it grows as you tune things.
This is not a problem: an absent key means "still at its default", which is exactly how import treats it. Add any documented key by hand and it applies immediately.
Partial edits and error handling
Import is always partial and never destructive:
- Keys you omit keep their current value. Writing
{"layoutMode": "list"}changes the layout and nothing else. - Keys we don't recognise are ignored, so a config from a newer version still applies.
- Invalid JSON is rejected wholesale. Your current settings are kept and the reason is logged; the same bad bytes are not re-parsed on every save, and fixing the file applies it.
- Values of the wrong shape — a JSON
null, say — are skipped key by key. That key keeps its value and the rest of the file still applies. - Out-of-range numbers are clamped to the accepted range shown in the reference, not rejected.
- Unrecognised enum values fall back to that key's default.
What is not in the file
Some state is deliberately excluded, and neither exported nor imported:
| Excluded | Why |
|---|---|
The ⌘Tab / ⌘` trigger chords | Stored by the shortcut recorder under its own keys, outside the Switcher.* namespace |
disabledSymbolicHotKeys | A record of which native hotkeys this Mac has disabled — importing another Mac's copy could leave the native ⌘Tab dead |
recentlyClosed | Session history, not configuration |
customCommitSoundFilename | The sound file lives in this Mac's Application Support directory |
accentChoice, customAccentHex | Retired in 26.7 — the switcher always follows the macOS accent colour |
Scoped shortcuts are a middle case: scopedShortcutList records each entry's id, scope
and name, but the chord itself is recorded outside this file. Syncing the file to a new
Mac carries the shortcuts over; you re-record their key combinations there.
Export and import instead
If you don't want a live file, Settings → General → Backup also has one-shot
Export settings and Import settings buttons. Export writes exactly the same
flat JSON shape, as a .json file you choose the location of. Import accepts that,
and also the legacy .cmdtab envelope older versions produced.