Skip to content

feat(notifications): add Pushcut as a notification channel#1062

Open
MHoroszowski wants to merge 1 commit intodanielmiessler:mainfrom
MHoroszowski:feat/notifications-pushcut-channel
Open

feat(notifications): add Pushcut as a notification channel#1062
MHoroszowski wants to merge 1 commit intodanielmiessler:mainfrom
MHoroszowski:feat/notifications-pushcut-channel

Conversation

@MHoroszowski
Copy link
Copy Markdown

Summary

Adds Pushcut as an iOS-native notification channel alongside ntfy, following the existing `sendPush()` pattern in `hooks/lib/notifications.ts`.

Pushcut delivers rich push notifications to iPhone with first-class iOS Shortcuts integration, action buttons, and an Automation Server for unattended Shortcut execution. It's purpose-built for the "webhook → iPhone notification → optional Shortcuts automation" use case.

Why Pushcut

The Apple-native alternative is a custom Apple Push Notification (APNs) integration, which requires:

  • Apple Developer account ($99/yr)
  • Registered iOS app + provisioning
  • APNs certificate management
  • Ongoing maintenance every iOS major

Pushcut already runs that infrastructure and exposes it via a single webhook URL. Strictly less complexity for the same delivery guarantees, plus the Shortcuts integration a DIY APNs solution would not provide. The free tier covers most personal notification volumes; Pro is ~$5/mo.

Changes

File Change
`hooks/lib/notifications.ts` Add `loadPushcutConfig()` + `sendPushcut()` mirroring the existing ntfy pattern. ~40 lines, no new dependencies.
`settings.json` Add `notifications.pushcut` block. `enabled: false` by default — opt-in. Reads `PUSHCUT_WEBHOOK_URL` from env via the existing `${VAR}` substitution.

`sendPushcut(message, options)` matches the `sendPush()` signature exactly:

  • Fire-and-forget POST
  • 5-second timeout via `AbortSignal.timeout`
  • Returns `boolean` (true on HTTP 2xx, false on misconfig/error)
  • Never throws — failures are silent the same way ntfy failures are

Usage

```ts
import { sendPushcut } from '~/.claude/hooks/lib/notifications';
await sendPushcut('Long task complete', { title: 'Athena' });
```

User setup (will need a docs entry — happy to add to a follow-up doc PR if useful)

  1. Install Pushcut from the App Store
  2. Create a named notification template in the Notifications tab (the name becomes the URL suffix)
  3. Copy the secret webhook URL from the notification's detail view
  4. Set `PUSHCUT_WEBHOOK_URL` in `~/.env`
  5. Set `notifications.pushcut.enabled = true` in `settings.json`

Test plan

  • `sendPushcut()` returns `true` on HTTP 200 from Pushcut API (verified end-to-end — notification arrived on iPhone)
  • Returns `false` (no throw) when env var missing or `enabled: false`
  • Returns `false` (no throw) on network error or 5s timeout
  • Pre-existing `sendPush()` (ntfy) behavior unchanged — same code, same loader pattern
  • No new dependencies
  • Defaults to `enabled: false` — zero impact on existing installs that don't opt in

Out of scope (intentional, possible follow-ups)

  • Multi-channel dispatcher that consults `notifications.routing` — the routing block exists in `settings.json` but no code reads it yet (only individual sender functions are exported). Adding a dispatcher would be a separate, larger PR.
  • Pushcut action buttons / defaultAction support — needs a concrete use case to design the API surface. Easy to extend the body shape later without breaking the current signature.
  • discord/twilio sender implementations — still settings-only stubs. Independent work.

Related

This is a feature add, not a bug fix. Happy to scope it down further or split if maintainers prefer a smaller initial surface.

Adds Pushcut (https://www.pushcut.io) as an iOS-native notification
channel alongside ntfy. Pushcut delivers rich push notifications to
iPhone with first-class iOS Shortcuts integration, action buttons,
and an Automation Server for unattended Shortcut execution.

Changes:

- hooks/lib/notifications.ts: add sendPushcut() following the existing
  sendPush() (ntfy) pattern. Fire-and-forget POST, 5s timeout, returns
  boolean, never throws. Loads config from settings.json with the
  same ${VAR} env-var expansion the ntfy loader uses.

- settings.json: add notifications.pushcut block (enabled: false by
  default — opt-in). Reads PUSHCUT_WEBHOOK_URL from env. Pattern
  mirrors the existing twilio block.

Usage:

```ts
import { sendPushcut } from '~/.claude/hooks/lib/notifications';
await sendPushcut('Long task complete', { title: 'Athena' });
```

Setup for users:
1. Install Pushcut from the App Store
2. Create a named notification in the Notifications tab
3. Copy the secret webhook URL from the notification's detail view
4. Set PUSHCUT_WEBHOOK_URL in ~/.env
5. Set notifications.pushcut.enabled = true in settings.json

Why Pushcut over a custom Apple Push Notification (APNs) integration:
APNs requires an Apple Developer account ($99/yr) and a registered
iOS app. Pushcut already runs that infrastructure and exposes it via
a single webhook URL — strictly less complexity for the same outcome,
plus the Shortcuts integration that DIY APNs would not provide.

Test plan:
- [x] sendPushcut() returns true on HTTP 200 from Pushcut API
- [x] Returns false (no throw) on misconfig (missing env var, disabled)
- [x] Returns false (no throw) on network error / timeout
- [x] Verified end-to-end: bun import + call → notification arrives
      on iPhone via Pushcut iOS app
- [x] Pre-existing sendPush() (ntfy) behavior unchanged
- [x] No new dependencies; only uses existing fetch + AbortSignal

Out of scope (intentional, follow-up candidates):
- Multi-channel dispatcher that consults notifications.routing — the
  routing block exists in settings.json but no code reads it yet.
  Would be a separate, larger PR.
- Pushcut action buttons / defaultAction support — needs concrete
  use case to design the API surface.
- discord/twilio sender implementations (still settings-only stubs).
MHoroszowski added a commit to MHoroszowski/Personal_AI_Infrastructure that referenced this pull request Apr 12, 2026
PAI currently owns the user's ~/.env file via a symlink created during
install. This is a home-directory namespace grab: ~/.env is a conventional,
user-owned file that many shells and tools look for. If the user installs
any other tool that expects to read ~/.env, it either collides with PAI's
secrets or is silently overwritten on the next PAI install. This is the
wrong shape.

The cause is that VoiceServer hardcodes `join(homedir(), '.env')` as the
only place it looks for ELEVENLABS_API_KEY. The installer created the
~/.env symlink to make that hardcoded read resolve to the real secrets
file at ~/.config/PAI/.env (which is already the XDG-compliant, correct
canonical location).

This change:

- VoiceServer/server.ts: load ~/.config/PAI/.env first (XDG canonical
  location), then optionally overlay from ~/.env if the user has chosen
  to put PAI-relevant keys there. Values in ~/.env win on key collisions,
  preserving the "explicit user override" mental model. The error message
  when ELEVENLABS_API_KEY is missing now points at the canonical path.

- PAI-Install/engine/actions.ts: stop creating the ~/.env symlink.
  ~/.claude/.env stays symlinked (that path is PAI's own namespace, so
  the symlink is safe and the hooks' existing reads continue working
  unchanged). The new comment explains why we no longer touch ~/.env.

Backward compatibility:

- Existing installs that already have the ~/.env symlink continue to
  work — both paths point at the same real file, so loading both is a
  no-op.
- Existing installs that already have a real ~/.env with PAI values in
  it continue to work — loadEnvFile() reads both paths and either one
  (or both) can contain the keys.
- New installs after this change will have a real file at
  ~/.config/PAI/.env and will NOT touch ~/.env. Users own ~/.env.

Related to the VoiceServer cross-platform audio work in danielmiessler#1061 and the
Pushcut notification channel in danielmiessler#1062.
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.

1 participant