Skip to content

feat: blocking permission hook with FIFO-based approval#17

Merged
hiskudin merged 3 commits into
mainfrom
feat/blocking-permission-hook
Apr 30, 2026
Merged

feat: blocking permission hook with FIFO-based approval#17
hiskudin merged 3 commits into
mainfrom
feat/blocking-permission-hook

Conversation

@hiskudin

@hiskudin hiskudin commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces the keystroke-injection approach for permission approval (which has been unreliable across all our attempts: AX tree walking, ctrl+backtick, TIOCSTI) with Claude Code's documented hook decision protocol.

When the PermissionRequest hook fires, notify.sh now creates a FIFO at /tmp/stack-nudge-perm-<id>.fifo and blocks reading from it. The notification carries the FIFO path. When the user clicks Allow (banner) or Enter (panel), the app writes allow to the FIFO. The hook reads it, outputs {"hookSpecificOutput": {"hookEventName": "PermissionRequest", "decision": {"behavior": "allow"}}} to stdout, and exits — Claude Code skips its own UI prompt entirely.

Why this works

Claude Code's hook docs explicitly support this pattern:

  • Hook can run for up to the configured timeout (we set 600s for PermissionRequest)
  • JSON output to stdout drives the decision
  • Synchronous semantics — Claude Code waits for the hook before proceeding

This is the proper supported way. No keystroke injection. No AX hacks. No flaky timing.

Changes

  • notify.shcreate_perm_fifo() + wait_for_permission_response() use Python select for portable timeout-aware FIFO reads
  • install.sh — PermissionRequest hook registered with timeout: 600
  • panel/EventStore.swiftNudgeEvent gains fifoPath
  • panel/EventListener.swift — decodes fifo_path from socket payload
  • panel/Panel.swiftwriteFIFO() helper; UNUserNotificationCenter Allow action and panel Enter key both write allow\n to the FIFO, short-circuiting the AppActivator path

Test plan

  • Trigger a permission request → Allow button on banner approves it cleanly, no terminal prompt shown
  • Same via panel Enter key
  • Dismiss the notification → after 550s, hook times out, Claude Code's terminal UI fallback appears
  • Stop events (no FIFO) still work via existing AppActivator path
  • No orphaned FIFOs in /tmp/ after normal use

🤖 Generated with Claude Code

hiskudin and others added 3 commits April 30, 2026 15:17
When Claude Code's PermissionRequest hook fires, notify.sh now creates a
FIFO and blocks reading from it. The notification carries the FIFO path;
when the user clicks Allow (banner) or Enter (panel), the app writes
"allow" to the FIFO. The hook reads the decision, outputs the proper
PermissionRequest JSON to stdout, and exits — Claude Code skips its own
UI prompt entirely. No keystroke injection, no AX hacks, no flaky timing.

- notify.sh: create_perm_fifo() + wait_for_permission_response() functions
  use Python+select for a portable timeout-aware FIFO read
- install.sh: PermissionRequest hook registered with timeout=600 (Claude
  Code's default) so the hook can wait long enough for user input
- panel/EventStore.swift: NudgeEvent gains fifoPath
- panel/EventListener.swift: decode fifo_path from socket payload
- panel/Panel.swift: writeFIFO() helper; both UNUserNotificationCenter
  Allow action and panel Enter key write "allow\n" to the FIFO directly,
  short-circuiting the AppActivator path

On timeout (no user response within 550s), the hook exits silently and
Claude Code falls back to its terminal UI prompt.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
New 'Pin panel' toggle in Settings (on by default). When off, the panel
auto-hides whenever it loses key focus — clicking outside, switching
apps, etc. When on (default), the panel stays visible until explicitly
dismissed via Esc, hotkey, or action.

- PanelNav: panelPinned defaults true, persists to STACKNUDGE_PANEL_PIN
- Settings: new toggle row 3, all subsequent rows shifted +1 (rowCount=11)
- Panel: observes NSWindow.didResignKeyNotification on the panel itself
  and calls hidePanel() when fired and pin is off

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@hiskudin hiskudin merged commit 84baaea into main Apr 30, 2026
4 checks passed
@hiskudin hiskudin deleted the feat/blocking-permission-hook branch April 30, 2026 13:58
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