kagi
Cross-platform key mapper with IME on/off as a first-class action.
Ctrl+[ → Esc and close the IME, with one config, on macOS, Windows and
Linux.
Why another remapper
kanata, kmonad, Karabiner-Elements and xremap all remap keys well. None of them
treats "turn the IME off" as an action — you end up shelling out, or writing
per-OS glue like an AutoHotkey ImmSetOpenStatus helper next to a Karabiner
JSON rule next to an xremap YAML. kagi makes that one line:
[[]]
= "ctrl-["
= ["esc", "ime:off"]
kagi also needs no kernel extension and no DriverKit driver on macOS — an event tap plus Accessibility permission is enough.
Install
kagi keeps itself up to date in the background and exposes kagi update.
Packagers who own updates themselves can drop that (and with it the
reqwest/rustls tree) entirely:
Configure
kagi reads $KAGI_CONFIG, else ~/.config/kagi/kagi.toml, else
%APPDATA%\kagi\kagi.toml. See config/kagi.toml.
The file is rendered by teravars
before it is parsed, so one config can cover several machines: [vars] for
your own values, {{ system.os }} / {{ system.host }} / {{ system.user }}
for the machine, and include to pull in a shared fragment.
[[]]
= "Ctrl+[ -> Esc, and close the IME"
= "ctrl-["
= ["esc", "ime:off"]
[[]]
= "Esc also closes the IME"
= "~esc"
= ["ime:off"]
[[]]
= "~henkan"
= ["ime:on"]
= ["windows", "linux"]
[[]]
= "~muhenkan"
= ["ime:off"]
= ["windows", "linux"]
from
A chord such as ctrl-[, ctrl-shift-a, esc. - separates modifiers, so a
literal hyphen is minus (or the trailing - in ctrl--).
Modifiers: ctrl, shift, alt (Option on macOS), meta (Command / Win).
Modifiers must match exactly unless the rule is wildcarded.
Two prefixes, borrowed from AutoHotkey:
| prefix | meaning |
|---|---|
~ |
the original key still reaches the focused app |
* |
extra modifiers beyond the listed ones are tolerated |
to
| entry | effect |
|---|---|
esc, ctrl-a, … |
synthesize that key |
ime:on / ime:off / ime:toggle |
drive the platform IME |
source:<id> |
select an input source (macOS TIS id, Linux IME engine) |
cmd:<shell command> |
run a command, detached |
An empty to swallows the key.
os
os = ["windows", "linux"] restricts a rule to those platforms. Omit it for
every platform.
Commands
kagi watch is the way to find the name of a key your keyboard actually sends:
down ctrl-[ (keycode=0x21 flags=0x20040000)
Run it at login
kagi is a daemon, so this registers the mechanism each platform actually wants rather than one generic autostart entry. Everything is per-user; none of it needs root.
| mechanism | why not the obvious one | |
|---|---|---|
| macOS | launchd LaunchAgent | ProcessType = Interactive keeps launchd from throttling keyboard handling behind background QoS |
| Linux | systemd user unit, PartOf=graphical-session.target |
kagi grabs evdev devices, so it has to come up and go down with the session; a .desktop autostart entry gives no ordering and no restart-on-failure |
| Windows | logon scheduled task driving a wscript shim |
a console binary launched from the Startup folder leaves a window on screen for as long as the daemon runs |
install compiles your config first and refuses to register a service that
would die on startup. Pass --config to bake a non-default path into the
registration.
kagi service status reports registration and run state; uninstall,
start and stop do what they say.
macOS permissions
An event tap needs Accessibility and Input Monitoring, and macOS grants both per binary — the agent is not the terminal you installed from, so its first run fails regardless of what your terminal is allowed to do.
kagi service install deploys the agent to ~/Applications/kagi.app
(never ~/.cargo/bin/kagi directly) and signs it with a fixed
identifier, rather than running the raw binary as the launchd job. Two
problems this fixes, together:
- macOS keys a grant to the binary's signing identifier, and
cargo buildleaves a linker ad-hoc signature whose identifier embeds a hash of the binary (kagi-bef9cabe50a08b72) — a different one on every rebuild. Left alone, every rebuild looks like a new application to TCC, so yesterday's grant goes stale and the Privacy lists accumulate a deadkagirow per build. - macOS also appears to key a grant to the binary's path, sometimes
permanently — an early
~/.cargo/bin/kagibuild asked while its signature was still broken, and no amount of re-signing, re-toggling, or even removing and re-adding the Settings row ever got that exact path working again. A real, dedicated bundle escapes that path entirely.
A binary that has never asked does not even appear in those lists; kagi
asks for you, through IOHIDRequestAccess and AXIsProcessTrustedWithOptions,
which is what creates the entry. kagi service install walks both
permissions for you, one at a time with its own explanation dialog before
each OS prompt; kagi permissions repeats the same flow on demand; a daemon
that fails to start retries once in the background without a dialog.
Tick both entries, then kagi service start. Logs go to
~/Library/Logs/kagi.log.
If a permission ever gets stuck regardless (for instance, after a
codesign regression re-introduces an unstable signature), fix it with:
This runs tccutil reset <service> <kagi's bundle id> — surgical, because
~/Applications/kagi.app is a real, LaunchServices-registered bundle, unlike
a bare CLI path. It clears only kagi's own Accessibility and Input
Monitoring rows, never another application's grant for the same service.
(kagi service install must have deployed the bundle first.)
That is why the generated agent also sets KAGI_NO_AUTOUPDATE=1: a silent
self-update would swap the binary out and leave the agent running blind, with
nothing in the foreground to prompt you. Update deliberately with
kagi update, then kagi service install again to redeploy the bundle.
How each platform does it
| capture | IME | |
|---|---|---|
| macOS | CGEventTap at kCGHIDEventTap |
posts the JIS 英数/かな keycodes, or TISSelectInputSource |
| Windows | WH_KEYBOARD_LL hook + SendInput |
ImmGetDefaultIMEWnd + WM_IME_CONTROL/IMC_SETOPENSTATUS |
| Linux | evdev + uinput (works under X11 and Wayland) | fcitx5-remote, else ibus |
Permissions
- macOS — System Settings ▸ Privacy & Security ▸ Accessibility and
Input Monitoring, for
kagi.app(kagi service install's deployment target) or the terminal launchingkagi rundirectly. Granting them requires restarting the granted process. Event taps are bypassed while a secure input field has focus, and at the login window. - Windows — an elevated foreground window only receives hooked input if kagi runs elevated too.
- Linux — read access to
/dev/input/event*(sudo usermod -aG input $USER) and write access to/dev/uinput(udev rule, or run as root).
macOS IME method
[]
= "eisu" # or "source"
eisu(default) posts the 英数 / かな keycodes a JIS keyboard sends. Every Japanese IME honours them and the selected input method is preserved — the closest analogue to AutoHotkey'sImmSetOpenStatus.sourceswitches the input source outright via Text Input Services, usingascii_source/japanese_source.
Development
cargo run --example synth posts a synthetic Ctrl+[ so the macOS tap can be
exercised without a human at the keyboard. Run kagi watch first, then
kagi run, then synth: the watcher should report esc and eisu rather
than [.
License
MIT