A macOS daemon that intercepts Claude Code permission prompts and escalates them to your phone when you're away from your laptop.
When Claude Code asks for permission to run a command, this tool can:
- Show a macOS notification with Approve / Deny buttons (at t=15s by default)
- Send a Telegram message with inline buttons if the notification goes unanswered (at t=30s)
- Auto-apply a policy (
denyorapprove) after a hard timeout — or wait indefinitely if no timeout is set
Toggle the whole system on or off with a single command — no Claude Code restart required.
Claude Code (per permission request)
│
│ spawns: claude-code-approvals hook
▼
hook process (reads stdin JSON, POSTs to daemon)
│
│ POST /api/permission (blocking)
▼
claude-code-approvals daemon (localhost:9753)
│
├── t=15s ──► macOS notification (alerter)
│ [Approve] [Deny]
│
└── t=30s ──► Telegram message
[✅ Approve] [❌ Deny]
The daemon runs as a launchd service. Claude Code invokes the hook subcommand as a subprocess for each permission request; the hook blocks until the daemon returns a decision. When the daemon is disabled (cc off), it returns immediately and Claude Code falls back to its built-in interactive prompt.
- macOS (Apple Silicon or Intel)
- Go 1.21+
- alerter:
brew install vjeantet/tap/alerter - A Telegram bot token and chat ID (see setup)
git clone https://github.com/vokomarov/claude-code-approvals.git ~/go/src/github.com/vokomarov/claude-code-approvals
cd ~/go/src/github.com/vokomarov/claude-code-approvals
make installMake sure ~/go/bin is in your PATH:
echo 'export PATH="$HOME/go/bin:$PATH"' >> ~/.zshrc && source ~/.zshrccp config.example.yaml config.ymlEdit config.yml and set your Telegram credentials at minimum:
telegram:
bot_token: "YOUR_BOT_TOKEN"
chat_id: 123456789
paths:
claude_settings: "~/.claude/settings.json" # adjust to your actual pathconfig.yml is gitignored — it stays local.
Open launchd/com.vokomarov.cc-approvals.plist and update the paths to match your environment:
<!-- Path to the installed binary -->
<string>/Users/YOUR_USERNAME/go/bin/claude-code-approvals</string>
<!-- Working directory (where config.yml lives) -->
<string>/Users/YOUR_USERNAME/go/src/github.com/vokomarov/claude-code-approvals</string>
<!-- Log output paths -->
<string>/Users/YOUR_USERNAME/go/src/github.com/vokomarov/claude-code-approvals/logs/cc-approvals.log</string>
<string>/Users/YOUR_USERNAME/go/src/github.com/vokomarov/claude-code-approvals/logs/cc-approvals-error.log</string>Replace YOUR_USERNAME with your macOS username (whoami).
make service-installVerify the daemon is running:
make health
# → {"status":"ok"}claude-code-approvals installThis writes the PermissionRequest hook entry to your Claude Code settings.json. You only need to do this once — it persists across daemon restarts and session restarts.
- Open Telegram and message @BotFather →
/newbot - Copy the bot token into
config.ymlundertelegram.bot_token - Send any message to your new bot, then open:
Read
https://api.telegram.org/bot<TOKEN>/getUpdatesresult[0].message.chat.idand set it astelegram.chat_id
claude-code-approvals on # enable interception (no session restart needed)
claude-code-approvals off # disable — Claude Code uses its built-in prompt| Situation | Command |
|---|---|
| Check daemon is alive | make health |
| View logs | make logs |
| View error logs | make logs-err |
| Restart daemon | make service-reload |
| Stop daemon | make service-stop |
After making code changes:
make reinstall # rebuild, update plist, restart daemon, health check| Key | Default | Description |
|---|---|---|
timeouts.macos_notification_seconds |
15 |
Delay before macOS notification. 0 = skip |
timeouts.telegram_notification_seconds |
30 |
Delay before Telegram message. 0 = skip |
timeouts.total_timeout_seconds |
300 |
Hard ceiling in seconds; 0 = wait indefinitely (no timeout) |
timeouts.timeout_policy |
deny |
Decision on hard timeout: deny or approve. Only used when total_timeout_seconds > 0 |
telegram.message_template |
(built-in) | Go text/template; vars: .SessionID .ToolName .ToolInput .CreatedAt |
paths.claude_settings |
— | Path to Claude Code settings.json modified by install/uninstall |
daemon.port |
9753 |
Port for HTTP API and health endpoint |
Validation rules:
- If both notification timeouts are non-zero,
telegram_notification_secondsmust exceedmacos_notification_secondsby at least 5 total_timeout_seconds: 0means wait indefinitely; any positive value must exceed the largest non-zero notification timeouttimeout_policyis required only whentotal_timeout_seconds > 0- On daemon shutdown, all pending requests are always denied regardless of
timeout_policy telegram.bot_tokenandchat_idare always required
Timeout-only mode (no notifications, auto-deny after 60s):
timeouts:
macos_notification_seconds: 0
telegram_notification_seconds: 0
total_timeout_seconds: 60
timeout_policy: denyAfter claude-code-approvals install, your settings.json contains:
{
"hooks": {
"PermissionRequest": [
{
"hooks": [
{
"type": "command",
"command": "/Users/you/go/bin/claude-code-approvals hook"
}
]
}
]
}
}claude-code-approvals uninstall removes this entry. The on/off commands do not modify settings.json — they toggle the daemon's state via HTTP.
make test # run tests
make test-race # run tests with race detector
make vet # go vet
make lint # golangci-lint (requires golangci-lint installed)MIT