cmux-settings

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

View and edit cmux settings in ~/.config/cmux/cmux.json. Use when the user wants to change cmux preferences (appearance, sidebar, notifications, automation, browser, shortcuts), set a value by JSON path, validate the file, open it in an editor, or look up which keys cmux recognizes. Triggers on '/cmux-settings', 'change cmux setting', 'set <something> in cmux', 'cmux config', 'cmux.json', or 'rebind a cmux shortcut'.

skills/cmux-settings/SKILL.md

Download bundle ↓
main · 6f118af5 bundle filesScanned 2026-09-15

scripts/cmux-settings

3,364 tokens · o200k_base · 13,856 bytes

Source excerpt starting at line 1.
#!/usr/bin/env python3"""Inspect and edit ~/.config/cmux/cmux.json. cmux watches the file and auto-reloads on save, so writes take effectimmediately. The file is JSONC (JSON with // and /* */ comments); thisscript 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 argparseimport jsonimport osimport reimport shleximport shutilimport subprocessimport sysimport tempfilefrom pathlib import Pathfrom 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())