claude-remote-approver 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/bin/cli.mjs +5 -4
- package/package.json +1 -1
- package/src/hook.mjs +18 -5
- package/src/ntfy.mjs +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ cli.mjs hook
|
|
|
25
25
|
│ │
|
|
26
26
|
└──SSE───▶ ntfy.sh/<topic>-response ◀──POST──┘
|
|
27
27
|
│
|
|
28
|
-
│ stdout JSON:
|
|
28
|
+
│ stdout JSON: allow / deny / ask (CLI fallback)
|
|
29
29
|
▼
|
|
30
30
|
Claude Code continues or stops
|
|
31
31
|
```
|
|
@@ -34,9 +34,13 @@ Claude Code continues or stops
|
|
|
34
34
|
2. `cli.mjs hook` sends a notification to your ntfy topic with **Approve** and **Deny** action buttons.
|
|
35
35
|
3. The hook subscribes to a response topic (`<topic>-response`) via server-sent events.
|
|
36
36
|
4. When you tap a button on your phone, ntfy.sh publishes your decision to the response topic.
|
|
37
|
-
5. The hook reads the decision
|
|
37
|
+
5. The hook reads the decision and writes `{"behavior":"allow"}` or `{"behavior":"deny"}` to stdout. If the notification fails or times out, the hook returns `{"behavior":"ask"}` so Claude Code falls back to the CLI prompt.
|
|
38
38
|
6. Claude Code proceeds accordingly.
|
|
39
39
|
|
|
40
|
+
### AskUserQuestion support
|
|
41
|
+
|
|
42
|
+
When Claude Code calls the `AskUserQuestion` tool, the hook sends the question to your phone as a notification. Each option appears as an action button you can tap. If the question has more than 3 options, they are split across multiple notifications. If no response is received before the timeout, the hook falls back to the CLI prompt so you can answer at your terminal.
|
|
43
|
+
|
|
40
44
|
## Quick Start
|
|
41
45
|
|
|
42
46
|
```bash
|
|
@@ -179,7 +183,7 @@ Config file location: `~/.claude-remote-approver.json`
|
|
|
179
183
|
|---|---|---|---|
|
|
180
184
|
| `topic` | `string` | `""` | Your unique ntfy topic. Generated by `setup`. |
|
|
181
185
|
| `ntfyServer` | `string` | `"https://ntfy.sh"` | The ntfy server URL. Change this if you self-host. |
|
|
182
|
-
| `timeout` | `number` | `120` | Seconds to wait for a response before
|
|
186
|
+
| `timeout` | `number` | `120` | Seconds to wait for a response before falling back to CLI. |
|
|
183
187
|
| `planTimeout` | `number` | `300` | Seconds to wait for ExitPlanMode (plan approval) responses. Plan reviews need more reading time. |
|
|
184
188
|
| `autoApprove` | `string[]` | `[]` | Reserved for future use. |
|
|
185
189
|
| `autoDeny` | `string[]` | `[]` | Reserved for future use. |
|
|
@@ -225,7 +229,7 @@ The public ntfy.sh server is convenient but means your permission request detail
|
|
|
225
229
|
|
|
226
230
|
### Timeout behavior
|
|
227
231
|
|
|
228
|
-
If no response is received within the configured timeout (default: 120 seconds), the hook
|
|
232
|
+
If no response is received within the configured timeout (default: 120 seconds), the hook falls back to the **CLI prompt** (`ask`), so you can still respond at your terminal.
|
|
229
233
|
|
|
230
234
|
## Disclaimer
|
|
231
235
|
|
|
@@ -234,7 +238,7 @@ If no response is received within the configured timeout (default: 120 seconds),
|
|
|
234
238
|
The authors are not responsible for any damages or losses arising from the use of this tool, including but not limited to:
|
|
235
239
|
|
|
236
240
|
- Accidental approval of dangerous commands (e.g., mistapping Approve on your phone)
|
|
237
|
-
- Unintended
|
|
241
|
+
- Unintended CLI fallback when away from terminal (e.g., timeout, network issues)
|
|
238
242
|
- Security breaches if the topic name is compromised
|
|
239
243
|
|
|
240
244
|
**Not a substitute for careful review.** The push notification shows the tool name and a brief summary, but not the full context of what Claude Code is doing. Always review what you are approving.
|
package/bin/cli.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import fs from "node:fs";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import qrcode from "qrcode-terminal";
|
|
15
|
+
import { ASK } from "../src/hook.mjs";
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// main
|
|
@@ -87,8 +88,8 @@ export async function main(args, deps) {
|
|
|
87
88
|
try {
|
|
88
89
|
input = JSON.parse(deps.stdin);
|
|
89
90
|
} catch {
|
|
90
|
-
|
|
91
|
-
deps.stdout.write(JSON.stringify(
|
|
91
|
+
deps.stderr.write("[claude-remote-approver] Invalid hook input. Falling back to CLI.\n");
|
|
92
|
+
deps.stdout.write(JSON.stringify(ASK) + "\n");
|
|
92
93
|
break;
|
|
93
94
|
}
|
|
94
95
|
|
|
@@ -96,8 +97,8 @@ export async function main(args, deps) {
|
|
|
96
97
|
try {
|
|
97
98
|
result = await deps.processHook(input, deps);
|
|
98
99
|
} catch {
|
|
99
|
-
|
|
100
|
-
deps.stdout.write(JSON.stringify(
|
|
100
|
+
deps.stderr.write("[claude-remote-approver] Hook processing failed. Falling back to CLI.\n");
|
|
101
|
+
deps.stdout.write(JSON.stringify(ASK) + "\n");
|
|
101
102
|
break;
|
|
102
103
|
}
|
|
103
104
|
|
package/package.json
CHANGED
package/src/hook.mjs
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { DEFAULT_CONFIG } from "./config.mjs";
|
|
5
5
|
|
|
6
|
-
const ASK = Object.freeze({ hookSpecificOutput: Object.freeze({ hookEventName: "PermissionRequest", decision: Object.freeze({ behavior: "ask" }) }) });
|
|
6
|
+
export const ASK = Object.freeze({ hookSpecificOutput: Object.freeze({ hookEventName: "PermissionRequest", decision: Object.freeze({ behavior: "ask" }) }) });
|
|
7
7
|
const DENY = Object.freeze({ hookSpecificOutput: Object.freeze({ hookEventName: "PermissionRequest", decision: Object.freeze({ behavior: "deny" }) }) });
|
|
8
8
|
const MAX_RETRIES = 3;
|
|
9
|
+
export const RETRY_DELAY_MS = 1000;
|
|
10
|
+
/** @internal Replaceable delay for testing. Do not use outside of tests. */
|
|
11
|
+
export const _internal = { delay: ms => new Promise(r => setTimeout(r, ms)) };
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Build ntfy action buttons for Approve / Deny.
|
|
@@ -37,6 +40,7 @@ export function buildActions(server, topic, requestId) {
|
|
|
37
40
|
|
|
38
41
|
/**
|
|
39
42
|
* Send with retry, returning null on exhausted retries.
|
|
43
|
+
* Uses linear backoff: delay = RETRY_DELAY_MS * attempt (1s, 2s, …).
|
|
40
44
|
*/
|
|
41
45
|
export async function sendWithRetry(sendFn, params) {
|
|
42
46
|
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
@@ -44,9 +48,10 @@ export async function sendWithRetry(sendFn, params) {
|
|
|
44
48
|
return await sendFn(params);
|
|
45
49
|
} catch (err) {
|
|
46
50
|
if (i === MAX_RETRIES - 1) {
|
|
47
|
-
console.error(
|
|
51
|
+
console.error(`[claude-remote-approver] Notification failed after ${MAX_RETRIES} attempts:`, err.message, "— Falling back to CLI.");
|
|
48
52
|
return null;
|
|
49
53
|
}
|
|
54
|
+
await _internal.delay(RETRY_DELAY_MS * (i + 1));
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
return null;
|
|
@@ -139,13 +144,14 @@ export async function processAskUserQuestion(input, deps) {
|
|
|
139
144
|
timeout: config.timeout * 1000,
|
|
140
145
|
});
|
|
141
146
|
} catch (err) {
|
|
142
|
-
console.error("
|
|
147
|
+
console.error("[claude-remote-approver] Response listener failed:", err.message, "— Falling back to CLI.");
|
|
143
148
|
return ASK;
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
if (response.answer) {
|
|
147
152
|
answers[q.question] = response.answer;
|
|
148
153
|
} else {
|
|
154
|
+
console.error("[claude-remote-approver] No answer received. Falling back to CLI.");
|
|
149
155
|
return ASK;
|
|
150
156
|
}
|
|
151
157
|
}
|
|
@@ -211,11 +217,18 @@ export async function processHook(input, { loadConfig, sendNotification, waitFor
|
|
|
211
217
|
timeout,
|
|
212
218
|
});
|
|
213
219
|
} catch (err) {
|
|
214
|
-
console.error("
|
|
220
|
+
console.error("[claude-remote-approver] Response listener failed:", err.message, "— Falling back to CLI.");
|
|
215
221
|
return ASK;
|
|
216
222
|
}
|
|
217
223
|
|
|
218
|
-
if (response.timeout
|
|
224
|
+
if (response.timeout) {
|
|
225
|
+
console.error("[claude-remote-approver] Timed out waiting for response. Falling back to CLI.");
|
|
226
|
+
return ASK;
|
|
227
|
+
}
|
|
228
|
+
if (response.error) {
|
|
229
|
+
console.error("[claude-remote-approver] Response error:", response.error.message, "— Falling back to CLI.");
|
|
230
|
+
return ASK;
|
|
231
|
+
}
|
|
219
232
|
if (response.approved === false) return DENY;
|
|
220
233
|
return { hookSpecificOutput: { hookEventName: "PermissionRequest", decision: { behavior: "allow" } } };
|
|
221
234
|
}
|
package/src/ntfy.mjs
CHANGED
|
@@ -91,7 +91,7 @@ export async function waitForResponse({ server, topic, requestId, timeout }) {
|
|
|
91
91
|
if (err?.name === "AbortError") {
|
|
92
92
|
return { timeout: true };
|
|
93
93
|
}
|
|
94
|
-
console.error("[claude-remote-approver] waitForResponse error:", err);
|
|
94
|
+
console.error("[claude-remote-approver] waitForResponse error:", err.message ?? err);
|
|
95
95
|
return { error: err };
|
|
96
96
|
}
|
|
97
97
|
}
|