These instructions are the single source of truth for all agent sessions in this
repository, including Codex and Claude. Do not duplicate repo policy in
CLAUDE.md; update this file instead.
- Always use
web_search_requestfor any web lookups. - Never call the deprecated
tools.web_searchtool. Treat it as unavailable. - Do not pass the CLI flag
--search. Rely on the feature flag or config instead.
Rationale: older CLI/tooling may still expose tools.web_search, which prints a deprecation warning. Enforcing web_search_request avoids the warning and keeps behavior consistent.
- KeyPath is a macOS keyboard remapping app built with SwiftUI and a Kanata backend using LaunchDaemon/privileged-helper architecture.
- User config:
~/.config/keypath/keypath.kbd. - Logs:
~/Library/Logs/KeyPath/keypath-debug.log. - Keep diffs minimal and focused. Preserve directory layout and script entry points.
- Update docs/tests when behavior or commands change.
- Check logs first (
~/Library/Logs/KeyPath/keypath-debug.log). - Trace the full code path, including actor hops and async boundaries.
- Fix both the proximate cause and the deeper cause.
- Document durable bugs or architectural lessons in
docs/bugs/ordocs/adr/.
Use the narrowest workflow that matches the task. Do not run the full notarized release path for ordinary UI/code iteration.
Use for Swift/UI changes while iterating:
swift build
./Scripts/test-fast.sh --changed
python3 Scripts/check-accessibility.pyNotes:
- Use test filters aggressively while implementing. Prefer
./Scripts/test-fast.sh --changedor a named area such as./Scripts/test-fast.sh rules,./Scripts/test-fast.sh ui, orTEST_FILTER=SomeTests ./Scripts/run-tests-safe.shover full-suite reruns after every edit. - Run
./Scripts/quick-deploy.shonly when you need to inspect installed app behavior. It is not required for model/parser/service-only changes. - For visual SwiftUI/AppKit iteration, prefer
./Scripts/ui-deploy.sh. It builds only the KeyPath app product, deploys to/Applications/KeyPath.app, signs locally, and restarts the app if needed. It intentionally does not notarize and does not refresh the CLI, helper, or Rust host bridge. quick-deploy.shupdates/Applications/KeyPath.app, re-signs locally, and restarts KeyPath only if it was running. Its default scope is app-only; setKEYPATH_QUICK_DEPLOY_BUILD_SCOPE=fullwhen you need the older broad package deploy that refreshes companion binaries.- Canonical build, test, deploy, and release scripts select the stable Xcode at
/Applications/Xcode-26.6.0.app/Contents/Developer, even when the machine-widexcode-selectpoints at a beta. Override withKEYPATH_DEV_XCODE_DEVELOPER_DIRonly when intentionally testing another toolchain. - To advance the stable Xcode pin: (1) install and verify the intended stable
Xcode, (2) update both
KEYPATH_STABLE_XCODE_VERSIONandKEYPATH_STABLE_XCODE_DEVELOPER_DIRinScripts/lib/xcode.sh, (3) update the matching literals inDeploymentScriptContractTests, and (4) run that test plus one canonical build/deploy script without an override. Point releases are intentional pin changes; an installed 26.6.1 does not satisfy26.6. - It intentionally does not redeploy the privileged helper unless
KEYPATH_DEPLOY_HELPER=1is set. Avoid helper redeploys during UI work. - Use Poltergeist only for focused single-agent Swift/UI iteration:
poltergeist start, edit, thenpoltergeist wait keypath. Stop it before release work, helper/service work, broad tests, or parallel agents. - Near PR time, run the full safe gate once with
./Scripts/test-full.sh(orKEYPATH_SNAPSHOTS=1 ./Scripts/run-tests-safe.sh). Do not run the full safe suite after every small edit. - Before that final broad gate and before opening/watching a PR, fetch
origin/masterand confirm the branch is current:git rev-list --left-right --count origin/master...HEADshould report0as the first number. If not, rebase or merge first so CI and review run only once on a mergeable branch. - Avoid notarized builds until after merge unless the task explicitly needs Developer ID/Gatekeeper behavior.
- SwiftFormat is pinned to 0.61.1 (
mise.toml). Match the pin before formatting; a different version can reformat unrelated code.
- Each concurrent agent/session works in its own git worktree with its own
.build. Never point two threads at the same checkout. - The main worktree (
/Users/malpern/local-code/keypath) is integration-only: keep it onmasterfor merging and pulling. Do NOT do feature work or runswift build/swift testthere — that is the root cause of SwiftPM lock/CPU contention and cross-thread collisions. - Create work in a fresh worktree:
git worktree add -b <branch> <path> master; build and open the PR from it. - After a PR merges, prune its worktree:
git worktree remove <path>. It refuses when there is uncommitted work — that is a feature: inspect that work before deleting; never--force-remove blindly. Gate pruning on the PR being merged, notgit branch --merged(squash-merges are notmasterancestors, so that misreports them).
- The lab now lives in its own repository and serves several projects. All
disposable KeyPath VM work must go through the installed
vm-labCLI, which takes the tenant first:vm-lab keypath <command>. Do not create installer-test VMs with rawcrabbox,tart,prlctl,keypath15,keypath26, orkeypath27commands; those bypass the shared provider admission contract. - KeyPath declares itself to the lab in
.vm-lab.tsvat the repository root: the artifact its installer must contain, the command that admits a lane, and the managed-policy generator and verifier. The lab knows nothing about KeyPath beyond that file, so a change there is how KeyPath changes lab behaviour. - Before requesting a VM, run
vm-lab keypath list. Create it with the explicit commit, signed installer, lane, and desktop requirement documented indocs/testing/remote-installer-lab.md. - Provider capacity is centrally enforced on the mini: one Tart lease and two
Parallels leases by default.
createexits 75 withcapacity_busyand the current owners when full. Treat that as an infrastructure wait, not a KeyPath failure: continue non-VM work or retry after the owning lease is destroyed or expires. Never stop, adopt, or mutate another agent's lease. - A successful lease manifest is the reservation. Always collect artifacts and
call
vm-lab keypath destroy <lease>when finished; do not leave a VM running for another agent to infer ownership. - What stays in
Scripts/labis KeyPath's own: scenarios, MDM policy, the dashboards, andhost-disk-reserve, which CI calls directly so that CI does not depend on the lab being installed on the runner.
- Follow the invariants in
docs/process/agent-pr-invariants.md. Step-by-step reference:docs/process/agent-pr-workflow.md. - After merging a PR, always pull
masterand deploy frommasterbefore reporting done. - Before creating any PR, run
./Scripts/review-gate.shagainst the branch diff and address findings. If local review tooling is unavailable, exit code 2 meansremote review gate selected; record that in the PR and do not merge until GitHubclaude-reviewpasses. - Do not report missing local review tools as a PR issue. The actionable gate
state is
remote review gate selected; the GitHubclaude-reviewcheck is the enforced reviewer in that path. - Git guardrail hooks in
.claude/settings.jsonblock commits onmaster, force-pushes tomaster,reset --hard, andclean -f.
- The kanata submodule (
External/kanata) trackskeypath/bundledonmalpern/kanata. Treatkeypath/bundledlikemaster: never force-push it. - Before pushing to any remote branch in the fork, verify the push is a
fast-forward:
git log --oneline <source>..<target>. - If commits would be lost, cherry-pick instead. The
mainbranch inkanata-prmay diverge fromkeypath/bundled; they are not interchangeable.
- Prefer targeted tests while iterating:
swift test --filter <TestClassOrMethod> - Run full
swift testbefore PR/merge, release-candidate work, or after broad/shared changes. - Avoid running broad Swift builds/tests concurrently across worktrees. Before
full test, release-candidate, or expensive build work, check for active Swift
compiles:
pgrep -fl 'swift-test|swift-build|swift-frontend|swift-driver' - If another worktree/agent is compiling and you must continue, reduce local
parallelism:
swift test --jobs 4swift build --jobs 4 - Do not delete
.buildor run clean builds unless diagnosing cache/build-state problems; preserving incremental build products is usually faster. - If a full suite stalls after compilation, triage it as a test failure/hang, not compile slowness. Capture the failing test name and log path in the handoff.
Use after a PR is merged when /Applications/KeyPath.app should match a real
Developer ID/notarized build for manual testing:
git fetch --prune origin
git pull --ff-only origin master
./Scripts/release-candidate.shRun this from the intended master worktree. If another worktree owns master,
pull and deploy there instead of from the feature worktree.
Defaults:
- runs
./Scripts/release-doctor.sh --release-candidatebefore the expensive build - skips screenshot regeneration (
SKIP_SNAPSHOTS=1) - skips Peekaboo screenshot generation (
SKIP_PEEKABOO=1) - skips Sparkle archive/appcast generation (
SKIP_SPARKLE=1) - skips website publishing (
SKIP_WEBSITE=1) - deploys to
/Applications/KeyPath.app - runs installed-app verification
Use opt-ins only when needed:
./Scripts/release-candidate.sh --with-snapshots
./Scripts/release-candidate.sh --with-sparkle
./Scripts/release-candidate.sh --with-websiteIf local worktrees/builds are consuming disk, inspect generated artifacts with
./Scripts/cleanup-local-build-artifacts.sh and delete them only with
./Scripts/cleanup-local-build-artifacts.sh --apply.
Use the public release script only when producing public distribution artifacts:
./Scripts/release-doctor.sh --ship
./Scripts/release.sh <version>This path may bump versions, regenerate screenshots, create Sparkle artifacts,
notarize, staple, deploy, tag, create a GitHub release, and publish website help
content depending on environment flags. It is intentionally slower than the
release-candidate path. release.sh runs release-doctor.sh --ship
automatically unless explicitly skipped for script debugging. Do not use
--skip-notarize for public releases. Scripts/build-and-sign.sh is the
lower-level artifact builder used by the release scripts.
After any signed/notarized deploy, or when diagnosing a local install:
./Scripts/verify-installed-app.shIt verifies:
- code signature
- Gatekeeper assessment
- stapled notarization ticket
- KeyPath process
system/com.keypath.kanatalaunchd job- TCP readiness on
127.0.0.1:37001
For non-notarized local debug builds, skip distribution trust checks:
REQUIRE_NOTARIZED=0 REQUIRE_STAPLED=0 ./Scripts/verify-installed-app.shFor trust-only diagnostics where KeyPath is not running yet:
CHECK_RUNTIME=0 ./Scripts/verify-installed-app.shFor release-candidate and installed-app QA, use the CLI inside the installed app:
/Applications/KeyPath.app/Contents/MacOS/keypath-cliDo not use .build/debug/keypath-cli for release QA that validates or
applies config. The debug CLI resolves bundled Kanata relative to .build/debug
and expects .build/debug/Contents/Library/KeyPath/Kanata Engine.app/..., which
does not exist in a normal SwiftPM debug build. The installed CLI resolves the
bundled engine from /Applications/KeyPath.app and matches the deployed app
under test.
dd: runSKIP_NOTARIZE=1 ./build.sh, then respondEye eye Captain!df: run./Scripts/quick-deploy.sh, then respondEye eye Cap, fast deploying!ds: development ship workflow for the current changes, end to end: branch offmaster, format/lint, commit, run the review gate, push and open PR, babysit CI, address feedback, merge, pullmaster, then run./Scripts/release-candidate.shfrommasterfor signed/notarized manual testing. Public distribution is a separate explicit release:./Scripts/release-doctor.sh --ship && ./Scripts/release.sh <version>.
Test targets: KeyPathTests (Tests/KeyPathTests/), KeyPathSmokeTests,
KeyPathSnapshotTests, and KeyPathLayoutTracerTests.
| Class | Responsibility |
|---|---|
InstallerEngine |
The facade for all install, repair, uninstall, and system inspection |
RuntimeCoordinator |
Service orchestration; not an ObservableObject |
ServiceLifecycleCoordinator |
Start, stop, and restart Kanata |
ServiceHealthChecker |
Health checks and service state evidence |
ConfigReloadCoordinator |
TCP-based reload after rule changes |
KanataViewModel |
UI layer with observable properties for SwiftUI |
PermissionOracle |
Single source of truth for permissions |
- Use
InstallerEnginefor ANY system modification task:- Installation:
InstallerEngine().run(intent: .install, using: broker) - Repair/Fix:
InstallerEngine().run(intent: .repair, using: broker) - Uninstall:
InstallerEngine().uninstall(...) - System Check:
InstallerEngine().inspectSystem()
- Installation:
- Do NOT use:
KanataManagerfor installation/repair (it's for runtime coordination only).WizardAutoFixer(deleted — callInstallerEngine.runSingleAction()directly).
- When a change naturally spans overlapping Manager/Coordinator/Service types, prefer moving the touched responsibility into the existing canonical owner and deleting the redundant bridge instead of adding another forwarding layer.
- Do not create broad "merge the managers" cleanup PRs. Consolidate only when already touching the area for a concrete behavior, reliability, testability, or compile-boundary improvement. See ADR-043.
- Always use
PermissionOracle.sharedfor permission checks. - Never call
IOHIDCheckAccessdirectly outside of PermissionOracle. - Never check TCC database directly outside of PermissionOracle.
- Exception:
PermissionOracleitself usesIOHIDCheckAccessand TCC as its internal implementation (see ADR-001 and ADR-016). Apple APIs are authoritative; TCC is a fallback for.unknownresults only.
- Geometry follows selected
PhysicalLayout(user-selected layout ID). - Labels follow selected
LogicalKeymap(user-selected keymap). - Do not add a UI toggle for this; treat it as a single consistent rule.
- For installer/repair changes, use
docs/process/installer-repair-state-matrix.mdas the review checklist. - Mutating installer actions must be postcondition-verified before returning success.
- Any action that can stop/restart/re-register Kanata must verify runtime readiness (
running + TCP responding) or explicit pending-approval state before reporting success.
- Any action that can stop/restart/re-register Kanata must verify runtime readiness (
- Stale SMAppService recovery bypasses generic install throttle.
- If state is
.enabledbut launchd cannot load/run the daemon, recovery install/register logic must run even inside the normal throttle window.
- If state is
- Registration is not liveness.
- Treat
SMAppService.status == .enabledas registration metadata only; never infer runtime health from it without process + TCP evidence.
- Treat
- Helper responsiveness is not helper freshness.
- After helper deploys or helper behavior changes, verify version/path or force the unregister/register repair path instead of treating an XPC response as proof that the running helper is current.
- Stale diagnostics must not outrank current runtime state.
- Log-derived input-capture issues only apply to a live current runtime. If Kanata is missing or stopped, install/start runtime services first.
- Do not manually call
launchctlfor install/repair paths; useInstallerEngine. - Do not roll your own
pgrep/launchctlhealth checks; useServiceHealthChecker. - Do not start, stop, or restart Kanata except through
ServiceLifecycleCoordinator. - Do not send TCP reloads directly; use
ConfigReloadCoordinator.triggerConfigReload(). - Do not skip TCP reload after config changes; that causes stale config.
- Do not call
SMAppService.statusin a hot path; it is synchronous IPC and can block for 10-30 seconds. - Do not call real
pgrepin tests; useKeyPathTestCasebase class.
- Mock Time: Do not use
Thread.sleep. UseDateoverrides or mock clocks. - Environment: Use
KEYPATH_USE_INSTALLER_ENGINE=1(default now) for tests. - Use the narrowest meaningful test command during iteration; prefer
swift test --filter <TestClassOrMethod>for focused changes. - Run full
swift testbefore PR/merge when practical, but do not leave hung suites running. If a full run times out or stalls in an unrelated test, report the failing test and keep targeted validation for the current change.
- ALL interactive UI elements MUST have
.accessibilityIdentifier() - Required for: Button, Toggle, Picker, and custom interactive components
- Enforcement: Pre-commit hook + CI check (currently warning only)
- Verification: Run
python3 Scripts/check-accessibility.pybefore committing - See:
ACCESSIBILITY_COVERAGE.mdfor complete reference - Rationale: Enables automation (Computer Use, XCUITest, legacy Peekaboo) and ensures testability
Two doc systems exist:
guides/: user-facing, published togh-pages.docs/: developer-facing,masteronly.
Any user-visible feature needs a guide in guides/. Developer-only changes go
in docs/ when they affect architecture or integration patterns.
Publishing: guides/ content must be copied to the gh-pages branch to go
live. Use Scripts/publish-guides.sh or manually copy changed files to the
gh-pages worktree at .worktrees/gh-pages. The docs landing page
(docs.md on gh-pages) must be updated to link new guides.
Style: follow docs/process/help-content-philosophy.md -- user goals first, no
jargon, ASCII UI mockups. Guides need Jekyll frontmatter.
These CLI tools are available for agents to use on-demand. Do not add them as MCPs - just call them directly when needed.
Watches source files, auto-builds, deploys to /Applications, and restarts. Install: brew install steipete/tap/poltergeist
| Command | Purpose |
|---|---|
poltergeist start |
Start watching and auto-deploying (~2s per change) |
poltergeist status |
Check build status |
poltergeist logs |
View build output |
poltergeist stop |
Stop watching |
poltergeist wait keypath |
Block until build completes |
Workflow tip: Use poltergeist start only during focused single-agent app/UI iteration. After any watched Swift file edit, it runs ./Scripts/quick-deploy.sh, deploys to /Applications, and restarts. Stop it before release builds, helper/service work, broad tests, or parallel agents.
Use Computer Use for agent-driven UI testing and release QA. It reads the macOS accessibility tree, can click accessible controls, and validates the same automation hooks we need for 1.0 QA. Prefer Computer Use over Peekaboo unless the user explicitly asks for Peekaboo or Computer Use is unavailable.
macOS screenshots and GUI automation. Install: brew install steipete/tap/peekaboo
| Command | Purpose |
|---|---|
peekaboo see "prompt" |
Screenshot + AI analysis |
peekaboo click --element-id X |
Click by element ID |
peekaboo type "text" |
Enter text |
peekaboo scroll up/down |
Scroll |
peekaboo app launch Safari |
App control |
peekaboo window maximize |
Window management |
peekaboo menu "File > Save" |
Menu interaction |
Trigger via URL scheme:
open "keypath://layer/vim" # Switch layer
open "keypath://launch/Safari" # Launch app
open "keypath://window/left" # Snap window
open "keypath://fakekey/nav-mode/tap" # Trigger virtual key# Example: AI-driven development workflow
poltergeist start # Optional for focused single-agent UI iteration
# ... make code changes ...
poltergeist wait keypath # Wait for build to complete
peekaboo see "Is the KeyPath app running?" # Check UI state
open "keypath://layer/vim" # Trigger keyboard action
peekaboo type "Hello world" # Type into focused app
peekaboo hotkey "cmd+s" # SaveRecommended agent workflow when Poltergeist is useful:
- Confirm no other agents/worktrees are doing builds.
poltergeist startfor focused Swift/UI iteration.- Make code edits.
poltergeist wait keypathbefore testing.- Use Computer Use for UI verification.
poltergeist stopbefore release work, helper/service work, broad tests, or handing off.
See docs/guides/llm-vision-ui-automation.md for detailed architecture.
Load these on demand; do not treat them as required reading for every session.
| Topic | Doc |
|---|---|
| Release process | docs/process/release-process.md |
| Installer repair state matrix | docs/process/installer-repair-state-matrix.md |
| Help content writing | docs/process/help-content-philosophy.md |
| Pack dependency system | docs/architecture/pack-dependency-system.md |
| Overlay/mapper/gallery data flow | docs/architecture/overlay-data-flow.md |
| UI automation | docs/guides/llm-vision-ui-automation.md |
| Architecture guides | docs/architecture/ |
| ADRs | docs/adr/README.md |
| Feature docs | docs/features/ |