From karabiner
This skill should be used when the user asks to "write Karabiner rules", "create a karabiner complex modification", "remap keys on macOS", "edit karabiner.json", "set up keyboard shortcuts with Karabiner", "debug a Karabiner rule", or mentions Karabiner-Elements, key_code mappings, modifier remapping, or app-specific hotkeys on macOS. Also triggers on any request to remap keys or create keyboard shortcuts on macOS beyond what System Settings offers. Guides writing Karabiner-Elements complex modification rules in JavaScript (Duktape ES5.1) that generate JSON, instead of hand-authoring deeply nested JSON.
How this skill is triggered — by the user, by Claude, or both
Slash command
/karabiner:js-complex-modificationsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Since v15.9.6, Karabiner-Elements lets you write complex modification rules in JavaScript that generate JSON, instead of hand-writing deeply nested JSON. The JS is evaluated by a built-in Duktape engine.
Since v15.9.6, Karabiner-Elements lets you write complex modification rules in JavaScript that generate JSON, instead of hand-writing deeply nested JSON. The JS is evaluated by a built-in Duktape engine.
JS shines when rules have repetitive structure -- cycling through modes, generating per-app overrides, mapping ranges of keys. For a single simple remapping, raw JSON is fine. But once there are 3+ manipulators with similar shapes, JS pays for itself in readability and maintainability.
CLI alternative: karabiner_cli --eval-js <path-to-js-file>
The JS engine is Duktape, which only supports ES5.1. This means:
var, not let or constfunction() {}, not arrow functions () => {}+for...ofArray.from, Object.entries, Map, Set, PromiseJSON.stringify and JSON.parse are availableArray.prototype.map, .filter, .forEach, .indexOf work fineSee references/duktape-es5-constraints.md for the full list of what's available and what isn't.
Every JS script must return an array of rule objects. Each rule has a description and a manipulators array:
// The script's return value is the rules array
[
{
"description": "My rule",
"manipulators": [
{
"type": "basic",
"from": { "key_code": "a", "modifiers": { "mandatory": ["control"] } },
"to": [{ "key_code": "b" }]
}
]
}
]
| Field | Purpose |
|---|---|
from | The key + modifiers to match |
to | Events to emit when the key is pressed |
to_if_alone | Events to emit if the key is pressed and released without other keys |
to_if_held_down | Events to emit if the key is held |
to_after_key_up | Events to emit after the key is released |
to_delayed_action | Events for delayed press/cancel behavior |
conditions | When this manipulator should be active (app, variable, device, etc.) |
parameters | Timing parameters (alone timeout, held threshold, etc.) |
In the from.modifiers object:
mandatory: modifiers that must be held (the event won't match without them)optional: modifiers that may be held (won't prevent matching)Set "optional": ["any"] to allow the rule to fire regardless of extra modifiers being held.
Modifier names: control, shift, option, command, caps_lock, fn
Sided variants: left_control, right_control, left_shift, right_shift, left_option, right_option, left_command, right_command
Conditions control when a manipulator is active:
frontmost_application_if / frontmost_application_unless -- match by bundle ID regexvariable_if / variable_unless -- match on internal variables (set via set_variable)device_if / device_unless -- match by vendor_id / product_idinput_source_if / input_source_unless -- match by keyboard input sourceevent_changed_if / event_changed_unless -- match if keys were recently changedSet "shell_command" in to to run arbitrary commands:
{ "shell_command": "open -a 'Ghostty'" }
Set and check variables to create stateful rules (mode switching, toggles):
// Set a variable
{ "set_variable": { "name": "my_mode", "value": 1 } }
// Check a variable in conditions
{ "name": "my_mode", "type": "variable_if", "value": 1 }
Unset variables default to 0.
See references/key-codes.md for the full key_code reference and references/modifiers-and-conditions.md for detailed condition/modifier docs.
Map a hotkey to open an app:
[{
description: "Ctrl+1 to launch VS Code",
manipulators: [{
type: "basic",
from: {
key_code: "1",
modifiers: { mandatory: ["control"], optional: ["any"] }
},
to: [{ shell_command: "open -a 'Visual Studio Code'" }]
}]
}]
Cycle through modes with a single key, using variables to track state. This is much cleaner in JS than writing N separate manipulators by hand:
// Cycle through terminal apps with Ctrl+0, launch the active one with Ctrl+`
var modes = [
{ value: 0, name: "Ghostty", app: "Ghostty" },
{ value: 1, name: "Warp", app: "Warp" },
{ value: 2, name: "cmux", app: "cmux" }
];
var cycleManipulators = modes.map(function(mode, i) {
var next = modes[(i + 1) % modes.length];
return {
type: "basic",
conditions: [{ name: "terminal_mode", type: "variable_if", value: mode.value }],
from: { key_code: "0", modifiers: { mandatory: ["control"], optional: ["any"] } },
to: [
{ set_variable: { name: "terminal_mode", value: next.value } },
{ shell_command: "osascript -e 'display notification \"Terminal: " + next.name + "\" with title \"Terminal Mode Switched\"'" }
]
};
});
var launchManipulators = modes.map(function(mode) {
return {
type: "basic",
conditions: [{ name: "terminal_mode", type: "variable_if", value: mode.value }],
from: { key_code: "grave_accent_and_tilde", modifiers: { mandatory: ["control"], optional: ["any"] } },
to: [{ shell_command: "open -a '" + mode.app + "'" }]
};
});
// Return both rules
[
{ description: "Ctrl+0 to cycle terminal mode", manipulators: cycleManipulators },
{ description: "Ctrl+` to launch active terminal", manipulators: launchManipulators }
]
This replaces ~100 lines of JSON with ~25 lines of readable JS.
Remap keys only in specific apps using bundle ID conditions:
var appRemaps = [
{ app: "^com\\.tinyspeck\\.slackmacgap$", from_key: "p", to_key: "k", desc: "Cmd+P to Cmd+K in Slack" },
{ app: "^com\\.apple\\.dt\\.Xcode$", from_key: "p", to_key: "o", to_mods: ["left_command", "left_shift"], desc: "Cmd+P to Cmd+Shift+O in Xcode" }
];
appRemaps.map(function(r) {
var toMods = r.to_mods || ["left_command"];
return {
description: r.desc,
manipulators: [{
type: "basic",
conditions: [{ bundle_identifiers: [r.app], type: "frontmost_application_if" }],
from: { key_code: r.from_key, modifiers: { mandatory: ["command"] } },
to: [{ key_code: r.to_key, modifiers: toMods }]
}]
};
});
Caps Lock as Escape on tap, Control on hold:
[{
description: "Caps Lock: Escape on tap, Control on hold",
manipulators: [{
type: "basic",
from: { key_code: "caps_lock", modifiers: { optional: ["any"] } },
to: [{ key_code: "left_control" }],
to_if_alone: [{ key_code: "escape" }]
}]
}]
Use function keys normally in dev tools, media keys everywhere else:
var devApps = [
"^com\\.microsoft\\.VSCode$",
"^com\\.jetbrains\\.",
"^com\\.apple\\.dt\\.Xcode$"
];
var fnKeys = [];
for (var i = 1; i <= 12; i++) {
fnKeys.push({
type: "basic",
conditions: [{ bundle_identifiers: devApps, type: "frontmost_application_unless" }],
from: { key_code: "f" + i, modifiers: { optional: ["any"] } },
to: [{ apple_vendor_top_case_key_code: "keyboard_fn" }]
// Karabiner handles the actual media key mapping
});
}
[{ description: "Function keys as media keys outside dev apps", manipulators: fnKeys }]
/var/log/karabiner/ (core_service, grabber) and ~/.local/share/karabiner/log/ (console_user_server) for errors.variable_if conditions check for specific values but the variable was never set, the default is 0. Ensure a manipulator matches value: 0.osascript permissions -- shell commands using osascript to send keystrokes need accessibility permissions. Commands like open -a don't have this issue.karabiner_cli --list-system-variables should respond instantly. If it hangs, the core service needs restarting.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub tal/plugin-marketplace --plugin karabiner