Custom Commands and Actions
cmux.json is where you teach cmux your own routines: shell commands you run often, workspaces that open with a preset arrangement of terminals and browsers, and actions you can bind to shortcuts, tab bar buttons, and the Command Palette. Keep one at .cmux/cmux.json inside a project, at ~/.config/cmux/cmux.json for all projects, or both.
Where cmux reads cmux.json
A project file, a user-wide file, or both can supply entries. For project files, cmux starts at the current directory and also checks each parent directory above it.
- Project:
./.cmux/cmux.json(found by searching upward from the working directory; wins over the user-wide file) - Fallback local:
./cmux.json(still supported for existing repos) - User-wide:
~/.config/cmux/cmux.json(shared by every project; supplies whatever a project file leaves undefined)
Edit cmux.json, then press Cmd+Shift+, or run cmux reload-config to apply changes.
Shell commands
The smallest entry is a name plus a command string. Picking it in the Command Palette types that string into whichever terminal has focus and runs it.
{
"commands": [
{
"name": "Format Changed Files",
"description": "Run the formatter on files that differ from main",
"keywords": [
"fmt",
"prettier",
"style"
],
"command": "git diff --name-only main -- '*.ts' '*.tsx' | xargs npx prettier --write"
},
{
"name": "Reset Local Database",
"keywords": [
"db",
"wipe"
],
"command": "cd \"$(git rev-parse --show-toplevel)\" && ./scripts/db-reset.sh",
"confirm": true
}
]
}Shell command fields
name: Required, non-blank label you pick from the Command Palettecommand: The shell text cmux types into the focused terminal and runsdescription: Optional secondary text shown alongside the entrykeywords: More words that should match this entry when you search the Command Paletteconfirm: Set to true to get a confirmation prompt each time before it runs
A shell command executes wherever the focused terminal happens to be. When the command needs a predictable starting point, begin it with cd "$(git rev-parse --show-toplevel)" && to move to the top of the Git repository, or with cd /some/folder && to pin it to a fixed folder.
Commands that open a workspace
Give a command a workspace object in place of a command string and it opens a new workspace instead, arranged as a tree of split panes holding terminals and browsers. An entry needs exactly one of the two.
{
"commands": [
{
"name": "API Stack",
"keywords": [
"api",
"server",
"logs"
],
"restart": "confirm",
"workspace": {
"name": "API",
"cwd": "./services/api",
"color": "#10b981",
"env": {
"PORT": "8080"
},
"layout": {
"direction": "vertical",
"split": 0.65,
"children": [
{
"direction": "horizontal",
"children": [
{
"pane": {
"surfaces": [
{
"type": "terminal",
"name": "Server",
"command": "go run ./cmd/api",
"focus": true
}
]
}
},
{
"pane": {
"surfaces": [
{
"type": "terminal",
"name": "Worker",
"command": "go run ./cmd/worker",
"env": {
"QUEUE": "default"
}
}
]
}
}
]
},
{
"pane": {
"surfaces": [
{
"type": "terminal",
"name": "Logs",
"cwd": "/var/log",
"command": "tail -F system.log"
},
{
"type": "browser",
"name": "Health",
"url": "http://localhost:8080/healthz"
}
]
}
}
]
}
}
}
]
}Fields of a workspace object
name: Title for the workspace; falls back to the command namecwd: Base directory for the workspace; each tab cwd resolves against itlayout: The root node of the layout tree described belowcolor: Sidebar color, as #RRGGBB hex or a workspace color nameenv: Environment variables inherited by every shell in the workspacesetup: Bootstrap command sent to the workspace's first terminal before that terminal's own command
When the workspace is already open
Set restart on the command to decide what happens if a workspace with the same name already exists:
"new": Create a new workspace (default)"confirm": Prompt first, then replace it if you agree"recreate": Replace it right away with no prompt"ignore": Switch to the existing workspace
Describing a layout
A layout is a tree. Every inner node splits its area in two, and every leaf is a pane that holds tabs.
Split nodes
A split divides its area into two parts:
direction:"horizontal"or"vertical"children: An array of exactly two nodes, each of which is another split or a panesplit: Where the divider sits, as a fraction of the area. Defaults to 0.5; values outside 0.1 to 0.9 are clamped
Pane nodes
A pane is a leaf of the tree. Its surfaces array lists the tabs it opens with, and it must contain at least one entry.
Surfaces
Each entry in a pane's surfaces array opens one tab, which is either a terminal or a browser:
type:"terminal"or"browser"name: Tab title to show in place of the defaultfocus: Set to true to put keyboard focus on this tab once the layout is builtcommand: Terminals only: shell text to run as soon as the terminal opensurl: Browsers only: the page to loadcwd: Starting directory for this tab, resolved with the rules belowenv: An object of extra environment variable names and values
How cwd values resolve
~/path: resolved inside your home folder- A path that starts with /: taken literally
./subdir: appended to the workspace directory.or unset: inherits the workspace directory
File structure
actions, shortcut, or ui.surfaceTabBar.buttons.commands still define reusable shell commands and workspace layouts. Nightly builds add an actions registry. Actions are the public IDs shared by the surface tab bar, the Command Palette, and action-level shortcuts.
{
"actions": {
"cmux.newTerminal": {
"type": "agent",
"agent": "claude",
"title": "Claude Code",
"subtitle": "Start Claude Code in a new tab"
},
"lint": {
"type": "command",
"title": "Lint",
"subtitle": "Run the linter in a fresh tab",
"keywords": [
"eslint",
"check"
],
"command": "npm run lint",
"shortcut": [
"cmd+k",
"cmd+l"
],
"icon": {
"type": "symbol",
"name": "checkmark.seal"
}
},
"scratch-dir": {
"type": "command",
"title": "Scratch Directory",
"command": "cd \"$(mktemp -d)\"",
"target": "currentTerminal",
"palette": false,
"icon": {
"type": "emoji",
"value": "📝",
"scale": 0.85
}
},
"api-stack": {
"type": "workspaceCommand",
"title": "API Stack",
"commandName": "API Stack"
}
},
"ui": {
"surfaceTabBar": {
"buttons": [
"cmux.newTerminal",
"cmux.splitRight",
"lint",
{
"action": "scratch-dir",
"tooltip": "cd into a new temp folder"
}
]
}
},
"commands": [
{
"name": "API Stack",
"keywords": [
"api"
],
"workspace": {
"cwd": "./services/api",
"layout": {
"pane": {
"surfaces": [
{
"type": "terminal",
"name": "Server",
"command": "go run ./cmd/api"
}
]
}
}
}
}
]
}Nightly action registry
actions maps stable IDs to runnable behavior. Use the built-in IDs cmux.newTerminal, cmux.newBrowser, cmux.splitRight, and cmux.splitDown to override the defaults. Use your own IDs for project-specific tools.
palette defaults to true. Set it to false to keep an action out of Command Palette while still making it available to the surface tab bar or a shortcut. shortcut uses the same syntax as settings shortcuts, for example cmd+shift+c or ["cmd+k", "cmd+c"].
ui.surfaceTabBar.buttons replaces the default button list when present. Leave out a built-in ID to hide it. Icons always use an object shape: { "type": "symbol", "name": "play.circle" }, { "type": "emoji", "value": "🧪", "scale": 0.9 }, or { "type": "image", "path": "./icons/codex.svg" }. Image paths are relative to the config file. Emoji scale is optional and defaults to 1. SVG, PDF, PNG, JPEG, GIF, TIFF, BMP, HEIC, HEIF, WebP, AVIF, ICO, and ICNS are supported.
Each button entry can be either an action ID string or a button object. Use a button object when you want the same action with a different surface label, icon, or tooltip. The resolved button title is also used as the trust prompt title.
Put any approval or permission flags directly in the command string you actually want to run. The default action target is newTabInCurrentPane, so the common pattern is to open a new terminal tab in the current pane and start Codex, Claude Code, or OpenCode there.
Custom actions and Command Palette
An actions entry is the reusable thing cmux runs. Use actions when the same behavior should be available from the Command Palette, surface tab bar, shortcuts, or the plus-button menu. Keep commands for reusable shell commands and workspace layouts. Set palette to false when an action should stay out of Command Palette.
Action types
"command": Run shell text in a terminal. Use target to choose the current terminal or a new tab in the current pane."agent": Start a coding agent in a new terminal tab. Any CLI name works (claude, codex, opencode, or your own binary), with optional args."builtin": Alias a built-in cmux action such as cmux.newTerminal, cmux.newBrowser, cmux.splitRight, or cmux.splitDown. Copy built-ins: cmux.copyWorkingDirectory copies the terminal's working directory, cmux.copyProjectRoot copies the root of its git repository (or the working directory outside one), and cmux.copyScreen copies the visible screen. In an SSH, cloud, or remote tmux workspace, cmux.copyProjectRoot copies the remote working directory, because cmux can't look up the git repository on the other host."workspaceCommand": Run a named workspace definition from commands. Use this for multi-pane layouts, custom working directories, and startup commands."workspace": Create a workspace from an inline workspace definition, with optional restart behavior. Offered in the new-workspace plus-button menu automatically."setting": Change one setting in your global cmux.json, for example toggle a true/false setting or cycle through a list of values. See Setting actions below."settingPreset": Apply a named group of settings from settingPresets in one step.
Action fields
title: Command Palette row title, surface button label, menu item title, and trust prompt title unless an entry overrides it.subtitle/description: Command Palette secondary text. description is accepted as an alias for subtitle.keywords: Extra search terms for Command Palette.palette: Defaults to true for custom actions. Set false to hide the action from Command Palette while keeping it callable elsewhere.shortcut: Optional action shortcut, using the same single-stroke or two-stroke chord syntax as settings shortcuts.target: For command and agent actions only. Use currentTerminal or newTabInCurrentPane.confirm: Ask before running the action.newWorkspaceMenu: Set true to offer any action in the new-workspace plus-button menu, or false to hide a workspace action from it.
Command Palette behavior
Command Palette reads the resolved action registry. Custom action IDs are added as rows when palette is not false. Legacy commands are added automatically as custom rows unless an action with the same generated ID already exists. Built-in commands keep their normal palette labels, but overriding a built-in ID such as cmux.newTerminal changes the behavior behind that shared entrypoint.
Setting actions
A setting action names a settings path and exactly one of set (write a value), toggle: true (flip a true/false setting), cycle (move to the next value in a list, wrapping around), or unset: true (remove the key from cmux.json, so the value from the Settings window or the default applies). toggle and cycle start from the value cmux is using, including one you set in the Settings window. A settingPreset action applies an entry from settingPresets, a partial cmux.json whose nested objects merge key by key. cmux checks each change against the schema before saving, keeps your comments and other keys, and applies it right away. Setting actions and presets only run from your global cmux.json and the packs it references; project configs can't declare them.
{
"actions": {
"scroll.cycle": {
"type": "setting",
"title": "Cycle Scroll Speed",
"path": "terminal.scrollSpeed",
"cycle": [1.0, 1.4, 1.8]
},
"editor.wrap": {
"type": "setting",
"title": "Toggle Editor Word Wrap",
"path": "fileEditor.wordWrap",
"toggle": true
},
"sidebar.quiet": {
"type": "settingPreset",
"title": "Quiet Sidebar",
"preset": "sidebar.quiet"
}
},
"settingPresets": {
"sidebar.quiet": {
"sidebar": { "showPorts": false, "showPullRequests": false, "showLog": false }
}
}
}The same changes work from a terminal: cmux config set terminal.scrollSpeed 1.4, cmux config toggle fileEditor.wordWrap, and cmux config preset sidebar.quiet.
With confirm: true, cmux asks before saving and shows the equivalent cmux config command. Paths split on every ".", so a key that itself contains "." (such as a workspaceGroups.byCwd entry for ~/src/app.web) can't be changed this way. Edit it in cmux.json directly.
Custom plus button actions
Use ui.newWorkspace.action to override what the plus button does. Use ui.newWorkspace.contextMenu (or the rightClick alias) to define the ordered right-click menu. Menu entries can be action IDs, action objects, or { "type": "separator" }. When it is not set, the menu lists cmux.newWorkspace (⌘N), cmux.newCloudWorkspace (⌘Y), cmux.newCloudMachine (⇧⌘Y), cmux.newTerminal (⌘T), cmux.newBrowser (⇧⌘L), each showing its current shortcut. New Cloud Workspace uses the last selected Cloud workspace's machine, then the first machine in the right-hand sidebar; when no usable machine exists, it opens New Cloud Machine. New Cloud Machine provisions a machine deliberately. Cloud rows appear only when Cloud Machines is enabled.
{
"actions": {
"worktree-agents": {
"type": "workspaceCommand",
"title": "Agents in New Worktree",
"commandName": "Agents in New Worktree",
"icon": {
"type": "symbol",
"name": "arrow.triangle.branch"
}
}
},
"ui": {
"newWorkspace": {
"action": "worktree-agents",
"contextMenu": [
"worktree-agents",
{
"type": "separator"
},
{
"action": "cmux.newWorkspace",
"title": "Blank Workspace"
},
{
"action": "cmux.newBrowser",
"title": "Browser"
}
]
}
},
"commands": [
{
"name": "Agents in New Worktree",
"description": "Branch off a new Git worktree, then run Codex and Claude Code in it side by side",
"workspace": {
"name": "Worktree",
"layout": {
"direction": "vertical",
"split": 0.7,
"children": [
{
"direction": "horizontal",
"children": [
{
"pane": {
"surfaces": [
{
"type": "terminal",
"name": "Codex",
"command": "marker=\"${TMPDIR:-/tmp}/cmux-wt-${CMUX_WORKSPACE_ID:-local}\"; echo \"Waiting for the worktree...\"; until [ -s \"$marker\" ]; do sleep 0.25; done; cd \"$(cat \"$marker\")\" && exec codex --yolo"
}
]
}
},
{
"pane": {
"surfaces": [
{
"type": "terminal",
"name": "Claude",
"command": "marker=\"${TMPDIR:-/tmp}/cmux-wt-${CMUX_WORKSPACE_ID:-local}\"; echo \"Waiting for the worktree...\"; until [ -s \"$marker\" ]; do sleep 0.25; done; cd \"$(cat \"$marker\")\" && exec claude --dangerously-skip-permissions"
}
]
}
}
]
},
{
"pane": {
"surfaces": [
{
"type": "terminal",
"name": "Setup",
"command": "marker=\"${TMPDIR:-/tmp}/cmux-wt-${CMUX_WORKSPACE_ID:-local}\"; rm -f \"$marker\"; root=$(git rev-parse --show-toplevel) || exit 1; branch=\"wt-$(date +%m%d-%H%M%S)\"; wt=\"$root/../$(basename \"$root\")-$branch\"; git -C \"$root\" worktree add -b \"$branch\" \"$wt\" && printf \"%s\\n\" \"$wt\" > \"$marker\" && cd \"$wt\" && exec \"${SHELL:-/bin/zsh}\" -l",
"focus": true
}
]
}
}
]
}
}
}
]
}This example makes the normal plus click run the worktree-agents action. The workspace command from commands uses a visible setup terminal to create the Git worktree first. Codex and Claude start at the same time, wait for the workspace-specific state file, then cd into the created directory before exec.
Workspace layouts
A workspace layout is stored as a workspace action, embedding a full workspace definition directly in the action so it needs no separate commands entry. It supports the same workspace fields plus setup, a bootstrap command sent to the first terminal before that terminal's own command:
{
"actions": {
"pair-review": {
"type": "workspace",
"title": "Pair Review",
"icon": {
"type": "symbol",
"name": "person.2.wave.2"
},
"restart": "ignore",
"newWorkspaceMenu": true,
"workspace": {
"name": "Review",
"cwd": "~/src/app",
"setup": "git pull --ff-only",
"layout": {
"direction": "horizontal",
"split": 0.55,
"children": [
{
"pane": {
"surfaces": [
{
"type": "terminal",
"name": "Diff",
"command": "git log -p -n 5",
"focus": true
}
]
}
},
{
"pane": {
"surfaces": [
{
"type": "terminal",
"name": "Codex",
"command": "codex"
},
{
"type": "terminal",
"name": "Shell"
}
]
}
}
]
}
}
}
}
}Workspace layouts appear in the plus-button right-click menu automatically. Set newWorkspaceMenu to false to hide one, or to true on any other action to add it to the menu.
You can also create these without writing JSON: right-click the plus button and choose Save Workspace as Layout to capture the current workspace's splits, directories, running agents, and browser tabs into ~/.config/cmux/cmux.json. They are stored as entries in the actions block. Customize Workspace Layouts opens that file in your editor.
Default for new workspaces
Use the Default for New Workspace submenu to pick which saved workspace layout plain New Workspace should run, or choose None to return to a blank terminal. The save dialog also has a Use as default for new workspaces checkbox. The setting is ui.newWorkspace.action; a project-local .cmux/cmux.json value wins over ~/.config/cmux/cmux.json.
Full example
{
"actions": {
"cmux.newTerminal": {
"type": "command",
"title": "Codex",
"command": "codex --yolo",
"shortcut": "cmd+t",
"icon": {
"type": "image",
"path": "./icons/codex.svg"
}
},
"review": {
"type": "agent",
"agent": "claude",
"args": "--permission-mode plan",
"title": "Plan with Claude",
"shortcut": "cmd+shift+p",
"icon": {
"type": "symbol",
"name": "list.bullet.clipboard"
}
},
"storybook": {
"type": "workspaceCommand",
"commandName": "Storybook",
"newWorkspaceMenu": true
},
"typecheck": {
"type": "command",
"title": "Typecheck",
"command": "npx tsc --noEmit",
"target": "newTabInCurrentPane",
"confirm": false,
"icon": {
"type": "emoji",
"value": "🔎"
}
}
},
"ui": {
"surfaceTabBar": {
"buttons": [
"cmux.newTerminal",
"review",
{
"action": "typecheck",
"title": "tsc"
},
"cmux.newBrowser",
"cmux.splitDown"
]
},
"newWorkspace": {
"contextMenu": [
"storybook",
{
"type": "separator"
},
"cmux.newTerminal",
"cmux.newBrowser"
]
}
},
"commands": [
{
"name": "Storybook",
"description": "Component explorer with a live preview beside it",
"keywords": [
"ui",
"components",
"stories"
],
"workspace": {
"name": "Storybook",
"cwd": "./packages/ui",
"color": "#a855f7",
"layout": {
"direction": "horizontal",
"split": 0.4,
"children": [
{
"direction": "vertical",
"split": 0.5,
"children": [
{
"pane": {
"surfaces": [
{
"type": "terminal",
"name": "Storybook",
"command": "npx storybook dev -p 6006 --no-open",
"focus": true
}
]
}
},
{
"pane": {
"surfaces": [
{
"type": "terminal",
"name": "Tests",
"command": "npx vitest --watch",
"env": {
"CI": "false"
}
}
]
}
}
]
},
{
"pane": {
"surfaces": [
{
"type": "browser",
"name": "Preview",
"url": "http://localhost:6006"
}
]
}
}
]
}
}
},
{
"name": "Install Dependencies",
"keywords": [
"install",
"deps"
],
"command": "npm ci",
"confirm": true
},
{
"name": "Open Changelog",
"keywords": [
"changes",
"release"
],
"command": "${EDITOR:-vi} CHANGELOG.md"
}
]
}