DEV Community

Cover image for How I Built a Mobile Approval System for Claude Code So I Can Finally Leave My Desk
Kohei Aoki
Kohei Aoki

Posted on

How I Built a Mobile Approval System for Claude Code So I Can Finally Leave My Desk

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/"
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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, curl installed
  • ntfy app installed on your phone

Installation

git clone https://github.com/coa00/claude-push.git
cd claude-push
bash install.sh
Enter fullscreen mode Exit fullscreen mode

The installer walks you through:

  1. Dependency check — verifies jq and curl are available
  2. Topic name input — generates a random one if left blank (claude-push-a1b2c3d4)
  3. Config file creation — writes to ~/.config/claude-push/config
  4. Hook deployment — places script at ~/.local/share/claude-push/hooks/claude-push.sh
  5. Claude settings registration — safely merges the hook into ~/.claude/settings.json using jq
  6. 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
Enter fullscreen mode Exit fullscreen mode

Verification

# Send a test notification with Allow/Deny buttons
bash scripts/test.sh test-notify

# Check installation status
bash scripts/test.sh status
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Uninstall

bash uninstall.sh
Enter fullscreen mode Exit fullscreen mode

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:

  1. Tell Claude Code to refactor something, then leave my desk
  2. A few minutes later, my phone buzzes: [myproject] Bash: npm run build
  3. I check the command and tap Allow
  4. 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

Top comments (0)