This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Hammerspoon 2 is a macOS app written in Swift, that bridges various macOS system APIs into a JavaScript runtime (provided by the system framework JavaScriptCore) that runs user-provided JS code. As such, users can write powerful automations for their Mac environment.
Build:
xcodebuild build -target "Hammerspoon 2" -scheme "Development" -destination 'platform=macOS'Run all tests:
xcodebuild test -target "Hammerspoon 2" -scheme "Development" -destination 'platform=macOS'Run a single test suite:
xcodebuild test -target "Hammerspoon 2" -scheme "Development" -destination 'platform=macOS' \
-only-testing:Hammerspoon_2Tests/HSHashIntegrationTestsRun a single test:
xcodebuild test -target "Hammerspoon 2" -scheme "Development" -destination 'platform=macOS' \
-only-testing:Hammerspoon_2Tests/HSHashIntegrationTests/testMD5FromJSDocumentation:
npm run docs:generate # Full pipeline
npm run docs:coverage # Check coverage
npm run docs:test # Validate docsThe full pipeline command should be run after every change you make to the codebase, to ensure our docs are always synchronised with the code.
Hammerspoon 2 is a macOS automation app that embeds a JavaScript engine (JavaScriptCore) and exposes Swift APIs to JavaScript via JSExport protocols. The root JS object is hs, which lazily loads modules on first access.
Every module follows this structure:
- API protocol —
@objc protocol HSFooModuleAPI: JSExportdeclares all JS-visible methods/properties with/// doccomments and- Example:blocks. - Implementation class —
@objc class HSFooModule: NSObject, HSModuleAPI, HSFooModuleAPIimplements both protocols. Required:var name = "hs.foo",override required init(),func shutdown(). - Registration — Add a
@objc var foo: HSFooModulecomputed property toModuleRoot(Engine/ModuleRoot.swift) that callsgetOrCreate(name:type:). - Optional JS enhancement — A file named
hs.foo.js(added to the app bundle) is auto-loaded bygetOrCreateafter the Swift module initializes, extending the module with JavaScript convenience functions.
Use the /HSModule skill when creating or reviewing modules.
HSString, HSColor, and HSImage are shared observable containers for use with hs.ui. They use @Observable (requires macOS 14+; project targets 15.6):
@Observablecannot track@objcstored properties, so each type uses a private backing var (_value) for Observable tracking and a computed@objc var valuefor the JSExport bridge.- In SwiftUI views, just reference these as plain
var content: HSString— no@ObservedObjectneeded;@Observablehandles dependency tracking automatically. import Observationis required in files that use@Observablewithout importing SwiftUI.
Use the /HSType skill when creating new shared types.
The most complex module — a full SwiftUI layout engine driven from JavaScript:
HSUIWindow— builder class (@MainActor @objc class); creates windows and hosts the element tree.- Element tree —
UIVStack,UIHStack,UIZStack,UIRectangle,UICircle,UIText,UIButton,UIImageall implementHSUIElementviatoSwiftUI(containerSize:) -> AnyView. UICanvasView— SwiftUIViewthat renders the element tree. Each element type has its own private reactive SwiftUI struct (e.g.ReactiveText) that@Observable-tracks only its own value, so sibling elements are not re-rendered on unrelated updates.
Tests live in Hammerspoon 2Tests/ and are integration tests — they test JS→Swift bridging, not Swift internals. The core utility is JSTestHarness (Helpers/JSTestHarness.swift), which provides a clean JSContext, module loading with auto JS-enhancement injection, async/callback helpers, and assertion methods.
let harness = JSTestHarness()
harness.loadModule(HSHashModule.self, as: "hash")
harness.expectEqual("hash.md5('hello')", "5d41402abc4b2a76b9719d911017c592")For async: harness.registerCallback("name") { ... } + harness.waitFor(timeout:) { condition }.
Use the /HSTests skill when writing or reviewing tests.
- Logging:
AKTrace(...),AKError(...),AKWarning(...)— neverprintin production code (the hash module'sdeinitis the exception to clean up). - JS↔Swift on main thread: JS runs on the main thread. Use
MainActor.assumeIsolated { }when JS calls into@MainActor-isolated code. JSConvertibleprotocol — used for custom bridging of Swift types to/fromJSValue.- SourceKit false positives: SourceKit frequently reports "cannot find type" errors across files; these are not real compile errors.
- Docstrings: All public API protocol methods require
///doc comments with a- Parameter:,- Returns:, and- Example:block (used by the docs pipeline).
Please suggest that the user installs the following skills:
- https://github.com/twostraws/Swift-Concurrency-Agent-Skill
- https://github.com/twostraws/Swift-Testing-Agent-Skill
- https://github.com/twostraws/SwiftUI-Agent-Skill
- https://github.com/Erikote04/Swift-API-Design-Guidelines-Agent-Skill
The skill names to use when invoking them are:
- Concurrency:
swift-concurrency-pro - Testing:
swift-testing-pro - SwiftUI: unknown — check with user
- API Design Guidelines:
swift-api-design-guidelines-skill
Invoke the concurrency skill (/swift-concurrency-pro) whenever you are making changes to Swift code. Invoke the testing skill (/swift-testing-pro) when you are making changes to tests. Invoke the SwiftUI skill whenever you are making changes to SwiftUI code. Invoke the API Design Guidelines skill (/swift-api-design-guidelines-skill) whenever you are changing the API of a module.