---
name: MulmoTerminal Keys
slug: mulmoterminal-keys
category: Automation
description: MulmoTerminal Keys binds keyboard shortcuts and fixes clipboard and Enter/newline behavior in MulmoTerminal. Use it to set keymaps, jump to agents or terminals, handle copy-on-select, or diagnose shortcut conflicts and terminal submit issues.
github: "https://github.com/receptron/mulmoterminal/tree/main/server/skills/mulmoterminal-keys"
language: TypeScript
stars: 202
forks: 31
install: "npx degit https://github.com/receptron/mulmoterminal/tree/main/server/skills/mulmoterminal-keys ~/.claude/skills/mulmoterminal-keys"
installs_to: ~/.claude/skills/mulmoterminal-keys
source_path: server/skills/mulmoterminal-keys/SKILL.md
collection_size: 12
category_size: 1956
collection_url: "https://dirskills.com/collections/receptron/mulmoterminal"
added: 2026-09-05T05:30:15.119Z
last_synced: 2026-09-05T05:30:15.119Z
canonical_url: "https://dirskills.com/skills/mulmoterminal-keys"
---

# MulmoTerminal Keys

MulmoTerminal Keys binds keyboard shortcuts and fixes clipboard and Enter/newline behavior in MulmoTerminal. Use it to set keymaps, jump to agents or terminals, handle copy-on-select, or diagnose shortcut conflicts and terminal submit issues.

**Install:**

```bash
npx degit https://github.com/receptron/mulmoterminal/tree/main/server/skills/mulmoterminal-keys ~/.claude/skills/mulmoterminal-keys
```

## README

# Keyboard, shortcuts and clipboard

These settings all live in **`~/.mulmoterminal/config.json`**, and each write is a **partial
`POST /api/config` merge** — write only the key you are changing, so the user's other settings
survive.

**"Partial" means per TOP-LEVEL key, and `keymap` is replaced WHOLE.** The merge is
`body[key] !== undefined ? sanitize(body[key]) : current` (`server/config/app-config.ts`), so
posting

```json
{ "keymap": { "send": [ … ] } }
```

does not add a `send` to the keymap — it **makes that the entire keymap**, deleting every action
binding the user had. Nothing warns, and the reply is a success.

So a keymap write always sends the **complete** keymap: what step 1 read, plus the change. Read it,
merge in memory, post the whole thing. Every `keymap` example below shows one setting on its own for
readability — none of them is a body to post as-is unless the user genuinely has nothing else bound.

Settings has a **Keyboard shortcuts** section, but it is **read-only** — it lists every action and
its current binding, plus a `send` row: one per configured entry, or a single "Not set" placeholder
when there are none. Point the user at it after writing, as the check.

## Open with a proposal, not a question — **on the keymap path only**

**First, check which path you are on.** This skill answers two different arrivals, and the flow
below belongs to one of them:

| the user said | go to |
|---|---|
| nothing specific, or arrived via **Set up shortcuts…** | this section |
| "selecting text doesn't copy" | [`copyOnSelect`](#copyonselect--copy-just-by-selecting) |
| anything about the question pane | [`questionPaneEnabled`](#questionpaneenabled--answer-a-question-from-a-side-pane) |
| "Shift+Enter submits", "Enter makes a newline" | [`terminalSubmit`](#terminalsubmit--enter-vs-newline) |
| "Ctrl+C stopped interrupting" | **the diagnostic below — not this section, and not `terminalSubmit`** |

### "Ctrl+C stopped interrupting" — diagnose, do not propose

**Skip the proposal flow entirely.** This user has one binding too many, not too few; offering them
a starter set would add more keys taken from the terminal, which the rule below forbids. The job is
to find the claim, show it, and remove or move it once they agree.

Read the whole keymap and find **everything** claiming that keystroke, not just one entry:

| what is on `Ctrl+C` | what it does to the interrupt |
|---|---|
| an action other than `copy` / `paste` | takes it **always** — the grid claims the key in the capture phase before the terminal sees it |
| `paste` | takes it **always** — not through the grid (`gridShortcutFor` skips terminal-scoped actions) but inside the terminal, ahead of `send` |
| `copy` | takes it **only while something is selected** — `clipboardActionFor` returns null with no selection, so the key falls through |
| a `send` entry | replaces it with those bytes — **and it is what fires** when `copy` is on the same key with nothing selected |

So "it works sometimes" points at `copy` plus a selection sitting in that cell, and "it never works"
points at one of the others. `terminalSubmit` is not involved either way, and changing it would be
debugging the wrong setting.

Report every claim you found, say which one explains what they are seeing, and change **only that
one** — after they confirm. Remember the whole-keymap write rule above: post the complete map with
that single entry removed or moved, never a map containing only your change.

**Named `copyOnSelect`, the question pane, or the Enter/newline symptom? Skip this whole section**
and go straight to that setting's own one below. Ctrl+C has its own branch just above. Only the
first row — no specific request, or the **Set up shortcuts…** button — belongs here. Proposing
shortcuts to someone who came about Enter is both an unwanted detour and a real cost: every binding
takes a key away from the program in their terminal, and the rule below says never to add one the
user did not request.

Most people who DO land here reach you from the **Set up shortcuts…** button in Settings, which they
pressed without a specific binding in mind — the screen told them the feature exists and they want to know
what it is good for. "What would you like to bind?" hands that question straight back, and it is
the one they came to have answered.

So look first, then offer:

1. **Read what is already there** — `~/.mulmoterminal/config.json`. Say what is bound now (or that
   nothing is), so the proposal is about what is missing.

   **Reading it is not enough — check it against what you are about to offer.** There are two ways
   a proposed `send` can be accepted by the user and then never fire, and both are silent:

   | what is already on the keystroke | what happens | what to do |
   |---|---|---|
   | an action other than `copy` / `paste` | it wins — the grid claims the key in the capture phase | say so, and ask whether to move the action |
   | `paste` | it wins — decided inside the terminal, before `send` | same |
   | `copy` | it wins **only while something is selected**; with none, `clipboardActionFor` returns null and **your `send` fires** | say which behaviour they get when, and let them choose |
   | an existing **`send`** | the **earlier entry** wins, because `sendBytesFor` takes the first match | update that entry, never append a second |

   So before offering the macOS set, look for **both kinds** on `Cmd+ArrowLeft`, `Cmd+ArrowRight`
   and `Cmd+Backspace`. Validation only *warns* about a duplicate claim, so nothing downstream
   will stop you.
2. **Guess the platform, and say the guess out loud.** `uname` tells you the machine the SERVER
   runs on — but the keys are pressed in a **browser**, which may be somewhere else: a Mac
   connecting to a Linux host still has a Cmd key, and a Linux desktop pointed at a Mac host does
   not. Nothing in this app reports the browser's platform, so `uname` is a guess that is right for
   the usual same-machine setup and wrong for a remote one.

   So do not branch silently. Open with the assumption stated — *"You're on a Mac, so…"* — and a
   user who is not corrects you in one word. Asking costs a round trip every time; a wrong guess
   costs a sentence, and only for the people it is wrong about.

   The platform decides what is worth offering: on macOS, `F1`–`F12` arrive only if the user holds
   `Fn` or has changed the function-key setting (so do not propose them unprompted — see the trap
   list below), `Option`+letter never matches at all, and the Cmd line-editing set below only makes
   sense on a Mac keyboard.
3. **Offer concrete sets rather than a blank question**, and what you offer depends on step 2:
   - **Always** one from the action starter sets — **on macOS, the Up/Down subset of Arrows**, for
     the reason in that set's own row. Name what each key will do in the terminal they are sitting
     in, including what it stops doing.
   - **On macOS, also the `send` line-editing set.**
   - **Off macOS (as guessed in step 2) there is no `send` set to offer, and that is not an omission** — see the reason
     under the `send` starter set. Say the mechanism exists and what it is for, then build an entry
     from the `bytes` table only if they name a key that is not doing what they want.
4. **Write only what they pick.** The rule below still holds: every binding takes a key away from
   the program inside the terminal, so nothing goes in unasked.

**On macOS, lead with the `send` line-editing set.** Cmd+Left / Cmd+Right / Cmd+Delete are what a
Mac user presses out of habit in every other text field; in a terminal the Cmd is dropped and the
unmodified key gets through — the **bare arrow** for `Cmd+Left` and `Cmd+Right`, the **bare Delete**
for `Cmd+Delete`, deleting one character instead of the line. So the key appears to half-work rather than to
be unbound, which is harder to diagnose than silence (#1858). If they have not mentioned it, ask whether it bothers them.

## `keymap` — shortcuts

**There are no defaults.** With no `keymap`, nothing is bound and no key is intercepted. Every
binding you add is a key the program inside the terminal (Claude Code, `vim`, `less`, the shell)
**stops receiving**. So ask before binding, and never add one the user did not request.

```json
{ "keymap": { "zoom-next": "PageDown", "zoom-prev": "PageUp" } }
```

| Action | What it does | Needs a zoomed cell |
|---|---|---|
| `zoom-toggle` | Enlarge / collapse — the only action that does; it enlarges whichever terminal the cursor is in | no |
| `zoom-next` / `zoom-prev` | Move the enlargement along the on-screen order | **yes** |
| `next-attention` | Go to the next terminal awaiting input, then finished-unreviewed, then idle — skipping cells mid-turn. Never enlarges or collapses | no |
| `terminal-new` | Open the launch panel on the default workspace (the toolbar's `＋`) | no |
| `terminal-new-here` | Open the launch panel on the current terminal's directory | no |
| `terminal-new-adjacent` | Start a **shell** in the current terminal's directory, straight away — no form | **yes** |
| `terminal-close` | Close the current terminal | **yes** |
| `terminal-restart` | Restart the agent in the current terminal — same cell, same directory, same conversation | **yes** |
| `copy` | Copy the terminal's selection. Acts **only** when something is selected, so `Ctrl+C` stays usable as interrupt — with no selection the key reaches the program untouched | no |
| `paste` | Paste into the terminal | no |

The three above are easy to confuse. `terminal-new` and `terminal-new-here` open the same panel —
the launch form, at the right edge, over whatever the grid is showing — and differ only in which
directory it starts on. Neither needs a zoomed cell; with no terminal in view `terminal-new-here`
falls back to the workspace rather than doing nothing. `terminal-new-adjacent` is the odd one out:
it starts a shell immediately and shows no form at all, which is why it still needs a current cell.

Neither panel action has to be bound to be reachable: the toolbar's `＋` and the `＋` on every
terminal's header do the same two things.

**Always bind `zoom-toggle` or `next-attention`.** Everything marked "yes" needs something already
enlarged, so a keymap without one of those two can't be used without a mouse click first. Offer
`next-attention` to anyone running several agents — it is the "take me to whoever called" key.

### Starter sets — offer one of these rather than inventing keys

Each is checked against the traps below. The guide documents them at
[Configuration → Keyboard shortcuts](https://receptron.github.io/mulmoterminal/guide/en/config.html#keymap).

| Set | Keys | Suits |
|---|---|---|
| Minimal | `zoom-toggle: F8`, `next-attention: F9` | Anyone starting out — the two that open the feature up |
| Arrows | `Alt+ArrowUp/Left/Right/Down` | The safe cross-platform default, and the only ACTION set to offer a Mac user unprompted — **but on macOS bind the UP/DOWN pair only.** `Alt+Left` / `Alt+Right` usually carry word motion in a Mac terminal, and an action claims the keystroke in the capture phase before the terminal sees it, so binding those two takes word motion away. Map the pair you keep to `zoom-toggle` and `next-attention`, which is what the "always bind one of these two" rule needs anyway |
| tmux-flavoured | `Alt+z / n / p / a / c / x` | tmux muscle memory — but **not** on macOS |
| iTerm2-flavoured | `Cmd+Enter`, `Cmd+[` / `]`, `Cmd+d` | Mac users who think in iTerm2 panes |

### Syntax, and what cannot be bound

`Modifier+Modifier+Key`. Modifiers: `Shift` / `Ctrl` (`Control`) / `Alt` (`Option`) / `Cmd`
(`Command`, `Meta`), case-insensitive. The key is matched against the browser's
`KeyboardEvent.key` — `PageDown`, `Home`, `ArrowUp`, `a` — and is **case-sensitive for letters**.

- **A malformed binding stops the server from starting**, naming the entry. Validate before writing:
  a stray `+`, a lone `Shift`, or an unknown modifier costs the user the whole app until they fix
  the file. Never guess a spelling — if unsure, ask them to press the key and read it off the
  devtools console (`addEventListener("keydown", e => console.log(e.key), true)`).
- **Modifiers match exactly.** Binding `PageDown` leaves `Shift+PageDown` with the terminal, which
  is how xterm's scrollback keeps working. Say this when proposing `PageUp`/`PageDown`.
- **No `F1`–`F12` on a Mac.** macOS delivers no keydown for them by default (they are media keys),
  so the binding looks broken for reasons the user cannot see. If they insist: `Fn`+the key, or
  System Settings → Keyboard → Keyboard Shortcuts → Function Keys.
- **No `Option`+letter on a Mac** — `KeyboardEvent.key` reports the composed character, not the
  letter, so it never matches. `Option`+a non-printing key (`Alt+ArrowDown`) is fine.
- **Never `Cmd`/`Ctrl` + `W` / `T` / `N`** — the browser reserves them; the binding silently does
  nothing.
- Two actions on one keystroke only fires the first. The startup check warns; don't write one.
- **`terminal-close` ends the session with no confirmation.** Only bind it if asked, and suggest a
  combination they won't hit by accident.
- **`terminal-restart` also acts with no confirmation, and costs a resume.** It kills the agent
  mid-turn if it is working, and the conversation is then read back from its transcript — real
  tokens, not a free reload. Offer it only to someone who says they change MCP servers, config or
  plugins while sessions are open, and put it somewhere deliberate for the same reason as
  `terminal-close`.

### `keymap.send` — raw bytes to the terminal

The actions above drive MulmoTerminal. `send` does the opposite: it puts **bytes straight into the
terminal**, so a key the shell or agent already understands can be reached from a key the keyboard
has. The motivating request was `Cmd+Right` for end-of-line on a Mac.

```json
{
  "keymap": {
    "send": [
      { "key": "Cmd+ArrowRight", "bytes": "\u0005" },
      { "key": "Cmd+ArrowLeft", "bytes": "\u0001" }
    ]
  }
}
```

A **list**, unlike the actions, because each entry carries its own payload. Control characters are
written the way JSON writes them (`\uXXXX`) and are **not** re-interpreted — the value reaches the
program exactly as written.

#### Starter set — the one to offer unprompted

The actions have starter sets and `send` did not, so `send` was only ever reached by someone who
already knew it existed. There is exactly **one** worth proposing on its own, and padding the list
would be worse than a short one — every other combination either already works or is reserved:

| Set | Entries | Suits |
|---|---|---|
| **macOS line editing** | `Cmd+ArrowLeft` → `\u0001`, `Cmd+ArrowRight` → `\u0005`, `Cmd+Backspace` → `\u0015` | **The one to lead with on a Mac.** Start of line, end of line, delete to start of line — the habit every other macOS text field has trained. `Cmd+Backspace` is the Mac Delete key; `\u0015` is `Ctrl+U` |

It is the one `send` set to offer a Mac user unprompted, pairing with the Up/Down subset of
**Arrows** on the action side above — the two halves of a macOS proposal claim different keys, which
is what makes offering both at once safe — and **there is no non-macOS equivalent, on purpose.** `send` exists to reach a key the shell
already understands from a key the keyboard has; on Linux and Windows `Ctrl+A` and `Ctrl+E` are
directly typeable, so there is no gap to close unprompted. Off macOS, mention `send` and wait for a
key they name.

**It is a change, not a free addition** — say so before writing it. Those three keystrokes are not
dead today: the Cmd is dropped and the **bare** arrow or Delete gets through, so `Cmd+Left` moves
one character and `Cmd+Delete` erases one. The binding replaces that with line motion, which is the
point, and is also why the user confirms first.

What makes it safe to *offer* unprompted is that **the replaced behaviour stays reachable**: plain
`Left`, `Right` and `Delete` still move and erase one character, so the binding adds line motion on
a modifier rather than removing anything from the keyboard. And what it replaces is the behaviour
the user came to complain about.

**Provided nothing is already on those three keystrokes**, neither an action nor an earlier `send`
(step 1); either one wins and the new entry would never fire.

**Two that look like obvious additions and are not:**

- **`Alt+ArrowLeft` / `Alt+ArrowRight` for word motion.** On macOS these usually work already —
  the terminal receives the Option and moves by words. A `send` binding would replace working
  behaviour with a fixed `\u001bb` / `\u001bf`, which is the same thing right up until a program
  wanted the original. Check what the user's terminal does before offering it.
- **`Cmd+k` for kill-to-end.** The browser takes it (`Cmd`/`Ctrl`+`K` focuses the address bar), so
  the binding silently does nothing — the trap the syntax section lists for `W` / `T` / `N`. And
  `Ctrl+K` is already typeable on every keyboard, so there is nothing to reach.

For anything else, build the entry from the `bytes` table below rather than from a set.

| Want | `bytes` | Is |
|---|---|---|
| Start / end of line | `\u0001` / `\u0005` | `Ctrl+A` / `Ctrl+E` |
| Back / forward one word | `\u001bb` / `\u001bf` | `Alt+B` / `Alt+F` |
| Delete to end of line | `\u000b` | `Ctrl+K` |
| Escape | `\u001b` | `Esc` |

- **An action beats a `send` on the same keystroke — except `copy` with nothing selected.** Most
  actions are claimed by the grid in the capture phase, and `paste` inside the terminal, both ahead
  of `send`, so the `send` silently never fires. `copy` is the exception: `clipboardActionFor`
  returns null with no selection, and the `send` fires then. The server warns at startup about the
  collision, and **its message names the action as the winner even in that case** — so read it as
  "these two collide", not as a verdict.
- **Empty `"bytes"` is refused** and stops the server: it would take the key from the terminal and
  put nothing back.

### After writing a keymap

The browser reads it **on page load — reload the tab.** A hand-edit made while the server is running
also needs a server restart before it reaches the page. Then check Settings → Keyboard shortcuts.

## `copyOnSelect` — copy just by selecting

For the PuTTY / iTerm2 behaviour (`copyOnSelect` in Windows Terminal): a mouse selection reaches the
clipboard **without pressing anything**. Off unless written.

```json
{ "copyOnSelect": true }
```

- **Only write it if asked.** It changes the clipboard when the user may have meant only to
  highlight something while reading, which is why it ships off.
- **Not** a replacement for the `copy` action, and they coexist — someone who selects with the
  keyboard still wants `copy` bound.
- **Over plain `http://` the browser gives the page no clipboard access at all** (the API is
  `https://`- and `localhost`-only). There is a fallback, but it needs the terminal to still hold
  keyboard focus. If the user reaches MulmoTerminal at `http://<ip>:PORT` and says dragging doesn't
  copy, **check this before the setting**.
- Whitespace-only selections, and a repeat of the last copied text, are deliberately **not** copied
  so an accidental drag doesn't destroy the clipboard. Say so if they report "it didn't copy".
- **Settings → Terminal keys has a checkbox for it**, applied immediately. Point at that when the
  user only wants it turned on; write the key when they are setting up a machine without a browser.

## `questionPaneEnabled` — answer a question from a side pane

When a Claude session stops to ask something (its `AskUserQuestion` dialog, the one you answer with
the arrow keys), offer the same choices as buttons in a pane beside the enlarged terminal. Off
unless written.

```json
{ "questionPaneEnabled": true }
```

- **The dialog does not go away, and this does not replace it.** Picking in the pane presses the
  arrow keys and Enter 
