HomeClaw
AGENTS.md191 starsChanged 44 days agoMITView on GitHub
HomeClaw exposes Apple HomeKit accessories via a CLI tool, plugins for Claude Code and OpenClaw, and a stdio MCP server for Claude Desktop. It uses a unified Mac Catalyst architecture with an AppKit bridge bundle for the macOS menu bar. Single-process design. HMHomeManager requires a UIKit/Catalyst app with the HomeKit entitlement. By making the entire app Catalyst, HomeKit access is direct (no IPC), signing is unified (single archive), and App Store submission is clean. The macOSBridge plugin bundle provides the…
- Reads credentials
- Commits and pushes
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,774 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository20 other files it gives its agents.
- .github/copilot-instructions.mdCopilot instructions
- app-resizabilitySkill
- ios-app-intentsSkill
- ios-debugger-agentSkill
- ios-ettrace-performanceSkill
- ios-memgraph-leaksSkill
- spm-build-analysisSkill
- swiftui-expert-skillSkill
- swiftui-liquid-glassSkill
- swiftui-performance-auditSkill
- swiftui-specialistSkill
- swiftui-ui-patternsSkill
- swiftui-view-refactorSkill
- swiftui-whats-new-27Skill
- xcode-build-benchmarkSkill
- xcode-build-fixerSkill
- xcode-build-orchestratorSkill
- xcode-compilation-analyzerSkill
- xcode-project-analyzerSkill
- homeclawSkill
The file
Show its rules and text
We split this AGENTS.md into the rules it gives an agent
26 rules. About 0 are common, 12 are distinctive, and 14 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈12
Distinctive · estimate
This author's own choices. Worth a look.
14
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 113
If conversion fails, the finalizer restores the previous JSON (or the initial PBX project) and retains failed output for diagnosis.
- Distinctive: line 145
Developer ID: Not supported. HomeKit entitlement cannot be included in Developer ID provisioning profiles.
- Very specific: line 224
If status shows ready: false with 0 homes:
- Distinctive: line 225
Verify HomeKit entitlement is embedded (codesign check above)
- Distinctive: line 226
Verify TCC permission granted (auth_value=2)
- Distinctive: line 227
Verify iCloud is signed in with HomeKit data
- Very specific: line 277
If this repo needs hand-authored feature coverage, keep those curated definitions in tools/clawpatch/features/ and sync/copy them into …
- Very specific: line 315
Every App Store version must release automatically after approval (AFTER_APPROVAL).
- Very specific: line 317
After every archive + upload, generate a TestFlight tester update (see memory/testflight-updates.md)
- Distinctive: line 343
Fastmail is NOT built into chief-of-staff: Instead:
- Very specific: line 413
How to apply: When querying multiple Aqara homes, spawn one fresh stdio invocation per home (single initialize → switch_home(target) → …
- Very specific: line 427
Always use commit-commands:commit (fully qualified) when invoking the commit skill.
- Very specific: line 445
How to apply: When waiting for Greptile reviews on HomeClaw PRs, skip the MCP plugin entirely.
- Distinctive: line 463
If you want a sandbox-compatible TUI later, options are: TermKit (ncurses-backed, more mature) or Bubble Tea (Go).
- Very specific: line 481
Why: Use the private API in non-App-Store builds (Debug + local Release) so HomeClaw's CLI/MCP can create automations without polluting the …
- Very specific: line 523
Read all trigger-owned action sets (walk home.triggers → trigger.actionSets)
- Very specific: line 524
Create empty trigger-owned action set via private HMTrigger.addActionSetWithCompletionHandler: — persists, but is useless because it can't …
- Very specific: line 534
Write-side code (PrivateActionSets.swift, HOMECLAW_PRIVATE_API flag, createAutomation private branch) was rolled back.
- Very specific: line 608
After mutating, the bridge auto-recomputes FullyQualifiedName ([area_name, device_name]) on affected devices.
- Distinctive: line 647
How to apply: When a scene name says Sarah → target Double Bedroom devices.
- Distinctive: line 653
After every release build + upload, generate a tester update for App Store Connect.
- Distinctive: line 679
Keep it concise — testers skim
- Distinctive: line 683
No API access configured — generate text for manual paste into ASC
- Distinctive: line 688
Update README with new features
- Very specific: line 692
Create GitHub release: gh release create ...
- Very specific: line 717
After adding new files to Sources/HomeKitHelper/, must run cd Sources/HomeKitHelper && xcodegen then verify entitlements
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.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
HomeClaw
HomeClaw exposes Apple HomeKit accessories via a CLI tool, plugins for Claude Code and OpenClaw, and a stdio MCP server for Claude Desktop. It uses a unified Mac Catalyst architecture with an AppKit bridge bundle for the macOS menu bar.
Architecture
Claude Code → Plugin (.claude-plugin/) → stdio MCP server (Node.js) ─┐
Claude Desktop → stdio MCP server (Node.js) ─────────────────────────┤
OpenClaw → Plugin (openclaw/) → homeclaw-cli ────────────────────────┤
▼
/tmp/homeclaw.sock (JSON newline-delimited)
│
HomeClaw (Mac Catalyst UIKit app)
├── HomeKitManager (direct, in-process)
├── SocketServer (for CLI/MCP clients)
└── macOSBridge.bundle (NSStatusItem menu bar)Single-process design. HMHomeManager requires a UIKit/Catalyst app with the HomeKit entitlement. By making the entire app Catalyst, HomeKit access is direct (no IPC), signing is unified (single archive), and App Store submission is clean. The macOSBridge plugin bundle provides the native macOS menu bar via NSStatusItem.
Note: The native Streamable HTTP MCP server is off by default and can be enabled in Settings → Integrations. When enabled it binds only to loopback at http://127.0.0.1:9090/mcp, is owned by the Catalyst app lifecycle, and exposes /healthz for readiness. It uses SwiftNIO directly, with Host/Origin validation and a deny-by-default read-only tool allowlist; no bearer token or shell launch is required. The Node stdio server remains available independently. MCP/_disabled/ contains an excluded, superseded SDK/token prototype, not the active implementation.
Project Structure
Sources/
homeclaw/ # Unified Catalyst app (Xcode target via XcodeGen)
App/ # UIApplicationDelegate entry point, scene delegates
Bridge/ # BridgeProtocols.swift (Mac2iOS, iOS2Mac)
MCP/_disabled/ # Preserved HTTP MCP server code (not compiled)
HomeKit/ # HomeKitManager, SocketServer, CharacteristicMapper,
# AccessoryModel, DeviceMap, CharacteristicCache,
# HomeEventLogger, WebhookCircuitBreaker
Views/ # SettingsView, IntegrationsSettingsView
Shared/ # AppConfig, AppLogger, HomeClawConfig
Shared/_disabled/ # Preserved KeychainManager (not compiled)
macOSBridge/ # AppKit bundle (NSStatusItem menu bar)
MacOSController.swift # NSStatusItem + NSMenu
Info.plist # NSPrincipalClass: MacOSController
homeclaw-cli/ # CLI tool (SPM executable + Xcode target)
Commands/ # list, get, set, search, scenes, automations, status, config, device-map
Commands/_disabled/ # Preserved token command (not compiled)
SocketClient.swift # Direct socket communication
Resources/ # Info.plist, entitlements, app icons
scripts/build.sh # Build & install script (debug/release for local use)
scripts/release.sh # Release pipeline (asc): archive, TestFlight, App Store; see appstore/RELEASE.md
mcp-server/ # Node.js stdio MCP server (wraps homeclaw-cli)
openclaw/ # HomeClaw — OpenClaw plugin
openclaw.plugin.json # Plugin manifest (configurable binDir)
src/index.ts # Plugin entry point
skills/homekit/ # HomeKit skill definition
App bundle layout (after build):
Contents/MacOS/HomeClaw # Catalyst app executable
Contents/MacOS/homeclaw-cli # Bundled CLI binary
Contents/Resources/macOSBridge.bundle # AppKit menu bar plugin
Contents/Resources/mcp-server.js # Node.js stdio MCP server
Contents/Resources/openclaw/ # Bundled OpenClaw plugin filesBuild System
Two build systems:
- Xcode (
xcodebuild): BuildsHomeClaw(Catalyst),macOSBridge(macOS bundle), andhomeclaw-cli(macOS tool) - npm (esbuild): Builds
mcp-serverNode.js MCP server
SPM (Package.swift) is retained for CI — it builds homeclaw-cli only. The main app is Catalyst-only (Xcode).
The scripts/build.sh orchestrates xcodegen + xcodebuild:
scripts/build.sh --release --install # Full build + install to /Applications
scripts/build.sh --debug # Debug build only
scripts/build.sh --team-id ABCDE12345 # Use a different Apple Developer team
npm run build:mcp # Build Node.js MCP server onlynpm workspaces: Root package.json defines workspaces for openclaw and mcp-server. Run npm install from the project root.
Xcode version
The Xcode this project builds with is pinned in .xcode-version (currently
27.1). scripts/build.sh and scripts/release.sh resolve that pin to
an installed Xcode.app by its CFBundleShortVersionString, so a beta and a
release build can sit side by side in /Applications under any name.
Both print the toolchain in use on every run. If the pinned version is not installed they fail with the list of what is, rather than silently building on a different Xcode.
Override for a one-off build:
XCODE_APP=/Applications/Xcode.app scripts/build.sh --debug
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer scripts/release.sh archiveEither variable may also live in .env.local. When bumping the pin, edit
.xcode-version; CI picks it up automatically when the runner image has that
Xcode, and warns instead of failing when it does not.
XcodeGen
XcodeGen checks converter availability before generation, then runs
scripts/finalize-project.sh after generation. The script uses
Apple's xcodebuild -convert-project 'Xcode Project' to write
project.xcproj inside the existing .xcodeproj bundle. Generation requires an
installed Xcode 27.2 or later; it uses DEVELOPER_DIR when suitable, otherwise
finds a compatible Xcode in /Applications. This selection applies only to the
conversion process and does not change the selected build toolchain. Set
XCODE_PROJECT_CONVERTER_DIR to require a specific converter without discovery.
If conversion fails, the finalizer restores the previous JSON (or the initial
PBX project) and retains failed output for diagnosis. Xcode 27
and later can read the generated JSON project. Do not run a PBX-only patch after
the finalizer.
The root project.yml defines all three targets (HomeClaw, macOSBridge, homeclaw-cli). The generated .xcodeproj is gitignored — regenerate after cloning:
xcodegen generateDevelopment Workflow
# Generate project + build debug
scripts/build.sh --debug
# Open in Xcode for debugging
xcodegen generate && open HomeClaw.xcodeproj
# Test HomeKit connection over the socket
echo '{"command":"status"}' | nc -U /tmp/homeclaw.sockCritical: Entitlements & Distribution
HomeKit is App Store-only on macOS
Apple restricts com.apple.developer.homekit to App Store distribution. It cannot be included in Developer ID provisioning profiles. This means:
- Development signing (
Apple Development): Works. Xcode automatic signing creates a provisioning profile with HomeKit. - Mac App Store: Works. Single Catalyst app with unified signing.
- Developer ID: Not supported. HomeKit entitlement cannot be included in Developer ID provisioning profiles.
Reference: Apple DTS confirmation — "The HomeKit entitlement is only available for App Store apps on macOS."
HomeKit entitlement file
The HomeKit entitlement is in Resources/HomeClaw.entitlements:
<key>com.apple.developer.homekit</key>
<true/>Do NOT remove this. Without it, HMHomeManager silently returns zero homes (even with a valid provisioning profile).
For other developers
Developers need their Apple Developer Team ID. The build script reads it from .env.local (gitignored), --team-id flag, or HOMEKIT_TEAM_ID env var:
# One-time setup: create .env.local from the example
cp .env.local.example .env.local
# Edit .env.local and set your Team ID
# Build
scripts/build.sh --release --install
# Or pass directly
scripts/build.sh --release --install --team-id YOUR_TEAM_IDXcode automatic signing creates the required provisioning profile for the developer's team. The team ID is passed to xcodebuild via DEVELOPMENT_TEAM.
Key Configuration
| Setting | Location | Default |
| Device filter | ~/Library/Application Support/HomeClaw/config.json | "accessoryFilterMode": "all" |
| Default home | ~/Library/Application Support/HomeClaw/config.json | First home |
| Webhook endpoint | config.json → webhook.webhookEndpoint | "/hooks/homeclaw" |
| Socket path | App Group container or /tmp/homeclaw.sock | Auto-detected |
Mapped webhooks: HomeClaw uses mapped webhooks (/hooks/homeclaw) instead of direct /hooks/wake or /hooks/agent calls. OpenClaw's hooks.mappings config routes events to a dedicated HomeClaw agent. See openclaw/openclaw#33271 for the /hooks/wake bug that motivated this change.
MCP Tools
The stdio MCP server (mcp-server/) wraps homeclaw-cli and exposes tools for home/room/accessory listing, accessory control, scene management, search, home structure management (rename, rooms, zones), and automation management. Tool schemas are defined in lib/schemas.js and handlers in lib/handlers/homekit.js.
Automations (Button Programming)
HomeClaw can create HomeKit automations for programmable switches via the CLI, MCP tools, or socket API. Two creation modes:
- Inline actions (
--action/actionsarray): Creates a scene named after the automation. Each action specifies an accessory, property, and value. Note: the Home app uses a private API for hidden automation-only action sets; our action sets are always visible as scenes. - Scene reference (
--scene/scene_id): Links to an existing named scene visible in the Home app.
Implementation: HomeKitManager.createAutomation() creates an HMEventTrigger watching the button's input_event characteristic linked to an HMActionSet. The resolveActions helper reuses the same accessory/characteristic resolution as importScene. Multi-button accessories use service_index to target specific buttons (e.g., Aqara AR009 fast mode: Button 1 = index 1, Button 2 = index 2).
Concurrency Model
HomeKitManageris@MainActor(required byHMHomeManager). Socket server uses GCD with semaphore+ResponseBox to bridge to MainActor.- Settings views use
@State+Taskfor async data loading. - Swift 6 strict concurrency is enabled (
SWIFT_STRICT_CONCURRENCY: complete).
Debugging
# Check HomeKit status via socket
echo '{"command":"status"}' | nc -U /tmp/homeclaw.sock
# Check entitlements on installed app
codesign -d --entitlements :- "/Applications/HomeClaw.app"
# Check TCC (privacy) permissions
sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
"SELECT client, auth_value FROM access WHERE service = 'kTCCServiceWillow'"
# View HomeClaw logs
log show --predicate 'process == "HomeClaw"' --last 10m --style compactIf status shows ready: false with 0 homes:
- Verify HomeKit entitlement is embedded (codesign check above)
- Verify TCC permission granted (auth_value=2)
- Verify iCloud is signed in with HomeKit data
- Restart the app after rebuilding
Code Style
- Swift 6 with strict concurrency
@MainActorfor all HomeKit API interactionsos.LoggerviaAppLogger(categories: homekit, app, socket, cli)- JSON communication over Unix domain sockets (newline-delimited)
CI
GitHub Actions (.github/workflows/tests.yml) runs on macos-26:
- Build — builds
homeclaw-clivia SPM, runsswift test, buildsmcp-server(Node.js) - Catalyst App — runs XcodeGen, selects the
.xcode-versionXcode when the runner has it, checks complete Node/native schema parity, and executes the fullHomeClawTestssuite viascripts/test-catalyst.shwithCODE_SIGNING_ALLOWED=NO. The script preserves xcodebuild's exit status and requires passing, nonempty xcresult evidence with no skipped tests. CI uploads the log and result bundle. Catalyst deployment targets support the macOS 26 runner. - Version Consistency — checks the plugin manifest versions agree
The two macOS jobs (Build, Catalyst App) run only on pull requests — macOS minutes bill at 10x Linux, and re-running them on the merge commit's push to main duplicated what the PR already proved. Pushes to main run only the Linux Version Consistency job.
CI builds and tests the Catalyst app unsigned only. The HomeKit entitlement
cannot be provisioned on a runner. Unit tests avoid live HomeKit operations;
passing them proves native code execution, not real accessory access, signing,
or distributability. A signed build still has to happen locally
(scripts/build.sh) or via scripts/release.sh.
Clawpatch Code Review
This repo uses Clawpatch for local automated code review. Keep .clawpatch/ ignored; it is generated runtime state containing features, findings, reports, runs, and patch attempts.
Standard workflow:
clawpatch doctor
clawpatch init # first time only
clawpatch map
clawpatch review --limit 10
clawpatch report --output .clawpatch/reports/summary.md
clawpatch show --finding <id>
clawpatch fix --finding <id>
clawpatch revalidate --finding <id>If this repo needs hand-authored feature coverage, keep those curated definitions in tools/clawpatch/features/ and sync/copy them into .clawpatch/features/ before review. Do not commit .clawpatch/ generated state.
<!-- BEGIN CLAUDE MEMORY IMPORT: -Users-omarshahine-GitHub-HomeClaw -->Imported Claude Project Memory
Durable memory promoted from ~/.claude/projects/-Users-omarshahine-GitHub-HomeClaw/memory during the AGENTS.md migration. Keep this section current when project-specific operating knowledge changes.
memory/MEMORY.md
HomeClaw Project Memory
Onboarding System (added 2026-02-27)
- Files:
OnboardingView.swift,HomeKitSetupView.swift,IntegrationSetupView.swiftinSources/homeclaw/Views/ - Scene delegate:
OnboardingSceneDelegateinHomeClawApp.swift(follows same pattern asSettingsSceneDelegate) - State tracking:
UserDefaults.standard.bool(forKey: "isOnboardingCompleted")— matches OnboardingKit convention - Auto-open: In
didFinishLaunchingWithOptions, checks the key and opens onboarding scene with 0.5s delay if incomplete - Multi-step flow: Welcome → HomeKit Status → Home Selection (if 2+ homes) → Integration Setup → All Set
- Main view struct:
OnboardingFlowView(notOnboardingViewto avoid conflicts with OnboardingKit internal type)
SwiftUI-Onboarding (OnboardingKit) Dependency
- v1.0.0 API is limited: Module name is
OnboardingKit, product isOnboardingKit - The
mainbranch has a newer API (WelcomeScreen.apple(),Onboardingmodule) but v1.0.0 doesn't FeatureInfohas public init but INTERNAL stored properties — can't read them back- Currently using the library for future compatibility; custom welcome screen built instead
- To reset onboarding for testing:
defaults delete com.shahine.homeclaw isOnboardingCompleted
Key Patterns
- Scene gating:
static var onboardingRequestedflag prevents UIKit scene session restoration from showing the window - Catalyst ObjC runtime:
activateIgnoringOtherApps:,center,orderFrontRegardlessfor window management in accessory mode - HomeKit observation:
.homeKitStatusDidChangenotification carriesreadyandhomeNamesin userInfo - Integration install methods:
IntegrationSetupViewreplicates the install logic fromIntegrationsSettingsView(posix_spawn, AppleScript for admin)
Release Workflow
Every App Store version must release automatically after approval (AFTER_APPROVAL). Both release and submit correct and verify the existing version policy before submitting, including submissions without metadata. Dry runs never change the policy.
- After every archive + upload, generate a TestFlight tester update (see
memory/testflight-updates.md) - Release tag must match the uploaded build number:
v{version}+{build}. The build number is committed first (scripts/release.sh bump-build, commit, tag, push), thenbetabuilds exactly that number - CLI binary is sandboxed (
com.apple.security.app-sandbox) — file-based commands can only read from App Group container - App Store Connect API setup —
scripts/release.sh(asc) drives TestFlight and App Store releases - Release pipeline —
scripts/release.shsubcommands, Mac Catalyst archive + .pkg export, credential loading
Feedback: Skill Invocation
- Use fully qualified skill names for commit — always
commit-commands:commit, never barecommit - Greptile MCP plugin fails for HomeClaw — use
gh apifallback, not MCP tools
Feedback: External APIs
- Aqara MCP stdio sticky-session — second switch_home in one stdio session is silently ignored; spawn fresh process per home
References: External APIs
- Aqara Open Cloud API intents — verified intents for device rename, room rename, room move; use
aqara_call_v3.pyfrom chief-of-staff plugin - Lutron Caseta LEAP write primitives — verified UpdateRequest/DeleteRequest/CreateRequest patterns for areas + device-area moves; use
lutronCLI ≥0.1.8 (subcommands:area,move)
Feedback: Build / Distribution
- SwiftTUI is incompatible with Mac App Store sandbox — gate TUI behind
#if !APP_STORE; sandbox is non-negotiable for MAS bundles
Cabin / HomeKit
- Shahine Cabin room ownership — Sarah=Double Bedroom, Miles=Twin Bedroom, "Bedroom Blinds"=Main Bedroom only
- Trigger-owned (hidden) action sets — Home app creates per-press button automations as HMActionSets excluded from
home.actionSets. Read-side: walkstrigger.actionSets, public API (shipped). Write-side: conclusively gated by Apple-onlycom.apple.homekit.private-spi-accessSPI entitlement (investigated + ruled out 2026-05-17). Seedocs/PRIVATE_API.mdto skip redoing the dig.
Fastmail/Chief-of-Staff Architecture (audited 2026-03-07)
Fastmail is NOT built into chief-of-staff. Instead:
- Separate repo:
fastmail-mcp-remote(Cloudflare Worker, JMAP adapter, OAuth) - HTTP MCP pattern: Chief-of-staff connects via
FASTMAIL_MCP_URLenv var (Bearer token auth) - OpenClaw plugin exists:
fastmail-cli(npm v2.1.0) — 36 agent tools, CLI-based, zero deps - Permission gating: 9 categories (EMAIL_READ, INBOX_MANAGE, SEND, REPLY, etc.) controlled server-side
- Private extensions: Chief-of-staff-private adds fastmail rules management (browser automation, agent-based)
- Token efficiency: CLI formats output as compact text (5-7x savings vs raw JSON)
- Status: Mature, production-ready, no new OpenClaw plugin needed
memory/asc-api-setup.md
name: App Store Connect API setup description: ASC API credentials and the asc-based release pipeline for HomeClaw builds type: reference originSessionId: 0b8a6eb4-64d3-4fe3-9fa5-24b075852c4d
App Store Connect API is configured for HomeClaw via the asc CLI (scripts/release.sh).
- Credentials:
ASC_KEY_ID,ASC_ISSUER_ID,ASC_PRIVATE_KEY_PATHfrom~/.zshrc; otherwiserelease.shsources~/.secrets-macbook-pro.env(APP_STORE_CONNECT_API_*) and.env.local(HOMEKIT_TEAM_ID, optionalASC_*) - API key file:
~/.private_keys/AuthKey_<ASC_KEY_ID>.p8 - App:
6759682551, bundlecom.shahine.homeclaw, ASC platformMAC_OS(Mac Catalyst on the Mac App Store; no iOS listing) - Groups: "Internal Testers" (beta uploads), "External Testers" (
external) - Profile:
com.shahine.homeclaw AppStore, typeMAC_CATALYST_APP_STORE
scripts/release.sh status [--build 197] # read-only: versions, builds, groups (also the auth check)
scripts/release.sh bump-build # then commit "chore(release): build N", tag v<ver>+N, push
scripts/release.sh beta --dry-run # archive + export .pkg + asc xcode validate, no upload
TF_CHANGELOG="$(cat /tmp/notes.txt)" scripts/release.sh beta # -> Internal Testers
scripts/release.sh external [--build N] # -> External Testers + beta review (was submit_only)
scripts/release.sh archive # archive only, no uploadThe /upload command drafts tester notes from git log and runs beta then external via Monitor.
memory/asc-release-pipeline.md
name: Release pipeline (asc) description: HomeClaw release tooling is scripts/release.sh on the asc CLI (replaced fastlane on 2026-09-29, which had replaced scripts/archive.sh + scripts/asc-testflight.py on 2026-05-08) type: project
HomeClaw's release pipeline is scripts/release.sh, driving asc. Fastlane (Fastfile/Gymfile/Appfile, Gemfile) was removed on 2026-09-29; fastlane/ became appstore/ (metadata, raw screenshots/, framed/). Guide: appstore/RELEASE.md.
How to apply:
- Subcommands:
info | status | bump-build | archive | beta | external | metadata | metadata-pull | release | submit | screenshots | frame | upload-screenshots betastops at TestFlight Internal Testers;externalsends to External Testers;releasesubmits to App Review with automatic release after approval;submitenforces the same policy without pushing metadatapreparerunsxcodegen generate+npm ci+npm run build:mcp(and requires the committedmcp-server/dist/server.jsto match) before every archive- Mac Catalyst archive: destination
generic/platform=macOS,variant=Mac Catalyst; export is a.pkgwith manual signing (Apple Distribution + 3rd Party Mac Developer Installer) - Marketing version comes from the nearest
v*git tag with+buildstripped (Apple rejects 4-component versions) - Build number is
CFBundleVersionin the committedResources/Info.plist;betaseeds.build-numberso project.yml's pre-build script lands on it, and restores the plist afterwards - HomeKit entitlement is verified before archive AND on the signed app post-archive;
homeclaw-climust be sandboxed (ASC error 90296) - The Xcode comes from
.xcode-version; beta Xcodes are refused
memory/feedback_aqara_mcp_sticky_session.md
name: feedback-aqara-mcp-sticky-session description: Aqara MCP stdio binary (aqara-mcp-server) silently ignores the second switch_home call in one stdio session — always spawn a fresh process per home metadata: node_type: memory type: feedback originSessionId: c361650b-0648-474b-a31d-454a3bb334f5
The Aqara MCP stdio binary at ~/.local/bin/aqara-mcp-server run stdio has a sticky-session bug: the first switch_home call works correctly, but any subsequent switch_home in the same stdio session returns "Successfully switched" while the next device_query continues to return data from the originally selected home.
Why: Verified 2026-05-26 during HomeKit cleanup work — same stdio session that switched to "Home" then to "Shahine Cabin" returned identical 30-device main-residence data for both device_query calls.
How to apply: When querying multiple Aqara homes, spawn one fresh stdio invocation per home (single initialize → switch_home(target) → device_query per process). Do not batch multiple homes into one heredoc. The fresh-process pattern is what chief-of-staff/skills/home-ecosystem/scripts/fetch_aqara.py already does — follow that shape if writing new tooling.
Bonus context: Aqara backend home names + their device contents are independent — renaming a home in the Aqara app does not move devices between homes. If labels and contents disagree, the user must move devices manually OR rename strategically.
Related: [[shahine_cabin_rooms]]
memory/feedback_commit_skill_name.md
name: Use fully qualified skill names for commit description: Always use commit-commands:commit, never bare commit, when invoking the commit skill type: feedback
Always use commit-commands:commit (fully qualified) when invoking the commit skill. The bare name commit fails with "Unknown skill: commit".
Why: Skills are namespaced under their plugin. Claude Code requires the plugin:skill format. The bare name doesn't resolve.
How to apply: Any time you need to commit, use Skill("commit-commands:commit"). Same pattern for other commit-commands skills: commit-commands:commit-push-pr, commit-commands:clean_gone, etc.
memory/feedback_greptile_mcp.md
name: Greptile MCP plugin fails for HomeClaw description: Greptile MCP tool returns "Repository not found" for HomeClaw; use gh api fallback instead type: feedback
The Greptile MCP plugin (mcp__plugin_greptile_greptile__get_merge_request etc.) does not work for HomeClaw. It returns "Repository not found: omarshahine/HomeClaw on github". The Greptile GitHub App does review PRs successfully (as a check), but the MCP tools can't query it.
Why: The repo may not be indexed in Greptile's system, or the MCP token doesn't have access. The GitHub App integration works independently.
How to apply: When waiting for Greptile reviews on HomeClaw PRs, skip the MCP plugin entirely. Poll with gh pr checks for completion, then fetch comments via gh api repos/omarshahine/HomeClaw/pulls/{n}/comments and gh api repos/omarshahine/HomeClaw/issues/{n}/comments.
memory/feedback_swifttui_sandbox.md
name: SwiftTUI is incompatible with Mac App Store sandbox description: PR #55's TUI broke TestFlight uploads — sandbox is non-negotiable for MAS bundles, SwiftTUI's tcsetattr() fails silently under it type: feedback originSessionId: f43e9b52-76c8-40d8-ae02-2f798d72578b
SwiftTUI calls tcsetattr(STDIN_FILENO, ...) to enter raw mode. Under App Sandbox the call returns success but the terminal stays in cooked mode — the TUI renders but arrow keys/Enter leak through as raw ^[[A^[[B escape sequences.
Why: Confirmed empirically on 2026-05-09 while shipping build 157. Build 154 archived fine but ASC rejected it with error 90296 ("App sandbox not enabled" on homeclaw-cli). PR #55's claim that "Apple permits unsandboxed bundled CLI helpers — Xcode's git is the canonical example" is wrong for Mac App Store distribution: every bundled executable must have com.apple.security.app-sandbox=true or upload fails.
How to apply:
- Mac App Store distribution → CLI must have
ENABLE_APP_SANDBOX: YES. No exceptions. - TUI features that need raw terminal mode must be gated behind
#if !APP_STORE(we setSWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) APP_STORE"on the homeclaw-cli Release config). - The TUI still works for
swift run homeclaw-cli uifrom source — SPM doesn't apply the sandbox or the APP_STORE flag. - If you want a sandbox-compatible TUI later, options are: TermKit (ncurses-backed, more mature) or Bubble Tea (Go); SwiftTUI is a no-go.
- Adding the APP_STORE flag is the right pattern for any future code paths that need to differ between Mac App Store builds and source/dev builds.
memory/private_api_hidden_action_sets.md
name: private-api-hidden-action-sets description: "HomeKit private API for trigger-owned (hidden-from-Home-app) action sets — what we observed when the Home app creates per-press button automations, and how HomeClaw should treat them." metadata: node_type: memory type: project originSessionId: 38a6bec3-c4a5-4e9a-867f-e165352610ba
Hidden Action Sets (HMActionSetTypeTriggerOwned)
When the Apple Home app creates a per-press automation on a programmable switch (e.g., "Single Press do X"), it creates an HMActionSet of type HMActionSetTypeTriggerOwned via a private selector on HMHome (signature similar to addActionSetWithName:actionSetType:completionHandler:). The TriggerOwned constant exists in the HomeKit framework but is not declared public.
Why: Use the private API in non-App-Store builds (Debug + local Release) so HomeClaw's CLI/MCP can create automations without polluting the user's Scenes list. App Store archives must strip the flag — Apple rejects uploads that reference the selector or constant.
How to apply:
What we proved (2026-05-17, Shahine Cabin, Kitchen Blinds Button)
We snapshotted button B9A5C450-5802-51FA-82AA-5EAE233AB8AD before + after Omar manually rewired its 3 automations through the Home app UI. Diff at /tmp/homeclaw-private-api-snapshot/{before,after}/summary.json.
| Aspect | Public API (home.addActionSet(withName:)) | Hidden (created by Home app per-button automation) |
Appears in home.actionSets | ✅ yes | ❌ no |
homeclaw-cli scenes (pre-fix) | ✅ yes | ❌ silently missing |
homeclaw-cli scenes (post-fix) | ✅ yes, no hidden field | ✅ yes, with "hidden": true |
Reachable via trigger.actionSets | ✅ yes | ✅ yes |
actionSetType value | HMActionSetTypeUserDefined | HMActionSetTypeUserDefined (same!) |
Action set name | what you set | opaque UUID string (Home app generated) |
| Visible as scene tile in Home app | ✅ yes (annoying) | ❌ no (goal) |
Critical correction to original hypothesis: hidden action sets are NOT HMActionSetTypeTriggerOwned. They report actionSetType == HMActionSetTypeUserDefined — identical to visible action sets. The hidden behavior comes solely from being attached to a trigger and excluded from home.actionSets enumeration (Home app filters that list). The "trigger-owned" name is colloquial only.
Implication for the write-side: the trick may not be a different type constant — it may be a private creation path that registers the action set with a trigger atomically AND keeps it out of home.actionSets. To verify, we'd need to runtime-inspect the private selector Home app actually uses (likely on HMHome taking a trigger reference, e.g. _addActionSet:toTrigger: or similar).
Read-side (always-on, App Store safe)
home.actionSets does NOT include trigger-owned sets. To enumerate or inspect them:
- Walk
home.triggers, then each trigger's.actionSetsproperty - Same
HMActionSetAPI — actions, name, uniqueIdentifier all work normally - The
actionSetTypeproperty returnsHMActionSetTypeTriggerOwned(public string constant — safe to read)
homeclaw-cli scenes and get-scene should fall back to scanning trigger.actionSets when a scene isn't in home.actionSets. Tag results with "hidden": true and "type": "trigger_owned".
Write-side: GATED BY SPI ENTITLEMENT (conclusively investigated 2026-05-17)
The HomeKit daemon (homed) checks com.apple.homekit.private-spi-access on
the calling app for any mutation to a trigger-owned action set. This entitlement
is Apple-signed-apps-only — not in the com.apple.developer.homekit family,
not grantable via Apple Developer Program provisioning.
Confirmed by codesign -d --entitlements - /System/Applications/Home.app.
What works without the SPI
- Read all trigger-owned action sets (walk
home.triggers→trigger.actionSets) - Create empty trigger-owned action set via private
HMTrigger.addActionSetWithCompletionHandler:— persists, but is useless because it can't be populated - Modify a trigger-owned set's local in-memory
actionsarray via_doAddAction:uuid:— does NOT sync to homed, reverts on app restart
What does NOT work
- Populate (
addAction:,_addAction:) — fails with "Missing entitlement for API" - Any persisting mutation of a trigger-owned action set
Decision: rollback
Write-side code (PrivateActionSets.swift, HOMECLAW_PRIVATE_API flag,
createAutomation private branch) was rolled back. The investigation lives in
docs/PRIVATE_API.md so we don't redo it. Open Apple Feedback FB7775451 tracks
the policy if Apple ever opens it up.
Cleanup note
Two orphaned user_defined scenes from the old public-API setup linger in home.actionSets: BD006088-E998-5022-9D0E-AC1FCD9EB571 (Kitchen Blinds Button Single Press) and 9EDAED55-98B1-54E2-B865-2BEF24949DD3 (Kitchen Blinds Button Double Press). Safe to delete — no longer referenced.
Related
- [[onboarding-system]] — unrelated but tracks similar Catalyst-specific HomeKit quirks
- Code:
Sources/homeclaw/HomeKit/HomeKitManager.swift:1200-1210(current public-onlyaddActionSetcall site) - Code:
Sources/homeclaw/HomeKit/AccessoryModel.swift:679-690(actionSetTypehelper — already aware of public type constants)
memory/reference_aqara_open_api.md
name: reference-aqara-open-api description: "Aqara Open Cloud API intents we've verified for HomeKit / Aqara reconciliation work (rename device, move device, rename room, list rooms)" metadata: node_type: memory type: reference originSessionId: c361650b-0648-474b-a31d-454a3bb334f5
The chief-of-staff plugin's aqara_call_v3 module (at ~/GitHub/chief-of-staff/plugins/chief-of-staff/skills/home-ecosystem/scripts/aqara_call_v3.py) wraps the Aqara Open Cloud API. Auth is via aqara_api_key + aqara_base_url + aqara_open_* env vars (loaded from .secrets-macbook-pro.env).
Working intents (verified 2026-05-26)
| Intent | Purpose | Required payload | Notes |
query.device.info | List all devices in account | {} | Returns 41+ devices across all homes (filter by positionId for one home) |
config.device.name | Rename a device | {"did": "<did>", "name": "<new>"} | Used by rename_device() helper |
config.device.position | Move device to another room | {"dids": ["<did>"], "positionId": "<pos_id>"} | ⚠ MUST be dids (list), not singular did. The helper update_device_position() had this bug — fixed in PR #25 |
query.position.info | List positions (homes or rooms) | {"parentPositionId": "<home_pos>", "pageNum": 1, "pageSize": 100} for rooms; no parent = top-level homes | Position IDs look like real1.xxx (home) or real2.xxx (room) |
config.position.update | Rename a position (home or room) | {"positionId": "<pos_id>", "positionName": "<new>"} | ⚠ config.position.name returns 403 — use config.position.update |
Position IDs cached for this account
- Home (top-level):
real1.999309768942440448 - Shahine Cabin (top-level):
real1.1074271456971436032
Room IDs vary; query with query.position.info + parent home id.
Aqara MCP stdio binary
~/.local/bin/aqara-mcp-server run stdio exposes a subset of these as MCP tools (device_query, device_control, switch_home, etc.) but no rename / position-update tools. For renames, call the REST API directly via aqara_call_v3.call("<intent>", {...}).
Also note: [[feedback_aqara_mcp_sticky_session]] — the second switch_home in one stdio session is silently ignored.
memory/reference_lutron_leap_primitives.md
name: reference-lutron-leap-primitives description: "LEAP protocol primitives for Lutron Caseta area + device-area writes (rename, delete, create, move) — now exposed via lutron-cli ≥0.1.8" metadata: node_type: memory type: reference originSessionId: c361650b-0648-474b-a31d-454a3bb334f5
The Lutron Caseta bridge speaks LEAP (a custom JSON-over-TLS protocol). pylutron_caseta's Smartbridge._request(verb, href, body=None) is the underlying primitive — most reads/writes route through it. Verified live against an L-BDG2-WH bridge.
Verified write paths
| Operation | LEAP call | Body |
| Rename device | UpdateRequest /device/<id> | {"Device": {"Name": "<new>"}} |
| Move device to another area | UpdateRequest /device/<id> | {"Device": {"AssociatedArea": {"href": "/area/<id>"}}} |
| Rename area | UpdateRequest /area/<id> | {"Area": {"Name": "<new>"}} |
| Delete area | DeleteRequest /area/<id> | (none) |
| Create area | CreateRequest /area | {"Area": {"Name": "<n>", "Parent": {"href": "/area/<parent>"}}} |
After mutating, the bridge auto-recomputes FullyQualifiedName ([area_name, device_name]) on affected devices.
Gotchas
- Area-existence check is two-step. Some Caseta firmware versions answer not-found area reads with a 200 + empty body rather than raising. Always check
if response.Body.get("Area") is None: raise. config.device.position-style payload doesn't apply — that's the Aqara API. For Lutron, use theAssociatedArea.hrefpattern above.- Picos don't bridge to HomeKit — they exist in Lutron but HomeKit sees only WallDimmer/WallSwitch/Shade/Doorbell/etc.
- The
lutronCLI ≥ 0.1.8 wraps all of these as proper subcommands:lutron rename,lutron move,lutron area rename/delete/create/list. Prefer those over ad-hocbridge._requestscripts.
Smoke-test pattern (for new bridges or after upgrades)
import asyncio, sys
sys.path.insert(0, '/Users/omarshahine/GitHub/lutron-cli/src')
from lutron_cli.bridge import open_bridge
async def main():
async with open_bridge("192.168.1.57") as bridge:
r = await bridge._request("ReadRequest", "/area/1")
print(r.Body)
asyncio.run(main())memory/shahine_cabin_rooms.md
name: Shahine Cabin room ownership description: Which HomeKit room belongs to which family member at the cabin type: project originSessionId: 7a734e43-23a5-4874-9abf-0f9f7daf1ca0
Shahine Cabin HomeKit room mapping (confirmed 2026-04-26):
- Sarah = Double Bedroom (has Double Blinds + Transom Blinds + Overhead light)
- Miles = Twin Bedroom (has Twin Bedroom North + South Blinds + Overhead light)
- Owners' suite = Main Bedroom (Main Bedroom Blinds + Overhead + Closet Light)
Why: Several scenes named "Sarah" / "Miles" target their bedrooms. Several button automations are also named "Sarah's Bedroom Button" / "Miles's Bedroom Button" but live in the generically-named rooms.
How to apply: When a scene name says Sarah → target Double Bedroom devices. When it says Miles → target Twin Bedroom. "Bedroom Blinds" (without name) refers to Main Bedroom only — owners' suite, not all bedrooms.
memory/testflight-updates.md
TestFlight Tester Update Template
After every release build + upload, generate a tester update for App Store Connect.
Where to Post
App Store Connect > Apps > HomeClaw > TestFlight > (select build) > What to Test
Template Format
Build {BUILD_NUMBER} — {SHORT_TITLE}
New in this build:
- **{feature_1}** — {description}
- **{feature_2}** — {description}
What to test:
1. {specific test step}
2. {specific test step}
3. Verify existing commands still work as expected
{any caveats or known issues}Key Points
- Keep it concise — testers skim
- Lead with what's new, then what to test
- Mention
--dry-runwhen available so testers don't accidentally modify their HomeKit setup - Note sandbox restrictions if file-based commands are involved
- No API access configured — generate text for manual paste into ASC
- ASC API key exists at
~/.appstoreconnect/private_keys/AuthKey_[REDACTED:APP_STORE_CONNECT_API_KEY_ID].p8but issuer ID is unknown
Release Workflow Checklist
- Update README with new features
- Merge PR to main
scripts/release.sh bump-build, commit it, and tagv{version}+{build}- Upload:
scripts/release.sh beta(thenscripts/release.sh externalfor external TestFlight); the tag must be on the build-bump commit first - Create GitHub release:
gh release create ... - Generate tester update text for App Store Connect
<!-- END CLAUDE MEMORY IMPORT: -Users-omarshahine-GitHub-HomeClaw --><!-- BEGIN CLAUDE MEMORY IMPORT: -Users-omarshahine-GitHub-HomeKitBridge -->Imported Claude Project Memory
Durable memory promoted from ~/.claude/projects/-Users-omarshahine-GitHub-HomeKitBridge/memory during the AGENTS.md migration.
memory/MEMORY.md
HomeKit Bridge Memory
Critical: XcodeGen and Entitlements
NEVER run xcodegen without verifying entitlements afterward. XcodeGen regenerates HomeKitHelper.entitlements from project.yml. If the entitlements.properties block is missing or wrong, it writes an empty <dict/>, silently stripping com.apple.developer.homekit. This causes readValue() to return nil for all characteristics — accessories are enumerable but all values are nil.
The project.yml now has properties: com.apple.developer.homekit: true and the build script validates the entitlement before building. But always double-check after running xcodegen.
Build Commands
scripts/build.sh --debug --skip-helper— fast SPM-only iterationscripts/build.sh --release --install— full build + install to /Applications- After adding new files to
Sources/HomeKitHelper/, must runcd Sources/HomeKitHelper && xcodegenthen verify entitlements
Architecture Notes
HomeKitManageris@MainActor— long-running operations (like cache warming) needTask.yield()between iterations to avoid starving socket request processingHelperSocketServerbridges GCD → MainActor via semaphore — MainActor starvation causes socket request timeouts and UI freezes
<!-- END CLAUDE MEMORY IMPORT: -Users-omarshahine-GitHub-HomeKitBridge -->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.

