
Cmux Config
- 349 installs
- 23 repo stars
- Updated July 15, 2026
- manaflow-ai/cmux-skills
Helps with ai & agent building tasks.
About
cmux-config is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- cmux-config
- AI & Agent Building
- AI-coding skill
Cmux Config by the numbers
- 349 all-time installs (skills.sh)
- +54 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #2,104 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/manaflow-ai/cmux-skills --skill cmux-configAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 349 |
|---|---|
| repo stars | ★ 23 |
| Last updated | July 15, 2026 |
| Repository | manaflow-ai/cmux-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
cmux-config
Single entry point for editing a user's cmux configuration in ~/.config/cmux/cmux.json (JSONC). The app watches the file; saving applies changes immediately, no restart. Legacy ~/.config/cmux/settings.json is read only as a fallback for keys absent from cmux.json.
Three areas, each with a reference:
- Settings — typed preferences under
app,terminal,notifications,
sidebar, sidebarAppearance, workspaceColors, automation, browser, shortcuts. See the helper below and references/all-keys.md / references/shortcut-actions.md.
- Customization — structural config:
actions,ui.surfaceTabBar.buttons,
ui.newWorkspace (plus-button click + context menu), commands, rightSidebar, vault. See references/customize.md.
- Workspace groups — collapsible anchor-owned sidebar sections via the `cmux
workspace group CLI / socket API and workspaceGroups` config. See references/groups.md.
Helper script
Use the bundled helper for every settings/customize read and write. It strips JSONC comments, writes atomically, and validates keys against the schema. From the installed skill directory:
./scripts/cmux-settings <subcommand>For brevity below, assume it is on $PATH as cmux-settings (e.g. export PATH="$HOME/.claude/skills/cmux-config/scripts:$PATH").
| Command | What it does |
|---|---|
cmux-settings path | Print the config path. |
cmux-settings dump [--no-comments] | Print the raw file (or parsed JSON). |
cmux-settings get <a.b.c> | Print value at dotted JSON path. |
cmux-settings set <a.b.c> <value> | Set value (<value> parsed as JSON; bare words stored as strings). |
cmux-settings unset <a.b.c> | Delete key, reverting to the in-app default. |
cmux-settings list-supported | List every settings JSON path the app recognizes. |
cmux-settings validate | Parse the file and flag unknown settings keys. |
cmux-settings open | Open cmux.json in $EDITOR / VS Code / Cursor / TextEdit. |
--file <path> overrides the target file (use for the legacy settings.json).
Schema: https://raw.githubusercontent.com/manaflow-ai/cmux/main/web/data/cmux.schema.json.
Workflow
1. If the user named a setting in plain English, look it up first: cmux-settings list-supported | rg -i '<keywords>'. 2. Make the smallest edit (settings/customize via the helper; groups via the cmux workspace group CLI in references/groups.md). 3. Verify by surface:
- Typed settings: read back (
cmux-settings get <path>) andcmux-settings validate. - Customization (
actions,ui.*,commands,vault,rightSidebar): read back the
exact key (cmux-settings get <path>) and confirm the file still parses (cmux-settings dump --no-comments). Do NOT rely on validate here: it skips structural sections, so a typo like ui.surfaceTabbar.buttons passes validation yet cmux ignores it. Check the key against references/customize.md.
- Groups:
cmux workspace group list(the settings validator does not recognize
workspaceGroups). 4. Tell the user it auto-reloaded on save. No app restart. Revert with cmux-settings unset <key>.
Rules
- Only edit
cmux.json. Never editsettings.jsonunless asked; it is legacy. - Never tell the user to restart cmux to apply a change; the watcher reloads on save.
- Do not blindly overwrite top-level structural sections (
actions,ui,
commands, vault, rightSidebar); they hold hand-tuned non-settings config.
- Color values are
#RRGGBB; opacities are0..1. - Shortcut action ids must match the schema enum; look them up in
references/shortcut-actions.md before binding.
- Do not run a tagged app reload for config-only or skill-only changes.
- For workspace groups prefer CLI/socket operations over editing session JSON, and
preserve anchor semantics (see references/groups.md).
All settings keys
Auto-generated from repo/web/data/cmux.schema.json. For the rendered docs, see https://cmux.dev/docs/configuration.
app
General app preferences from Settings > App.
| Key | Type | Default | Description |
|---|---|---|---|
app.language | "system" or "en" or "ar" or "bs" or "zh-Hans" or "zh-Hant" or "da" or "de" or "es" or "fr" or "it" or "ja" or "ko" or "nb" or "pl" or "pt-BR" or "ru" or "th" or "tr" | "system" | Preferred app language. |
app.appearance | "system" or "light" or "dark" | "system" | App appearance mode. |
app.appIcon | "automatic" or "light" or "dark" | "automatic" | Dock and app switcher icon style. |
app.menuBarOnly | boolean | false | Hide the Dock icon and app switcher entry while keeping cmux available from the menu bar. |
app.newWorkspacePlacement | "top" or "afterCurrent" or "end" | "afterCurrent" | Where new workspaces are inserted in the sidebar. |
app.workspaceInheritWorkingDirectory | boolean | true | When true, new workspaces inherit the current workspace working directory. When false, new workspaces leave the working directory unset so Ghostty's working-directory setting can provide the default. |
app.minimalMode | boolean | false | Hide the workspace title bar and move controls into the sidebar. |
app.keepWorkspaceOpenWhenClosingLastSurface | boolean | false | When true, closing the last surface keeps the workspace open. |
app.focusPaneOnFirstClick | boolean | true | When cmux is inactive, the first click can activate and focus the clicked pane. |
app.preferredEditor | string | "" | Custom editor command used when Cmd-click file previews are disabled or a file is unsupported. Leave empty to use the default. |
app.openSupportedFilesInCmux | boolean | true | When enabled, Cmd-clicking readable local files opens supported previews in cmux, including text, code, PDFs, images, audio, video, and Quick Look files. Preview headers include an Open With menu based on the user's default and compatible macOS apps for that file. |
app.openMarkdownInCmuxViewer | boolean | false | When enabled together with openSupportedFilesInCmux, Cmd-clicking .md/.markdown/.mkd/.mdx files opens the rendered cmux markdown viewer panel (with live reload) instead of the generic file preview. |
app.reorderOnNotification | boolean | true | Move workspaces with new notifications toward the top. |
app.iMessageMode | boolean | false | Move a workspace to the top and show the submitted message when sending an agent prompt. |
app.sendAnonymousTelemetry | boolean | true | Allow anonymous telemetry. |
app.warnBeforeQuit | boolean | true | Show a confirmation before quitting cmux. |
app.warnBeforeClosingTab | boolean | true | Show a confirmation before closing a tab. |
app.renameSelectsExistingName | boolean | true | Select the current name when opening rename flows. |
app.commandPaletteSearchesAllSurfaces | boolean | false | Search every surface in the command palette switcher instead of only the active workspace. |
terminal
Terminal presentation settings from Settings > Terminal.
| Key | Type | Default | Description |
|---|---|---|---|
terminal.showScrollBar | boolean | true | Show the right-edge terminal scroll bar when scrollback is available. cmux automatically suppresses it for alternate-screen style TUI surfaces. |
terminal.autoResumeAgentSessions | boolean | true | Automatically run agent resume commands for restored terminal sessions when cmux reopens after quit. Set false to restore panes while keeping Claude Code, Codex, OpenCode, and other saved agent sessions idle until you resume them manually. |
notifications
Notification behavior from Settings > Notifications.
| Key | Type | Default | Description |
|---|---|---|---|
notifications.dockBadge | boolean | true | Show the unread count in the Dock tile. |
notifications.showInMenuBar | boolean | true | Show the menu bar extra. |
notifications.unreadPaneRing | boolean | true | Highlight panes with unread notifications. |
notifications.paneFlash | boolean | true | Flash the focused pane when requested. |
notifications.sound | "default" or "Basso" or "Blow" or "Bottle" or "Frog" or "Funk" or "Glass" or "Hero" or "Morse" or "Ping" or "Pop" or "Purr" or "Sosumi" or "Submarine" or "Tink" or "custom_file" or "none" | "default" | Notification sound preset. |
notifications.customSoundFilePath | string | "" | Local path to the custom notification sound file. |
notifications.command | string | "" | Optional shell command to run alongside notification delivery. |
notifications.hooksMode | "append" or "replace" | "append" | Controls whether project-local notification hooks append to inherited hooks or replace them. |
notifications.hooks | array<object> | [] | Composable shell hooks that receive notification policy JSON on stdin and return updated policy JSON on stdout. |
sidebar
Sidebar content and metadata visibility from Settings > Sidebar.
| Key | Type | Default | Description |
|---|---|---|---|
sidebar.hideAllDetails | boolean | false | Hide all per-workspace detail rows. |
sidebar.showWorkspaceDescription | boolean | true | Show custom workspace descriptions in the sidebar. |
sidebar.branchLayout | "vertical" or "inline" | "vertical" | Show git branch details stacked vertically or inline. |
sidebar.showNotificationMessage | boolean | true | Show the latest notification text in the sidebar. |
sidebar.showBranchDirectory | boolean | true | Show the workspace working directory. |
sidebar.showPullRequests | boolean | true | Show pull request metadata in the sidebar. |
sidebar.makePullRequestsClickable | boolean | true | Allow sidebar pull request metadata to open links when clicked. |
sidebar.openPullRequestLinksInCmuxBrowser | boolean | true | Open sidebar pull request links in the embedded cmux browser. |
sidebar.openPortLinksInCmuxBrowser | boolean | true | Open sidebar port links in the embedded cmux browser. |
sidebar.showSSH | boolean | true | Show SSH connection details. |
sidebar.showPorts | boolean | true | Show listening ports. |
sidebar.showLog | boolean | true | Show recent log snippets. |
sidebar.showProgress | boolean | true | Show progress indicators. |
sidebar.showCustomMetadata | boolean | true | Show custom metadata pills. |
workspaceColors
Workspace tab and badge colors from Settings > Workspace Colors.
| Key | Type | Default | Description |
|---|---|---|---|
workspaceColors.indicatorStyle | "leftRail" or "solidFill" or "rail" or "border" or "wash" or "lift" or "typography" or "washRail" or "blueWashColorRail" | "leftRail" | Active workspace indicator style. Legacy aliases are accepted and normalized. |
workspaceColors.selectionColor | colorHexOrNull | null | Override the selected workspace background color. |
workspaceColors.notificationBadgeColor | colorHexOrNull | null | Override the unread notification badge color. |
workspaceColors.colors | object | {"Red": "#C0392B", "Crimson": "#922B21", "Orange": "#A04000", "Amber": "#7D6608", "Olive": "#4A5C18", "Green": "#196F3D", "Teal": "#006B6B", "Aqua": "#0E6B8C", "Blue": "#1565C0", "Navy": "#1A5276", "Indigo": "#283593", "Purple": "#6A1B9A", "Magenta": "#AD1457", "Rose": "#880E4F", "Brown": "#7B3F00", "Charcoal": "#3E4B5E"} | Full named workspace color palette. Include built-in entries you want to keep, remove keys to remove colors, and add more named entries to extend the picker. |
workspaceColors.paletteOverrides | object | {} | Legacy workspace color overrides for built-in palette names. Prefer workspaceColors.colors for new configs. |
workspaceColors.customColors | array<colorHex> | [] | Legacy list of custom workspace colors. Prefer workspaceColors.colors for new configs. |
sidebarAppearance
Sidebar tint settings from Settings > Sidebar Appearance.
| Key | Type | Default | Description |
|---|---|---|---|
sidebarAppearance.matchTerminalBackground | boolean | false | Use the terminal background instead of the sidebar tint. |
sidebarAppearance.tintColor | colorHex | "#000000" | Base sidebar tint color used when light/dark overrides are not set. |
sidebarAppearance.lightModeTintColor | colorHexOrNull | null | Sidebar tint override for light appearance. |
sidebarAppearance.darkModeTintColor | colorHexOrNull | null | Sidebar tint override for dark appearance. |
sidebarAppearance.tintOpacity | number | 0.03 | Sidebar tint opacity from 0 to 1. Note: this only controls the sidebar tint, not terminal/window transparency. For terminal background transparency or blur, set background-opacity and background-blur in ~/.config/ghostty/config and run cmux reload-config. |
automation
Socket control and automation settings from Settings > Automation.
| Key | Type | Default | Description |
|---|---|---|---|
automation.socketControlMode | "off" or "cmuxOnly" or "automation" or "password" or "allowAll" or "openAccess" or "fullOpenAccess" or "notifications" or "full" | "cmuxOnly" | Socket control mode. Legacy aliases are accepted and normalized. |
automation.socketPassword | string or null | "" | Password for password-mode socket access. Use null or an empty string to clear it. |
automation.claudeCodeIntegration | boolean | true | Enable cmux integration hooks for Claude Code. |
automation.claudeBinaryPath | string | "" | Custom path to the claude binary. |
automation.cursorIntegration | boolean | true | Enable cmux integration hooks for Cursor. |
automation.geminiIntegration | boolean | true | Enable cmux integration hooks for Gemini. |
automation.portBase | integer | 9100 | Starting value for workspace CMUX_PORT assignments. |
automation.portRange | integer | 10 | Number of ports reserved per workspace. |
browser
Embedded browser settings from Settings > Browser.
| Key | Type | Default | Description |
|---|---|---|---|
browser.defaultSearchEngine | "google" or "duckduckgo" or "bing" or "kagi" or "startpage" | "google" | Default search engine for non-URL queries. |
browser.showSearchSuggestions | boolean | true | Show omnibar search suggestions. |
browser.theme | "system" or "light" or "dark" | "system" | Embedded browser theme. |
browser.openTerminalLinksInCmuxBrowser | boolean | true | Open clicked terminal links in the embedded browser. |
browser.interceptTerminalOpenCommandInCmuxBrowser | boolean | true | Intercept terminal open http(s) commands and route them through the embedded browser. |
browser.hostsToOpenInEmbeddedBrowser | array<string> | [] | Allowlist of hosts that should stay inside the embedded browser. |
browser.urlsToAlwaysOpenExternally | array<string> | [] | Rules that always open matching URLs in the system browser. |
browser.insecureHttpHostsAllowedInEmbeddedBrowser | array<string> | ["localhost", "*.localhost", "127.0.0.1", "::1", "0.0.0.0", "*.localtest.me"] | HTTP hosts allowed in the embedded browser without a warning prompt. |
browser.showImportHintOnBlankTabs | boolean | true | Show the browser import hint on blank tabs. |
browser.reactGrabVersion | string | "0.1.29" | Pinned react-grab version for the browser toolbar helper. |
shortcuts
Keyboard shortcut settings from Settings > Keyboard Shortcuts.
| Key | Type | Default | Description |
|---|---|---|---|
shortcuts.bindings | object | {} | Shortcut overrides keyed by cmux action id. Use a string for a single shortcut, an array for a chord, null, an empty string, none, clear, unbound, or disabled to unbind. |
cmux Customization (cmux-config reference)
Personal cmux configuration changes stored outside the app source tree, especially ~/.config/cmux/cmux.json and project-local .cmux/cmux.json files. Covers action registry entries, surface tab bar buttons, plus-button click and right-click menu behavior, custom workspace commands, right sidebar wiring, and related config cleanup.
Prerequisites
- Use this skill's
cmux-settingshelper script (scripts/cmux-settings) for global~/.config/cmux/cmux.jsonreads and writes. - For standard typed settings under
app,terminal,notifications,sidebar,sidebarAppearance,workspaceColors,automation,browser, orshortcuts, see the Settings section ofcmux-configSKILL.md andreferences/all-keys.md.
Reference
Global config:
cmux-settings path
cmux-settings dump --no-commentsCommon structural sections in cmux.json:
actions: action registry used by custom UI and commands.ui.surfaceTabBar.buttons: buttons shown in the surface tab bar.ui.newWorkspace.action: action triggered by a normal click on the plus button.ui.newWorkspace.contextMenu: menu items shown when right-clicking the plus button.commands: named workspace commands and layouts.rightSidebar: custom right sidebar wiring.vault: custom Vault agent registrations.
ui.surfaceTabBar.buttons can contain built-in action ids as strings or object references:
[
"cmux.newTerminal",
"cmux.newBrowser",
"cmux.splitRight",
"cmux.splitDown",
{ "action": "custom-action-id" }
]Use ui.newWorkspace.contextMenu for plus-button right-click menus. ui.newWorkspace.rightClick is accepted by cmux as a legacy alias, but new edits should use contextMenu.
Workflow
Step 1: Inspect current config
Read the existing config before changing anything.
cmux-settings dump --no-commentsIf the user names a visible UI surface, search for the related ids.
rg -n "surfaceTabBar|newWorkspace|rightSidebar|<action-id>|<label>" ~/.config/cmux/cmux.jsonStep 2: Make the smallest config edit
Use the helper for global config edits. It parses JSONC and writes atomically.
Remove custom agent launch buttons from the surface tab bar while keeping built-in surface controls:
cmux-settings set ui.surfaceTabBar.buttons \
'["cmux.newTerminal","cmux.newBrowser","cmux.splitRight","cmux.splitDown"]'Remove one named action from a tab bar without deleting the action registry entry:
cmux-settings get ui.surfaceTabBar.buttons
# Edit the array to remove only { "action": "<action-id>" }, then set it back.Delete an action entirely only when the user asks for the command itself to go away from every surface.
cmux-settings unset actions.<action-id>Customize the plus button and its right-click menu:
cmux-settings set ui.newWorkspace.action '"workspace-new-cmux-worktree"'
cmux-settings set ui.newWorkspace.contextMenu \
'[{"action":"workspace-new-cmux-worktree","title":"New cmux Worktree"},{"action":"workspace-terminal-browser","title":"Terminal + Browser"},{"type":"separator"},{"action":"cmux.newTerminal","title":"New Terminal"},{"action":"cmux.newBrowser","title":"New Browser"}]'Step 3: Verify
Always read back the changed path.
cmux-settings get ui.surfaceTabBar.buttons
cmux-settings get ui.newWorkspace.contextMenuValidate parseability. The strict supported-key validator may flag valid structural or newer app keys when the checked-out source list is older than the running app. If that happens, do not remove unrelated keys. Confirm the file parses and report the unrelated validator warning.
cmux-settings dump --no-comments >/tmp/cmux-config.jsonStep 4: Report apply behavior
cmux watches ~/.config/cmux/cmux.json, so config edits apply after save without a rebuild. Do not run a tagged app reload for pure config or skill changes.
Rules
- Preserve unrelated custom actions, commands, menus, Vault entries, and right sidebar config.
- Do not delete action definitions just because their buttons were removed from the tab bar.
- Do not use
defaults writefor persistent product config. - Do not edit
repo/for user config changes. - Do not run app reloads for config-only or skill-only changes.
- Keep responses short and include the exact path or key changed.
cmux workspace groups (cmux-config reference)
Workspace groups are collapsible named sections in the cmux sidebar. Each group is owned by an anchor workspace. The group header is the anchor's sidebar representation, so there is no separate row for the anchor. Closing the anchor dissolves the group while preserving the other members as ungrouped workspaces.
CLI
Prefer the canonical noun form:
cmux workspace group list [--json]
cmux workspace group create --name <name> [--cwd <path>] [--from <id>,<id>]
cmux workspace group ungroup <group-id>
cmux workspace group delete <group-id>
cmux workspace group rename <group-id> --name <new>
cmux workspace group collapse <group-id>
cmux workspace group expand <group-id>
cmux workspace group pin <group-id>
cmux workspace group unpin <group-id>
cmux workspace group add --group <group-id> --workspace <workspace-id>
cmux workspace group remove --workspace <workspace-id>
cmux workspace group set-anchor --group <group-id> --workspace <workspace-id>
cmux workspace group new-workspace <group-id> [--placement afterCurrent|top|end]
cmux workspace group set-color <group-id> [--hex #RRGGBB]
cmux workspace group set-icon <group-id> [--symbol <sf-symbol>]
cmux workspace group move <group-id> --to-index <n> | --before <group-id> | --after <group-id>
cmux workspace group focus <group-id>cmux workspace-group ... is a compatibility alias for the same operations. <group-id> accepts UUIDs or refs such as workspace_group:1. All commands honor --json.
create inserts a fresh anchor workspace. It does not promote an existing workspace into the anchor. --from lists workspaces that become children under the new anchor. When --from is omitted, cmux uses the active sidebar selection or caller workspace when available.
ungroup dissolves a group while keeping every member workspace alive, including the anchor, which becomes a regular ungrouped workspace. delete is destructive and closes every workspace in the group.
new-workspace runs the group's new-workspace behavior: a new workspace is created at the anchor cwd and joined to the group. Placement can be passed explicitly, resolved from per-cwd config, or resolved from the global group default.
Socket methods
The CLI is a thin wrapper over the v2 JSON socket API. Anything callable via CLI is also callable directly:
workspace.group.list
workspace.group.create { name, cwd?, child_workspace_ids? }
workspace.group.ungroup { group_id }
workspace.group.delete { group_id }
workspace.group.rename { group_id, name }
workspace.group.collapse { group_id }
workspace.group.expand { group_id }
workspace.group.pin { group_id }
workspace.group.unpin { group_id }
workspace.group.add { group_id, workspace_id }
workspace.group.remove { workspace_id }
workspace.group.set_anchor { group_id, workspace_id }
workspace.group.new_workspace { group_id, placement? }
workspace.group.set_color { group_id, hex? }
workspace.group.set_icon { group_id, symbol? }
workspace.group.move { group_id, to_index? | before_group_id? | after_group_id? }
workspace.group.focus { group_id }All accept the standard window_id / window_ref parameter to target a non-focused window.
Configuration (~/.config/cmux/cmux.json)
Per-group settings are keyed by the anchor cwd. Keys with * or ? are fnmatch globs with ~ expansion. Other keys are path prefixes. Longest match wins.
{
"workspaceGroups": {
"newWorkspacePlacement": "afterCurrent",
"byCwd": {
"/abs/path/to/repo": {
"color": "#7A4FD8",
"icon": "ladybug.fill",
"newWorkspacePlacement": "top",
"contextMenu": [
{ "action": "newWorktreeAction", "title": "New Worktree" },
{ "action": "newWorkspace" }
]
},
"~/projects/*": {
"icon": "leaf.fill",
"newWorkspacePlacement": "end"
}
}
}
}workspaceGroups.newWorkspacePlacement and workspaceGroups.byCwd[...].newWorkspacePlacement accept:
afterCurrent: after the active in-group workspace. It falls back to top when there is no active member reference.top: second slot, right after the anchor.end: after the last member.
Resolution order for group new-workspace placement:
1. Explicit --placement afterCurrent|top|end on the CLI, or "placement" in workspace.group.new_workspace. 2. Per-cwd workspaceGroups.byCwd[...].newWorkspacePlacement. 3. Global workspaceGroups.newWorkspacePlacement. 4. Built-in default afterCurrent.
Cmd-N inside a group uses the active group workspace as the placement reference. The group header + button and CLI path use the anchor as the reference, so afterCurrent behaves like top there.
Group context-menu items use the same schema as ui.newWorkspace.contextMenu; actions must be defined in the global actions block or be built-in actions such as newWorkspace.
Keyboard
Default shortcut: Cmd-Shift-G groups the sidebar-selected workspaces. It falls back to the focused workspace when no sidebar selection exists. Rebind it in Settings > Keyboard.
Anchor semantics
- Anchor identity lives on the group (
anchorWorkspaceId), not as a flag on the workspace. - The anchor renders as the group header, with no separate sidebar row.
- Closing the anchor dissolves the group after a confirm dialog.
set-anchorreassigns which member is the anchor; the previously-anchor workspace becomes a regular member with its own sidebar row.
Persistence
Groups and membership round-trip through ~/Library/Application Support/cmux/session-<bundle-id>.json via the existing SessionPersistenceStore. Atomic rename-into-place writes; no WAL.
Rules
- Prefer CLI or socket operations over editing session JSON.
- Do not write unsupported future config keys. Check
web/data/cmux.schema.jsonin cmux when unsure. - Preserve anchor semantics when scripting: moving a group means moving the anchor section, not only a child workspace row.
- Use
ungroupwhen workspaces should survive; usedeleteonly when every workspace in the group should close.
Keyboard shortcut action ids
Auto-generated from repo/web/data/cmux.schema.json (shortcuts.bindings.propertyNames.enum). Regenerate by re-running the snippet inline in SKILL.md.
Values for shortcuts.bindings.<action>:
- A string like
"cmd+b"for a single shortcut. - A two-element array like
["ctrl+b","c"]for a chord. nullor an empty string ("","none","clear","unbound","disabled") to unbind.
App
shortcuts.bindings.openSettingsshortcuts.bindings.reloadConfigurationshortcuts.bindings.showHideAllWindowsshortcuts.bindings.globalSearchshortcuts.bindings.newWindowshortcuts.bindings.closeWindowshortcuts.bindings.toggleFullScreenshortcuts.bindings.quitshortcuts.bindings.openFoldershortcuts.bindings.sendFeedback
Tabs
shortcuts.bindings.newTabshortcuts.bindings.reopenPreviousSessionshortcuts.bindings.renameTabshortcuts.bindings.closeTabshortcuts.bindings.closeOtherTabsInPane
Workspace
shortcuts.bindings.goToWorkspaceshortcuts.bindings.selectWorkspaceByNumbershortcuts.bindings.renameWorkspaceshortcuts.bindings.editWorkspaceDescriptionshortcuts.bindings.closeWorkspace
Panes and surfaces
shortcuts.bindings.nextSurfaceshortcuts.bindings.prevSurfaceshortcuts.bindings.selectSurfaceByNumbershortcuts.bindings.newSurfaceshortcuts.bindings.toggleTerminalCopyModeshortcuts.bindings.focusLeftshortcuts.bindings.focusRightshortcuts.bindings.focusUpshortcuts.bindings.focusDownshortcuts.bindings.splitRightshortcuts.bindings.splitDownshortcuts.bindings.toggleSplitZoomshortcuts.bindings.equalizeSplits
Command palette
shortcuts.bindings.commandPaletteshortcuts.bindings.commandPaletteNextshortcuts.bindings.commandPalettePrevious
Notifications
shortcuts.bindings.showNotificationsshortcuts.bindings.jumpToUnreadshortcuts.bindings.toggleUnreadshortcuts.bindings.markOldestUnreadAndJumpNextshortcuts.bindings.triggerFlash
Right sidebar
shortcuts.bindings.toggleSidebarshortcuts.bindings.focusRightSidebarshortcuts.bindings.switchRightSidebarToFilesshortcuts.bindings.switchRightSidebarToFindshortcuts.bindings.switchRightSidebarToSessionsshortcuts.bindings.switchRightSidebarToFeedshortcuts.bindings.switchRightSidebarToDockshortcuts.bindings.nextSidebarTabshortcuts.bindings.prevSidebarTab
Browser
shortcuts.bindings.reopenClosedBrowserPanelshortcuts.bindings.splitBrowserRightshortcuts.bindings.splitBrowserDownshortcuts.bindings.openBrowsershortcuts.bindings.focusBrowserAddressBarshortcuts.bindings.browserBackshortcuts.bindings.browserForwardshortcuts.bindings.browserReloadshortcuts.bindings.browserZoomInshortcuts.bindings.browserZoomOutshortcuts.bindings.browserZoomResetshortcuts.bindings.toggleBrowserDeveloperToolsshortcuts.bindings.showBrowserJavaScriptConsole
Find
shortcuts.bindings.findshortcuts.bindings.findInDirectoryshortcuts.bindings.findNextshortcuts.bindings.findPreviousshortcuts.bindings.hideFindshortcuts.bindings.useSelectionForFind
Files and React Grab
shortcuts.bindings.toggleFileExplorershortcuts.bindings.saveFilePreviewshortcuts.bindings.toggleReactGrab
#!/usr/bin/env python3
"""Inspect and edit ~/.config/cmux/cmux.json.
cmux watches the file and auto-reloads on save, so writes take effect
immediately. The file is JSONC (JSON with // and /* */ comments); this
script preserves comment-free formatting on write (2-space indent, trailing
newline) and never touches comments inside untouched sections by writing
only when the parsed value actually changes.
Usage:
cmux-settings path print the config path
cmux-settings dump [--no-comments] print current settings (raw or stripped)
cmux-settings get <dotted.path> print value at path (JSON)
cmux-settings set <dotted.path> <v> set value (v parsed as JSON, falls back to string)
cmux-settings unset <dotted.path> remove value at path
cmux-settings list-supported print every settings path the schema recognizes
cmux-settings validate check JSON parses and all set keys are recognized
cmux-settings open open the file in $EDITOR (or VS Code, then TextEdit)
Examples:
cmux-settings set app.appearance dark
cmux-settings set notifications.dockBadge false
cmux-settings set shortcuts.bindings.toggleSidebar '"cmd+b"'
cmux-settings set shortcuts.bindings.newTab '["ctrl+b","c"]'
cmux-settings unset app.appearance
"""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any
DEFAULT_PATH = Path.home() / ".config" / "cmux" / "cmux.json"
LEGACY_PATH = Path.home() / ".config" / "cmux" / "settings.json"
SCHEMA_URL = (
"https://raw.githubusercontent.com/manaflow-ai/cmux/main/web/data/cmux.schema.json"
)
def strip_jsonc(text: str) -> str:
"""Remove // line comments and /* block comments outside strings."""
out: list[str] = []
i, n = 0, len(text)
in_string = False
string_quote = ""
while i < n:
ch = text[i]
if in_string:
out.append(ch)
if ch == "\\" and i + 1 < n:
out.append(text[i + 1])
i += 2
continue
if ch == string_quote:
in_string = False
i += 1
continue
if ch in ('"', "'"):
in_string = True
string_quote = ch
out.append(ch)
i += 1
continue
if ch == "/" and i + 1 < n and text[i + 1] == "/":
j = text.find("\n", i + 2)
i = n if j == -1 else j
continue
if ch == "/" and i + 1 < n and text[i + 1] == "*":
j = text.find("*/", i + 2)
i = n if j == -1 else j + 2
continue
out.append(ch)
i += 1
# Drop trailing commas before ] or } (also legal in JSONC).
return re.sub(r",(\s*[}\]])", r"\1", "".join(out))
def load_settings(path: Path) -> dict[str, Any]:
if not path.exists():
return {"$schema": SCHEMA_URL, "schemaVersion": 1}
raw = path.read_text()
try:
return json.loads(strip_jsonc(raw))
except json.JSONDecodeError as e:
raise SystemExit(f"error: {path} is not valid JSONC: {e}")
def atomic_write(path: Path, data: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
encoded = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", dir=path.parent, delete=False, suffix=".tmp"
) as tmp:
tmp.write(encoded)
tmp_path = Path(tmp.name)
os.replace(tmp_path, path)
def split_path(dotted: str) -> list[str]:
if not dotted:
raise SystemExit("error: empty key path")
return dotted.split(".")
def get_at(data: Any, parts: list[str]) -> Any:
cur = data
for p in parts:
if not isinstance(cur, dict) or p not in cur:
raise SystemExit(f"error: path not present: {'.'.join(parts)}")
cur = cur[p]
return cur
def set_at(data: dict[str, Any], parts: list[str], value: Any) -> bool:
cur = data
for p in parts[:-1]:
if p not in cur or not isinstance(cur[p], dict):
cur[p] = {}
cur = cur[p]
leaf = parts[-1]
changed = cur.get(leaf, "__cmux_missing__") != value
cur[leaf] = value
return changed
def unset_at(data: dict[str, Any], parts: list[str]) -> bool:
trail: list[tuple[dict[str, Any], str]] = []
cur: Any = data
for p in parts[:-1]:
if not isinstance(cur, dict) or p not in cur:
return False
trail.append((cur, p))
cur = cur[p]
if not isinstance(cur, dict) or parts[-1] not in cur:
return False
del cur[parts[-1]]
# Drop now-empty ancestor objects so the file stays tidy.
for parent, key in reversed(trail):
if isinstance(parent[key], dict) and not parent[key]:
del parent[key]
else:
break
return True
def parse_value(raw: str) -> Any:
"""Try JSON first; if it fails, fall back to a plain string."""
try:
return json.loads(raw)
except json.JSONDecodeError:
return raw
def flatten(prefix: str, value: Any) -> list[str]:
if isinstance(value, dict):
out: list[str] = []
for k, v in value.items():
child = f"{prefix}.{k}" if prefix else k
out.extend(flatten(child, v))
return out
return [prefix]
def supported_paths(workspace_root: Path | None) -> list[str]:
"""Read the canonical supported-paths list from the cmux source."""
if workspace_root is None:
return []
source = (
workspace_root
/ "repo"
/ "Sources"
/ "CmuxSettingsJSONPathSupport.swift"
)
if not source.exists():
return []
text = source.read_text()
return sorted(set(re.findall(r'"([a-zA-Z]+\.[a-zA-Z0-9_.]+)"', text)))
def find_workspace_root() -> Path | None:
here = Path(__file__).resolve()
for parent in here.parents:
if (parent / "repo" / "Sources" / "CmuxSettingsJSONPathSupport.swift").exists():
return parent
return None
def cmd_path(args: argparse.Namespace) -> int:
print(args.file)
return 0
def cmd_dump(args: argparse.Namespace) -> int:
path = Path(args.file)
if args.no_comments:
data = load_settings(path)
print(json.dumps(data, indent=2, ensure_ascii=False))
else:
if path.exists():
sys.stdout.write(path.read_text())
else:
print(f"# {path} does not exist", file=sys.stderr)
return 1
return 0
def cmd_get(args: argparse.Namespace) -> int:
data = load_settings(Path(args.file))
value = get_at(data, split_path(args.key))
print(json.dumps(value, indent=2, ensure_ascii=False))
return 0
def cmd_set(args: argparse.Namespace) -> int:
path = Path(args.file)
data = load_settings(path)
parts = split_path(args.key)
value = parse_value(args.value)
changed = set_at(data, parts, value)
if not changed:
print(f"unchanged: {args.key} = {json.dumps(value)}")
return 0
atomic_write(path, data)
print(f"set: {args.key} = {json.dumps(value)}")
return 0
def cmd_unset(args: argparse.Namespace) -> int:
path = Path(args.file)
data = load_settings(path)
if not unset_at(data, split_path(args.key)):
print(f"unchanged: {args.key} (not present)")
return 0
atomic_write(path, data)
print(f"unset: {args.key}")
return 0
def cmd_list_supported(args: argparse.Namespace) -> int:
paths = supported_paths(find_workspace_root())
if not paths:
print(
"error: could not locate repo/Sources/CmuxSettingsJSONPathSupport.swift",
file=sys.stderr,
)
return 1
for p in paths:
print(p)
return 0
def cmd_validate(args: argparse.Namespace) -> int:
path = Path(args.file)
data = load_settings(path)
supported = set(supported_paths(find_workspace_root()))
if not supported:
print(
"warn: could not load schema path list; only checked JSON parses",
file=sys.stderr,
)
print(f"ok: {path} parses")
return 0
unknown: list[str] = []
structural = {
"$schema",
"schemaVersion",
"actions",
"ui",
"commands",
"vault",
"newWorkspaceCommand",
"surfaceTabBarButtons",
"rightSidebar",
}
for top, value in data.items():
if top in structural:
continue
if not isinstance(value, dict):
unknown.append(top)
continue
for full in flatten(top, value):
if full in supported:
continue
if any(full.startswith(s + ".") for s in supported):
continue
unknown.append(full)
if unknown:
print("unknown settings keys:")
for u in unknown:
print(f" {u}")
return 1
print(f"ok: {path} parses and all settings keys are recognized")
return 0
def cmd_open(args: argparse.Namespace) -> int:
path = Path(args.file)
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
atomic_write(path, {"$schema": SCHEMA_URL, "schemaVersion": 1})
editor = os.environ.get("EDITOR") or os.environ.get("VISUAL")
if editor:
return subprocess.call([*editor.split(), str(path)])
for app in (["code", "--wait"], ["cursor", "--wait"], ["open", "-e"]):
if shutil.which(app[0]):
return subprocess.call([*app, str(path)])
return subprocess.call(["open", str(path)])
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
"--file",
default=str(DEFAULT_PATH),
help=f"settings file path (default: {DEFAULT_PATH})",
)
sub = parser.add_subparsers(dest="cmd", required=True)
sub.add_parser("path").set_defaults(func=cmd_path)
p_dump = sub.add_parser("dump")
p_dump.add_argument("--no-comments", action="store_true")
p_dump.set_defaults(func=cmd_dump)
p_get = sub.add_parser("get")
p_get.add_argument("key")
p_get.set_defaults(func=cmd_get)
p_set = sub.add_parser("set")
p_set.add_argument("key")
p_set.add_argument("value")
p_set.set_defaults(func=cmd_set)
p_unset = sub.add_parser("unset")
p_unset.add_argument("key")
p_unset.set_defaults(func=cmd_unset)
sub.add_parser("list-supported").set_defaults(func=cmd_list_supported)
sub.add_parser("validate").set_defaults(func=cmd_validate)
sub.add_parser("open").set_defaults(func=cmd_open)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())