keyhac
CLAUDE.md14 starsChanged 8 days agoMITView on GitHub
Keyhac2 is the unification of Keyhac for Windows (../keyhac-win, v1.83) and Keyhac for macOS (../keyhac-mac, v1.68): a keyboard customization / macro tool whose behavior is scripted by the user in Python (~/.keyhac/config.py). It has one shared Python codebase for both OSes, with thin per-OS platform modules. All UI is built on PuiKit (../puikit), the author's portable Python UI toolkit. Documentation: - End-user docs: README.md, doc/ — installation, configuration, API reference (generated), migration guides from keyhac-mac / keyhac-win. - Developer docs:…
In this repository2 other files it gives its agents.
- keyhac-releaseSkill
- quality-loopSkill
The file
We split this CLAUDE.md into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 86
After an intended API change, run make api-snapshot, which refuses a breaking change within a major version.
- Very specific: line 107
When a page with a .ja.md changes, update the .ja.md in the same commit.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md — Keyhac2
What this project is
Keyhac2 is the unification of Keyhac for Windows (../keyhac-win, v1.83) and
Keyhac for macOS (../keyhac-mac, v1.68): a keyboard customization / macro tool
whose behavior is scripted by the user in Python (~/.keyhac/config.py). It has one
shared Python codebase for both OSes, with thin per-OS platform modules. All UI is
built on PuiKit (../puikit), the author's portable Python UI toolkit.
Documentation:
- End-user docs: README.md, doc/ — installation, configuration, API reference (generated), migration guides from keyhac-mac / keyhac-win.
- Developer docs: doc/dev/ — overview, architecture, platform-layer, design-notes, puikit, packaging, testing.
- Remaining tasks and open decisions live in the GitHub issues
(
gh issue list), not in the docs. One exception: doc/dev/next-major.md is the ledger of breaking API changes the additive-only policy defers to the next major release — record such ideas there, not as issues.
Sibling repositories (read-only references)
| Repo | What it is | What to learn from it |
../keyhac-win | Python 3.13 + thin C++ launcher. UI via ckit, input via pyauto (external C++ extension repos, not present in this checkout). | Synchronous WH_KEYBOARD_LL hook semantics, modifier-state engine, one-shot/multi-stroke logic, hook-recovery, clipboard history, list window UX, full feature set. |
../keyhac-mac | Swift/SwiftUI menu-bar app embedding CPython 3.13 via a C++ bridge. | CGEventTap semantics (re-enable, event-source filtering, deferred-event reordering), AX focus paths, the modern snake_case config API that Keyhac2 adopted as its baseline, ThreadedAction model. |
../puikit | Pure-Python UI toolkit, PyPI puikit. Backends: curses / macOS (PyObjC+AppKit) / Windows (ctypes+Direct2D) / web / memory. | The UI layer for Keyhac2. Its CLAUDE.md documents a strict additive API-compatibility policy — all Keyhac2-driven extensions must follow it. |
Feature reference is frozen at win 1.83 / mac 1.68; changes upstream after that are reviewed one-off.
Key decisions (rationale in doc/dev/)
- Languages: Python 3.14 for everything at runtime. Platform bindings via ctypes (Windows) and PyObjC (macOS) — no custom compiled extension modules. The only non-Python code is a tiny PEP 587 embedding launcher per OS, needed for packaging and a stable app identity (macOS Accessibility permission is granted per bundle). See doc/dev/packaging.md.
- Key hook: both OS hooks are synchronous consume-decisions running on the main
thread; the differences (tap re-enable + injected/real event reordering on macOS;
silent-unhook recovery on Windows) are encapsulated behind one
InputHookinterface. See doc/dev/platform-layer.md. - Config API: keyhac-mac's snake_case API is the base, extended with portable
focus conditions (
app=,title=) and the keyhac-win features it lacked. Oneconfig.pyruns on both OSes; keyhac-win configs require migration. There is no separate public API layer between a config and the core, unlike XeFM's. See The public API stays thin. - PuiKit is extended additively per its compatibility policy; everything Keyhac2
needs is in the PyPI release
pyproject.tomlpins. See doc/dev/puikit.md. - Single process, main-thread rule: the native event loop on the main thread
services the hook and all PuiKit windows. Slow work goes to
ThreadedAction; results come back viacall_on_main_thread.
Source layout
keyhac/
core/ # OS-independent: keymap engine, key expressions, input context,
# actions, clipboard history, replay, config loader, settings, logging
actions.py # action objects needing platform/UI wiring (MoveWindow, choosers, ...)
platform/ # base.py interface definitions + fake.py test doubles
win/ # ctypes: WH_KEYBOARD_LL/WH_MOUSE_LL, SendInput, UIA, Win32 windows
mac/ # PyObjC: CGEventTap, CGEventPost, AXUIElement, NSWorkspace, NSPasteboard
ui/ # PuiKit-based: console, chooser, balloon, tray, runtime (backend holder)
main.py # bootstrap: loop setup, hook install, config load
_config.py # the config.py template copied on first run
windows_app/ # Keyhac.exe launcher + bundle build (build.ps1)
macos_app/ # Keyhac.app launcher + bundle build (build.sh, create_dmg.sh)
art/ # hand-maintained SVG icon sources (rendered by tools/make_icons.py)
tools/ # icon pipeline, Pages banner render, release scripts, hook_echo diagnosticConventions
- Public API is snake_case (keyhac-mac style). No new camelCase API.
tests/data/api_snapshot.txtlists the public API with signatures and status. After an intended API change, runmake api-snapshot, which refuses a breaking change within a major version. New API that may still change carries@preview(keyhac/core/api_marks.py). A callback never gains an argument: see The public API stays thin.keyhac/core/must not import OS modules (ctypes.windll,AppKit,Quartz, Win32 constants). All OS access goes throughkeyhac/platform/interfaces.- UI code talks to PuiKit only; no direct AppKit/Win32 in
keyhac/ui/. - Windows ctypes: every prototype (
argtypes/restype) is declared explicitly — mandatory on 64-bit, where the default c_int restype truncates handles (this brokeSetWindowsHookExWwith error 126 once). - Docstrings on the config-facing API are user documentation: Google style
with
Args:/Returns:, no porting history (that goes in comments).doc/config-api.mdis generated from them and committed — runmake api-referenceafter changing one,make api-reference-checkverifies the two have not drifted. Members that are public only in the naming sense (hook callbacks, wiring called bymain()) carry alazydocs: ignoreline;tools/generate_api_reference.pyexplains the rules, including which docstring shapes lazydocs reads structurally. - Japanese translations:
README.mdand the user docs indoc/*.mdhave Japanese translations next to them as.ja.md— notdoc/dev/, and not the generateddoc/config-api.md/doc/action-api.md. English is the source. When a page with a.ja.mdchanges, update the.ja.mdin the same commit; a new user doc gets one too. Links in a.ja.mdpoint to the other page's.ja.mdwhere one exists (make fix-docsdoes it). Style rules and the glossary are in doc/dev/TRANSLATION.md; runmake check-docsbefore committing doc changes. - Tests: pytest (
.venv/bin/python -m pytest). The engine is tested with scripted fakes (no OS needed); UI against PuiKit'sMemoryBackend; live platform tests and harness patterns are described in doc/dev/testing.md. A test that takes focus, moves windows, sends real input or changes the clipboard or input source is markeddesktopand runs only in a VM; a test of the host-side tooling ishost_only. A release runs the suite in the VMs (make release-test), not here. - Subtle inherited behaviors are load-bearing — do not "simplify" them. The list
(L/R-agnostic
KeyCondition.__eq__,force_LRoutput, never-emitted user modifiers, unmatched-consume in multi-stroke, pass-through-on-error, the three keyhac-mac hook bugs fixed inplatform/mac/hook.py's docstring) is in doc/dev/design-notes.md. - PuiKit changes are developed in
../puikitand must respect its additive API policy (new capability flags default off; newBackendmethods get base no-op/raise). Always on a feature branch with a pull request — never commit directly to its main. Keyhac2 depends on the PyPI puikit named by thepuikit>=pin inpyproject.toml, which is the only place that floor is written; an editable checkout is for puikit development only (setPUIKIT_DIRin gitignoredMakefile.local;make install-puikitswitches an existing venv). ../keyhac-winand../keyhac-macare read-only references. Never modify them.- Build/release infra follows XeFM's Makefile/bundle system (the author's shipped PuiKit app) — port from it rather than inventing new schemes.
Status
Released as 2.0.0 stable (version in keyhac/__init__.py).
The engine, both platform layers, all UI (console / chooser / balloon / tray), the
config API surface, clipboard history, macros, window actions and mouse output are
implemented and live-verified on both macOS and Windows; both native launchers build,
and the macOS bundle is signed/notarized and verified end-to-end. The live
verification record — including which passes caught which real bugs — is in
doc/dev/testing.md.
AI integration is officially supported — the MCP endpoint (keyhac/mcp/),
the action API (keymap.ui, UINode) and the authoring skill. It is off unless
the user ticks AI Integration > MCP Server in the console or tray menu
(persisted in settings.json; there is deliberately no config API), and it
carries the same additive-only expectation as the rest of the public surface.
Two properties of UINode are what that expectation is really about — they are
the ceiling on action expressiveness, and changing either breaks every action
already written:
- Element identity. Address by
identifier(DOM id / AutomationId) where there is one, since it survives relabelling and localisation; then by role plus name or text.identity_key()is a different thing — the raw platform ref, used only for the DAG dedupe inget_ui_tree, and not public shape. - Handle lifetime is snapshot. A
UINoderecords what an element was; the screen moves on and the node does not notice.reread()refreshes one deliberately. Nodes that quietly re-read themselves were rejected: that hides exactly the change per-step preconditions exist to catch.StaleElementdistinguishes the screen moved (re-findable) from the selector is wrong (regenerate the action).
Both are settled. They change only in a major release.
What remains is tracked in the GitHub issues: the deferred features (i18n, migemo, rich clipboard formats, balloon help UI). The genuinely-interactive verification passes are through (issue #10, closed), but they are a standing pre-release routine, not a finished backlog — what to repeat, and what each check is looking for, is in doc/dev/testing.md.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

