-
Notifications
You must be signed in to change notification settings - Fork 1
Add macOS and Windows idle detection support #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Added comprehensive implementation plan for Mac and Windows idle detection support. Plan includes: - 6 milestones with detailed tasks - Abstraction layer with BaseIdleMonitor - Mac implementation using ioreg - Windows implementation using ctypes/Win32 API - Factory pattern for platform detection - Full test coverage requirements - Documentation updates Awaiting human approval before starting implementation. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Created base_idle_monitor.py with abstract interface for idle monitoring: - BaseIdleMonitor ABC with abstract methods: start(), stop(), run() - Abstract properties: idle, running - Default restart() implementation - IdleCallback type alias - IdleMonitorError exception class - Comprehensive docstrings documenting the lifecycle and contract This provides the foundation for platform-specific implementations. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Refactored existing idle monitor implementation to inherit from base class: - Renamed IdleMonitor class to SwayidleMonitor - Made SwayidleMonitor inherit from BaseIdleMonitor - Imported IdleCallback and IdleMonitorError from base class - Updated all log messages to use SwayidleMonitor name - Added backward compatibility alias: IdleMonitor = SwayidleMonitor - Updated .tool-versions to Python 3.14.2 - All 185 tests passing This refactoring maintains backward compatibility while preparing for platform-specific implementations. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Created idle_monitor_mac.py with MacIdleMonitor implementation: - Polls ioreg -c IOHIDSystem every second - Parses HIDIdleTime (nanoseconds since last input) - Triggers callbacks when crossing idle threshold - Handles subprocess errors gracefully - Proper logging with debug output every 60 seconds - Inherits from BaseIdleMonitor Implementation uses polling approach compatible with existing architecture. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added MacIdleConfig to config.py:
- New MacIdleConfig dataclass with ioreg binary path
- DEFAULT_IOREG_BINARY constant ("ioreg")
- Added mac field to Config class
- Load mac config from [mac] TOML section
- Environment variable override: CLAUDE_PERM_IOREG_BINARY
- All config tests passing
Configuration now supports platform-specific idle monitor settings.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Created comprehensive test suite for MacIdleMonitor: - 25 new tests covering all functionality - Tests for binary finding and error handling - Tests for idle time retrieval from ioreg - Tests for state transitions (active <-> idle) - Tests for subprocess error handling - Tests for ioreg output parsing - All 210 tests passing (185 original + 25 new) Test coverage matches existing idle_monitor tests. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Created idle_monitor_windows.py with WindowsIdleMonitor implementation: - Uses ctypes to call GetLastInputInfo Win32 API - Polls every second to check idle time - Calculates idle time from GetTickCount difference - Handles tick count rollover (49.7 day wraparound) - Triggers callbacks when crossing idle threshold - Handles Windows API errors gracefully - Proper logging with debug output every 60 seconds - Inherits from BaseIdleMonitor Implementation uses polling approach consistent with Mac version. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added WindowsIdleConfig to config.py: - New WindowsIdleConfig dataclass (currently empty) - Added windows field to Config class - Load windows config from [windows] TOML section - Configuration structure in place for future extensibility - All config tests passing Windows implementation uses built-in APIs with no binary paths needed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Created comprehensive test suite for WindowsIdleMonitor: - 21 new tests covering all functionality - Tests for GetLastInputInfo API mocking - Tests for tick count calculations and rollover handling - Tests for state transitions (active <-> idle) - Tests for API error handling - Tests work on non-Windows platforms via mocking Updated idle_monitor_windows.py for cross-platform compatibility: - Conditional import of Windows-specific ctypes items - WINDOWS_AVAILABLE flag for runtime platform detection - Stubs for non-Windows platforms to enable testing - All 231 tests passing (185 + 25 Mac + 21 Windows) Test coverage matches existing idle monitor implementations. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Created idle_monitor_factory.py with platform detection: - create_idle_monitor() function detects OS using platform.system() - Returns SwayidleMonitor for Linux - Returns MacIdleMonitor for macOS (Darwin) - Returns WindowsIdleMonitor for Windows - Raises descriptive IdleMonitorError for unsupported platforms - Comprehensive error messages with troubleshooting steps - Proper logging of detected platform and chosen backend Factory provides single entry point for all platform-specific implementations. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Updated daemon.py to use idle monitor factory: - Import create_idle_monitor instead of IdleMonitor directly - Import IdleMonitorError for error handling - Use factory to create platform-specific idle monitor - Handle factory errors with descriptive logging - Exit cleanly if idle monitor unavailable Updated test_daemon.py: - Patch create_idle_monitor instead of IdleMonitor class - All 231 tests passing Daemon now automatically selects correct idle monitor for platform. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Created comprehensive test suite for idle monitor factory: - 9 new tests covering factory functionality - Tests for platform detection (Linux, macOS, Windows) - Tests for unsupported platforms with error messages - Tests for creation failures with descriptive errors - Tests verify error messages include resolution steps - All 240 tests passing (231 + 9 factory) Factory tests ensure correct monitor selection for each platform. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Updated project documentation for cross-platform support: README.md: - Updated overview to mention cross-platform idle detection - Listed platform-specific implementations (Linux/macOS/Windows) - Updated requirements section for all platforms example/config.toml: - Added [mac] section for ioreg binary configuration - Added [windows] section (no config needed) - Clarified [swayidle] is Linux-only CLAUDE.md: - Updated architecture diagram showing factory pattern - Added all idle monitor implementations to component list - Documented base_idle_monitor.py abstract interface - Documented idle_monitor_factory.py platform detection Documentation now accurately reflects multi-platform support. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Mark the Mac and Windows support feature as complete and move the feature document to the completed folder. All six milestones have been successfully implemented and tested: - Milestone 1: Created BaseIdleMonitor abstraction layer - Milestone 2: Implemented MacIdleMonitor using ioreg for macOS - Milestone 3: Implemented WindowsIdleMonitor using GetLastInputInfo API - Milestone 4: Added platform detection factory - Milestone 5: Integration and cross-platform polish - Milestone 6: Documentation updates and acceptance criteria validation The daemon now supports Linux (swayidle), macOS (ioreg), and Windows (GetLastInputInfo API) with automatic platform detection. All 240 tests are passing with 76% overall coverage. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Make the installation and setup instructions platform-specific for Linux, macOS, and Windows instead of being Linux-only. Key changes: Installation: - Add Windows PowerShell commands alongside Unix bash commands - Use platform-appropriate paths (%LOCALAPPDATA%, %APPDATA% for Windows) - Show .exe extensions for Windows executables Automatic Startup: - Linux: systemd (existing) - macOS: launchd with example plist configuration - Windows: Task Scheduler with manual and automated options Configuration: - Show config file locations for all platforms - Document socket/named pipe differences - Add platform-specific config sections ([swayidle], [mac], [windows]) Troubleshooting: - Platform-specific log checking (journalctl, launchd logs, manual runs) - Separate troubleshooting for swayidle (Linux), ioreg (macOS), and GetLastInputInfo (Windows) - Add installation instructions for swayidle on various Linux distros Development: - Platform-specific venv activation and testing commands - Cross-platform hook testing examples This makes the daemon fully usable on all three supported platforms without users needing to mentally translate Linux-specific instructions. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds comprehensive cross-platform idle detection support for macOS and Windows, enabling the Claude Permission Daemon to run on all major operating systems beyond its original Linux-only implementation.
Changes:
- Introduced abstract
BaseIdleMonitorclass and factory pattern for platform-specific idle monitor instantiation - Implemented
MacIdleMonitorusing ioreg to poll IOHIDSystem idle time (nanoseconds) with 1-second polling interval - Implemented
WindowsIdleMonitorusing GetLastInputInfo Win32 API via ctypes with 1-second polling interval - Added platform-specific configuration sections (
[mac]and[windows]) with environment variable overrides - Refactored existing
IdleMonitortoSwayidleMonitorwith backward compatibility alias - Updated documentation with platform-specific installation and troubleshooting instructions
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
base_idle_monitor.py |
New abstract base class defining idle monitor interface with properties and lifecycle methods |
idle_monitor_mac.py |
macOS implementation polling ioreg for HIDIdleTime every second |
idle_monitor_windows.py |
Windows implementation polling GetLastInputInfo API every second with tick rollover handling |
idle_monitor_factory.py |
Platform detection factory with comprehensive error messages for each platform |
idle_monitor.py |
Refactored to SwayidleMonitor inheriting from base class, maintains backward compatibility |
daemon.py |
Updated to use factory for monitor creation with error handling |
config.py |
Added MacIdleConfig and WindowsIdleConfig with environment variable support |
example/config.toml |
Added example Mac and Windows configuration sections |
README.md |
Comprehensive platform-specific installation, configuration, and troubleshooting |
CLAUDE.md |
Updated architecture diagram showing all three platform implementations |
docs/features/completed/mac-windows.md |
Complete feature implementation document with design decisions |
.tool-versions |
Added Python 3.14.2 version specification |
test_idle_monitor_mac.py |
24 tests covering all Mac monitor functionality |
test_idle_monitor_windows.py |
22 tests covering all Windows monitor functionality |
test_idle_monitor_factory.py |
9 tests covering factory logic and error handling |
test_daemon.py |
Updated mock paths to use factory instead of direct IdleMonitor |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ### Idle Detection Approaches | ||
|
|
||
| **Linux (existing)**: `swayidle` subprocess that prints IDLE/ACTIVE to stdout | ||
| **Mac**: Poll `ioreg -c IOHIDSystem` every second to read `HIDIdleTime` (milliseconds since last input) |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation states that HIDIdleTime is in milliseconds, but it should be nanoseconds. The actual implementation in idle_monitor_mac.py correctly uses nanoseconds and divides by 1,000,000,000 to convert to seconds. This documentation should be updated to say "nanoseconds" instead of "milliseconds" to match the implementation and PR description.
| **Mac**: Poll `ioreg -c IOHIDSystem` every second to read `HIDIdleTime` (milliseconds since last input) | |
| **Mac**: Poll `ioreg -c IOHIDSystem` every second to read `HIDIdleTime` (nanoseconds since last input) |
| slack_data = data.get("slack", {}) | ||
| swayidle_data = data.get("swayidle", {}) | ||
| mac_data = data.get("mac", {}) | ||
| windows_data = data.get("windows", {}) |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable windows_data is not used.
| windows_data = data.get("windows", {}) |
| import logging | ||
| import re | ||
| import shutil | ||
| from typing import Any |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import of 'Any' is not used.
| from typing import Any |
| @@ -0,0 +1,216 @@ | |||
| """Tests for idle_monitor_factory module.""" | |||
|
|
|||
| from unittest.mock import AsyncMock, MagicMock, patch | |||
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import of 'MagicMock' is not used.
| from unittest.mock import AsyncMock, MagicMock, patch | |
| from unittest.mock import AsyncMock, patch |
| try: | ||
| await self._poll_task | ||
| except asyncio.CancelledError: | ||
| pass |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| pass | |
| # Expected when stopping: the polling task is cancelled intentionally. | |
| logger.debug("MacIdleMonitor poll task cancelled during stop()") |
| self._poll_task.cancel() | ||
| try: | ||
| await self._poll_task | ||
| except asyncio.CancelledError: |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| except asyncio.CancelledError: | |
| except asyncio.CancelledError: | |
| # Task cancellation during shutdown is expected and safe to ignore. |
| async def dummy_task(): | ||
| try: | ||
| await asyncio.sleep(100) | ||
| except asyncio.CancelledError: |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| except asyncio.CancelledError: | |
| except asyncio.CancelledError: | |
| # Expected in this test: ignore cancellation of the dummy task. |
| async def dummy_task(): | ||
| try: | ||
| await asyncio.sleep(100) | ||
| except asyncio.CancelledError: |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| except asyncio.CancelledError: | |
| except asyncio.CancelledError: | |
| # Expected when the monitor stops and cancels the poll task; ignore. |
Fix all 8 issues identified by GitHub Copilot code review: 1. Fix documentation: Change HIDIdleTime from "milliseconds" to "nanoseconds" in mac-windows.md to match actual implementation 2. Remove unused variable: Delete windows_data in config.py since WindowsIdleConfig has no fields yet 3. Remove unused imports: - Remove unused 'Any' import from idle_monitor_mac.py - Remove unused 'MagicMock' import from test_idle_monitor_factory.py 4. Add explanatory comments to empty except clauses: - idle_monitor_mac.py: Add comment for expected CancelledError - idle_monitor_windows.py: Add comment for expected shutdown cancellation - test_idle_monitor_mac.py: Add comment for test dummy task cancellation - test_idle_monitor_windows.py: Add comment for test dummy task cancellation All 240 tests still passing after changes. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The daemon was crashing on macOS trying to create a socket at
/run/user/1000/claude-permissions.sock, which is Linux-specific and
doesn't exist on macOS (and /run is read-only).
Changes:
- Add _get_default_socket_path() function with platform detection
- macOS: defaults to /tmp/claude-permissions.sock
- Linux: tries XDG_RUNTIME_DIR, then /run/user/{uid}, falls back to /tmp
- Windows: returns \\.\pipe\claude-permissions (named pipe)
- Unknown platforms: falls back to /tmp
This fixes the startup crash on macOS:
OSError: [Errno 30] Read-only file system: '/run'
All 240 tests still passing.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Apply the same platform-aware socket path fix to hook.py. The hook script
was also hardcoded to use the Linux-specific /run/user/1000 path, causing
connection failures on macOS.
Changes:
- Add _get_default_socket_path() function to hook.py (duplicates config.py
logic since hook.py must remain stdlib-only with no imports)
- Platform-specific defaults same as config.py:
- macOS: /tmp/claude-permissions.sock
- Linux: XDG_RUNTIME_DIR, /run/user/{uid}, or /tmp fallback
- Windows: \\.\pipe\claude-permissions
This ensures the hook script can connect to the daemon on macOS.
All 240 tests still passing.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… polling tasks The _poll_task attribute in MacIdleMonitor and WindowsIdleMonitor was declared but never assigned, causing the stop() cancellation logic to silently fail since _poll_task was always None. This prevented proper cleanup of the background polling loop. Solution: Make idle monitors fully self-managing by creating the background task in start() instead of relying on external task management. This matches the pattern used by SwayidleMonitor where the subprocess lifecycle is self-contained. Changes: - MacIdleMonitor.start(): Create and store asyncio task for run() loop - WindowsIdleMonitor.start(): Create and store asyncio task for run() loop - Daemon.start(): Remove idle_monitor from external task list (self-manages now) - test_daemon.py: Update task count assertions from 3 to 2 The existing stop() cancellation logic now works correctly since _poll_task is properly assigned. All 240 tests pass. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Summary
Add cross-platform idle detection support for macOS and Windows, enabling the Claude Permission Daemon to run on all major operating systems. Previously, the daemon only supported Linux with swayidle for Wayland/X11.
ioreg -c IOHIDSystemto poll HIDIdleTime (nanoseconds since last input)GetLastInputInfo()Win32 API via ctypes to poll idle timeplatform.system()Key Changes
Architecture
BaseIdleMonitorabstract base class defining the idle monitor interfaceIdleMonitortoSwayidleMonitor(maintains backward compatibility)MacIdleMonitorwith 1-second polling of ioreg outputWindowsIdleMonitorwith 1-second polling of GetLastInputInfo APIidle_monitor_factory.pyfor platform detection and monitor instantiationConfiguration
[mac]section with optionalbinarypath for ioreg[windows]section (reserved for future configuration)CLAUDE_PERM_IOREG_BINARYError Handling
Testing
Implementation Details
macOS Idle Detection:
Windows Idle Detection:
Platform Factory:
Files Changed
base_idle_monitor.py- Abstract base classidle_monitor_mac.py- macOS implementation (238 lines)idle_monitor_windows.py- Windows implementation (235 lines)idle_monitor_factory.py- Platform detection factory (124 lines)idle_monitor.py- Refactored to SwayidleMonitordaemon.py- Uses factory for monitor creationconfig.py- Added Mac/Windows config sectionsTest Results
All tests passing with 76% overall coverage.
Documentation Updates
Test Plan
🤖 Generated with Claude Code