# Reported issues for hammerspoon-mcp

Pod holds 18 of 32 problems reported by people outside the maintainer team. Issues filed by the project's own owners, members and collaborators are excluded entirely — a maintainer's release checklist is not a warning to a prospective user.

Back to [hammerspoon-mcp](/mcp/hammerspoon-mcp).

## Most discussed

### Fix tools hs_ui_press label chain diverges from hs_ui_inspect

Found reviewing the #27 follow-up (`6a30a9e`).

`hs_ui_press` builds the label it compares `expectLabel` against from **three** attributes. `hs_ui_inspect` builds the label it reports from **five**. Any element whose label comes from the last two can never satisfy the guard, so the press is refused every time, and the error tells the caller to re-inspect - which returns the same label.

### Verified

`src/tools/safe/accessibility.ts:61-67`:

```lua
local function labelOf(element)
  return attr(e

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/31) · 2026-08-14 · closed · 2 comments

### Fix bridge harden against hs CLI client-side crashes and IPC failure windows

### Context
Hammerspoon crashed during development on 2026-08-11 with `EXC_BREAKPOINT` in `ipc_sendMessage` -> `CFMessagePortSendRequest` -> `__CFCheckCFInfoPACSignature`, a pointer-authentication failure on an invalidated Mach port. The trigger appears to be `hs.reload()` running while another IPC call was in flight: the config was being saved (auto-reload) while an integration test run was hammering `hs -c`.

This is Hammerspoon fragility rather than a defect in this server, but `hs_reload_con

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/13) · 2026-08-11 · open · 2 comments

### Chr publish v0.1.0 to npm and register in MCP directories

### Context
Discovery is most of the value for a niche tool. The name `hammerspoon-mcp` is already taken on npm by another project, so this publishes under the scope `@vukvukovich/hammerspoon-mcp`. The GitHub repository and the MCP registry both namespace by owner, so the plain name is retained in those two places.

### Goal
An installable, discoverable v0.1.0 that a Hammerspoon user searching for MCP support will actually find.

### Acceptance criteria
- [ ] Version bumped in package.json, with

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/11) · 2026-08-11 · closed · 2 comments

### Opt bridge codec cleanups: normalize the unencodable flag once

Found in the second review pass over the #29 plumbing. Small cleanups in `src/bridge/codec.ts`, none load-bearing.

### The unencodable flag is normalized twice

`readEnvelope` coerces the flag to a mandatory boolean, then `envelopeToResult` re-coerces that boolean to optional-true via a two-branch ternary that duplicates `ok: true, value: envelope.value`. Two representations of one flag (`LuaEnvelope`'s `unencodable: boolean` vs `BridgeResult`'s `unencodable?: true`) and two conversion points t

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/37) · 2026-08-14 · closed · 1 comment

### Opt tools hs_ui_press verification should be structural, not advisory

Follow-up to #27 (`6a30a9e`). The identity guard works, but its guarantees are weaker than they read, in three ways. None is a bug in what the code does - each is a gap between what it does and what it appears to promise.

### 1. Verification is opt-in, enforced by prose

A press with neither `expectLabel` nor `expectRole` proceeds exactly as before, reporting `verified: false`. The only thing demanding an expectation is the ALL-CAPS instruction in the tool description - so the safety property h

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/36) · 2026-08-14 · closed · 1 comment

### Opt tools one vocabulary for values that cannot be represented

Found in the second review pass. Cleanup, not a defect - but it is one concept shipped as two wire shapes in a single release, and the next tool with the same problem will coin a third.

### The duplication

"This value could not be represented; here is its string form, plus a hint" exists twice:

- `src/tools/registry.ts:112` (fromBridge's unencodable branch): `{ value, encodable: false, hint }`
- `src/tools/unsafe/applescript.ts:64` (built in Lua): `{ raw, representable: false, hint }`

Differ

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/35) · 2026-08-14 · closed · 1 comment

### Fix bridge a throwing __tostring escapes the codec fallback and destroys the envelope

Found in the second review pass over `da5d14c..HEAD`. Pre-existing, but the #29 fix makes the codec's fallback path the sole producer of the `unencodable` flag, so its one hole now matters.

### Verified live

In `buildProgram` (`src/bridge/codec.ts:116`), the fallback encodes the tostring form:

```lua
local __ok2, __alt = pcall(hs.json.encode, { ok = true, value = tostring(__res), unencodable = true })
```

`pcall` protects `hs.json.encode` - but `tostring(__res)` is evaluated **while building

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/34) · 2026-08-14 · closed · 1 comment

### Tst hs_ui_press guard has no automatic coverage and its test is destructive

Found reviewing the #27 fix (`6a30a9e`). The guard that stops `hs_ui_press` clicking the wrong element is a security property, and nothing that runs automatically checks it.

### No automatic coverage

`grep -rn "expectLabel\|expectRole\|verified" test/unit/` returns nothing. The only coverage is `test/integration/bridge.test.ts:640-720`, and:

- the file is wrapped in `describe.skipIf(!available)` (line 30) and its own header says these tests never run in CI
- `npm test` runs the unit project o

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/33) · 2026-08-14 · closed · 1 comment

## Most recent

### Fix tools hs_applescript follow-ups: surrogate escapes, missing value, dead branch

Found reviewing the #28 fix (`0f28b4a`). Three defects in the new code, all verified live against Hammerspoon on macOS 26.

### 1. The `\Uxxxx` decoder splits surrogate pairs

`readable()` (`src/tools/unsafe/applescript.ts:39-44`) decodes each `\Uxxxx` escape independently. NSError descriptions are UTF-16, so any non-BMP character arrives as a **surrogate pair** of two escapes. Decoding each half separately produces two lone surrogates - CESU-8, not valid UTF-8.

Live, from a real error dictiona

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/32) · 2026-08-14 · closed · 1 comment

### Doc tools hs_eval returns only the first of several return values

Found in the gated-tier test pass. Minor, but it is a silent truncation.

`return 1, 2, 3` yields `1`. The other two are dropped without comment, because the codec wraps the body in `pcall(function() ... end)` and keeps a single result.

Lua's multiple returns are idiomatic (`local ok, err = pcall(...)`, `string.find` returning two indexes), so an agent writing Lua will hit this. The tool's description says "Use \`return\` to produce a value", which is technically singular but easy to read past.

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/30) · 2026-08-14 · closed · 1 comment

### Fix bridge the unencodable flag is dropped before it reaches the caller

Found in the gated-tier test pass, but it affects every tool.

The wire protocol has three shapes, and ARCHITECTURE.md documents the third:

```json
{ "ok": true, "value": "hs.window: Safari", "unencodable": true }
```

The Lua side produces it correctly. The TypeScript side then throws the flag away:

```ts
export function envelopeToResult(envelope, toLuaError) {
  if (envelope.ok) return { ok: true, value: envelope.value };  // unencodable lost
  ...
}
```

`LuaEnvelope` even declares the fiel

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/29) · 2026-08-14 · closed · 1 comment

### Fix tools hs_applescript throws away every error detail

Found in the gated-tier test pass. Every AppleScript failure returns the bare string `AppleScript failed`, with nothing after the colon, so the caller learns only that something went wrong.

Observed for all of: syntax error, `error "deliberate failure"`, undefined variable, division by zero, and a valid-looking record literal. Five different causes, one indistinguishable message.

### Root cause, verified

`hs.osascript.applescript(source)` returns **three** values: `ok`, `result`, `descriptor`

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/28) · 2026-08-14 · closed · 1 comment

### Fix tools hs_ui_press presses the wrong element when indexes shift

Found in the gated-tier test pass. This is the most dangerous defect found so far: a destructive tool that presses something other than what the caller intended, and reports success.

### Reproduction, on Calculator

1. Inspect Calculator's button row. Index 7 is the button labelled `7`; index 12 is `5`.
2. `hs_ui_press { path: "/1/1/1/1/1/7", app: "Calculator" }` -> presses `7`. Correct.
3. `hs_ui_press { path: "/1/1/1/1/1/12", app: "Calculator" }` -> **presses `6`**, returns `{ label: "6", ...

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/27) · 2026-08-14 · closed · 1 comment

### Opt tools poll instead of fixed settle sleeps and share one sleep helper

Found by code review of the v0.2.0 changes. Quality, not correctness.

### 1. hs_move_window pays worst-case latency on every call
The fixed 400ms settle runs before every read-back although the measured correction window starts at ~40ms and most moves are never adjusted. Poll the frame every ~100ms and return as soon as it matches the request (same early-exit pattern waitForSpace already uses in spaces.ts); unadjusted moves finish in ~100ms with the same 400ms worst case.

### 2. Hand-rolled sl

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/26) · 2026-08-13 · closed · 1 comment

### Tst restore the per-tool bridge call-count guard

Found by code review of the v0.2.0 changes.

### Root cause
The 'runs a static Lua program' unit test was relaxed from exactly-one bridge call to at-least-one so the new read-back handlers (hs_move_window, hs_goto_space, hs_music_control) could pass — but the relaxation applies to every tool, so a single-call tool that regresses into issuing extra side-effecting programs per invocation (a double hs_launch_app, say) no longer fails anything.

### Fix
A per-tool expected-call-count map: default 1,

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/25) · 2026-08-13 · closed · 1 comment

### Fix tools remaining output gaps: empty sub-tables, post-skip track, scheme case

Found by code review of the v0.2.0 changes. Follow-up to #18; three smaller correctness gaps.

### 1. hs_machine_status: battery and audio can still encode as []
The empty-table-encodes-as-[] fix was applied only to the network sub-table (by adding an always-present field). battery on a desktop Mac and audio when the device reads fail can still hold only nils and encode as a bare array. Fix the mechanism: every sub-table carries at least one always-present field (e.g. `present = ... ~= nil`).

#

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/24) · 2026-08-13 · closed · 1 comment

### Fix tools hs_reload_config anchor the reload timer against garbage collection

Found by code review of the v0.2.0 changes.

### Root cause
RELOAD_LUA discards the hs.timer.doAfter return value. An unanchored Hammerspoon object can be garbage-collected before it fires, and this very PR documents that exact footgun for hs.task in shortcuts.ts (a collected started task is killed mid-run; a collected timer simply never fires). The 0.15s -> 0.5s delay change widened the window.

### Failure scenario
hs_reload_config returns 'Reload scheduled', a GC cycle collects the timer insi

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/23) · 2026-08-13 · closed · 1 comment

### Fix tools hs_speak substring match uses short names and exact match hides duplicates

Found by code review of the v0.2.0 changes. Two holes in the new voice validation, both re-opening the silent-wrong-voice bug (#17) it was built to close.

### 1. Single substring match resolves to the short name
The unambiguous-substring path sets `voiceName = matches[1]`, the short-list entry, while the exact-match path deliberately maps to the full identifier because hs.speech.new silently falls back to the default voice for names it cannot resolve. 'Dani' -> 'Daniel' -> hs.speech.new('Daniel

[Read the thread](https://github.com/vukvukovich/hammerspoon-mcp/issues/22) · 2026-08-13 · closed · 1 comment

The remaining reports are on [the project's issue tracker](https://github.com/vukvukovich/hammerspoon-mcp/issues).
