claude-remote-approver 0.5.2 → 0.6.1
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 +12 -2
- package/bin/cli.mjs +7 -4
- package/package.json +1 -1
- package/src/hook.mjs +24 -6
- package/src/ntfy.mjs +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ cli.mjs hook
|
|
|
21
21
|
│
|
|
22
22
|
├──POST──▶ ntfy.sh/<topic> ──push──▶ Phone (ntfy app)
|
|
23
23
|
│ │
|
|
24
|
-
│
|
|
24
|
+
│ Approve / Always Approve / Deny tap
|
|
25
25
|
│ │
|
|
26
26
|
└──SSE───▶ ntfy.sh/<topic>-response ◀──POST──┘
|
|
27
27
|
│
|
|
@@ -31,7 +31,7 @@ Claude Code continues or stops
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
1. Claude Code invokes the hook, piping the tool request as JSON to stdin.
|
|
34
|
-
2. `cli.mjs hook` sends a notification to your ntfy topic with **Approve** and **Deny** action buttons.
|
|
34
|
+
2. `cli.mjs hook` sends a notification to your ntfy topic with **Approve**, **Always 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
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.
|
|
@@ -41,6 +41,16 @@ Claude Code continues or stops
|
|
|
41
41
|
|
|
42
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
43
|
|
|
44
|
+
### Always Approve
|
|
45
|
+
|
|
46
|
+
When Claude Code sends a `permission_suggestions` field with the hook request (indicating the tool can be auto-approved in future), the notification shows three buttons: **Approve**, **Always Approve**, and **Deny**.
|
|
47
|
+
|
|
48
|
+
- **Approve** -- Allow this one request.
|
|
49
|
+
- **Always Approve** -- Allow this request and tell Claude Code to auto-approve this tool in future sessions. Claude Code adds the permission rule to its settings so it won't ask again.
|
|
50
|
+
- **Deny** -- Reject this request.
|
|
51
|
+
|
|
52
|
+
If the hook request does not include `permission_suggestions`, only the standard **Approve** and **Deny** buttons are shown.
|
|
53
|
+
|
|
44
54
|
## Quick Start
|
|
45
55
|
|
|
46
56
|
```bash
|
package/bin/cli.mjs
CHANGED
|
@@ -36,15 +36,18 @@ export async function main(args, deps) {
|
|
|
36
36
|
deps.stdout.write(`Setup complete. Topic: ${result.topic}\n\n`);
|
|
37
37
|
|
|
38
38
|
try {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
39
|
+
const serverUrl = new URL(result.ntfyServer);
|
|
40
|
+
const isHttps = serverUrl.protocol === "https:";
|
|
41
|
+
const ntfyUrl = isHttps
|
|
42
|
+
? `ntfy://${serverUrl.host}/${result.topic}`
|
|
43
|
+
: `${result.ntfyServer.replace(/\/+$/, "")}/${result.topic}`;
|
|
44
|
+
const subscribeUrl = `${result.ntfyServer.replace(/\/+$/, "")}/${result.topic}`;
|
|
42
45
|
|
|
43
46
|
deps.stdout.write("Scan this QR code in the ntfy app to subscribe:\n\n");
|
|
44
47
|
// qrcode-terminal invokes the callback synchronously
|
|
45
48
|
deps.generateQR(ntfyUrl, { small: true }, (qrString) => {
|
|
46
49
|
deps.stdout.write(qrString + "\n\n");
|
|
47
|
-
deps.stdout.write(`Subscribe URL: ${
|
|
50
|
+
deps.stdout.write(`Subscribe URL: ${subscribeUrl}\n`);
|
|
48
51
|
});
|
|
49
52
|
} catch {
|
|
50
53
|
deps.stderr.write(`Warning: Invalid ntfyServer URL in config: ${result.ntfyServer}\n`);
|
package/package.json
CHANGED
package/src/hook.mjs
CHANGED
|
@@ -11,16 +11,18 @@ export const RETRY_DELAY_MS = 1000;
|
|
|
11
11
|
export const _internal = { delay: ms => new Promise(r => setTimeout(r, ms)) };
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Build ntfy action buttons for Approve / Deny.
|
|
14
|
+
* Build ntfy action buttons for Approve / Deny (and optionally Always Approve).
|
|
15
15
|
*
|
|
16
16
|
* @param {string} server - ntfy server URL
|
|
17
17
|
* @param {string} topic - ntfy topic
|
|
18
18
|
* @param {string} requestId - Unique request identifier
|
|
19
|
-
* @
|
|
19
|
+
* @param {object} [options] - Optional settings
|
|
20
|
+
* @param {string[]} [options.permissionSuggestions] - When non-empty, adds an "Always Approve" button
|
|
21
|
+
* @returns {Array<object>} Array of action objects
|
|
20
22
|
*/
|
|
21
|
-
export function buildActions(server, topic, requestId) {
|
|
23
|
+
export function buildActions(server, topic, requestId, { permissionSuggestions } = {}) {
|
|
22
24
|
const url = `${server}/${topic}-response`;
|
|
23
|
-
|
|
25
|
+
const actions = [
|
|
24
26
|
{
|
|
25
27
|
action: "http",
|
|
26
28
|
label: "Approve",
|
|
@@ -36,6 +38,16 @@ export function buildActions(server, topic, requestId) {
|
|
|
36
38
|
method: "POST",
|
|
37
39
|
},
|
|
38
40
|
];
|
|
41
|
+
if (permissionSuggestions?.length > 0) {
|
|
42
|
+
actions.splice(1, 0, {
|
|
43
|
+
action: "http",
|
|
44
|
+
label: "Always Approve",
|
|
45
|
+
url,
|
|
46
|
+
body: JSON.stringify({ requestId, approved: true, alwaysAllow: true }),
|
|
47
|
+
method: "POST",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return actions;
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
/**
|
|
@@ -194,7 +206,9 @@ export async function processHook(input, { loadConfig, sendNotification, waitFor
|
|
|
194
206
|
|
|
195
207
|
const requestId = crypto.randomUUID();
|
|
196
208
|
const { title, message } = formatToolInfo(input);
|
|
197
|
-
const actions = buildActions(config.ntfyServer, config.topic, requestId
|
|
209
|
+
const actions = buildActions(config.ntfyServer, config.topic, requestId, {
|
|
210
|
+
permissionSuggestions: input.permission_suggestions,
|
|
211
|
+
});
|
|
198
212
|
|
|
199
213
|
const sent = await sendWithRetry(sendNotification, {
|
|
200
214
|
server: config.ntfyServer,
|
|
@@ -230,5 +244,9 @@ export async function processHook(input, { loadConfig, sendNotification, waitFor
|
|
|
230
244
|
return ASK;
|
|
231
245
|
}
|
|
232
246
|
if (response.approved === false) return DENY;
|
|
233
|
-
|
|
247
|
+
const decision = { behavior: "allow" };
|
|
248
|
+
if (response.alwaysAllow === true && input.permission_suggestions?.length > 0) {
|
|
249
|
+
decision.updatedPermissions = input.permission_suggestions;
|
|
250
|
+
}
|
|
251
|
+
return { hookSpecificOutput: { hookEventName: "PermissionRequest", decision } };
|
|
234
252
|
}
|
package/src/ntfy.mjs
CHANGED
|
@@ -73,7 +73,7 @@ export async function waitForResponse({ server, topic, requestId, timeout }) {
|
|
|
73
73
|
if (typeof parsed.answer === 'string') {
|
|
74
74
|
return { answer: parsed.answer };
|
|
75
75
|
}
|
|
76
|
-
return { approved: parsed.approved };
|
|
76
|
+
return { approved: parsed.approved, alwaysAllow: parsed.alwaysAllow === true };
|
|
77
77
|
}
|
|
78
78
|
} catch {
|
|
79
79
|
// skip non-JSON lines
|