claude-remote-approver 0.1.0 → 0.2.0

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 CHANGED
@@ -108,6 +108,39 @@ claude-remote-approver status
108
108
  # Timeout: 120s
109
109
  ```
110
110
 
111
+ ### `enable`
112
+
113
+ Re-enable the hook after it has been disabled.
114
+
115
+ ```bash
116
+ claude-remote-approver enable
117
+ # Hook enabled.
118
+ ```
119
+
120
+ This reads the existing configuration and re-registers the hook in Claude Code's settings. You must have run `setup` at least once before using this command.
121
+
122
+ ### `disable`
123
+
124
+ Temporarily disable the hook without removing your configuration.
125
+
126
+ ```bash
127
+ claude-remote-approver disable
128
+ # Hook disabled. Run 'claude-remote-approver enable' to re-enable.
129
+ ```
130
+
131
+ Your topic and settings are preserved. Use `enable` to re-activate.
132
+
133
+ ### `uninstall`
134
+
135
+ Remove the hook and delete all configuration.
136
+
137
+ ```bash
138
+ claude-remote-approver uninstall
139
+ # Uninstalled. Hook removed and configuration deleted.
140
+ ```
141
+
142
+ This removes the hook entry from Claude Code's settings.json and deletes `~/.claude-remote-approver.json`. To use the tool again, run `setup`.
143
+
111
144
  ### `hook`
112
145
 
113
146
  Internal command. Claude Code calls this automatically via the registered hook. It reads a JSON payload from stdin and writes a decision to stdout. You do not need to run this manually.
package/bin/cli.mjs CHANGED
@@ -3,12 +3,14 @@
3
3
  /**
4
4
  * CLI entry point for claude-remote-approver.
5
5
  *
6
- * Subcommands: setup | test | status | hook
6
+ * Subcommands: setup | test | status | enable | disable | uninstall | hook
7
7
  * All I/O goes through the injected `deps` object so the module is fully testable.
8
8
  */
9
9
 
10
10
  import { fileURLToPath } from "node:url";
11
- import { realpathSync } from "node:fs";
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import os from "node:os";
12
14
 
13
15
  // ---------------------------------------------------------------------------
14
16
  // main
@@ -16,11 +18,11 @@ import { realpathSync } from "node:fs";
16
18
 
17
19
  export async function main(args, deps) {
18
20
  if (args.includes("--help") || args.includes("-h")) {
19
- deps.stdout.write("Usage: claude-remote-approver <command>\n\nCommands:\n setup Set up remote approval\n test Send a test notification\n status Show current configuration\n hook Process a Claude Code hook (internal)\n");
21
+ deps.stdout.write("Usage: claude-remote-approver <command>\n\nCommands:\n setup Set up remote approval\n test Send a test notification\n status Show current configuration\n enable Re-enable the hook\n disable Temporarily disable the hook\n uninstall Remove hook and delete configuration\n hook Process a Claude Code hook (internal)\n");
20
22
  return;
21
23
  }
22
24
  if (args.includes("--version") || args.includes("-v")) {
23
- deps.stdout.write("0.1.0\n");
25
+ deps.stdout.write("0.2.0\n");
24
26
  return;
25
27
  }
26
28
 
@@ -86,9 +88,56 @@ export async function main(args, deps) {
86
88
  break;
87
89
  }
88
90
 
91
+ case "uninstall": {
92
+ try {
93
+ deps.unregisterHook(deps.settingsPath);
94
+ } catch (err) {
95
+ deps.stderr.write(`Error: Failed to remove hook: ${err.message}\n`);
96
+ break;
97
+ }
98
+ try {
99
+ deps.unlinkSync(deps.configPath);
100
+ } catch (err) {
101
+ if (err.code !== "ENOENT") {
102
+ deps.stderr.write(`Error: Failed to delete config: ${err.message}\n`);
103
+ break;
104
+ }
105
+ }
106
+ deps.stdout.write("Uninstalled. Hook removed and configuration deleted.\n");
107
+ break;
108
+ }
109
+
110
+ case "disable": {
111
+ try {
112
+ deps.unregisterHook(deps.settingsPath);
113
+ } catch (err) {
114
+ deps.stderr.write(`Error: Failed to disable hook: ${err.message}\n`);
115
+ break;
116
+ }
117
+ deps.stdout.write("Hook disabled. Run 'claude-remote-approver enable' to re-enable.\n");
118
+ break;
119
+ }
120
+
121
+ case "enable": {
122
+ const config = deps.loadConfig();
123
+ if (!config.topic) {
124
+ deps.stderr.write("Error: No topic configured. Run 'claude-remote-approver setup' first.\n");
125
+ deps.exit(1);
126
+ break;
127
+ }
128
+ try {
129
+ deps.registerHook(deps.settingsPath, deps.getHookCommand());
130
+ } catch (err) {
131
+ deps.stderr.write(`Error: Failed to enable hook: ${err.message}\n`);
132
+ break;
133
+ }
134
+ deps.stdout.write("Hook enabled.\n");
135
+ break;
136
+ }
137
+
89
138
  default: {
90
139
  deps.stderr.write(
91
- "Usage: claude-remote-approver <command>\n\nCommands:\n setup Configure topic and register hook\n test Send a test notification\n status Show current configuration\n hook Process a Claude Code hook (reads JSON from stdin)\n",
140
+ "Usage: claude-remote-approver <command>\n\nCommands:\n setup Set up remote approval\n test Send a test notification\n status Show current configuration\n enable Re-enable the hook\n disable Temporarily disable the hook\n uninstall Remove hook and delete configuration\n hook Process a Claude Code hook (internal)\n",
92
141
  );
93
142
  deps.exit(1);
94
143
  break;
@@ -105,7 +154,7 @@ const isMain =
105
154
  process.argv[1] &&
106
155
  (() => {
107
156
  try {
108
- return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
157
+ return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(process.argv[1]);
109
158
  } catch {
110
159
  return false;
111
160
  }
@@ -119,7 +168,7 @@ if (isMain) {
119
168
  "../src/ntfy.mjs"
120
169
  );
121
170
  const { processHook } = await import("../src/hook.mjs");
122
- const { runSetup } = await import("../src/setup.mjs");
171
+ const { runSetup, registerHook, getHookCommand, unregisterHook } = await import("../src/setup.mjs");
123
172
 
124
173
  const args = process.argv.slice(2);
125
174
 
@@ -141,6 +190,12 @@ if (isMain) {
141
190
  formatToolInfo,
142
191
  processHook,
143
192
  runSetup,
193
+ registerHook,
194
+ getHookCommand,
195
+ unregisterHook,
196
+ unlinkSync: fs.unlinkSync,
197
+ configPath: (await import("../src/config.mjs")).CONFIG_PATH,
198
+ settingsPath: path.join(os.homedir(), ".claude", "settings.json"),
144
199
  stdout: process.stdout,
145
200
  stderr: process.stderr,
146
201
  stdin: stdinData,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote-approver",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Approve or deny Claude Code permission prompts remotely from your phone via ntfy.sh",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "README.md"
14
14
  ],
15
15
  "scripts": {
16
- "test": "node --test test/**/*.test.mjs"
16
+ "test": "node --test test/*.test.mjs"
17
17
  },
18
18
  "keywords": [
19
19
  "claude",
package/src/setup.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
 
4
5
  /**
5
6
  * Returns the hook command string: `node <absolute_path_to_src/hook.mjs>`
6
7
  */
7
8
  export function getHookCommand() {
8
- const hookPath = path.resolve(import.meta.dirname, "hook.mjs");
9
+ const hookPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "hook.mjs");
9
10
  return `node "${hookPath}"`;
10
11
  }
11
12
 
@@ -48,6 +49,41 @@ export function registerHook(settingsPath, hookCommand) {
48
49
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
49
50
  }
50
51
 
52
+ /**
53
+ * Removes the claude-remote-approver hook entry from Claude's settings.json.
54
+ * If the file does not exist, does nothing.
55
+ */
56
+ export function unregisterHook(settingsPath) {
57
+ let settings;
58
+ try {
59
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
60
+ } catch (err) {
61
+ if (err.code === "ENOENT") return;
62
+ throw err;
63
+ }
64
+
65
+ if (!settings.hooks?.PermissionRequest) return;
66
+
67
+ const original = settings.hooks.PermissionRequest;
68
+ const filtered = original.filter(
69
+ (h) => !(h.command && h.command.includes("claude-remote-approver"))
70
+ );
71
+
72
+ if (filtered.length === original.length) return;
73
+
74
+ if (filtered.length === 0) {
75
+ delete settings.hooks.PermissionRequest;
76
+ } else {
77
+ settings.hooks.PermissionRequest = filtered;
78
+ }
79
+
80
+ if (Object.keys(settings.hooks).length === 0) {
81
+ delete settings.hooks;
82
+ }
83
+
84
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
85
+ }
86
+
51
87
  /**
52
88
  * Runs the full setup flow:
53
89
  * 1. Generate a topic