
Cmux Settings
- 4.4k installs
- 25.6k repo stars
- Updated August 5, 2026
- manaflow-ai/cmux
cmux-settings is an agent skill that views and edits cmux preferences in ~/.config/cmux/cmux.json using schema-validated cmux-settings CLI subcommands.
About
cmux-settings is an agent skill for reading and writing cmux terminal app preferences stored in ~/.config/cmux/cmux.json as JSONC with immediate reload via a file watcher and no restart required. Legacy ~/.config/cmux/settings.json is a fallback only for keys absent from cmux.json. A bundled cmux-settings helper script strips JSONC comments, writes atomically, and validates keys against the published schema with subcommands for path, dump, get, set, unset, list-supported, validate, and open. Top-level settings sections include app, terminal, notifications, sidebar, sidebarAppearance, workspaceColors, automation, browser, and shortcuts, while non-settings sections such as actions, ui, commands, vault, and rightSidebar must not be blindly overwritten. Workflow steps look up plain-English setting names, set JSON-literal values, read back with get, and run validate after bulk edits. Quick reference documents appearance, sidebar tint, notification sounds, browser hosts, automation socket control modes, and shortcut bindings using schema enum action ids. Rules forbid editing settings.json unless asked, require #RRGGBB colors and 0..1 opacities, and map UI labels to dotted JSON paths bef.
- Edits ~/.config/cmux/cmux.json with atomic writes and schema validation via cmux-settings helper.
- File watcher applies changes immediately without telling users to restart cmux.
- Subcommands include get, set, unset, list-supported, validate, dump, and open in an editor.
- Covers appearance, sidebar, notifications, browser, automation, and shortcut binding keys.
- Warns against overwriting non-settings sections like actions, ui, commands, vault, and rightSidebar.
Cmux Settings by the numbers
- 4,363 all-time installs (skills.sh)
- +363 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #45 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
cmux-settings capabilities & compatibility
- Capabilities
- jsonc config read and write · schema key validation · shortcut binding management · settings lookup and translation
- Use cases
- devops
- Platforms
- macOS
npx skills add https://github.com/manaflow-ai/cmux --skill cmux-settingsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4.4k |
|---|---|
| repo stars | ★ 25.6k |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 5, 2026 |
| Repository | manaflow-ai/cmux ↗ |
How do I change cmux appearance, sidebar, notifications, browser, automation, or shortcut settings without breaking unrecognized keys?
View and edit cmux user settings in ~/.config/cmux/cmux.json with schema-validated get, set, and shortcut bindings.
Who is it for?
cmux users who want programmatic or agent-assisted changes to terminal app settings and keyboard shortcuts.
Skip if: Skip when you do not use cmux or need generic shell configuration outside the cmux.json schema.
When should I use this skill?
User asks to change cmux settings, rebind shortcuts, validate cmux.json, or edit cmux config paths.
What you get
Validated cmux.json updates that reload on save with correct dotted JSON paths and shortcut bindings.
- updated cmux.json
- validation result
Files
cmux-settings
cmux reads user settings from ~/.config/cmux/cmux.json (JSONC). The app installs a file watcher; saving the file applies changes immediately, no restart needed. Legacy ~/.config/cmux/settings.json is read only as a fallback for keys not present in cmux.json.
Schema: https://raw.githubusercontent.com/manaflow-ai/cmux/main/web/data/cmux.schema.json. The authoritative path list lives in Sources/CmuxSettingsJSONPathSupport.swift in the cmux checkout, and the installed skill includes a generated copy in references/all-keys.md. Top-level sections are app, terminal, notifications, sidebar, sidebarAppearance, workspaceColors, automation, browser, and shortcuts. Non-settings sections (actions, ui, commands, vault, rightSidebar) coexist in the same file.
Helper script
Use the bundled helper for every read/write. It strips JSONC comments, writes atomically, and validates keys against the schema.
# From a cmux checkout
skills/cmux-settings/scripts/cmux-settings <subcommand>
# From an installed Codex skill
~/.codex/skills/cmux-settings/scripts/cmux-settings <subcommand>For brevity in the rest of this doc, assume the script is on $PATH as cmux-settings. To make it so for a session from a checkout: export PATH="$PWD/skills/cmux-settings/scripts:$PATH".
Subcommands:
| Command | What it does |
|---|---|
cmux-settings path | Print the config path. |
cmux-settings dump | Print the raw file (preserves comments). |
cmux-settings dump --no-comments | Print the parsed JSON. |
cmux-settings get <a.b.c> | Print value at dotted JSON path. |
cmux-settings set <a.b.c> <value> | Set value. <value> is parsed as JSON (true, 42, "text", […], {…}); plain strings without quotes are 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 any unknown settings keys. |
cmux-settings open | Open cmux.json in $EDITOR, VS Code, Cursor, or TextEdit. |
--file <path> overrides the target file (useful for --file ~/.config/cmux/settings.json when the user keeps things in the legacy file).
Workflow
1. Confirm the change. If the user named a setting in plain English (e.g. "make the sidebar tint match the terminal background"), look it up first.
cmux-settings list-supported | rg -i 'sidebar.*terminal|terminal.*sidebar'2. Set the value. JSON literals (true, false, numbers, arrays, objects) must be valid JSON. Plain words are stored as strings.
cmux-settings set sidebarAppearance.matchTerminalBackground true
cmux-settings set app.appearance dark
cmux-settings set shortcuts.bindings.toggleSidebar cmd+b
cmux-settings set shortcuts.bindings.newTab '["ctrl+b","c"]'
cmux-settings set browser.hostsToOpenInEmbeddedBrowser '["localhost","*.internal.example"]'3. Verify by reading back and validating.
cmux-settings get sidebarAppearance.matchTerminalBackground
cmux-settings validate4. Tell the user it auto-reloaded. No app restart. If they want to revert, run cmux-settings unset <key>.
Quick reference
- Appearance:
app.appearance="system" | "light" | "dark",app.appIcon,app.menuBarOnly,app.minimalMode. - Sidebar tint:
sidebarAppearance.matchTerminalBackground,sidebarAppearance.tintColor,sidebarAppearance.tintOpacity(0..1). - Sidebar details:
sidebar.hideAllDetails,sidebar.showBranchDirectory,sidebar.showPullRequests,sidebar.showPorts,sidebar.showLog. - Notifications:
notifications.dockBadge,notifications.sound(enum incl."none","custom_file"),notifications.customSoundFilePath,notifications.hooks(array). - Browser:
browser.defaultSearchEngine,browser.theme,browser.openTerminalLinksInCmuxBrowser,browser.hostsToOpenInEmbeddedBrowser. - Automation:
automation.socketControlMode(off | cmuxOnly | automation | password | allowAll),automation.portBase,automation.portRange. - Shortcuts:
shortcuts.bindings.<actionId>="cmd+b",["ctrl+b","c"],null, or""to unbind. Seereferences/shortcut-actions.md.
For the full list of settings, defaults, and descriptions, run cmux-settings list-supported or read references/all-keys.md.
Rules
- Only edit
cmux.json. Never editsettings.jsonunless the user explicitly asks; it is legacy and only read when the key is absent fromcmux.json. - Never tell the user to restart cmux to apply a change. The file watcher reloads on save.
- Always validate after a bulk edit:
cmux-settings validate. Unknown keys mean the user pasted a key the app does not consume. - Do not blindly overwrite top-level sections (
actions,ui,commands,vault,rightSidebar). They live in the same file and contain non-settings config the user has hand-tuned. - Shortcut action ids must match the schema enum. Look them up in references/shortcut-actions.md before binding.
- Color values must be
#RRGGBB. Opacities are0..1. - For settings the user expressed in app-level language (e.g. "Settings > Notifications > Dock badge"), translate to the matching JSON path first; the docs page at
web/app/[locale]/docs/configuration/page.tsxmirrors the schema 1:1.
interface:
display_name: "cmux Settings"
short_description: "Inspect, edit, validate, and reload cmux.json settings."
default_prompt: "Use this skill to change cmux settings safely: map user-facing preferences to cmux.json paths, edit with the bundled helper, validate recognized keys, and rely on cmux's live config reload."
All settings keys
Auto-generated from web/data/cmux.schema.json. For the rendered docs, see https://cmux.com/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 | true | When enabled, 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. |
Keyboard shortcut action ids
Auto-generated from web/data/cmux.schema.json (shortcuts.bindings.propertyNames.enum).
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.newBrowserWorkspaceshortcuts.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.clearScreenKeepScrollbackshortcuts.bindings.focusLeftshortcuts.bindings.focusRightshortcuts.bindings.focusUpshortcuts.bindings.focusDownshortcuts.bindings.splitRightshortcuts.bindings.splitDownshortcuts.bindings.toggleSplitZoomshortcuts.bindings.equalizeSplits
Canvas
shortcuts.bindings.toggleCanvasLayoutshortcuts.bindings.canvasRevealFocusedPaneshortcuts.bindings.canvasOverviewshortcuts.bindings.canvasZoomInshortcuts.bindings.canvasZoomOutshortcuts.bindings.canvasZoomResetshortcuts.bindings.canvasTidyshortcuts.bindings.canvasAlignLeftshortcuts.bindings.canvasAlignRightshortcuts.bindings.canvasAlignTopshortcuts.bindings.canvasAlignBottomshortcuts.bindings.canvasEqualizeWidthsshortcuts.bindings.canvasEqualizeHeightsshortcuts.bindings.canvasDistributeHorizontallyshortcuts.bindings.canvasDistributeVertically
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 writes comment-free formatting (2-space indent, trailing newline)
and only writes 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 shlex
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any
DEFAULT_PATH = Path.home() / ".config" / "cmux" / "cmux.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 strip_trailing_commas_outside_strings("".join(out))
def strip_trailing_commas_outside_strings(text: str) -> str:
"""Remove JSONC trailing commas without touching string contents."""
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 == ",":
j = i + 1
while j < n and text[j].isspace():
j += 1
if j < n and text[j] in "}]":
i += 1
continue
out.append(ch)
i += 1
return "".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")
parts = dotted.split(".")
if any(part == "" for part in parts):
raise SystemExit("error: empty key path segment")
return parts
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:
cur[p] = {}
elif not isinstance(cur[p], dict):
raise SystemExit(
f"error: intermediate key '{p}' is not an object "
f"(got {type(cur[p]).__name__}); cannot set nested path"
)
cur = cur[p]
leaf = parts[-1]
changed = (leaf not in cur) or cur[leaf] != 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_from_source(source: Path) -> list[str]:
text = source.read_text()
known_sections = (
"app.",
"terminal.",
"notifications.",
"sidebar.",
"workspaceColors.",
"sidebarAppearance.",
"automation.",
"browser.",
"shortcuts.",
)
candidates = re.findall(r'"([a-zA-Z]+\.[a-zA-Z0-9_.]+)"', text)
return sorted({p for p in candidates if p.startswith(known_sections)})
def supported_paths_from_reference(skill_root: Path | None) -> list[str]:
if skill_root is None:
return []
reference = skill_root / "references" / "all-keys.md"
if not reference.exists():
return []
text = reference.read_text()
return sorted(set(re.findall(r"^\| `([^`]+)` \|", text, re.MULTILINE)))
def find_source_file() -> Path | None:
here = Path(__file__).resolve()
for parent in here.parents:
direct = parent / "Sources" / "CmuxSettingsJSONPathSupport.swift"
if direct.exists():
return direct
hq_checkout = parent / "repo" / "Sources" / "CmuxSettingsJSONPathSupport.swift"
if hq_checkout.exists():
return hq_checkout
return None
def find_skill_root() -> Path | None:
here = Path(__file__).resolve()
for parent in here.parents:
if parent.name == "cmux-settings" and (parent / "SKILL.md").exists():
return parent
if (parent / "references" / "all-keys.md").exists():
return parent
return None
def supported_paths() -> list[str]:
source = find_source_file()
if source is not None:
return supported_paths_from_source(source)
return supported_paths_from_reference(find_skill_root())
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()
if not paths:
print(
"error: could not load the supported-paths list; "
"run from a cmux checkout or reinstall the cmux-settings skill",
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())
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] = []
# Keep this in sync with top-level structural keys in web/data/cmux.schema.json.
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:
try:
editor_args = shlex.split(editor)
except ValueError as e:
print(f"error: invalid editor command: {e}", file=sys.stderr)
return 1
if not editor_args:
print("error: editor command is empty", file=sys.stderr)
return 1
try:
return subprocess.call([*editor_args, str(path)])
except FileNotFoundError:
print(f"error: editor not found: {editor_args[0]}", file=sys.stderr)
return 1
for app in (["code", "--wait"], ["cursor", "--wait"], ["open", "-e"], ["xdg-open"]):
if shutil.which(app[0]):
try:
return subprocess.call([*app, str(path)])
except FileNotFoundError:
continue
print(
"error: no suitable editor found; set $EDITOR or install code, cursor, open, or xdg-open",
file=sys.stderr,
)
return 1
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())
Related skills
How it compares
Use cmux-settings over generic JSON editing when you need cmux-specific key lookup, validation, and trigger-aware preference changes.
FAQ
Does cmux require a restart after settings change?
No. The file watcher reloads cmux.json on save, and the skill forbids telling users to restart for normal preference edits.
Which file should cmux-settings edit by default?
Only cmux.json unless the user explicitly asks to use the legacy settings.json fallback file.
Is Cmux Settings safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.