Skip to content

Conversation

@jantman
Copy link
Owner

@jantman jantman commented Jan 22, 2026

Summary

This PR implements the Claude Permission Daemon, a Python-based daemon that enables remote approval of Claude Code permission requests via Slack when the user is idle.

Features Implemented

1. Initial Implementation (Milestones 1-6)

  • config.py - TOML configuration with environment variable overrides
  • state.py - State management with async-safe locking
  • idle_monitor.py - swayidle subprocess management for Wayland idle detection
  • socket_server.py - Unix domain socket server for hook communication
  • slack_handler.py - Slack Socket Mode with Block Kit messages and button actions
  • daemon.py - Main orchestration coordinating all components
  • hook.py - Standalone hook script (stdlib only) for Claude Code integration
  • systemd service - User service file with security hardening
  • README.md - Comprehensive documentation

2. Test Coverage & CI (TestCI feature)

  • Added 36 daemon unit tests covering initialization, start/stop, permission handling, Slack actions, idle state changes
  • Coverage improved from 60% to 75%
  • GitHub Actions workflow for automated testing

Key Architectural Decisions

  • Single asyncio event loop with asyncio.gather() for concurrent components
  • Race condition handling: when user returns, pending Slack requests get passthrough and messages update to "Answered Locally"
  • Hook script uses only stdlib for fast startup without venv activation

Test Results

  • 142 tests passing
  • 75% overall coverage (target met)
  • daemon.py: 0% → 77%

Test plan

  • All 142 unit tests pass
  • Coverage meets 75% target
  • Manual testing with real Slack app (requires user setup)
  • Verify systemd service starts correctly

🤖 Generated with Claude Code

jantman and others added 30 commits January 22, 2026 05:41
Added comprehensive implementation plan to the feature document including:
- 6 milestones covering foundation, core components, Slack integration,
  hook script, deployment, and acceptance criteria
- Specific tasks with commit message prefix format (Init - M.T)
- Latest dependency versions from PyPI research (slack-bolt 1.27.0,
  aiohttp 3.13.3, pytest 9.x, pytest-asyncio 1.3.0, pytest-cov 7.0.0)
- Progress tracking checkboxes for each task
- Exit criteria for each milestone

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Added pyproject.toml with:
  - Project metadata and Python 3.14 requirement
  - Dependencies: slack-bolt>=1.27.0, aiohttp>=3.13.0
  - Dev dependencies: pytest>=9.0.0, pytest-asyncio>=1.3.0, pytest-cov>=7.0.0
  - Entry points for daemon and hook scripts
  - pytest and coverage configuration
- Created src/claude_permission_daemon/__init__.py with version
- Created directory structure for tests, systemd, and example

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Configuration loading module with:
- DaemonConfig: socket_path, idle_timeout, request_timeout
- SlackConfig: bot_token, app_token, channel with validation
- SwayidleConfig: binary path
- Config.load() classmethod for TOML parsing
- Environment variable overrides (CLAUDE_PERM_* prefix)
- Validation method returning list of errors

Uses Python 3.11+ tomllib from stdlib (no external dependency).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
State management module with:
- Action enum: APPROVE, DENY, PASSTHROUGH
- PermissionRequest dataclass: request_id, tool_name, tool_input, timestamp
- PermissionResponse dataclass: action, reason, to_dict()
- PendingRequest dataclass: tracks request, hook_writer, Slack message info
- StateManager class:
  - Async-safe via asyncio.Lock
  - idle property with callbacks on state change
  - CRUD operations for pending requests
  - update_slack_info() for tracking Slack messages
  - clear_all_pending() for race condition handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added example/config.toml with:
- All configuration options documented
- Default values shown in comments
- Instructions for Slack token setup
- Notes on environment variable overrides

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added test infrastructure:
- tests/__init__.py
- tests/conftest.py with fixtures:
  - temp_dir: temporary directory for tests
  - sample_config_content/config_file: full config fixtures
  - minimal_config_content/minimal_config_file: minimal config fixtures
- tests/test_config.py: 15 tests covering DaemonConfig, SlackConfig,
  SwayidleConfig, and Config loading/validation/env overrides
- tests/test_state.py: 24 tests covering Action, PermissionRequest,
  PermissionResponse, PendingRequest, and StateManager

All 39 tests pass.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Milestone 1 complete with all 5 tasks:
- 1.1: Project skeleton (pyproject.toml, __init__.py, directories)
- 1.2: config.py (TOML loading, env var overrides, validation)
- 1.3: state.py (dataclasses, StateManager with async locks)
- 1.4: example/config.toml
- 1.5: pytest infrastructure (39 tests passing)

Updated progress log in feature document.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
IdleMonitor class for managing swayidle subprocess:
- Spawns swayidle with timeout/resume commands
- Reads stdout asynchronously for IDLE/ACTIVE messages
- Tracks current idle state with callback on changes
- Graceful shutdown with terminate/kill fallback
- Binary path resolution (PATH search or explicit)
- Error handling and logging throughout
- restart() method for recovery

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SocketServer class for Unix domain socket communication:
- Accepts connections from hook scripts
- Parses JSON requests with tool_name and tool_input
- Creates PermissionRequest and passes to handler with writer
- Supports deferred responses (writer passed to handler)
- Socket permissions set to 0600 (user-only)
- Graceful shutdown with connection cleanup
- send_response() helper for sending and closing

Protocol: newline-delimited JSON for both request and response.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Main daemon orchestration module:
- Daemon class coordinating IdleMonitor and SocketServer
- asyncio.gather for running components concurrently
- Signal handling (SIGTERM, SIGINT) for graceful shutdown
- StateManager integration with idle change callbacks
- Permission request handling (passthrough for now)
- _resolve_request() for sending responses to hooks
- Race condition handling (user returns while pending)
- CLI with --config, --debug, --version options
- Logging configuration with debug mode

Slack integration is stubbed for Milestone 3.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added comprehensive tests for Milestone 2 components:

test_idle_monitor.py (15 tests):
- Initial state, command building, binary resolution
- Output handling (IDLE/ACTIVE/unknown)
- Start/stop lifecycle with mocked subprocess
- Restart behavior and state reset
- Read loop with mocked stdout

test_socket_server.py (15 tests):
- Initial state, socket creation and permissions
- Start/stop lifecycle and cleanup
- Valid request handling with response
- Error cases (invalid JSON, missing fields)
- send_response helper with PermissionResponse and dict

All 69 tests passing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Milestone 2 complete with all 4 tasks:
- 2.1: idle_monitor.py (swayidle subprocess management)
- 2.2: socket_server.py (Unix socket for hook connections)
- 2.3: daemon.py shell (orchestration, signal handling)
- 2.4: Unit tests (30 new tests, 69 total passing)

Updated progress log in feature document.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SlackHandler class with Socket Mode connection:
- AsyncApp with slack-bolt for Socket Mode
- start/stop lifecycle management
- post_permission_request() to send messages
- update_message_* methods for state changes
- Action handlers registered for approve/deny buttons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Integrated SlackHandler into daemon orchestration:
- Import and initialize SlackHandler
- Add slack_handler.run() to asyncio tasks
- Stop Slack handler during shutdown
- _handle_slack_action() callback for button clicks
- Post to Slack when user is idle
- Update Slack messages on approve/deny/answered locally
- Handle Slack failures with passthrough fallback

All 69 tests still passing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added comprehensive tests for slack_handler module (20 tests):

TestFormatPermissionRequest:
- Bash command formatting with buttons and request_id
- File operation formatting with path and content
- Description field inclusion
- Long content truncation

TestFormatApproved/Denied/AnsweredLocally:
- Correct header text and emoji
- Context message verification

TestSlackHandler:
- Initial state, start/stop lifecycle
- run() without start raises error
- post_permission_request without app returns None
- Action handlers call callback with correct params

TestSlackHandlerWithMockedApp:
- Successful permission request posting
- Posting failure handling
- Message update methods (approved/denied/answered locally)
- Update methods without app (no-op)

All 89 tests passing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Milestone 3 complete with all 5 tasks:
- 3.1: slack_handler.py base (Socket Mode, AsyncApp)
- 3.2: Block Kit messages (request, approved, denied, answered locally)
- 3.3: Button action handlers (approve/deny with request_id)
- 3.4: Wire Slack into daemon (full integration)
- 3.5: Unit tests (20 new tests, 89 total passing)

Updated progress log in feature document.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Standalone hook script for Claude Code permission requests:
- Uses only Python stdlib (no external dependencies)
- Reads JSON request from stdin
- Connects to daemon via Unix socket
- Sends request and waits for response (5min timeout)
- Outputs JSON for approve/deny, no output for passthrough
- Graceful fallback to passthrough on any error

Entry point registered in pyproject.toml as claude-permission-hook.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Daemon orchestration was already completed in task 3.4. This commit
adds egg-info to .gitignore and confirms the race condition handling:

- _on_idle_change() resolves pending requests on user return
- Slack messages updated to "Answered Locally"
- Hook receives passthrough response for local handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added comprehensive integration tests (17 tests):

TestSocketServerIntegration:
- Full request/response flow through socket
- Multiple concurrent connections handling
- Deferred response (simulating Slack wait)

TestStateManagerIntegration:
- Idle callback trigger on state changes
- Pending request lifecycle (add, update, remove)
- Clear pending with callback (race condition simulation)

TestHookScript:
- Passthrough when daemon not running
- Format output for approve/deny/passthrough/unknown
- Read request from stdin (empty, valid, invalid)

TestEndToEndFlow:
- Complete approve flow
- Complete deny flow
- Complete passthrough flow

All 106 tests passing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Milestone 4 complete with all 3 tasks:
- 4.1: hook.py (stdlib only, socket client, JSON output)
- 4.2: daemon orchestration (already done in M3)
- 4.3: Integration tests (17 new tests, 106 total passing)

Updated progress log in feature document.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added systemd user service file with:
- ExecStart pointing to ~/.local/bin/claude-permission-daemon
- Automatic restart on failure
- Security hardening (NoNewPrivileges, ProtectSystem, etc.)
- ReadWritePaths for XDG_RUNTIME_DIR socket
- ReadOnlyPaths for config directory
- WAYLAND_DISPLAY environment for swayidle

Install with:
  cp systemd/*.service ~/.config/systemd/user/
  systemctl --user daemon-reload
  systemctl --user enable --now claude-permission-daemon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Comprehensive README with:
- Overview and key features
- Requirements (Python 3.14+, Wayland, Slack)
- Installation instructions:
  - Package installation
  - Slack app setup (detailed steps)
  - Configuration file setup
  - Systemd service installation
  - Claude Code hook configuration
- Configuration reference (all options documented)
- Environment variable overrides
- How it works explanation
- Troubleshooting section
- Development instructions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Final acceptance tasks completed:
- Documentation review: All source files have proper docstrings, README is comprehensive
- Test coverage verification: 106 tests, 60% overall coverage (98-100% for core modules)
- Full test suite passes
- Feature moved to completed

Feature implementation is now complete.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New feature to:
- Add unit tests for daemon.py module
- Create GitHub Actions workflow for CI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Plan includes:
- Milestone 1: Daemon unit tests (6 tasks)
- Milestone 2: GitHub Actions workflow (2 tasks)
- Milestone 3: Acceptance criteria (3 tasks)

Target: Improve coverage from 60% to 75%+

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests for Daemon.__init__:
- Creates StateManager
- Stores config
- Components are None before start
- Shutdown event not set
- Tasks list empty

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests with mocked components:
- start() creates and starts all components
- start() registers idle callback
- stop() cancels tasks
- stop() stops all components
- stop() sends passthrough to pending requests
- request_shutdown() sets event

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests for _handle_permission_request and _resolve_request:
- Active user gets passthrough immediately
- Idle user gets request posted to Slack
- Slack failure results in passthrough
- Missing Slack handler results in passthrough
- Resolve with approve/deny
- Unknown request ID doesn't raise error

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
jantman and others added 5 commits January 22, 2026 17:08
Tests for _handle_slack_action:
- Approve action updates message and sends approve response
- Deny action updates message and sends deny response
- Unknown request ID doesn't raise error
- Request without Slack info still gets response

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests for _on_idle_change (race condition logic):
- Going idle doesn't resolve pending requests
- Becoming active resolves pending with passthrough
- Becoming active updates Slack message to 'answered locally'
- Multiple pending requests all get resolved
- No pending requests is fine

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests for setup_logging:
- Default INFO level
- Debug mode
- Third-party logger noise reduction

Tests for parse_args:
- Default values
- Config option (-c, --config)
- Debug option (-d, --debug)
- Combined options

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Workflow test-permission-daemon.yml:
- Runs on push to claude_permission_daemon/**
- Runs on PR to main
- Runs on workflow_dispatch (manual)
- Uses Python 3.14
- Installs dev dependencies
- Runs pytest with coverage
- Uploads coverage report artifact

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Final results:
- Coverage improved from 60% to 75% (target met)
- daemon.py: 0% -> 77%
- 142 tests passing (36 new daemon tests)
- Feature moved to completed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@jantman jantman merged commit df5647e into main Jan 22, 2026
2 checks passed
@jantman jantman deleted the feature/initial-implementation branch January 22, 2026 22:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants