Hammerspoon Config Reload Loops After Plugin Updates and the Lua Sandbox Cleanup Users Applied to Fix It
Over the years, Hammerspoon has remained one of the most powerful automation tools for macOS, allowing users to customize their workflows through Lua scripting. However, with great extensibility often comes complex behavior, and one disruptive issue that recently gained traction in the Hammerspoon community is the unexpected behavior of configuration reload loops occurring after specific plugin updates. This issue has not only disrupted daily automation routines but prompted a larger discussion about Lua’s sandboxing and how users can proactively maintain a clean environment.
TLDR:
Table of Contents
Some Hammerspoon users faced persistent config reloading loops after updating certain plugins or modules. The root of the problem lies in how Hammerspoon handles Lua states and sandboxing during config and plugin reload sequences. Advanced users found that stale global variables and residual plugin states were the culprits. By manually cleaning the Lua environment and restructuring their init.lua files with surgical precision, users managed to stabilize their setups and restore Hammerspoon to its ideal performance.
Understanding the Reload Loop Issue
Early in 2023, several power users began reporting issues where Hammerspoon would enter a continuous reload state shortly after startup. The expected behavior of reloading the configuration once (often governed by a `hs.pathwatcher`) would instead cause:
- Terminal flooding with reload logs
- Keyboard shortcuts breaking or partially registering
- Severe CPU usage due to infinite reload loops
All signs pointed to a problem in how Hammerspoon’s Lua runtime was handling reloaded plugin code. Specifically, updated bundles—especially those involving file system watchers, event taps, or hotkey bindings—seemed to corrupt or maintain stale function references which would immediately retrigger another config reload.
Root Cause Analysis: The Lua Environment and Plugin State
Hammerspoon executes all user configurations through Lua statements found in the `~/.hammerspoon/init.lua` file. Naturally, if a plugin or script has globally scoped variables or failed to unregister observers when reloaded, ghost instances of handlers and watchers persist.
This is particularly problematic with objects like:
hs.hotkey.bindhs.pathwatcher.newhs.eventtap.new
All of which engage underlying macOS services that cannot tolerate accidental duplication or improper release.
Community investigations, primarily sourced from GitHub issues and Reddit discussions, identified that reload loops often coincided with:
- Plugins being updated which registered new global variables on load
- Improperly cleared watchers or event taps from previous sessions
- Reuse of the same variable names without cleanup
In essence, without a properly constructed teardown routine in the Lua environment, Hammerspoon treats a reload like a layered augmentation of the current state — rather than a fresh restart.
Lua Sandbox Cleanup as a Solution
Advanced users who experienced this discovered that restructuring their configuration files and properly sandboxing their plugin scopes could alleviate the issue. The main approach included:
1. Manual Teardown Routines
Before reloading or re-initializing any watcher or event handler, users started explicitly stopping and setting them to nil to remove any reference from memory.
if myWatcher then
myWatcher:stop()
myWatcher = nil
end
This practice became a staple before defining updated bindings or watchers, ensuring stale states were purged.
2. Scoping Variables Locally
Switching from global-scope declarations to local variables provided much-needed insulation. For example:
-- BEFORE
pathWatcher = hs.pathwatcher.new(...)
-- AFTER
local pathWatcher = hs.pathwatcher.new(...)
By reducing the footprint of persistent objects within the Lua environment, these reloads became less volatile.
3. Deferred Initialization
Many users adopted a pattern where they would defer heavy operations in the config file behind hs.timer.doAfter. This helped avoid race conditions arising from multiple plugins loading simultaneously.
hs.timer.doAfter(2, function()
initializeWatchers()
end)
Community Recommendations and Best Practices
In the wake of these issues, the community came together to create a number of preventative best practices for all Hammerspoon users, especially those leveraging dozens of modules and watchers on startup.
Top Recommendations:
- Modularize Each Script: Break configurations into separate files and require them within
init.luaconditionally. - Unbind and Unwatch Before Rebinding: Always stop watchers and hotkeys explicitly before re-declaring them.
- Monitor Hotkey Conflicts: Use
hs.hotkey.getHotkeys()to track active keymaps and clean them up if necessary. - Use Local Scope Wherever Possible: Avoid global namespace pollution for stability.
Additionally, some users wrote their own cleaner modules that would walk through known bindings and dispose of them before a reload began. Tools were also shared to introspect into live watchers and unregistered taps that might cause a reload loop.
Future Developments: Open Issues and Plugin Evolution
Ongoing development efforts within Hammerspoon itself also recognize this challenge, and some proposed solutions include:
- Implementing lifecycle hooks in the Hammerspoon API (e.g.,
onUnload()) - Centralizing plugin state management to allow structured teardown
- Restricting auto-binding behavior in certain modules until a full configuration reload finishes
The maintainers urge plugin authors to follow defensive programming principles — treat every load as the first load and remove any assumptions about the Lua environment being pristine.
Conclusion
Hammerspoon’s reload loop problem served as both a frustration and a learning moment for macOS tinkerers. While it exposed gaps in the way Lua handles long-lived states across reloads, it also highlighted the resilience and ingenuity of its user community.
Treating the Lua environment as a temporary sandbox—rather than a persistent container—helped many resolve the issue. But until official mechanisms for plugins to manage their life cycles are built into the core, it remains on users to clean up after themselves manually. With proper teardown logic, scoped variables, and thoughtful configuration design, reload loops can be entirely eliminated, making Hammerspoon a stable ally in your daily productivity stack.