A Rust CLI tool that integrates Claude Code hooks with the ntfy notification service, enabling real-time notifications about Claude Code activities on your mobile device or desktop.
- 🔔 Full Hook Support: Supports all Claude Code hooks (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, Notification, SubagentStop)
- 📱 Ntfy Integration: Send notifications to any ntfy server (default: ntfy.sh) with both text and JSON formats
- 🎨 Rich Templates: Built-in Handlebars templates for all hooks with custom template support
- 🚀 Background Daemon: Async daemon process with retry logic, file-based IPC, and graceful shutdown
- ⚙️ Dual Configuration: Project-level (
.claude/ntfy-service/) or global (~/.claude/ntfy-service/) configuration - 🔧 Advanced Features: Hook filtering, multiple topics, priority levels, and enhanced PostToolUse data processing
- 📝 Comprehensive Logging: File and console logging with configurable levels
Two-Binary Design:
claude-ntfy: Main CLI tool for configuration, testing, and daemon managementclaude-ntfy-daemon: Background daemon process for async notification processing
Communication: Unix socket IPC system with binary serialization for high-performance communication.
# Clone the repository
git clone https://github.com/pppobear/claude-code-ntfy-service.git
cd claude-code-ntfy-service
# Build both binaries
cargo build --release
# Install both binaries system-wide (recommended)
sudo cp target/release/claude-ntfy /usr/local/bin/
sudo cp target/release/claude-ntfy-daemon /usr/local/bin/
# Or use the example setup script
chmod +x example-setup.sh
./example-setup.sh# Initialize global configuration
claude-ntfy init --global
# Or initialize project-specific configuration
claude-ntfy init
# Force overwrite existing configuration
claude-ntfy init --global --forceAdd to your .claude/settings.json or ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "claude-ntfy --project $CLAUDE_PROJECT_DIR"
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "claude-ntfy --project $CLAUDE_PROJECT_DIR"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "claude-ntfy --project $CLAUDE_PROJECT_DIR"
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "claude-ntfy --project $CLAUDE_PROJECT_DIR"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "claude-ntfy --project $CLAUDE_PROJECT_DIR"
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "claude-ntfy --project $CLAUDE_PROJECT_DIR"
}
]
}
],
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "claude-ntfy --project $CLAUDE_PROJECT_DIR"
}
]
}
]
}
}# Start daemon in foreground (default - good for debugging)
claude-ntfy daemon start
# Start daemon in background (detached)
claude-ntfy daemon start -d
claude-ntfy daemon start --detach-
Install the ntfy app on your device:
- Android: Google Play
- iOS: App Store
- Desktop: Web App
-
Subscribe to your topic (default:
claude-code-hooks)
Configuration is stored in .claude/ntfy-service/config.toml (project-level) or ~/.claude/ntfy-service/config.toml (global):
[ntfy]
server_url = "https://ntfy.sh"
default_topic = "claude-code-hooks"
default_priority = 3 # Optional: 1-5
default_tags = ["claude-code"] # Optional: array of strings
auth_token = "" # Optional: for private servers
timeout_secs = 30 # Optional: HTTP timeout
send_format = "text" # "text" or "json"
[hooks]
enabled = true
# Per-hook topic routing
[hooks.topics]
PreToolUse = "claude-tools-start"
PostToolUse = "claude-tools-complete"
UserPromptSubmit = "claude-prompts"
# Per-hook priority levels
[hooks.priorities]
PreToolUse = 2
PostToolUse = 3
UserPromptSubmit = 3
Stop = 5
# Hook filtering (inclusion/exclusion)
[hooks.filters]
PreToolUse = ["!Read", "!Grep"] # Exclude Read and Grep tools
PostToolUse = ["Write", "Edit"] # Only notify for Write and Edit
[templates]
use_custom = false
variables = {} # Custom template variables
# Custom templates using Handlebars syntax
[templates.custom_templates]
PreToolUse = """
🔧 Starting Tool: {{tool_name}}
{{#if tool_input.file_path}}File: {{tool_input.file_path}}{{/if}}
Time: {{timestamp}}
"""
PostToolUse = """
{{#if tool_response.error}}❌{{else}}✅{{/if}} Tool: {{tool_name}}
{{#if tool_response.filePath}}File: {{tool_response.filePath}}{{/if}}
{{#if tool_response.error}}Error: {{tool_response.error}}{{/if}}
Status: {{#if tool_response.error}}Failed{{else}}Success{{/if}}
"""
[daemon]
enabled = true
socket_path = "" # Optional: custom socket path
log_level = "info" # trace, debug, info, warn, error
log_path = "" # Optional: file logging path
max_queue_size = 1000
retry_attempts = 3
retry_delay_secs = 5claude-ntfy init [--global] [--force]# View current configuration
claude-ntfy config show
# Set configuration values
claude-ntfy config set ntfy.server_url "https://ntfy.example.com"
claude-ntfy config set ntfy.default_topic "my-topic"
claude-ntfy config set ntfy.auth_token "your-token"
claude-ntfy config set daemon.enabled true
claude-ntfy config set daemon.log_path "/path/to/daemon.log"
# Get configuration values
claude-ntfy config get ntfy.server_url
claude-ntfy config get daemon.enabled
claude-ntfy config get daemon.log_path
# Configure hook-specific settings
claude-ntfy config hook PreToolUse --topic "tool-alerts" --priority 4
claude-ntfy config hook PostToolUse --priority 3 --filter "Write"# Check daemon status
claude-ntfy daemon status
# Stop daemon
claude-ntfy daemon stop
# Reload daemon configuration
claude-ntfy daemon reload# Test notification sending
claude-ntfy test "Hello from Claude Code!" --title "Test Notification"
claude-ntfy test "Custom message" --title "Test" --priority 5 --topic "test-topic"# List available templates
claude-ntfy templates
# Show specific template content
claude-ntfy templates --show PreToolUse
claude-ntfy templates --show PostToolUse# Manual hook processing (for testing)
echo '{"tool_name": "Read", "success": true}' | CLAUDE_HOOK=PostToolUse claude-ntfy hook
# Process hook directly (bypass daemon)
echo '{"tool_name": "Write"}' | CLAUDE_HOOK=PreToolUse claude-ntfy hook --no-daemon
# Dry run to see what would be sent
echo '{"message": "test"}' | CLAUDE_HOOK=UserPromptSubmit claude-ntfy hook --dry-runThe tool supports all official Claude Code hooks:
PreToolUse: Runs before tool calls (can block execution if exit code 2)PostToolUse: Runs after tool calls completeUserPromptSubmit: When user submits a promptSessionStart: When a Claude Code session startsStop: When a Claude Code session endsNotification: General notifications from Claude CodeSubagentStop: When a subagent stops
Each hook has a carefully crafted template optimized for mobile notifications:
🔧 Tool Starting: {{tool_name}}
File: {{tool_input.file_path}}
Command: {{tool_input.command}}
Time: {{timestamp}}
✅ Tool Completed: {{tool_name}}
Status: {{#if tool_response.error}}Failed{{else}}Success{{/if}}
File: {{tool_response.filePath}}
Error: {{tool_response.error}}
Duration: {{duration_ms}}ms
Time: {{timestamp}}
💬 User Prompt
Message: {{prompt}}
Session: {{session_id}}
Time: {{timestamp}}
🚀 Claude Code Session Started
Session: {{session_id}}
Working Dir: {{cwd}}
Source: {{source}}
Time: {{timestamp}}
🏁 Claude Code Session Ended
Session: {{session_id}}
Time: {{timestamp}}
Control which notifications are sent:
[hooks.filters]
# Exclude specific tools (prefix with !)
PreToolUse = ["!Read", "!Grep", "!LS"]
# Only notify for specific tools
PostToolUse = ["Write", "Edit", "MultiEdit"]
# Combine inclusion and exclusion
UserPromptSubmit = ["deploy", "!debug"]Route different hooks to different ntfy topics:
[hooks.topics]
PreToolUse = "claude-tools-start"
PostToolUse = "claude-tools-complete"
UserPromptSubmit = "claude-prompts"
SessionStart = "claude-sessions"
Stop = "claude-sessions"
Notification = "claude-alerts"
SubagentStop = "claude-subagents"Set notification priorities (1-5, where 5 is highest):
[hooks.priorities]
Stop = 5 # Max - Session ended
Notification = 4 # High - Important alerts
PostToolUse = 3 # Default - Tool completed
PreToolUse = 2 # Low - Tool starting
SessionStart = 1 # Min - Session startedChoose between text and JSON sending modes:
[ntfy]
send_format = "text" # Default - uses HTTP headers, better compatibility
# send_format = "json" # Alternative - structured JSON body- PID Files:
.claude/ntfy-service/daemon.pid - Socket Files:
.claude/ntfy-service/daemon.sock(Unix socket IPC)
# Console only (foreground mode)
claude-ntfy daemon start
# File logging (background mode)
claude-ntfy config set daemon.log_path "/path/to/daemon.log"
claude-ntfy daemon start -d
# Dual logging (foreground with file backup)
claude-ntfy config set daemon.log_path "/path/to/daemon.log"
claude-ntfy daemon startThe daemon uses Unix socket IPC for high-performance communication:
- CLI connects to Unix socket at
.claude/ntfy-service/daemon.sock - Messages are serialized using bincode for efficiency
- Bidirectional communication with length-prefixed protocol
- Supports: Submit, Shutdown, Reload, Status, Ping
# Check for existing daemon
claude-ntfy daemon status
# View daemon logs
tail -f ~/.claude/ntfy-service/daemon.log
# or project-specific logs:
tail -f .claude/ntfy-service/daemon.log
# Clean up stale PID files
claude-ntfy daemon stop
rm -f .claude/ntfy-service/daemon.pid
# Start with verbose logging
RUST_LOG=debug claude-ntfy daemon start-
Verify configuration:
claude-ntfy config show
-
Test notification directly:
claude-ntfy test "Test message" --title "Direct Test"
-
Check hook filtering:
claude-ntfy config get hooks.enabled claude-ntfy config show | grep -A5 "\[hooks.filters\]"
-
Test hook processing:
echo '{"tool_name": "Write", "success": true}' | \ CLAUDE_HOOK=PostToolUse claude-ntfy hook --dry-run
-
Bypass daemon for debugging:
echo '{"test": "data"}' | \ CLAUDE_HOOK=UserPromptSubmit claude-ntfy hook --no-daemon
"Daemon already running" error:
claude-ntfy daemon stop
# Wait a moment, then:
claude-ntfy daemon start -d"Failed to send notification" errors:
- Check internet connection
- Verify ntfy server URL:
claude-ntfy config get ntfy.server_url - Test with a simple message:
claude-ntfy test "hello" - Check auth token if using private server
Hook not triggering:
- Verify Claude Code settings.json syntax
- Check that
claude-ntfybinary is in PATH - Test with verbose logging:
claude-ntfy -v daemon start
The tool automatically reads these Claude Code environment variables:
CLAUDE_HOOK: Hook event name (automatically set by Claude Code)CLAUDE_TOOL: Current tool being usedCLAUDE_PROJECT_DIR: Absolute path to project root directoryCLAUDE_WORKSPACE: Workspace pathCLAUDE_TOOL_NAME: Specific tool nameCLAUDE_TOOL_STATUS: Tool execution statusCLAUDE_TOOL_DURATION: Tool execution duration
Additional environment variables:
RUST_LOG: Set logging level (trace, debug, info, warn, error)
Following Claude Code hook conventions:
- Exit code 0: Success, hook executed normally
- Exit code 2: Blocking error, prevents the tool from executing (PreToolUse only)
- Exit code 1: General error (non-blocking)
# In your project directory
claude-ntfy init
claude-ntfy config set ntfy.default_topic "myproject-claude"
claude-ntfy daemon start -dclaude-ntfy init --global
claude-ntfy config set ntfy.server_url "https://ntfy.mycompany.com"
claude-ntfy config set ntfy.auth_token "tk_mytoken123"
claude-ntfy config set ntfy.default_topic "dev-claude-notifications"
claude-ntfy daemon start -d# Only notify for file operations, not reads
claude-ntfy config hook PreToolUse --filter "Write" --filter "Edit" --filter "MultiEdit"
claude-ntfy config hook PostToolUse --topic "dev-file-changes" --priority 4- Memory Usage: ~5-10MB for daemon process
- CPU Usage: Minimal when idle, brief spikes during notification processing
- Network: Only when sending notifications (HTTP POST requests)
- Disk I/O: Minimal for configuration and IPC files
- Latency: <100ms from hook trigger to notification sent (local processing)
- Auth Tokens: Stored in plain text in config files - ensure proper file permissions
- Network: Uses HTTPS by default for ntfy.sh
- IPC: File-based IPC is readable by same user - consider file permissions in shared environments
- PID Files: Standard Unix conventions for daemon process management
MIT License - see LICENSE file for details.
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass:
cargo test - Submit a pull request
- Claude Code by Anthropic for the amazing AI-powered development experience
- ntfy by Philipp Heckel for the excellent notification service
- Rust community for the fantastic ecosystem of crates used in this project
- Full Claude Code hook support for all 7 official hooks
- Background daemon with async processing and retry logic
- Rich Handlebars templating system with built-in templates
- Comprehensive configuration management with project/global scopes
- Advanced features: filtering, multiple topics, priority levels
- File-based IPC system with PID management
- Dual-format support (text/JSON) for ntfy compatibility
- Enhanced PostToolUse data processing for better success detection
- Cross-platform compatibility (Unix/Windows)
- Extensive CLI with 20+ commands and options
- Comprehensive logging with file and console output
- Integration tests and example setup scripts