TL;DR: Claude Code hooks + ntfy.sh = approve/deny permissions from your phone. 60 lines of bash, 3-minute setup, open source.
The Problem Every AI Coding Agent User Faces
I came back with my coffee and found Claude Code had been frozen for 15 minutes.
As AI coding agents gain autonomy — writing code, running builds, modifying files — the question of human oversight becomes critical. You want the agent to keep working, but you also want to know what it's doing. The permission prompt is the checkpoint, but it's also the bottleneck.
Sure, you can add commands to the allowlist to reduce prompts, but blanket-approving unknown commands and file writes is risky. On the other hand, staying glued to your terminal isn't realistic either.
So I built claude-push — an async human-in-the-loop approval layer for Claude Code. It uses PermissionRequest hooks and ntfy.sh to send Allow/Deny push notifications straight to your phone. Open source, 3-minute setup.
The Dilemma: Stay at Your Desk or Allow Everything
When you delegate code generation and refactoring to Claude Code, you'll inevitably see prompts like this:
Claude wants to run: rm -rf dist && npm run build
Allow? (y/n)
Every time this pops up, you have to go back to the terminal and hit y. If you're deep in focus (or just grabbing coffee), you miss it and Claude Code sits there doing nothing.
| Approach | Pros | Cons |
|---|---|---|
| Manual approval in terminal | Safe | Can't leave your desk |
| Allowlist everything | Convenient | Unknown commands get through |
| Mobile push notifications | Away from desk + safe | Requires setup |
claude-push makes option 3 a reality.
Prior Art
konsti-web/claude_push tackled this same problem, but it's Windows + PowerShell + keystroke injection — doesn't work on macOS or Linux. I had the same pain point, so I rebuilt the concept from scratch using bash + PermissionRequest hooks.
How It Works: PermissionRequest Hook + ntfy.sh
Claude Code has a Hooks system that lets you run external scripts on specific events. By registering a hook on the PermissionRequest event, you can intercept the permission prompt and return your own decision.
Here's the full flow:
Claude Code requests permission
→ Hook script fires
→ Sends notification with Allow/Deny buttons to ntfy.sh
→ Phone receives the push notification
→ You tap Allow or Deny
→ Response received via ntfy.sh SSE
→ Hook returns allow/deny JSON
→ Claude Code continues or stops
ntfy.sh is a free, HTTP-based push notification service. No account required — if you know the topic name, you can send and receive notifications. Unlike Pushover or LINE Notify, you can send a notification with a single curl command, which makes it a perfect fit for hook scripts.
Implementation Details
The hook script is about 60 lines of bash. Here are the three key design decisions.
1. Using ntfy.sh HTTP Actions for Buttons
ntfy.sh supports Action Buttons on notifications. I'm using http actions so that tapping a button POSTs to a separate response topic.
curl -s -H "Content-Type: application/json" \
-d "$(jq -n \
--arg topic "$TOPIC" \
--arg title "[$PROJECT] $TOOL_NAME" \
--arg message "$TOOL_INPUT" \
--arg allow_url "https://ntfy.sh/${RESPONSE_TOPIC}" \
--arg allow_body "allow|$REQ_ID" \
--arg deny_url "https://ntfy.sh/${RESPONSE_TOPIC}" \
--arg deny_body "deny|$REQ_ID" \
'{
topic: $topic, title: $title, message: $message,
priority: 4, tags: ["lock"],
actions: [
{action:"http",label:"Allow",url:$allow_url,method:"POST",body:$allow_body},
{action:"http",label:"Deny",url:$deny_url,method:"POST",body:$deny_body}
]
}')" "https://ntfy.sh/"
The key detail: the notification topic and the response topic are separate channels. This prevents the SSE stream from being polluted by your own outgoing notifications.
2. SSE + Request ID for Response Matching
After sending the notification, the hook waits for a response on the ntfy.sh SSE endpoint.
REQ_ID="$(date +%s)-$$"
while IFS= read -r line; do
if [[ "$line" == data:* ]]; then
DATA="${line#data: }"
MSG=$(echo "$DATA" | jq -r '.message // empty' 2>/dev/null)
if [[ "$MSG" == *"|$REQ_ID" ]]; then
DECISION="${MSG%%|*}"
break
fi
fi
done < <(curl -s -N --max-time "$WAIT_TIMEOUT" \
-H "Accept: text/event-stream" \
"https://ntfy.sh/${RESPONSE_TOPIC}/sse")
The REQ_ID (timestamp + PID) is embedded in the notification body and matched against the response. This ensures that even when multiple permission requests fire simultaneously, each response is matched to the correct request. Without this, you could accidentally apply a previous notification's response to a new one.
3. Timeout Fallback to Terminal
If no response comes within the timeout window, the script outputs nothing and exits with code 0.
if [ "$DECISION" = "allow" ]; then
jq -n '{hookSpecificOutput:{...decision:{behavior:"allow"}}}'
elif [ "$DECISION" = "deny" ]; then
jq -n '{hookSpecificOutput:{...decision:{behavior:"deny"}}}'
fi
# Timeout: no output → falls back to interactive prompt
In Claude Code's hook specification, no output + exit 0 means "the hook didn't make a decision," which falls back to the standard terminal prompt. So even if you're not looking at your phone, you can still handle it from the terminal after the timeout. No permissions are silently granted.
Setup
Prerequisites
- macOS or Linux
-
bash,jq,curlinstalled - ntfy app installed on your phone
Installation
git clone https://github.com/coa00/claude-push.git
cd claude-push
bash install.sh
The installer walks you through:
-
Dependency check — verifies
jqandcurlare available -
Topic name input — generates a random one if left blank (
claude-push-a1b2c3d4) -
Config file creation — writes to
~/.config/claude-push/config -
Hook deployment — places script at
~/.local/share/claude-push/hooks/claude-push.sh -
Claude settings registration — safely merges the hook into
~/.claude/settings.jsonusingjq - Test notification — sends a test push to verify everything works
After installation, subscribe to your topic in the ntfy app and you're good to go.
Configuration
Edit ~/.config/claude-push/config. No reinstallation needed.
# Topic name (acts as a shared secret for your ntfy.sh channel)
CLAUDE_PUSH_TOPIC="my-unique-topic"
# Timeout in seconds (falls back to terminal prompt after this)
CLAUDE_PUSH_TIMEOUT=90
Verification
# Send a test notification with Allow/Deny buttons
bash scripts/test.sh test-notify
# Check installation status
bash scripts/test.sh status
The status command checks Config / Hook / Settings / Dependencies:
=== claude-push status ===
[OK] Config: ~/.config/claude-push/config
Topic: my-unique-topic
Timeout: 90s
[OK] Hook: ~/.local/share/claude-push/hooks/claude-push.sh
[OK] Settings: hook registered in ~/.claude/settings.json
[OK] Dependency: jq
[OK] Dependency: curl
Uninstall
bash uninstall.sh
Removes the hook from Claude settings and cleans up all installed files.
What It Looks Like in Practice
After setup, my daily workflow looks like this:
- Tell Claude Code to refactor something, then leave my desk
- A few minutes later, my phone buzzes:
[myproject] Bash: npm run build - I check the command and tap Allow
- When I get back to my desk, the build is already done
I can handle permission requests during meetings, walks, or coffee runs — as long as I have my phone.
Before / After
| Before | After | |
|---|---|---|
| Approval method |
y/n in terminal |
Allow/Deny on phone |
| While away | Claude Code stops and waits | Handle via push notification |
| Timeout | None (waits forever) | Falls back to terminal after 90s |
| Security | Allowlist or manual check | Case-by-case approval per notification |
| Setup cost | — |
bash install.sh in 3 minutes |
Security Considerations
The ntfy.sh topic name acts as a shared secret. Anyone who knows the topic name can send notifications or forge responses.
- Use a random, hard-to-guess string for your topic name (the installer generates one by default)
- For stricter control, configure ntfy.sh access control
- For dangerous commands (
rm -rf, etc.), keep them explicitly blocked in your allowlist as an extra safety layer
Wrapping Up
Claude Code's permission system used to be a binary choice: either babysit your terminal or allowlist everything. claude-push adds a third option — mobile approval.
The implementation is deliberately boring: bash + ntfy.sh HTTP Actions + SSE. The hook itself is about 60 lines. What made this possible is Claude Code's well-designed Hooks API — you just return a JSON object with allow or deny and you're done.
The broader pattern here applies to any AI coding agent, not just Claude Code: as agents get more capable, we need lightweight, async approval mechanisms that don't require us to sit and watch. Mobile push notifications hit the sweet spot — fast enough to keep the agent moving, visible enough to maintain oversight.
The best developer tool is the one that lets you stop staring at a screen for 5 minutes. claude-push is that tool.
How do you handle AI agent permissions? Whether you're an allowlist person or a manual-check person, I'd love for you to give this a try.
https://github.com/coa00/claude-push
References
- Claude Code Hooks Documentation
- ntfy.sh — HTTP-based push notification service
- ntfy.sh Action Buttons
- konsti-web/claude_push — Original inspiration (Windows/PowerShell version)
Top comments (0)