Hmbown/CodeWhale · error · ExecError

no non-modifier key in

Error message

no non-modifier key in "${text}" — use hold_key for modifier-only holds

What it means

After parsing, the chord must contain at least one non-modifier key. A string consisting solely of modifiers ('cmd', 'cmd+shift') parses as flags with key==null, which parseChord rejects: modifier-only presses are not key presses here — the dedicated hold_key API covers modifier holds.

Solutions

  1. Add the actual key: 'cmd+c' instead of 'cmd'.
  2. To press/hold a modifier by itself, use hold_key with press and release phases.
  3. To combine a modifier with a mouse action, hold the modifier via hold_key, perform the click, then release.
  4. Guard your wrapper: if all chord tokens are modifiers, route to hold_key instead of key.

Example fix

// before
await computer.key("cmd");
// after
await computer.hold_key("cmd", "press");
await computer.click({ x: 100, y: 200 });
await computer.hold_key("cmd", "release");
Defensive patterns

Strategy: validation

Validate before calling

const MODS = new Set(["cmd", "ctrl", "alt", "shift", "fn"]);
const parts = chord.split("+").filter(Boolean).map((p) => p.trim().toLowerCase());
const hasMainKey = parts.some((p) => !MODS.has(p));
if (!hasMainKey) throw new Error("chord needs a non-modifier key; use hold_key for modifier-only holds");

Try / catch

try {
  await computer.key(chord);
} catch (e) {
  if (String(e.message).includes("use hold_key")) {
    await computer.hold_key(extractModifier(chord), "press");
    // ... perform action ...
    await computer.hold_key(extractModifier(chord), "release");
  } else throw e;
}

Prevention

When it happens

Trigger: Calling key with 'cmd', 'shift+ctrl', or 'fn' — all parts are modifiers, no main key.

Common situations: Trying to press a modifier alone to toggle a mode (e.g. sticky keys); building a chord programmatically where the main key variable was empty but modifiers were set; intending to hold cmd while clicking (should use hold_key + click, not key).

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/b68b2457575f299f. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/darwin.mjs:323

    const lease = await nativeLease("key_event", { code, flags, down: true });
    try {
      return await action();
    } finally {
      await withSignal(null, () => lease.release());
    }
  }

  function parseChord(text) {
    const parts = String(text).split("+").map((s) => s.trim().toLowerCase()).filter(Boolean);
    if (!parts.length) throw new ExecError("empty key text");
    let flags = 0;
    let key = null;
    for (const p of parts) {
      if (MODIFIERS[p] != null) flags |= MODIFIERS[p];
      else if (KEY_CODES[p] != null) { if (key) throw new ExecError(`multiple non-modifier keys in "${text}"`); key = p; }
      else throw new ExecError(`unknown key "${p}" (supported: ${Object.keys(KEY_CODES).join(", ")} + modifiers cmd/ctrl/alt/shift/fn)`);
    }
    if (key == null) throw new ExecError(`no non-modifier key in "${text}" — use hold_key for modifier-only holds`);
    return { flags, code: KEY_CODES[key], key };
  }

  // ---------- displays ----------
  async function displayInfo() { return native("displays"); }

  // ---------- screenshots ----------
  function recordingsDir() {
    return process.env.CODEWHALE_CU_RECORDINGS_DIR || path.join(os.homedir(), ".codewhale-cu", "recordings");
  }

  async function screenshot({ display, region, app_ref, window_id, path: outPath } = {}) {
    // Once an app is selected, ordinary observations follow it behind the
    // user's work. An explicit display/region remains a deliberate desktop capture.
    if (app_ref === undefined && display === undefined && region === undefined) app_ref = state.inputApp ?? undefined;
    const dir = recordingsDir();
    fs.mkdirSync(dir, { recursive: true });
    // JPEG, not PNG. A screen is photographic content — gradients, wallpaper,

View on GitHub (pinned to 433685b202)